diff options
| author | Leitwolf11 | 2026-10-03 18:46:59 +0300 |
|---|---|---|
| committer | Leitwolf11 | 2026-10-03 18:46:59 +0300 |
| commit | b84a2216c26cc5392d0dc300b7a605ccb288d8c8 (patch) | |
| tree | 048ae623f49cc5f2f94234d747351f9e659f5267 /apps/api/Accounts/AccountService.cs | |
| parent | ec774db15976385a6d5ea6f48d3622fe507d3843 (diff) | |
| parent | cf444eb3bccf99c10f59ed5330b61c0397e0aeaa (diff) | |
| download | progmaweb-fix/api-without-google.tar.gz | |
Merge remote-tracking branch 'origin/main' into fix/api-without-googlefix/api-without-google
Diffstat (limited to 'apps/api/Accounts/AccountService.cs')
| -rw-r--r-- | apps/api/Accounts/AccountService.cs | 53 |
1 files changed, 53 insertions, 0 deletions
diff --git a/apps/api/Accounts/AccountService.cs b/apps/api/Accounts/AccountService.cs index 7168eb6..cd3e711 100644 --- a/apps/api/Accounts/AccountService.cs +++ b/apps/api/Accounts/AccountService.cs @@ -5,10 +5,14 @@ using Microsoft.AspNetCore.Identity; namespace Progmasoft.Progmaweb.Api.Accounts; +/// <summary>Registration and sign-in rules of the account service.</summary> +/// <param name="store">Store that keeps accounts.</param> +/// <param name="timeProvider">Clock used for creation times.</param> internal sealed class AccountService( IAccountStore store, TimeProvider timeProvider) { + /// <summary>An account that never exists; the subject of the dummy verification.</summary> private static readonly AccountRecord DummyAccount = new( Guid.Empty, "Missing00", @@ -18,11 +22,24 @@ internal sealed class AccountService( null, string.Empty, DateTimeOffset.UnixEpoch); + /// <summary> + /// A hash that no submitted password matches. Verifying against it makes a sign-in with an unknown address cost + /// as much as one with a wrong password. + /// </summary> private static readonly string DummyPasswordHash = new PasswordHasher<AccountRecord>().HashPassword(DummyAccount, "not-the-provided-password"); + /// <summary>Hashes and verifies passwords with the ASP.NET Core Identity algorithm.</summary> private readonly PasswordHasher<AccountRecord> passwordHasher = new(); + /// <summary>Creates a password account and opens a session for it.</summary> + /// <param name="request">The registration body.</param> + /// <param name="sessions">Session service.</param> + /// <param name="cancellationToken">Cancels the operation.</param> + /// <returns> + /// The signed-in account and no errors, or no account and the messages of each refused field, keyed by + /// <c>accountName</c>, <c>email</c> and <c>password</c>. + /// </returns> public async ValueTask<(AuthenticatedAccount? Authentication, Dictionary<string, string[]> Errors)> RegisterAsync( RegisterAccountRequest request, SessionService sessions, @@ -63,6 +80,15 @@ internal sealed class AccountService( return (authentication, errors); } + /// <summary>Signs in with email address and password.</summary> + /// <remarks> + /// An unknown address costs one password verification against a private dummy hash, so the response time does not + /// reveal whether an address is registered. + /// </remarks> + /// <param name="request">The login body.</param> + /// <param name="sessions">Session service.</param> + /// <param name="cancellationToken">Cancels the operation.</param> + /// <returns>The signed-in account, or <see langword="null"/> when the credentials are refused.</returns> public async ValueTask<AuthenticatedAccount?> LoginAsync( LoginAccountRequest request, SessionService sessions, @@ -92,6 +118,18 @@ internal sealed class AccountService( return await sessions.CreateAsync(account, cancellationToken); } + /// <summary>Signs in with a Google identity, creating the account on first use.</summary> + /// <remarks> + /// An identity that is already linked signs in to its account. A new identity creates an account under the + /// requested name. A new identity whose email address belongs to an existing account is refused: password + /// registrations do not verify the address, so linking by address could hand an account to someone else. + /// </remarks> + /// <param name="providerSubject">Subject identifier Google issued for the identity.</param> + /// <param name="providerEmail">Email address Google reported for the identity.</param> + /// <param name="requestedAccountName">Account name to create for a new identity; may be <see langword="null"/>.</param> + /// <param name="sessions">Session service.</param> + /// <param name="cancellationToken">Cancels the operation.</param> + /// <returns>The signed-in account, or no account and the reason.</returns> public async ValueTask<(AuthenticatedAccount? Authentication, GoogleAccountFailure Failure)> AuthenticateGoogleAsync( string? providerSubject, @@ -165,6 +203,12 @@ internal sealed class AccountService( return (await sessions.CreateAsync(account, cancellationToken), GoogleAccountFailure.None); } + /// <summary>Validates every field of a registration and collects the messages of the refused ones.</summary> + /// <param name="request">The registration body.</param> + /// <param name="accountName">The canonical account name.</param> + /// <param name="email">The email address as entered, trimmed.</param> + /// <param name="normalizedEmail">The email address in its lookup form.</param> + /// <returns>Messages keyed by field name; empty when the registration is valid.</returns> private static Dictionary<string, string[]> ValidateRegistration( RegisterAccountRequest request, out string accountName, @@ -192,11 +236,20 @@ internal sealed class AccountService( } } +/// <summary>Reason a sign-in with Google did not produce a session.</summary> internal enum GoogleAccountFailure { + /// <summary>The sign-in succeeded.</summary> None, + /// <summary> + /// The identity cannot be used: Google reported no usable subject or address, or the address belongs to another + /// account. + /// </summary> ProviderRejected, + /// <summary>The identity is new and no account name was supplied.</summary> AccountNameRequired, + /// <summary>The supplied account name breaks <see cref="AccountNamePolicy"/>.</summary> InvalidAccountName, + /// <summary>The supplied account name is taken.</summary> AccountNameUnavailable } |