diff options
| author | Leitwolf11 | 2026-08-30 21:45:01 +0300 |
|---|---|---|
| committer | Leitwolf11 | 2026-08-30 21:45:01 +0300 |
| commit | 76c4c1876a86dbca0adf86750362b3581d6fc69d (patch) | |
| tree | 52bc17d6a977d6c1b13c26c1e016a0e00db99ca5 /apps/api/Accounts/AccountService.cs | |
| parent | c89a3f20a0f477573c41b831d993850e0adb30e7 (diff) | |
| download | progmaweb-1.0.0.tar.gz | |
Add secure Google sign-in to Progmasoft Account. Use the server-side authorization-code flow with an exact account-domain callback, temporary hardened external cookies, verified provider email matching, canonical Account-name creation for new users, and no separate ViGet publisher identity. Add frontend entry points, provider failure guidance, locked Google authentication dependencies, service secret injection, and backend regression coverage without committing OAuth credentials.1.0.0
Diffstat (limited to 'apps/api/Accounts/AccountService.cs')
| -rw-r--r-- | apps/api/Accounts/AccountService.cs | 66 |
1 files changed, 65 insertions, 1 deletions
diff --git a/apps/api/Accounts/AccountService.cs b/apps/api/Accounts/AccountService.cs index a3f83ad..3e7ced1 100644 --- a/apps/api/Accounts/AccountService.cs +++ b/apps/api/Accounts/AccountService.cs @@ -62,7 +62,7 @@ internal sealed class AccountService( } AccountRecord? account = await store.FindByEmailAsync(normalizedEmail, cancellationToken); - if (account is null) + if (account is null || account.PasswordHash is null) { // A fixed dummy record keeps the missing-account path on the same password-hashing primitive. AccountRecord dummyBase = new(Guid.Empty, "Missing00", "MISSING00", "missing@example.invalid", @@ -85,6 +85,61 @@ internal sealed class AccountService( return await sessions.CreateAsync(account, cancellationToken); } + public async ValueTask<(AuthenticatedAccount? Authentication, GoogleAccountFailure Failure)> + AuthenticateGoogleAsync( + string? providerEmail, + string? requestedAccountName, + SessionService sessions, + CancellationToken cancellationToken) + { + if (!EmailAddressPolicy.TryNormalize(providerEmail, out string email, out string normalizedEmail, out _)) + { + return (null, GoogleAccountFailure.ProviderRejected); + } + + AccountRecord? existing = await store.FindByEmailAsync(normalizedEmail, cancellationToken); + if (existing is not null) + { + return (await sessions.CreateAsync(existing, cancellationToken), GoogleAccountFailure.None); + } + + if (requestedAccountName is null) + { + return (null, GoogleAccountFailure.AccountNameRequired); + } + + if (!AccountNamePolicy.TryNormalize(requestedAccountName, out string accountName, out _)) + { + return (null, GoogleAccountFailure.InvalidAccountName); + } + + AccountRecord account = new( + Guid.NewGuid(), + accountName, + AccountNamePolicy.NormalizeForLookup(accountName), + email, + normalizedEmail, + null, + timeProvider.GetUtcNow()); + CreateAccountResult result = await store.CreateAsync(account, cancellationToken); + + if (result.Status is CreateAccountStatus.AccountNameUnavailable) + { + return (null, GoogleAccountFailure.AccountNameUnavailable); + } + + if (result.Status is CreateAccountStatus.EmailUnavailable) + { + // A concurrent callback may have created the same verified Google identity. + AccountRecord? concurrent = await store.FindByEmailAsync(normalizedEmail, cancellationToken); + return concurrent is null + ? (null, GoogleAccountFailure.ProviderRejected) + : (await sessions.CreateAsync(concurrent, cancellationToken), GoogleAccountFailure.None); + } + + return (await sessions.CreateAsync(account, cancellationToken), GoogleAccountFailure.None); + } + private static Dictionary<string, string[]> ValidateRegistration( RegisterAccountRequest request, out string accountName, @@ -111,3 +166,12 @@ internal sealed class AccountService( return errors; } } + +internal enum GoogleAccountFailure +{ + None, + ProviderRejected, + AccountNameRequired, + InvalidAccountName, + AccountNameUnavailable +} |