Updated front-end authentication.

This commit is contained in:
2026-02-22 21:47:34 -05:00
parent 8348603b13
commit 80ca1296e5
13 changed files with 184 additions and 56 deletions

View File

@@ -1,4 +1,5 @@
@using JSMR.UI.Blazor.Services @using JSMR.UI.Blazor.Components.Authentication
@using JSMR.UI.Blazor.Services
@inject SessionState Session @inject SessionState Session
@@ -6,7 +7,8 @@
<RadzenTheme Theme="material-dark" /> <RadzenTheme Theme="material-dark" />
</HeadContent> </HeadContent>
<Router AppAssembly="@typeof(App).Assembly"> <AuthenticationGate>
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData"> <Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" /> <FocusOnNavigate RouteData="@routeData" Selector="h1" />
@@ -17,11 +19,12 @@
<p role="alert">Sorry, there's nothing at this address.</p> <p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView> </LayoutView>
</NotFound> </NotFound>
</Router> </Router>
</AuthenticationGate>
@code { @code {
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
await Session.RefreshAsync(); //await Session.RefreshAsync();
} }
} }

View File

@@ -0,0 +1,72 @@
@using JSMR.UI.Blazor.Services
@using Microsoft.AspNetCore.Components.Routing
@inject SessionState Session
@inject NavigationManager Navigation
@if (!_ready)
{
<p>Loading...</p>
}
else
{
@ChildContent
}
@code {
[Parameter] public RenderFragment? ChildContent { get; set; }
private bool _ready;
// Add any routes you want public here.
// Use absolute-path form (leading slash).
private static readonly HashSet<string> _allowAnonymous = new(StringComparer.OrdinalIgnoreCase)
{
"/login",
"/login/", // optional
};
protected override async Task OnInitializedAsync()
{
Navigation.LocationChanged += OnLocationChanged;
// One-time refresh at app start
await Session.RefreshAsync();
_ready = true;
await EnforceAsync();
}
private async void OnLocationChanged(object? sender, LocationChangedEventArgs e)
{
// If your Session can change based on navigation/cookies, you *may* refresh here,
// but avoid doing it on every navigation unless necessary.
// await Session.RefreshAsync();
await EnforceAsync();
}
private Task EnforceAsync()
{
if (!_ready) return Task.CompletedTask;
var path = "/" + Navigation.ToBaseRelativePath(Navigation.Uri);
var qIndex = path.IndexOf('?', StringComparison.Ordinal);
if (qIndex >= 0) path = path[..qIndex];
// allow anonymous routes
if (_allowAnonymous.Contains(path))
return Task.CompletedTask;
if (!Session.IsAuthenticated)
{
var returnUrl = Uri.EscapeDataString(Navigation.Uri);
Navigation.NavigateTo($"/login?returnUrl={returnUrl}", forceLoad: false);
}
return Task.CompletedTask;
}
public void Dispose()
=> Navigation.LocationChanged -= OnLocationChanged;
}

View File

@@ -5,7 +5,11 @@
<div class="@GetClasses()" @onclick="@OnClickAsync"> <div class="@GetClasses()" @onclick="@OnClickAsync">
@if (Graphic != null) @if (Graphic != null)
{ {
<Icon Graphic="@Graphic.Value" Varient="@(IconVarient ?? Enums.IconVarient.None)" Color="@Color"></Icon> <Icon Graphic="@Graphic.Value"
Varient="@(IconVarient ?? Enums.IconVarient.None)"
Size="@(IconSize ?? Enums.SizeVarient.Small)"
Color="@Color">
</Icon>
} }
<span>@ChildContent</span> <span>@ChildContent</span>
</div> </div>
@@ -15,7 +19,12 @@ else
<a class="@GetClasses()" href="@Url" target="@Target"> <a class="@GetClasses()" href="@Url" target="@Target">
@if (Graphic != null) @if (Graphic != null)
{ {
<Icon Graphic="@Graphic.Value" Varient="@(IconVarient ?? Enums.IconVarient.None)" Color="@Color"></Icon> <Icon
Graphic="@Graphic.Value"
Varient="@(IconVarient ?? Enums.IconVarient.None)"
Size="@(IconSize ?? Enums.SizeVarient.Small)"
Color="@Color">
</Icon>
} }
<span>@ChildContent</span> <span>@ChildContent</span>
</a> </a>
@@ -32,6 +41,9 @@ else
[Parameter] [Parameter]
public IconVarient? IconVarient { get; set; } public IconVarient? IconVarient { get; set; }
[Parameter]
public SizeVarient? IconSize { get; set; }
[Parameter] [Parameter]
public ColorVarient Color { get; set; } = ColorVarient.Primary; public ColorVarient Color { get; set; } = ColorVarient.Primary;

