Files
decePubClient/Services/TokenAuthStateProvider.cs
T
thepraandClaude Opus 5.5 fd2f2e7c65
Build / Build (push) Successful in 51s
R1 cleanup: collAnon and dead code gone, no IndexedDB, honest PWA files
Starts the completion program (SocialPub docs/ROADMAP.md, "Owner decisions on finishing everything", 2026-10-17).

- The localizer's namespace is decePubClient.Services; collAnon is gone from the repo.
- Dead code: PagesBase, VConstants, SettingsOptions, ViewLanguage, unused APIs routes, InstanceService.Self,
  RequestStats.LastOf/AverageMs, PostAnon/DeleteTotallyAnon, unused AppStatusService methods and their main.js helpers
  (the app badge stays for Web Push), ExtensionMethods/SUtility leftovers (Markdig and BlazorZXingJs packages go),
  unused PublicCacheData/PageSettings/AppConfiguration fields, commented-out code.
- The client log in IndexedDB, which nothing ever read, goes with the whole IndexedDB layer (DnetIndexedDb, IStorage):
  logs go to the browser console; main.js deletes the old "data" database once.
- The solution names the real ClientModels project. Unused vendor assets (fontawesome, solid, open-iconic, rxjs) go.
- robots.txt disallows everything; manifest.json and index.html say decePub; the service worker's cache name follows
  each publish's assets version, so old caches are purged.
- MessageType is Visibility (member names kept: they are resx keys); ExpandMessage.razor is Thread.razor (same route).
- i18n: dead keys removed, Italian "camera" is "punto di vista", DInputBase/DCalendar validation messages come from
  ErrorMessages, DataTransfer's spinner has its label.
- CLAUDE.md follows: client storage, JS interop, PWA, the D fields' messages.

Verified: dotnet build decePubClient.csproj and decePubClient.sln -c Release (0 errors, no new warnings); a Pasture
publish served by the pasture's decepub peer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
2026-10-17 02:56:57 +02:00

127 lines
4.6 KiB
C#

using Blazored.LocalStorage;
using decePubClient.Models;
using Microsoft.AspNetCore.Components.Authorization;
using PrivaPub.ClientModels;
using PrivaPub.ClientModels.User;
using System.Security.Claims;
namespace decePubClient.Services
{
/// <summary>
/// The session (AuthData in localStorage, cached in memory) and the authentication state built from it. It only
/// stores and hands out tokens; signing in, exchanging and revoking them is AuthService's job.
/// </summary>
public class TokenAuthStateProvider(ILocalStorageService storage, ILogger<TokenAuthStateProvider> logger)
: AuthenticationStateProvider
{
/// <summary>The claim naming the persona the client acts as (an avatar id).</summary>
public const string PersonaClaim = "persona";
AuthData authData;
bool loaded;
/// <summary>The stored session, or an empty one.</summary>
public async Task<AuthData> GetAuthData(CancellationToken cancellationToken = default)
{
if (loaded)
return authData ?? new();
authData = await storage.GetItemAsync<AuthData>(nameof(AuthData), cancellationToken);
loaded = true;
return authData ?? new();
}
/// <summary>The root's JWT while it is still valid, otherwise null.</summary>
public async Task<string> GetToken(CancellationToken cancellationToken = default)
{
var session = await GetAuthData(cancellationToken);
if (string.IsNullOrEmpty(session.Token) || session.TokenExpiration is not { } expiration || expiration <= DateTime.UtcNow.Ticks)
return null;
return session.Token;
}
/// <summary>The current persona's Mastodon token, or null when the persona has none yet.</summary>
public async Task<string> GetPersonaToken(CancellationToken cancellationToken = default)
{
var session = await GetAuthData(cancellationToken);
if (session.PersonaId is null)
return null;
return session.PersonaTokens.GetValueOrDefault(session.PersonaId);
}
public async Task<bool> IsAuthenticatedAsync(CancellationToken cancellationToken = default) =>
await GetToken(cancellationToken) is not null;
/// <summary>Starts or renews the session with a JWT from login or sniff/again; persona tokens are kept.</summary>
public async Task SetSession(JwtUser jwtUser, CancellationToken cancellationToken = default)
{
var session = await GetAuthData(cancellationToken);
session.Token = jwtUser.Token;
session.TokenExpiration = jwtUser.Expiration;
session.TokenIssuedAt = DateTime.UtcNow.Ticks;
session.UserName = jwtUser.Username;
session.Policies = jwtUser.Policies ?? [];
if (jwtUser.UserSettings?.LanguageCode is { Length: > 0 } language)
session.CurrentLanguageCode = language;
await Save(session, cancellationToken);
}
/// <summary>Makes the persona current, remembering its token when one is given.</summary>
public async Task SetPersona(string personaId, string token = default, CancellationToken cancellationToken = default)
{
var session = await GetAuthData(cancellationToken);
session.PersonaId = personaId;
if (token is not null)
session.PersonaTokens[personaId] = token;
await Save(session, cancellationToken);
}
/// <summary>Drops a persona's token that PrivaPub no longer accepts.</summary>
public async Task ForgetPersonaToken(string personaId, CancellationToken cancellationToken = default)
{
var session = await GetAuthData(cancellationToken);
if (!session.PersonaTokens.Remove(personaId))
return;
await Save(session, cancellationToken);
}
public async Task LogoutAsync(CancellationToken cancellationToken = default)
{
logger.LogInformation($"set null({nameof(LogoutAsync)})");
authData = null;
loaded = true;
await storage.RemoveItemAsync(nameof(AuthData), cancellationToken);
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var token = await GetToken();
if (token is null)
return new(new());
var session = await GetAuthData();
var claims = new List<Claim> { new(ClaimTypes.Name, session.UserName ?? string.Empty) };
foreach (var policy in new[] { Policies.IsUser, Policies.IsModerator, Policies.IsAdmin })
claims.Add(new(policy, session.Policies.Contains(policy) ? "true" : "false"));
if (session.PersonaId is not null)
claims.Add(new(PersonaClaim, session.PersonaId));
return new(new(new ClaimsIdentity(claims, "jwt")));
}
async Task Save(AuthData session, CancellationToken cancellationToken)
{
authData = session;
loaded = true;
await storage.SetItemAsync(nameof(AuthData), session, cancellationToken);
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
}
}