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
@@ -6,6 +7,7 @@
<RadzenTheme Theme="material-dark" />
</HeadContent>
<AuthenticationGate>
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
@@ -18,10 +20,11 @@
</LayoutView>
</NotFound>
</Router>
</AuthenticationGate>
@code {
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">
@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>
</div>
@@ -15,7 +19,12 @@ else
<a class="@GetClasses()" href="@Url" target="@Target">
@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>
</a>
@@ -32,6 +41,9 @@ else
[Parameter]
public IconVarient? IconVarient { get; set; }
[Parameter]
public SizeVarient? IconSize { get; set; }
[Parameter]
public ColorVarient Color { get; set; } = ColorVarient.Primary;

View File

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

View File

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

View File

@@ -15,6 +15,7 @@ public enum Graphic
Circle,
Tag,
Person,
Avatar,
Sort,
Grid,
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 NavigationManager Navigation
@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>
<MudAppBar Elevation="1" Dense="@_dense">
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="@ToggleDrawer" />
<MudText>JSMR</MudText>
<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>
<MudDrawer @bind-Open="@_open" ClipMode="_clipMode" Breakpoint="@_breakpoint" Elevation="1" Variant="@DrawerVariant.Mini">
<MudNavMenu>
@@ -89,4 +92,10 @@
{
Session.Changed -= OnSessionChanged;
}
private async Task LogoutAsync()
{
await Session.LogoutAsync();
Navigation.NavigateTo("/login");
}
}

View File

@@ -1,4 +1,5 @@
@page "/login"
@layout LoginLayout
@using JSMR.UI.Blazor.Services
@@ -9,29 +10,23 @@
@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>
}
else
{
<div style="max-width: 360px;">
<div>
<label>Username</label><br />
<input @bind="username" />
</div>
<div style="margin-top: 8px;">
<label>Password</label><br />
<input type="password" @bind="password" />
</div>
<div style="margin-top: 12px;">
<button @onclick="LoginAsync" disabled="@busy">Login</button>
</div>
<BitCard>
<BitStack>
<BitTextField Label="Username" @bind-Value="username"></BitTextField>
<BitTextField Label="Password" @bind-Value="password" Type="BitInputType.Password"></BitTextField>
<BitButton OnClick="LoginAsync" IsEnabled="@(!busy)">Login</BitButton>
@if (!string.IsNullOrWhiteSpace(error))
{
<p style="color: crimson; margin-top: 8px;">@error</p>
}
</BitStack>
</BitCard>
</div>
}

View File

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

View File

@@ -25,5 +25,5 @@ public class AuthenticationClient(HttpClient http)
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 {
cursor: pointer;
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 {
@@ -703,6 +709,21 @@ code {
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 {
background: #ffe073;
}
@@ -784,6 +805,14 @@ code {
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 {
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