using System.Collections.Concurrent; using System.Collections.Immutable; using System.Runtime.CompilerServices; using System.Security.Cryptography; using Phantom.Common.Data.Web.Users; using Phantom.Controller.Database; using Phantom.Controller.Database.Repositories; namespace Phantom.Controller.Services.Users; sealed class UserLoginManager { private const int SessionIdBytes = 20; private readonly ConcurrentDictionary<Guid, UserSession> sessionsByUserGuid = new (); private readonly UserManager userManager; private readonly PermissionManager permissionManager; private readonly IDbContextProvider dbProvider; public UserLoginManager(UserManager userManager, PermissionManager permissionManager, IDbContextProvider dbProvider) { this.userManager = userManager; this.permissionManager = permissionManager; this.dbProvider = dbProvider; } public async Task<LogInSuccess?> LogIn(string username, string password) { var user = await userManager.GetAuthenticated(username, password); if (user == null) { return null; } var permissions = await permissionManager.FetchPermissionsForUserId(user.UserGuid); var userInfo = new AuthenticatedUserInfo(user.UserGuid, user.Name, permissions); var token = ImmutableArray.Create(RandomNumberGenerator.GetBytes(SessionIdBytes)); sessionsByUserGuid.AddOrUpdate(user.UserGuid, UserSession.Create, UserSession.Add, new NewUserSession(userInfo, token)); await using (var db = dbProvider.Lazy()) { var auditLogWriter = new AuditLogRepository(db).Writer(user.UserGuid); auditLogWriter.UserLoggedIn(user); await db.Ctx.SaveChangesAsync(); } return new LogInSuccess(userInfo, token); } public async Task LogOut(Guid userGuid, ImmutableArray<byte> token) { while (true) { if (!sessionsByUserGuid.TryGetValue(userGuid, out var oldSession)) { return; } if (sessionsByUserGuid.TryUpdate(userGuid, oldSession.RemoveToken(token), oldSession)) { break; } } await using var db = dbProvider.Lazy(); var auditLogWriter = new AuditLogRepository(db).Writer(userGuid); auditLogWriter.UserLoggedOut(userGuid); await db.Ctx.SaveChangesAsync(); } public AuthenticatedUserInfo? GetAuthenticatedUser(Guid userGuid, ImmutableArray<byte> token) { return sessionsByUserGuid.TryGetValue(userGuid, out var session) && session.Tokens.Contains(token, TokenEqualityComparer.Instance) ? session.UserInfo : null; } private readonly record struct NewUserSession(AuthenticatedUserInfo UserInfo, ImmutableArray<byte> Token); private sealed record UserSession(AuthenticatedUserInfo UserInfo, ImmutableList<ImmutableArray<byte>> Tokens) { public static UserSession Create(Guid userGuid, NewUserSession newSession) { return new UserSession(newSession.UserInfo, ImmutableList.Create(newSession.Token)); } public static UserSession Add(Guid userGuid, UserSession oldSession, NewUserSession newSession) { return new UserSession(newSession.UserInfo, oldSession.Tokens.Add(newSession.Token)); } public UserSession RemoveToken(ImmutableArray<byte> token) { return this with { Tokens = Tokens.Remove(token, TokenEqualityComparer.Instance) }; } public bool Equals(UserSession? other) { return ReferenceEquals(this, other); } public override int GetHashCode() { return RuntimeHelpers.GetHashCode(this); } } private sealed class TokenEqualityComparer : IEqualityComparer<ImmutableArray<byte>> { public static TokenEqualityComparer Instance { get; } = new (); private TokenEqualityComparer() {} public bool Equals(ImmutableArray<byte> x, ImmutableArray<byte> y) { return x.SequenceEqual(y); } public int GetHashCode(ImmutableArray<byte> obj) { throw new NotImplementedException(); } } }