// SPDX-FileCopyrightText: 2026 Progmasoft // SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 namespace Progmasoft.Progmaweb.Api.Accounts; /// An that keeps everything in the memory of the process. /// /// It keeps the uniqueness and session rules of under one lock, for development /// and for tests that need no database. Nothing survives a restart of the service: every account and every session /// is lost when the process stops. /// internal sealed class InMemoryAccountStore : IAccountStore { /// Guards every dictionary below; each operation holds it for its whole duration. private readonly Lock gate = new(); /// Every account, by identifier. private readonly Dictionary accountsById = []; /// Account identifiers by normalized email address; the uniqueness index of addresses. private readonly Dictionary accountIdsByEmail = new(StringComparer.Ordinal); /// Account identifiers by Google subject; the uniqueness index of linked identities. private readonly Dictionary accountIdsByGoogleSubject = new(StringComparer.Ordinal); /// Account identifiers by normalized account name; the uniqueness index of names. private readonly Dictionary accountIdsByName = new(StringComparer.Ordinal); /// Sessions by the hexadecimal form of their token digest. 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 FindByAccountNameAsync( string normalizedAccountName, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); lock (gate) { return ValueTask.FromResult( accountIdsByName.TryGetValue(normalizedAccountName, 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) { // The durable store refuses a session of an account that does not exist; so does this one. if (!accountsById.ContainsKey(session.AccountId)) { throw new InvalidOperationException("A session refers to an account that is not stored."); } 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; } } /// public ValueTask RemoveExpiredSessionsAsync(DateTimeOffset now, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); lock (gate) { string[] expired = [.. sessionsByDigest.Where(entry => entry.Value.ExpiresAt <= now).Select(entry => entry.Key)]; foreach (string digest in expired) { sessionsByDigest.Remove(digest); } return ValueTask.FromResult(expired.Length); } } }