Updated front-end authentication.
This commit is contained in:
@@ -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,22 +7,24 @@
|
|||||||
<RadzenTheme Theme="material-dark" />
|
<RadzenTheme Theme="material-dark" />
|
||||||
</HeadContent>
|
</HeadContent>
|
||||||
|
|
||||||
<Router AppAssembly="@typeof(App).Assembly">
|
<AuthenticationGate>
|
||||||
<Found Context="routeData">
|
<Router AppAssembly="@typeof(App).Assembly">
|
||||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
<Found Context="routeData">
|
||||||
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||||
</Found>
|
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
||||||
<NotFound>
|
</Found>
|
||||||
<PageTitle>Not found</PageTitle>
|
<NotFound>
|
||||||
<LayoutView Layout="@typeof(MainLayout)">
|
<PageTitle>Not found</PageTitle>
|
||||||
<p role="alert">Sorry, there's nothing at this address.</p>
|
<LayoutView Layout="@typeof(MainLayout)">
|
||||||
</LayoutView>
|
<p role="alert">Sorry, there's nothing at this address.</p>
|
||||||
</NotFound>
|
</LayoutView>
|
||||||
</Router>
|
</NotFound>
|
||||||
|
</Router>
|
||||||
|
</AuthenticationGate>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
await Session.RefreshAsync();
|
//await Session.RefreshAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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()}"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ public enum Graphic
|
|||||||
Circle,
|
Circle,
|
||||||
Tag,
|
Tag,
|
||||||
Person,
|
Person,
|
||||||
|
Avatar,
|
||||||
Sort,
|
Sort,
|
||||||
Grid,
|
Grid,
|
||||||
Age,
|
Age,
|
||||||
|
|||||||
5
JSMR.UI.Blazor/Layout/LoginLayout.razor
Normal file
5
JSMR.UI.Blazor/Layout/LoginLayout.razor
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
|
<div class="login-layout">
|
||||||
|
@Body
|
||||||
|
</div>
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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 />
|
@if (!string.IsNullOrWhiteSpace(error))
|
||||||
<input type="password" @bind="password" />
|
{
|
||||||
</div>
|
<p style="color: crimson; margin-top: 8px;">@error</p>
|
||||||
|
}
|
||||||
<div style="margin-top: 12px;">
|
</BitStack>
|
||||||
<button @onclick="LoginAsync" disabled="@busy">Login</button>
|
</BitCard>
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(error))
|
|
||||||
{
|
|
||||||
<p style="color: crimson; margin-top: 8px;">@error</p>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -659,8 +659,14 @@ 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 {
|
||||||
padding: .4rem .8rem;
|
padding: .4rem .8rem;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
4
JSMR.UI.Blazor/wwwroot/svg/person-circle.svg
Normal file
4
JSMR.UI.Blazor/wwwroot/svg/person-circle.svg
Normal 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 |
Reference in New Issue
Block a user