// SPDX-FileCopyrightText: 2026 Progmasoft // SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 namespace Progmasoft.Progmaweb.Api.Accounts; // This development store models the uniqueness and session semantics required by the future PostgreSQL implementation. // It deliberately exposes no enumeration or bulk-delete surface to public endpoints. internal sealed class InMemoryAccountStore : IAccountStore { private readonly Lock gate = new(); private readonly Dictionary accountsById = []; private readonly Dictionary accountIdsByEmail = new(StringComparer.Ordinal); private readonly Dictionary accountIdsByGoogleSubject = new(StringComparer.Ordinal); private readonly Dictionary accountIdsByName = new(StringComparer.Ordinal); private readonly Dictionary sessionsByDigest = new(StringComparer.Ordinal); public ValueTask CreateAsync(AccountRecord account, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); lock (gate) { if (account.GoogleSubject is not null && accountIdsByGoogleSubject.ContainsKey(account.GoogleSubject)) { return ValueTask.FromResult(CreateAccountResult.GoogleSubjectUnavailable()); } if (accountIdsByName.ContainsKey(account.NormalizedAccountName)) { return ValueTask.FromResult(CreateAccountResult.AccountNameUnavailable()); } if (accountIdsByEmail.ContainsKey(account.NormalizedEmail)) { return ValueTask.FromResult(CreateAccountResult.EmailUnavailable()); } accountsById.Add(account.Id, account); accountIdsByName.Add(account.NormalizedAccountName, account.Id); accountIdsByEmail.Add(account.NormalizedEmail, account.Id); if (account.GoogleSubject is not null) { accountIdsByGoogleSubject.Add(account.GoogleSubject, account.Id); } return ValueTask.FromResult(CreateAccountResult.Created(account)); } } public ValueTask FindByGoogleSubjectAsync( string googleSubject, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); lock (gate) { return ValueTask.FromResult( accountIdsByGoogleSubject.TryGetValue(googleSubject, out Guid id) ? accountsById[id] : null); } } public ValueTask FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); lock (gate) { return ValueTask.FromResult( accountIdsByEmail.TryGetValue(normalizedEmail, out Guid id) ? accountsById[id] : null); } } public ValueTask FindByIdAsync(Guid id, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); lock (gate) { accountsById.TryGetValue(id, out AccountRecord? account); return ValueTask.FromResult(account); } } public ValueTask StoreSessionAsync(SessionRecord session, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); lock (gate) { sessionsByDigest[Convert.ToHexString(session.TokenDigest)] = session; return ValueTask.CompletedTask; } } public ValueTask FindSessionAsync(byte[] tokenDigest, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); lock (gate) { sessionsByDigest.TryGetValue(Convert.ToHexString(tokenDigest), out SessionRecord? session); return ValueTask.FromResult(session); } } public ValueTask RevokeSessionAsync(byte[] tokenDigest, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); lock (gate) { sessionsByDigest.Remove(Convert.ToHexString(tokenDigest)); return ValueTask.CompletedTask; } } }