View File

@@ -7,7 +7,7 @@
public Graphic Graphic { get; set; } public Graphic Graphic { get; set; }
[Parameter] [Parameter]
public SizeVarient Size { get; set; } = SizeVarient.Medium; public SizeVarient Size { get; set; } = SizeVarient.Small;
[Parameter] [Parameter]
public IconVarient Varient { get; set; } = IconVarient.None; public IconVarient Varient { get; set; } = IconVarient.None;
@@ -25,7 +25,7 @@
[ [
$"j-icon", $"j-icon",
$"j-icon-{graphic}", $"j-icon-{graphic}",
$"j-icon-size-{Size.ToString().ToLower()}", $"size-{Size.ToString().ToLower()}",
$"background-color-{Color.ToString().ToLower()}" $"background-color-{Color.ToString().ToLower()}"
]; ];

View File

@@ -42,12 +42,12 @@
<ProductTag Tag="tag"></ProductTag> <ProductTag Tag="tag"></ProductTag>
} }
</div> </div>
<div class="j-tags"> @* <div class="j-tags">
@foreach (var tag in Product.Tags) @foreach (var tag in Product.Tags)
{ {
@* <TagChip Tag="tag"></TagChip> *@ <TagChip Tag="tag"></TagChip>
} }
</div> </div> *@
</div> </div>
<div class="j-voice-work-info"> <div class="j-voice-work-info">
<div class="j-release-date-container"> <div class="j-release-date-container">

View File

@@ -15,6 +15,7 @@ public enum Graphic
Circle, Circle,
Tag, Tag,
Person, Person,
Avatar,
Sort, Sort,
Grid, Grid,
Age, Age,

View File

@@ -0,0 +1,5 @@
@inherits LayoutComponentBase
<div class="login-layout">
@Body
</div>

View File

@@ -1,27 +1,30 @@
@using JSMR.UI.Blazor.Services @using JSMR.UI.Blazor.Components
@using JSMR.UI.Blazor.Services
@inject SessionState Session @inject SessionState Session
@inject NavigationManager Navigation
@inherits LayoutComponentBase @inherits LayoutComponentBase
<div class="topbar">
@if (Session.IsAuthenticated)
{
<span>Logged in as <b>@Session.Me?.name</b> (@Session.Me?.role)</span>
<a href="" @onclick="OnLogout" style="margin-left: 12px;">Logout</a>
}
else
{
<a href="/login">Login</a>
}
</div>
<MudLayout> <MudLayout>
<MudAppBar Elevation="1" Dense="@_dense"> <MudAppBar Elevation="1" Dense="@_dense">
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="@ToggleDrawer" /> <MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="@ToggleDrawer" />
<MudText>JSMR</MudText> <MudText>JSMR</MudText>
<MudSpacer /> <MudSpacer />
<MudIconButton Icon="@Icons.Custom.Brands.GitHub" Color="Color.Inherit" Href="https://github.com/MudBlazor/MudBlazor" Target="_blank" /> @* <MudIconButton Icon="@Icons.Custom.Brands.GitHub" Color="Color.Inherit" Href="https://github.com/MudBlazor/MudBlazor" Target="_blank" /> *@
@if (Session.IsAuthenticated)
{
@* <span>Logged in as <b>@Session.Me?.Name</b> (@Session.Me?.Role)</span> *@
<Chip Graphic="Enums.Graphic.Avatar" IconVarient="Enums.IconVarient.Fill" IconSize="Enums.SizeVarient.Small" Color="Enums.ColorVarient.Blue">
<span>@Session.Me?.Name @* (@Session.Me?.Role) *@</span>
</Chip>
<Chip Graphic="Enums.Graphic.Headphones" Color="Enums.ColorVarient.Primary" Click="@LogoutAsync">Logout</Chip>
}
else
{
<Chip Graphic="Enums.Graphic.Headphones" Color="Enums.ColorVarient.Primary" Url="/login">Login</Chip>
}
</MudAppBar> </MudAppBar>
<MudDrawer @bind-Open="@_open" ClipMode="_clipMode" Breakpoint="@_breakpoint" Elevation="1" Variant="@DrawerVariant.Mini"> <MudDrawer @bind-Open="@_open" ClipMode="_clipMode" Breakpoint="@_breakpoint" Elevation="1" Variant="@DrawerVariant.Mini">
<MudNavMenu> <MudNavMenu>
@@ -89,4 +92,10 @@
{ {
Session.Changed -= OnSessionChanged; Session.Changed -= OnSessionChanged;
} }
private async Task LogoutAsync()
{
await Session.LogoutAsync();
Navigation.NavigateTo("/login");
}
} }

