R6 moderation in decePub: users and personas, accounts, report actions, deletions, suspended personas
Administration's Users & personas (administrators): logins searched and opened into their personas and groups; roles, bans, two-factor reset and deletion of a login; silence, suspension and deletion of a persona; a group deleted. Accounts (moderators): local personas and remote accounts searched or listed as moderated, silenced and suspended with a staff note and a warning. Reports resolve with an action. ActDialog asks for the note, the warning or the password and is the act's confirmation. A login deletes its own persona and the groups it owns with its password. Suspended personas can't be chosen; one refused a token mid-session moves the session to another, or to none, once. Warnings show in notifications, limited accounts on their profile, and moderators remove a post from its card. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
This commit is contained in:
1 parent
4c88980374
commit
c572471b88
30 files changed
+2604
-67
No files matched your search
@@ -9,11 +9,12 @@ using PrivaPub.ClientModels.Admin;
|
||||
namespace decePubClient.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// What PrivaPub's moderators and administrators do as the root: reports and the staff's log (/clientapi/moderator),
|
||||
/// What PrivaPub's moderators and administrators read as the root: reports and the staff's log (/clientapi/moderator),
|
||||
/// servers blocked for the whole instance, the federation statistics (the overview, the servers, one server's days and
|
||||
/// events, the ledger's events, the server's own days) and the backups (/clientapi/admin). The server's own settings,
|
||||
/// announcements and relays are InstanceAdminService's. PrivaPub checks the root's role on every call. Lists come back
|
||||
/// empty and values null on failure; nothing throws.
|
||||
/// announcements and relays are InstanceAdminService's; resolving reports and every act on accounts, posts and logins
|
||||
/// are ModerationService's. PrivaPub checks the root's role on every call. Lists come back empty and values null on
|
||||
/// failure; nothing throws.
|
||||
/// </summary>
|
||||
public class AdminService(IHttpService httpService, ILoggingService logger, CoalescingStringLocalizer localizer)
|
||||
{
|
||||
@@ -29,12 +30,6 @@ namespace decePubClient.Services
|
||||
return [];
|
||||
}
|
||||
|
||||
public async Task<WebResult> Resolve(string reportId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var response = await httpService.Post(APIs.ClientApi.POST_M_ResolveReport(reportId), cancellationToken: cancellationToken);
|
||||
return response.IsSuccessStatusCode ? new WebResult() : await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>One page of the staff's log, newest first: those before `before` (the previous page's Next) when given.
|
||||
/// Administrators read all of it with login names; a moderator reads what isn't about a login. Null on failure.</summary>
|
||||
public async Task<ViewAdminActionsPage> Log(string before, int limit, CancellationToken cancellationToken = default)
|
||||
|
||||
@@ -106,6 +106,21 @@ namespace decePubClient.Services
|
||||
return new WebResult { Data = await response.DefaultReadFromJsonAsync<ViewAvatar>(cancellationToken) };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes one of the root's personas, the root's password given (a wrong one is PrivaPub's 403): everything public it
|
||||
/// had goes, here and, as PrivaPub tells them, on the servers that hold it, and the groups it owns with it. Its name
|
||||
/// stays taken. A suspended persona may be deleted too. The caller drops it from the session (CascadingState.PersonaDeleted).
|
||||
/// </summary>
|
||||
public async Task<WebResult> DeletePersona(string personaId, string password, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var response = await httpService.Post(APIs.ClientApi.POST_DeletePersona, new DeletePersonaForm { AvatarId = personaId, Password = password },
|
||||
cancellationToken: cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return await ReadPasswordFailure(response, cancellationToken);
|
||||
|
||||
return new WebResult();
|
||||
}
|
||||
|
||||
/// <summary>Acts as the persona from now on, exchanging a token for it unless it has one.</summary>
|
||||
public async Task<WebResult> UsePersona(string personaId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Net;
|
||||
|
||||
using decePubClient.Extensions;
|
||||
using decePubClient.Helpers;
|
||||
using decePubClient.Models;
|
||||
|
||||
using PrivaPub.ClientModels;
|
||||
@@ -59,6 +60,24 @@ namespace decePubClient.Services
|
||||
public Task<WebResult> Remove(string personaId, string groupId, string actorUri, CancellationToken cancellationToken = default) =>
|
||||
Membership(APIs.ClientApi.POST_RemoveGroupMember, personaId, groupId, actorUri, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a group the persona owns, the login's password given: its members and followers are told, its name stays
|
||||
/// taken. PrivaPub answers a wrong password, or a persona that isn't the owner, with 403.
|
||||
/// </summary>
|
||||
public async Task<WebResult> Delete(string personaId, string groupId, string password, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var response = await httpService.Post(APIs.ClientApi.POST_DeleteGroup,
|
||||
new DeleteGroupForm { AvatarId = personaId, GroupId = groupId, Password = password }, cancellationToken: cancellationToken);
|
||||
if (response.IsSuccessStatusCode)
|
||||
return new WebResult();
|
||||
|
||||
// the owner is the only one offered it, so PrivaPub's own 403 is the password's
|
||||
if (response.StatusCode is HttpStatusCode.Forbidden && !response.Content.Headers.Contains(SUtility.FailureHeader))
|
||||
return new WebResult().Invalidate(localizer["Wrong password."], (int)HttpStatusCode.Forbidden);
|
||||
|
||||
return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>Who is in a group the persona runs, and who asks to join; empty on failure.</summary>
|
||||
public async Task<List<ViewGroupMember>> Members(string personaId, string groupId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
||||
@@ -239,12 +239,23 @@ namespace decePubClient.Services
|
||||
/// <summary>
|
||||
/// A Mastodon API request as the current persona. A missing token is exchanged first; a refused one (revoked,
|
||||
/// or PrivaPub's database was reset) is exchanged once more and the request sent again. Without a usable JWT the
|
||||
/// session ends.
|
||||
/// session ends. A persona PrivaPub refuses a token to while the JWT is good (suspended by the staff, or gone) is
|
||||
/// marked refused (TokenAuthStateProvider.Refuse, which tells CascadingState once) and no request acts as it again.
|
||||
/// </summary>
|
||||
async Task<HttpResponseMessage> SendAsPersona(HttpRequestMessage request, Func<HttpRequestMessage> requestFactory, CancellationToken cancellationToken)
|
||||
{
|
||||
var session = await authStateProvider.GetAuthData(cancellationToken);
|
||||
var token = await authStateProvider.GetPersonaToken(cancellationToken) ?? await Exchange(session.PersonaId, cancellationToken);
|
||||
if (authStateProvider.IsRefused(session.PersonaId))
|
||||
return PersonaRefused();
|
||||
|
||||
var token = await authStateProvider.GetPersonaToken(cancellationToken);
|
||||
if (token is null)
|
||||
{
|
||||
var exchanged = await Exchange(session.PersonaId, cancellationToken);
|
||||
if (exchanged.Refused)
|
||||
return PersonaRefused(session.PersonaId);
|
||||
token = exchanged.Token;
|
||||
}
|
||||
if (token is null)
|
||||
return await EndSession(cancellationToken);
|
||||
|
||||
@@ -255,7 +266,10 @@ namespace decePubClient.Services
|
||||
|
||||
response.Dispose();
|
||||
await authStateProvider.ForgetPersonaToken(session.PersonaId, cancellationToken);
|
||||
token = await Exchange(session.PersonaId, cancellationToken);
|
||||
var renewed = await Exchange(session.PersonaId, cancellationToken);
|
||||
if (renewed.Refused)
|
||||
return PersonaRefused(session.PersonaId);
|
||||
token = renewed.Token;
|
||||
if (token is null)
|
||||
return await EndSession(cancellationToken);
|
||||
|
||||
@@ -269,12 +283,29 @@ namespace decePubClient.Services
|
||||
return await EndSession(cancellationToken);
|
||||
}
|
||||
|
||||
async Task<string> Exchange(string personaId, CancellationToken cancellationToken)
|
||||
/// <summary>A new token for the persona, or none; Refused when PrivaPub itself answered no (its 400 invalid_grant or a
|
||||
/// 403) to a JWT the client still holds as good, rather than the request failing on the way.</summary>
|
||||
async Task<(string Token, bool Refused)> Exchange(string personaId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (personaId is null)
|
||||
return null;
|
||||
return (null, false);
|
||||
using var response = await ExchangePersonaToken(personaId, cancellationToken);
|
||||
return response.IsSuccessStatusCode ? await authStateProvider.GetPersonaToken(cancellationToken) : null;
|
||||
if (response.IsSuccessStatusCode)
|
||||
return (await authStateProvider.GetPersonaToken(cancellationToken), false);
|
||||
|
||||
var refused = response.StatusCode is HttpStatusCode.BadRequest or HttpStatusCode.Forbidden
|
||||
&& !response.Content.Headers.Contains(SUtility.FailureHeader)
|
||||
&& await authStateProvider.GetToken(cancellationToken) is not null;
|
||||
return (null, refused);
|
||||
}
|
||||
|
||||
/// <summary>The answer for a request as a persona PrivaPub refuses, marking it so (once).</summary>
|
||||
HttpResponseMessage PersonaRefused(string personaId = default)
|
||||
{
|
||||
if (personaId is not null)
|
||||
authStateProvider.Refuse(personaId);
|
||||
return SUtility.ClassifiedFailure(FailureCodes.PersonaRefused,
|
||||
localizer["This persona can't act now: the server's staff may have suspended it."], HttpStatusCode.Forbidden);
|
||||
}
|
||||
|
||||
async Task<HttpResponseMessage> EndSession(CancellationToken cancellationToken)
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
using System.Net;
|
||||
|
||||
using decePubClient.Extensions;
|
||||
using decePubClient.Helpers;
|
||||
using decePubClient.Models;
|
||||
|
||||
using PrivaPub.ClientModels;
|
||||
using PrivaPub.ClientModels.Admin;
|
||||
using PrivaPub.ClientModels.User;
|
||||
|
||||
namespace decePubClient.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// The staff's acts on accounts, posts and logins (PrivaPub's completion program R6). Moderators (/clientapi/moderator)
|
||||
/// find accounts here and elsewhere, read the ones the staff moderated, silence and suspend them and undo it, remove
|
||||
/// posts and resolve reports with one of those acts; a Note is the staff's own (kept in the log and on the account), a
|
||||
/// Warning is what a persona here is told (Mastodon's moderation_warning; an account elsewhere is told nothing).
|
||||
/// Administrators (/clientapi/admin) also read the logins with their personas and the groups those own, change a login's
|
||||
/// role, ban, unban and delete it, turn its two-factor sign-in off, and delete a persona or a group with their own
|
||||
/// password. PrivaPub checks the role on every call. Lists and pages come back null on failure; nothing throws.
|
||||
/// </summary>
|
||||
public class ModerationService(IHttpService httpService, ILoggingService logger, CoalescingStringLocalizer localizer)
|
||||
{
|
||||
/// <summary>Accounts found by a name, user@host or an actor's address: personas here (never their logins) and the
|
||||
/// accounts elsewhere PrivaPub holds. Null on failure.</summary>
|
||||
public async Task<ViewModeratedAccountsPage> SearchAccounts(string query, int limit, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var response = await httpService.Get(APIs.ClientApi.GET_ModeratorAccounts, [$"q={Uri.EscapeDataString(query ?? "")}", $"limit={limit}"],
|
||||
cancellationToken: cancellationToken);
|
||||
if (response.IsSuccessStatusCode)
|
||||
return await response.DefaultReadFromJsonAsync<ViewModeratedAccountsPage>(cancellationToken);
|
||||
|
||||
await LogFailure(response, nameof(SearchAccounts), cancellationToken);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>What the staff silenced or suspended, here and elsewhere, most recently first; Next is the next page's
|
||||
/// offset, null on the last. Null on failure.</summary>
|
||||
public async Task<ViewModeratedAccountsPage> ModeratedAccounts(int offset, int limit, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var response = await httpService.Get(APIs.ClientApi.GET_ModeratedAccounts, [$"offset={offset}", $"limit={limit}"],
|
||||
cancellationToken: cancellationToken);
|
||||
if (response.IsSuccessStatusCode)
|
||||
return await response.DefaultReadFromJsonAsync<ViewModeratedAccountsPage>(cancellationToken);
|
||||
|
||||
await LogFailure(response, nameof(ModeratedAccounts), cancellationToken);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Silences, unsilences, suspends or unsuspends a persona here or an account elsewhere (ModerationActions);
|
||||
/// Data is the account as moderators see it now (ViewModeratedAccount).</summary>
|
||||
public async Task<WebResult> Moderate(ModerateAccountForm form, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var response = await httpService.Post(APIs.ClientApi.POST_ModerateAccount, form, cancellationToken: cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
||||
|
||||
return new WebResult { Data = await response.DefaultReadFromJsonAsync<ViewModeratedAccount>(cancellationToken) };
|
||||
}
|
||||
|
||||
/// <summary>Takes a post down: a post here is deleted as its author would delete it (and its author warned), a copy of
|
||||
/// one from elsewhere is hidden here only.</summary>
|
||||
public async Task<WebResult> RemovePost(RemovePostForm form, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var response = await httpService.Post(APIs.ClientApi.POST_ModeratorRemovePost, form, cancellationToken: cancellationToken);
|
||||
return response.IsSuccessStatusCode ? new WebResult() : await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>Resolves a report, and with it what the staff does (none, silence, suspend, remove_posts). The act comes
|
||||
/// first, so one that fails leaves the report open; remove_posts on a report naming no post is a 422.</summary>
|
||||
public async Task<WebResult> ResolveReport(string reportId, ResolveReportForm form, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var response = await httpService.Post(APIs.ClientApi.POST_M_ResolveReport(reportId), form, cancellationToken: cancellationToken);
|
||||
return response.IsSuccessStatusCode ? new WebResult() : await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>The logins, newest first, each with its personas and the groups they own; query finds a login by its name
|
||||
/// or by one of its personas' names, before is the previous page's Next. Null on failure.</summary>
|
||||
public async Task<ViewAdminRootsPage> Roots(string query, string before, int limit, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var parameters = new List<string> { $"limit={limit}" };
|
||||
if (query is { Length: > 0 })
|
||||
parameters.Add($"q={Uri.EscapeDataString(query)}");
|
||||
if (before is { Length: > 0 })
|
||||
parameters.Add($"before={Uri.EscapeDataString(before)}");
|
||||
using var response = await httpService.Get(APIs.ClientApi.GET_AdminRoots, [.. parameters], cancellationToken: cancellationToken);
|
||||
if (response.IsSuccessStatusCode)
|
||||
return await response.DefaultReadFromJsonAsync<ViewAdminRootsPage>(cancellationToken);
|
||||
|
||||
await LogFailure(response, nameof(Roots), cancellationToken);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>One login as it is now; null when it is gone (deleted) or can't be read.</summary>
|
||||
public async Task<ViewAdminRoot> Root(string rootId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var response = await httpService.Get(APIs.ClientApi.GET_M_AdminRoot(rootId), cancellationToken: cancellationToken);
|
||||
if (response.IsSuccessStatusCode)
|
||||
return await response.DefaultReadFromJsonAsync<ViewAdminRoot>(cancellationToken);
|
||||
|
||||
if (response.StatusCode is not HttpStatusCode.NotFound)
|
||||
await LogFailure(response, nameof(Root), cancellationToken);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Makes a login a user, a moderator or an administrator; never one's own nor the last administrator's (409).
|
||||
/// Data is the login as it is now (ViewAdminRoot).</summary>
|
||||
public async Task<WebResult> SetRole(string rootId, string role, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var response = await httpService.Post(APIs.ClientApi.POST_M_AdminRootRole(rootId), new RootRoleForm { Role = role },
|
||||
cancellationToken: cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
||||
|
||||
return new WebResult { Data = await response.DefaultReadFromJsonAsync<ViewAdminRoot>(cancellationToken) };
|
||||
}
|
||||
|
||||
/// <summary>Turns another login's two-factor sign-in off (it lost its authenticator and its recovery codes); its
|
||||
/// password and sessions stay.</summary>
|
||||
public async Task<WebResult> ResetTotp(string rootId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var response = await httpService.Post(APIs.ClientApi.POST_M_AdminRootTotpReset(rootId), cancellationToken: cancellationToken);
|
||||
return response.IsSuccessStatusCode ? new WebResult() : await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>Bans a login: its sessions end and its personas are held back as suspended ones, without a word written on
|
||||
/// any of them.</summary>
|
||||
public async Task<WebResult> Ban(string rootId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var response = await httpService.Post(APIs.ClientApi.POST_AdminBanRoots, new UsersIds { UserIdList = [rootId] },
|
||||
cancellationToken: cancellationToken);
|
||||
return response.IsSuccessStatusCode ? new WebResult() : await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>Lifts a login's ban: its personas are given back as they were.</summary>
|
||||
public async Task<WebResult> Unban(string rootId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var response = await httpService.Post(APIs.ClientApi.POST_AdminUnbanRoots, new UsersIds { UserIdList = [rootId] },
|
||||
cancellationToken: cancellationToken);
|
||||
return response.IsSuccessStatusCode ? new WebResult() : await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>Deletes a login, every persona of it with everything public they had; its names stay taken.</summary>
|
||||
public async Task<WebResult> DeleteRoot(string rootId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var response = await httpService.Delete(APIs.ClientApi.DELETE_AdminRemoveRoots, payload: new UsersIds { UserIdList = [rootId] },
|
||||
cancellationToken: cancellationToken);
|
||||
return response.IsSuccessStatusCode ? new WebResult() : await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>Deletes a persona, the groups it owns with it, the administrator's own password given (a wrong one is a
|
||||
/// 403); the note is the staff's.</summary>
|
||||
public async Task<WebResult> DeletePersona(string personaId, AdminPasswordForm form, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var response = await httpService.Post(APIs.ClientApi.POST_M_AdminPersonaDelete(personaId), form, cancellationToken: cancellationToken);
|
||||
return response.IsSuccessStatusCode ? new WebResult() : await ReadPasswordFailure(response, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>Deletes a group, the administrator's own password given (a wrong one is a 403): its members are told, its
|
||||
/// name stays taken.</summary>
|
||||
public async Task<WebResult> DeleteGroup(string groupId, AdminPasswordForm form, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var response = await httpService.Post(APIs.ClientApi.POST_M_AdminGroupDelete(groupId), form, cancellationToken: cancellationToken);
|
||||
return response.IsSuccessStatusCode ? new WebResult() : await ReadPasswordFailure(response, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>PrivaPub refuses a wrong password with its own 403, which ReadWebResult would take for a missing
|
||||
/// permission.</summary>
|
||||
async Task<WebResult> ReadPasswordFailure(HttpResponseMessage response, CancellationToken cancellationToken)
|
||||
{
|
||||
if (response.StatusCode is not HttpStatusCode.Forbidden || response.Content.Headers.Contains(SUtility.FailureHeader))
|
||||
return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
||||
|
||||
return new WebResult().Invalidate(localizer["Wrong password."], (int)HttpStatusCode.Forbidden);
|
||||
}
|
||||
|
||||
async Task LogFailure(HttpResponseMessage response, string caller, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
||||
if (result.StatusCode is not ((int)HttpStatusCode.Gone or (int)HttpStatusCode.Forbidden))
|
||||
await logger.ProcessWarning(result.ToString(localizer), $"{nameof(ModerationService)}.{caller}", cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,12 @@ namespace decePubClient.Services
|
||||
|
||||
AuthData authData;
|
||||
bool loaded;
|
||||
/// <summary>The personas PrivaPub refused a token to while the JWT was good (suspended, or gone): no request acts as
|
||||
/// them again until the reader picks one anew or the session is read again.</summary>
|
||||
readonly HashSet<string> refusedPersonas = [];
|
||||
|
||||
/// <summary>Raised once for a persona PrivaPub refused a token to (Refuse); CascadingState moves the session on.</summary>
|
||||
public event Action<string> PersonaRefused;
|
||||
|
||||
/// <summary>The stored session, or an empty one.</summary>
|
||||
public async Task<AuthData> GetAuthData(CancellationToken cancellationToken = default)
|
||||
@@ -89,6 +95,25 @@ namespace decePubClient.Services
|
||||
await Save(session, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>Whether PrivaPub refused the persona a token in this session (Refuse).</summary>
|
||||
public bool IsRefused(string personaId) => personaId is not null && refusedPersonas.Contains(personaId);
|
||||
|
||||
/// <summary>PrivaPub refused the persona a token while the JWT was good: no request acts as it from now on, and
|
||||
/// PersonaRefused tells the session, once.</summary>
|
||||
public void Refuse(string personaId)
|
||||
{
|
||||
if (personaId is null || !refusedPersonas.Add(personaId))
|
||||
return;
|
||||
PersonaRefused?.Invoke(personaId);
|
||||
}
|
||||
|
||||
/// <summary>The persona may be tried again: the reader picked it, or PrivaPub lists it as usable again.</summary>
|
||||
public void Unrefuse(string personaId)
|
||||
{
|
||||
if (personaId is not null)
|
||||
refusedPersonas.Remove(personaId);
|
||||
}
|
||||
|
||||
/// <summary>Drops every persona's token: PrivaPub revoked them all (a password change), and each is exchanged anew when used.</summary>
|
||||
public async Task ForgetPersonaTokens(CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -104,6 +129,7 @@ namespace decePubClient.Services
|
||||
logger.LogInformation($"set null({nameof(LogoutAsync)})");
|
||||
authData = null;
|
||||
loaded = true;
|
||||
refusedPersonas.Clear();
|
||||
await storage.RemoveItemAsync(nameof(AuthData), cancellationToken);
|
||||
|
||||
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
|
||||
|
||||
Reference in new issue
Block a user