Added authenication/authorization. Refactored api startup.
Some checks failed
ci / build-test (push) Has been cancelled
ci / publish-image (push) Has been cancelled

This commit is contained in:
2026-02-16 00:20:02 -05:00
parent a85989a337
commit 9f30ef446a
25 changed files with 685 additions and 154 deletions

View File

@@ -0,0 +1,29 @@
using System.Net;
using System.Net.Http.Json;
namespace JSMR.UI.Blazor.Services;
public class AuthenticationClient(HttpClient http)
{
public async Task<bool> LoginAsync(string username, string password, CancellationToken ct = default)
{
var resp = await http.PostAsJsonAsync("/auth/login", new { username, password }, ct);
return resp.IsSuccessStatusCode;
}
public async Task LogoutAsync(CancellationToken ct = default)
=> await http.PostAsync("/auth/logout", content: null, ct);
public async Task<MeResponse?> GetMeAsync(CancellationToken ct = default)
{
var resp = await http.GetAsync("/api/me", ct);
if (resp.StatusCode == HttpStatusCode.Unauthorized)
return null;
resp.EnsureSuccessStatusCode();
return await resp.Content.ReadFromJsonAsync<MeResponse>(cancellationToken: ct);
}
public sealed record MeResponse(string? name, string? id, string? role);
}

View File

@@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Components.WebAssembly.Http;
namespace JSMR.UI.Blazor.Services;
public sealed class IncludeRequestCredentialsMessageHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// Tells browser fetch() to send cookies/auth headers on cross-origin requests
request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);
return base.SendAsync(request, cancellationToken);
}
}

View File

@@ -0,0 +1,21 @@
using System.Net.Http.Headers;
namespace JSMR.UI.Blazor.Services;
public sealed class JwtAuthorizationMessageHandler(TokenStore tokens) : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken ct)
{
if (!string.IsNullOrWhiteSpace(tokens.AccessToken))
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);
}
return base.SendAsync(request, ct);
}
}
public sealed class TokenStore
{
public string? AccessToken { get; set; }
}

View File

@@ -0,0 +1,28 @@
namespace JSMR.UI.Blazor.Services;
public sealed class SessionState(AuthenticationClient auth)
{
public AuthenticationClient.MeResponse? Me { get; private set; }
public bool IsAuthenticated => Me is not null;
public event Action? Changed;
public async Task RefreshAsync(CancellationToken ct = default)
{
Me = await auth.GetMeAsync(ct);
Changed?.Invoke();
}
public async Task<bool> LoginAsync(string username, string password, CancellationToken ct = default)
{
var ok = await auth.LoginAsync(username, password, ct);
await RefreshAsync(ct);
return ok;
}
public async Task LogoutAsync(CancellationToken ct = default)
{
await auth.LogoutAsync(ct);
await RefreshAsync(ct);
}
}