View File

@@ -1,4 +1,5 @@
@page "/login" @page "/login"
@layout LoginLayout
@using JSMR.UI.Blazor.Services @using JSMR.UI.Blazor.Services
@@ -9,29 +10,23 @@
@if (Session.IsAuthenticated) @if (Session.IsAuthenticated)
{ {
<p>You're already logged in as <b>@Session.Me?.name</b>.</p> <p>You're already logged in as <b>@Session.Me?.Name</b>.</p>
<button @onclick="Logout">Logout</button> <button @onclick="Logout">Logout</button>
} }
else else
{ {
<div style="max-width: 360px;"> <div style="max-width: 360px;">
<div> <BitCard>
<label>Username</label><br /> <BitStack>
<input @bind="username" /> <BitTextField Label="Username" @bind-Value="username"></BitTextField>
</div> <BitTextField Label="Password" @bind-Value="password" Type="BitInputType.Password"></BitTextField>
<div style="margin-top: 8px;"> <BitButton OnClick="LoginAsync" IsEnabled="@(!busy)">Login</BitButton>
<label>Password</label><br />
<input type="password" @bind="password" />
</div>
<div style="margin-top: 12px;">
<button @onclick="LoginAsync" disabled="@busy">Login</button>
</div>
@if (!string.IsNullOrWhiteSpace(error)) @if (!string.IsNullOrWhiteSpace(error))
{ {
<p style="color: crimson; margin-top: 8px;">@error</p> <p style="color: crimson; margin-top: 8px;">@error</p>
} }
</BitStack>
</BitCard>
</div> </div>
} }

View File

@@ -12,7 +12,6 @@
@inherits SearchPageBase<VoiceWorkFilterState, VoiceWorkSearchResult> @inherits SearchPageBase<VoiceWorkFilterState, VoiceWorkSearchResult>
<RequireAuthentication>
<PageTitle>Voice Works</PageTitle> <PageTitle>Voice Works</PageTitle>
<h3>Voice Works</h3> <h3>Voice Works</h3>
@@ -42,7 +41,6 @@
</RightContent> </RightContent>
</JPagination> </JPagination>
} }
</RequireAuthentication>
@code { @code {
[Inject] [Inject]

View File

@@ -25,5 +25,5 @@ public class AuthenticationClient(HttpClient http)
return await resp.Content.ReadFromJsonAsync<MeResponse>(cancellationToken: ct); return await resp.Content.ReadFromJsonAsync<MeResponse>(cancellationToken: ct);
} }
public sealed record MeResponse(string? name, string? id, string? role); public sealed record MeResponse(string? Name, string? Id, string? Role);
} }

View File

@@ -659,6 +659,12 @@ code {
.j-chip.is-clickable { .j-chip.is-clickable {
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
--chip-hover-alpha: 0.2;
transition: .2s linear;
}
.j-chip.is-clickable:hover {
background: rgb(var(--chip-rgb) / var(--chip-hover-alpha));
} }
.j-chip.varient-filled { .j-chip.varient-filled {
@@ -703,6 +709,21 @@ code {
width: 16px; width: 16px;
} }
.j-icon.size-small {
width: 1rem;
height: 1rem;
}
.j-icon.size-medium {
width: 1.5rem;
height: 1.5rem;
}
.j-icon.size-large {
width: 2rem;
height: 2rem;
}
.j-icon-color-yellow { .j-icon-color-yellow {
background: #ffe073; background: #ffe073;
} }
@@ -784,6 +805,14 @@ code {
mask-image: url("../svg/person-fill.svg"); mask-image: url("../svg/person-fill.svg");
} }
.j-icon-avatar {
mask-image: url("../svg/person-circle.svg");
}
.j-icon-avatar-fill {
mask-image: url("../svg/person-circle.svg");
}
.j-icon-sort { .j-icon-sort {
mask-image: url("../svg/sort.svg"); mask-image: url("../svg/sort.svg");
} }

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-person-circle" viewBox="0 0 16 16">
<path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0"/>
<path fill-rule="evenodd" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8m8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1"/>
</svg>

After

Width:  |  Height:  |  Size: 342 B