Build / Build (push) Successful in 1m3s
Completion program R3, with PrivaPub's TOTP routes of the same release. - Settings, the login: TotpEditor turns two-factor sign-in on (the password, then the QR code, the secret and the otpauth:// link, then the app's first code; the ten recovery codes shown once with Copy), shows how many recovery codes are left (warned at three), and turns it off with the password and a code, asked first. - Turning it on, like a password change, ends every other session: AuthService.ContinueSession takes the new JWT and exchanges the persona's token again. - Sign-in: PrivaPub's 428 keeps the name and password and asks for the six digits (DOtp), or a recovery code instead; a wrong code says so; Back returns to the password. - window.copyText (main.js) through AppStatusService.CopyText. - 29 keys in both resx files; CLAUDE.md follows. Verified: dotnet build decePubClient.csproj -c Release (0 errors, no new warnings). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
448 lines
22 KiB
C#
448 lines
22 KiB
C#
using System.Net;
|
|
using System.Text.Json;
|
|
|
|
using decePubClient.Extensions;
|
|
using decePubClient.Helpers;
|
|
using decePubClient.Models;
|
|
using decePubClient.Models.Mastodon;
|
|
|
|
using PrivaPub.ClientModels;
|
|
using PrivaPub.ClientModels.User;
|
|
using PrivaPub.ClientModels.User.Avatar;
|
|
|
|
namespace decePubClient.Services
|
|
{
|
|
/// <summary>
|
|
/// One sign-in for both of PrivaPub's APIs: the root signs in on /clientapi, then each persona it uses gets its own
|
|
/// Mastodon token in exchange for the JWT (PrivaPub's PersonaExchange), so the Mastodon API never learns the root.
|
|
/// Results are WebResults, lists come back empty on failure; nothing throws.
|
|
/// </summary>
|
|
public class AuthService(IHttpService httpService, TokenAuthStateProvider authStateProvider, AppConfiguration appConfiguration,
|
|
ILoggingService logger, CoalescingStringLocalizer localizer)
|
|
{
|
|
/// <summary>The JWT is renewed (sniff/again) once less than this, or a quarter of its lifetime, is left.</summary>
|
|
static readonly TimeSpan RenewBefore = TimeSpan.FromDays(2);
|
|
|
|
/// <summary>
|
|
/// Signs the root in; the JWT never goes with the request (PrivaPub redirects a login that carries one). With two-factor
|
|
/// sign-in on, a right password without LoginForm.OneTimeCode comes back invalid with StatusCode 428
|
|
/// (NeedsOneTimeCode): the caller asks for the code and sends the same form again with it. A wrong code is a 400, like a
|
|
/// wrong password, with PrivaPub's own message.
|
|
/// </summary>
|
|
public async Task<WebResult> Login(LoginForm form, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.PostTotallyAnon(APIs.ClientApi.POST_Login, form, cancellationToken: cancellationToken);
|
|
if (response.StatusCode is HttpStatusCode.PreconditionRequired)
|
|
return new WebResult().Invalidate(localizer["Enter the code from your authenticator app, or a recovery code."],
|
|
(int)HttpStatusCode.PreconditionRequired);
|
|
|
|
return await StartSession(response, cancellationToken);
|
|
}
|
|
|
|
/// <summary>Whether a sign-in failed only for want of the authenticator app's code or a recovery code (PrivaPub's 428).</summary>
|
|
public static bool NeedsOneTimeCode(WebResult result) => result.IsInvalid && result.StatusCode is (int)HttpStatusCode.PreconditionRequired;
|
|
|
|
/// <summary>Whether the server waits for its first login, which becomes its administrator; false when it can't say.</summary>
|
|
public async Task<bool> SetupPending(CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.GetTotallyAnon(APIs.ClientApi.GET_Setup, cancellationToken: cancellationToken);
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
await LogFailure(response, nameof(SetupPending), cancellationToken);
|
|
return false;
|
|
}
|
|
|
|
return (await response.DefaultReadFromJsonAsync<ViewServerSetup>(cancellationToken))?.IsPending ?? false;
|
|
}
|
|
|
|
/// <summary>Makes the server's first login, its administrator, with its first persona, and signs it in.</summary>
|
|
public async Task<WebResult> SetUp(SetupForm form, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.PostTotallyAnon(APIs.ClientApi.POST_Setup, form, cancellationToken: cancellationToken);
|
|
return await StartSession(response, cancellationToken);
|
|
}
|
|
|
|
/// <summary>Makes a login through a group's invitation, its first persona a member of the group, and signs it in.</summary>
|
|
public async Task<WebResult> Join(InvitationLoginForm form, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.PostTotallyAnon(APIs.ClientApi.POST_InvitationSignUp, form, cancellationToken: cancellationToken);
|
|
return await StartSession(response, cancellationToken);
|
|
}
|
|
|
|
/// <summary>Keeps the JWT a login, a setup or an invitation answered with.</summary>
|
|
async Task<WebResult> StartSession(HttpResponseMessage response, CancellationToken cancellationToken)
|
|
{
|
|
if (!response.IsSuccessStatusCode)
|
|
return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
|
|
|
var jwtUser = await response.DefaultReadFromJsonAsync<JwtUser>(cancellationToken);
|
|
if (string.IsNullOrEmpty(jwtUser?.Token))
|
|
return new WebResult().Invalidate(localizer["The server sent an invalid response."], (int)HttpStatusCode.BadGateway);
|
|
|
|
await authStateProvider.SetSession(jwtUser, cancellationToken);
|
|
return new WebResult { Data = jwtUser };
|
|
}
|
|
|
|
/// <summary>The root's personas.</summary>
|
|
public async Task<List<ViewAvatar>> Personas(CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Get(APIs.ClientApi.GET_Personas, cancellationToken: cancellationToken);
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
await LogFailure(response, nameof(Personas), cancellationToken);
|
|
return [];
|
|
}
|
|
|
|
return await response.DefaultReadFromJsonAsync<List<ViewAvatar>>(cancellationToken) ?? [];
|
|
}
|
|
|
|
/// <summary>Makes a new persona for the root; Data is the persona. No other persona is linked to it, anywhere.</summary>
|
|
public async Task<WebResult> CreatePersona(InsertAvatarForm form, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Post(APIs.ClientApi.POST_InsertPersona, form, cancellationToken: cancellationToken);
|
|
if (!response.IsSuccessStatusCode)
|
|
return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
|
|
|
return new WebResult { Data = await response.DefaultReadFromJsonAsync<ViewAvatar>(cancellationToken) };
|
|
}
|
|
|
|
/// <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)
|
|
{
|
|
var session = await authStateProvider.GetAuthData(cancellationToken);
|
|
if (session.PersonaTokens.ContainsKey(personaId))
|
|
{
|
|
await authStateProvider.SetPersona(personaId, cancellationToken: cancellationToken);
|
|
return new();
|
|
}
|
|
|
|
using var response = await httpService.ExchangePersonaToken(personaId, cancellationToken);
|
|
return response.IsSuccessStatusCode ? new() : await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
|
}
|
|
|
|
/// <summary>The current persona's Mastodon account, or null.</summary>
|
|
public async Task<Account> CurrentAccount(CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Get(APIs.Mastodon.GET_VerifyCredentials, cancellationToken: cancellationToken);
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
await LogFailure(response, nameof(CurrentAccount), cancellationToken);
|
|
return null;
|
|
}
|
|
|
|
return await response.MastodonReadFromJsonAsync<Account>(cancellationToken);
|
|
}
|
|
|
|
/// <summary>Renews the JWT when it is about to expire; persona tokens do not expire and are kept.</summary>
|
|
public async Task RefreshIfNeeded(CancellationToken cancellationToken = default)
|
|
{
|
|
var session = await authStateProvider.GetAuthData(cancellationToken);
|
|
if (session.TokenExpiration is not { } expiration)
|
|
return;
|
|
var lifetime = session.TokenIssuedAt is { } issued ? TimeSpan.FromTicks(expiration - issued) : RenewBefore * 4;
|
|
var renewBefore = lifetime / 4 < RenewBefore ? lifetime / 4 : RenewBefore;
|
|
if (new DateTime(expiration, DateTimeKind.Utc) - DateTime.UtcNow > renewBefore)
|
|
return;
|
|
if (await authStateProvider.GetToken(cancellationToken) is null)
|
|
return;
|
|
|
|
using var response = await httpService.Get(APIs.ClientApi.GET_SniffAgain, cancellationToken: cancellationToken);
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
await LogFailure(response, nameof(RefreshIfNeeded), cancellationToken);
|
|
return;
|
|
}
|
|
|
|
var jwtUser = await response.DefaultReadFromJsonAsync<JwtUser>(cancellationToken);
|
|
if (!string.IsNullOrEmpty(jwtUser?.Token))
|
|
await authStateProvider.SetSession(jwtUser, cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Changes the root's password, the current one given. PrivaPub ends every session of the login with it, and revokes
|
|
/// every persona's token, this device's too; it answers with a new JWT, so the session goes on with it and the current
|
|
/// persona gets a new token at once (the others when they are next used).
|
|
/// </summary>
|
|
public async Task<WebResult> ChangePassword(UserPasswordForm form, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Post(APIs.ClientApi.POST_UpdatePassword, form, cancellationToken: cancellationToken);
|
|
if (!response.IsSuccessStatusCode)
|
|
return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
|
|
|
// a server from before sessions answered with nothing, and ended none
|
|
var jwtUser = await response.DefaultReadFromJsonAsync<JwtUser>(cancellationToken);
|
|
if (string.IsNullOrEmpty(jwtUser?.Token))
|
|
return new WebResult();
|
|
|
|
await ContinueSession(jwtUser, nameof(ChangePassword), cancellationToken);
|
|
return new WebResult { Data = jwtUser };
|
|
}
|
|
|
|
/// <summary>
|
|
/// Begins two-factor sign-in, the password given (a wrong one is PrivaPub's 403, read as such). Data is what the
|
|
/// authenticator app needs (ViewTotpSetup): the secret as text, as an otpauth:// URI and as that URI's QR code. PrivaPub
|
|
/// keeps it an hour for TotpEnable; a login with two-factor on already is a 409.
|
|
/// </summary>
|
|
public async Task<WebResult> TotpSetup(string password, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Post(APIs.ClientApi.POST_TotpSetup, new PasswordForm { Password = password },
|
|
cancellationToken: cancellationToken);
|
|
if (!response.IsSuccessStatusCode)
|
|
return await ReadPasswordFailure(response, cancellationToken);
|
|
|
|
var setup = await response.DefaultReadFromJsonAsync<ViewTotpSetup>(cancellationToken);
|
|
if (string.IsNullOrEmpty(setup?.Secret))
|
|
return new WebResult().Invalidate(localizer["The server sent an invalid response."], (int)HttpStatusCode.BadGateway);
|
|
|
|
return new WebResult { Data = setup };
|
|
}
|
|
|
|
/// <summary>
|
|
/// Turns two-factor sign-in on with the app's first code (a wrong one is a 400; a setup older than an hour a 409, to
|
|
/// begin again). Data is the ten recovery codes (List<string>), which PrivaPub never shows again. PrivaPub ends every
|
|
/// session of the login with it and revokes every persona's token, this device's too, and answers with this device's new
|
|
/// JWT: the session goes on with it, as after a password change (ContinueSession).
|
|
/// </summary>
|
|
public async Task<WebResult> TotpEnable(string code, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Post(APIs.ClientApi.POST_TotpEnable, new TotpCodeForm { Code = code },
|
|
cancellationToken: cancellationToken);
|
|
if (!response.IsSuccessStatusCode)
|
|
return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
|
|
|
var enabled = await response.DefaultReadFromJsonAsync<ViewTotpEnabled>(cancellationToken);
|
|
if (string.IsNullOrEmpty(enabled?.Session?.Token))
|
|
return new WebResult().Invalidate(localizer["The server sent an invalid response."], (int)HttpStatusCode.BadGateway);
|
|
|
|
await ContinueSession(enabled.Session, nameof(TotpEnable), cancellationToken);
|
|
return new WebResult { Data = enabled.RecoveryCodes };
|
|
}
|
|
|
|
/// <summary>
|
|
/// Turns two-factor sign-in off with the password (a wrong one is PrivaPub's 403) and a code: the app's, or a recovery
|
|
/// code (a wrong one is a 400). Data is the login as it is now (ViewAccount).
|
|
/// </summary>
|
|
public async Task<WebResult> TotpDisable(string password, string code, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Post(APIs.ClientApi.POST_TotpDisable, new TotpDisableForm { Password = password, Code = code },
|
|
cancellationToken: cancellationToken);
|
|
if (!response.IsSuccessStatusCode)
|
|
return await ReadPasswordFailure(response, cancellationToken);
|
|
|
|
return new WebResult { Data = await response.DefaultReadFromJsonAsync<ViewAccount>(cancellationToken) };
|
|
}
|
|
|
|
/// <summary>
|
|
/// Goes on with the JWT PrivaPub answered after it ended every session of the login and revoked every persona's token,
|
|
/// this device's too (a password change, two-factor sign-in turned on): the stored persona tokens are dropped and the
|
|
/// current persona gets a new one at once, the others when they are next used.
|
|
/// </summary>
|
|
async Task ContinueSession(JwtUser jwtUser, string caller, CancellationToken cancellationToken)
|
|
{
|
|
await authStateProvider.SetSession(jwtUser, cancellationToken);
|
|
await authStateProvider.ForgetPersonaTokens(cancellationToken);
|
|
var session = await authStateProvider.GetAuthData(cancellationToken);
|
|
if (session.PersonaId is null)
|
|
return;
|
|
|
|
using var exchanged = await httpService.ExchangePersonaToken(session.PersonaId, cancellationToken);
|
|
if (!exchanged.IsSuccessStatusCode)
|
|
await LogFailure(exchanged, caller, cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asks PrivaPub to mail a recovery link to the login named, or to the login whose verified address is given. Data is
|
|
/// PrivaPub's answer, the same sentence whether or not there is such a login. Sent with no credentials.
|
|
/// </summary>
|
|
public async Task<WebResult> RequestRecovery(PasswordRecoveryForm form, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.PostTotallyAnon(APIs.ClientApi.POST_RecoverPassword, form, cancellationToken: cancellationToken);
|
|
if (!response.IsSuccessStatusCode)
|
|
return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
|
|
|
return new WebResult { Data = await ReadSentence(response, cancellationToken) };
|
|
}
|
|
|
|
/// <summary>Whether a recovery link's code still works (Data, a bool): it lasts an hour and is used once.</summary>
|
|
public async Task<WebResult> CheckRecoveryCode(string recoveryCode, CancellationToken cancellationToken = default)
|
|
{
|
|
// [FromBody] string: the code goes as a JSON string
|
|
using var response = await httpService.PostTotallyAnon(APIs.ClientApi.POST_RecoverValid, recoveryCode, cancellationToken: cancellationToken);
|
|
if (!response.IsSuccessStatusCode)
|
|
return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
|
|
|
return new WebResult { Data = await response.DefaultReadFromJsonAsync<bool>(cancellationToken) };
|
|
}
|
|
|
|
/// <summary>Sets a new password through a recovery link's code; PrivaPub ends every session the login had.</summary>
|
|
public async Task<WebResult> Recover(NewPasswordForm form, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.PostTotallyAnon(APIs.ClientApi.POST_RecoverUpdatePassword, form, cancellationToken: cancellationToken);
|
|
if (!response.IsSuccessStatusCode)
|
|
return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
|
|
|
return new WebResult();
|
|
}
|
|
|
|
/// <summary>The login itself (its name, its email address and whether it is verified, one waiting for its code, whether
|
|
/// two-factor sign-in is on and how many recovery codes are left), or null.</summary>
|
|
public async Task<ViewAccount> Account(CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Get(APIs.ClientApi.GET_Account, cancellationToken: cancellationToken);
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
await LogFailure(response, nameof(Account), cancellationToken);
|
|
return null;
|
|
}
|
|
|
|
return await response.DefaultReadFromJsonAsync<ViewAccount>(cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asks for the login's address to become another, the password given: PrivaPub mails a code to it, which VerifyEmail
|
|
/// brings back. Data is PrivaPub's answer, the same sentence whether or not the address is free.
|
|
/// </summary>
|
|
public async Task<WebResult> ChangeEmail(EmailChangeForm form, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Post(APIs.ClientApi.POST_Email, form, cancellationToken: cancellationToken);
|
|
if (!response.IsSuccessStatusCode)
|
|
return await ReadPasswordFailure(response, cancellationToken);
|
|
|
|
return new WebResult { Data = await ReadSentence(response, cancellationToken) };
|
|
}
|
|
|
|
/// <summary>Makes the address waiting for its code the login's, verified; Data is the login as it is now.</summary>
|
|
public async Task<WebResult> VerifyEmail(string code, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Post(APIs.ClientApi.POST_EmailVerify, new EmailCodeForm { Code = code }, cancellationToken: cancellationToken);
|
|
if (!response.IsSuccessStatusCode)
|
|
return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
|
|
|
return new WebResult { Data = await response.DefaultReadFromJsonAsync<ViewAccount>(cancellationToken) };
|
|
}
|
|
|
|
/// <summary>Removes the login's address, the password given; without one a forgotten password can't be recovered.
|
|
/// Data is the login as it is now.</summary>
|
|
public async Task<WebResult> RemoveEmail(string password, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Post(APIs.ClientApi.POST_EmailRemove, new PasswordForm { Password = password },
|
|
cancellationToken: cancellationToken);
|
|
if (!response.IsSuccessStatusCode)
|
|
return await ReadPasswordFailure(response, cancellationToken);
|
|
|
|
return new WebResult { Data = await response.DefaultReadFromJsonAsync<ViewAccount>(cancellationToken) };
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ends every session of the login, this device's too, and PrivaPub revokes every persona's token with them. The client
|
|
/// forgets its session at once; the caller signs out, which then has nothing left to tell PrivaPub.
|
|
/// </summary>
|
|
public async Task<WebResult> LogoutEverywhere(CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Post(APIs.ClientApi.POST_LogoutEverywhere, cancellationToken: cancellationToken);
|
|
if (!response.IsSuccessStatusCode)
|
|
return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
|
|
|
await authStateProvider.LogoutAsync(cancellationToken);
|
|
return new WebResult();
|
|
}
|
|
|
|
/// <summary>The theme and language the login keeps for every device, or null; SavedAt is null until a client saved them.</summary>
|
|
public async Task<ViewUserSettings> Settings(CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Get(APIs.ClientApi.GET_UserSettings, cancellationToken: cancellationToken);
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
await LogFailure(response, nameof(Settings), cancellationToken);
|
|
return null;
|
|
}
|
|
|
|
return await response.DefaultReadFromJsonAsync<ViewUserSettings>(cancellationToken);
|
|
}
|
|
|
|
/// <summary>Saves the theme and language as the login's, for every device signed in to it. A failure is logged here.</summary>
|
|
public async Task<WebResult> SaveSettings(ViewUserSettings settings, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Post(APIs.ClientApi.POST_UpdateUserSettings, settings, cancellationToken: cancellationToken);
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
var result = await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
|
if (result.StatusCode is not (int)HttpStatusCode.Gone)
|
|
await logger.ProcessWarning(result.ToString(localizer), $"{nameof(AuthService)}.{nameof(SaveSettings)}", cancellationToken);
|
|
return result;
|
|
}
|
|
|
|
return new WebResult();
|
|
}
|
|
|
|
/// <summary>Deletes the root, its password given: every persona of it and everything public they had, here and, as
|
|
/// PrivaPub tells them, on the servers that hold it. The caller signs out afterwards.</summary>
|
|
public async Task<WebResult> DeleteAccount(RemoveSelfForm form, CancellationToken cancellationToken = default)
|
|
{
|
|
using var response = await httpService.Post(APIs.ClientApi.POST_DeleteAccount, form, cancellationToken: cancellationToken);
|
|
if (!response.IsSuccessStatusCode)
|
|
return await response.Content.ReadWebResult(response.StatusCode, localizer, cancellationToken);
|
|
|
|
return new WebResult();
|
|
}
|
|
|
|
/// <summary>Revokes every persona token the client holds, tells PrivaPub, then forgets the session and the local data.</summary>
|
|
public async Task Logout(CancellationToken cancellationToken = default)
|
|
{
|
|
var session = await authStateProvider.GetAuthData(cancellationToken);
|
|
foreach (var token in session.PersonaTokens.Values)
|
|
{
|
|
using var revoked = await httpService.PostTotallyAnon(APIs.OAuth.POST_Revoke, new Dictionary<string, string>
|
|
{
|
|
["token"] = token,
|
|
["client_id"] = appConfiguration.ClientId
|
|
}, cancellationToken: cancellationToken);
|
|
if (!revoked.IsSuccessStatusCode)
|
|
await LogFailure(revoked, nameof(Logout), cancellationToken);
|
|
}
|
|
|
|
if (await authStateProvider.GetToken(cancellationToken) is not null)
|
|
{
|
|
using var response = await httpService.Get(APIs.ClientApi.GET_Logout, cancellationToken: cancellationToken);
|
|
if (!response.IsSuccessStatusCode)
|
|
await LogFailure(response, nameof(Logout), cancellationToken);
|
|
}
|
|
|
|
await authStateProvider.LogoutAsync(cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The failure of what only the password may do: PrivaPub refuses a wrong one with 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);
|
|
}
|
|
|
|
/// <summary>A sentence PrivaPub answered with: a JSON string, or plain text (ASP.NET writes a string so unless JSON is asked for).</summary>
|
|
static async Task<string> ReadSentence(HttpResponseMessage response, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
var text = await response.Content.ReadAsStringAsync(cancellationToken);
|
|
return text.StartsWith('"') ? JsonSerializer.Deserialize<string>(text, SUtility.DefaultSerializer) : text;
|
|
}
|
|
catch (Exception ex) when (ex is OperationCanceledException or JsonException)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
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)
|
|
await logger.ProcessWarning(result.ToString(localizer), $"{nameof(AuthService)}.{caller}", cancellationToken);
|
|
}
|
|
}
|
|
}
|