Vibe passkey
This commit is contained in:
@@ -15,12 +15,15 @@
|
||||
<link href="/css/app.css" rel="stylesheet"/>
|
||||
<link href="/Server.styles.css" rel="stylesheet"/>
|
||||
<script defer src="/_content/Telerik.UI.for.Blazor/js/telerik-blazor.js"></script>
|
||||
<script src="/js/passkey.js"></script>
|
||||
<link href="/favicon.png" rel="icon" type="image/png"/>
|
||||
<HeadOutlet/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Routes/>
|
||||
<CascadingAuthenticationState>
|
||||
<Routes/>
|
||||
</CascadingAuthenticationState>
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
@page "/login"
|
||||
@rendermode InteractiveServer
|
||||
@attribute [Microsoft.AspNetCore.Authorization.AllowAnonymous]
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@inject IJSRuntime JS
|
||||
@inject NavigationManager Nav
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
|
||||
<PageTitle>Sign In — Chrono CCG</PageTitle>
|
||||
|
||||
<div class="login-container">
|
||||
<div class="login-card">
|
||||
<h1 class="login-title">Chrono CCG</h1>
|
||||
|
||||
@if (_error is not null)
|
||||
{
|
||||
<div class="alert alert-danger">@_error</div>
|
||||
}
|
||||
|
||||
@if (_enrolled)
|
||||
{
|
||||
<p class="login-hint">Use your registered passkey to sign in.</p>
|
||||
<button class="btn btn-primary btn-lg w-100" @onclick="SignIn" disabled="@_busy">
|
||||
@if (_busy)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm me-2" role="status"></span>
|
||||
}
|
||||
Sign in with Passkey
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="login-hint">No passkey enrolled yet. Register your device to get started.</p>
|
||||
<button class="btn btn-success btn-lg w-100" @onclick="Enroll" disabled="@_busy">
|
||||
@if (_busy)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm me-2" role="status"></span>
|
||||
}
|
||||
Enroll Passkey
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool _enrolled;
|
||||
private bool _busy;
|
||||
private string? _error;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
if (authState.User.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
Nav.NavigateTo("/", replace: true);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var http = new HttpClient { BaseAddress = new Uri(Nav.BaseUri) };
|
||||
var status = await http.GetFromJsonAsync<StatusResponse>("/api/auth/status");
|
||||
_enrolled = status?.Enrolled ?? false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
_enrolled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SignIn()
|
||||
{
|
||||
_busy = true;
|
||||
_error = null;
|
||||
try
|
||||
{
|
||||
await JS.InvokeVoidAsync("passkeyLogin");
|
||||
Nav.NavigateTo("/", forceLoad: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_error = ex.Message;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Enroll()
|
||||
{
|
||||
_busy = true;
|
||||
_error = null;
|
||||
try
|
||||
{
|
||||
await JS.InvokeVoidAsync("passkeyEnroll");
|
||||
Nav.NavigateTo("/", forceLoad: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_error = ex.Message;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private record StatusResponse(bool Enrolled, bool Authenticated);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
.login-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
background: var(--bs-body-bg, #f8f9fa);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 24px rgba(0,0,0,0.10);
|
||||
padding: 2.5rem 2rem;
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.login-hint {
|
||||
color: #6c757d;
|
||||
margin-bottom: 1.25rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
@inject NavigationManager Nav
|
||||
|
||||
@code {
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Nav.NavigateTo("/login", forceLoad: true);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
<Router AppAssembly="@typeof(Program).Assembly" AdditionalAssemblies="new[] { typeof(Home).Assembly }">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
|
||||
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
|
||||
<NotAuthorized>
|
||||
<RedirectToLogin/>
|
||||
</NotAuthorized>
|
||||
</AuthorizeRouteView>
|
||||
<FocusOnNavigate RouteData="@routeData" Selector="h1"/>
|
||||
</Found>
|
||||
</Router>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Chrono.Model
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
|
||||
Reference in New Issue
Block a user