using Microsoft.EntityFrameworkCore; using Nexus.Api.Data; namespace Nexus.Api.Repositories; public sealed class UserRepository(NexusDbContext db) : IUserRepository { public ValueTask GetByIdAsync(Guid userId, CancellationToken ct = default) => db.Users.FindAsync([userId], ct); public Task GetByEmailAsync(string normalizedEmail, CancellationToken ct = default) => db.Users.FirstOrDefaultAsync(u => u.NormalizedEmail == normalizedEmail, ct); public Task AnyUsersAsync(CancellationToken ct = default) => db.Users.AnyAsync(ct); public async Task AddAsync(NexusUser user, CancellationToken ct = default) { db.Users.Add(user); await db.SaveChangesAsync(ct); return user; } public Task UpdateAsync(NexusUser user, CancellationToken ct = default) => db.SaveChangesAsync(ct); public Task GetRefreshTokenByHashAsync(string tokenHash, CancellationToken ct = default) => db.RefreshTokens .Include(r => r.User) .FirstOrDefaultAsync(r => r.TokenHash == tokenHash, ct); public Task> GetActiveTokensByFamilyAsync(Guid familyId, CancellationToken ct = default) => db.RefreshTokens .Where(r => r.FamilyId == familyId && r.RevokedAt == null) .ToListAsync(ct); public async Task AddRefreshTokenAsync(RefreshToken token, CancellationToken ct = default) { db.RefreshTokens.Add(token); await db.SaveChangesAsync(ct); } public Task UpdateRefreshTokenAsync(RefreshToken token, CancellationToken ct = default) => db.SaveChangesAsync(ct); public async Task RevokeTokenAsync(string tokenHash, CancellationToken ct = default) { var token = await db.RefreshTokens.FirstOrDefaultAsync(r => r.TokenHash == tokenHash, ct); if (token is null || token.RevokedAt is not null) return; token.RevokedAt = DateTimeOffset.UtcNow; token.ConcurrencyStamp = Guid.NewGuid(); await db.SaveChangesAsync(ct); } public async Task RevokeFamilyAsync(Guid familyId, CancellationToken ct = default) { var activeTokens = await db.RefreshTokens .Where(r => r.FamilyId == familyId && r.RevokedAt == null) .ToListAsync(ct); if (activeTokens.Count == 0) return; var now = DateTimeOffset.UtcNow; foreach (var token in activeTokens) { token.RevokedAt = now; token.ConcurrencyStamp = Guid.NewGuid(); } await db.SaveChangesAsync(ct); } public async Task RemoveExpiredTokensAsync(Guid userId, CancellationToken ct = default) { var cutoff = DateTimeOffset.UtcNow.AddDays(-30); var oldTokens = await db.RefreshTokens .Where(r => r.UserId == userId && (r.ExpiresAt < DateTimeOffset.UtcNow || r.RevokedAt < cutoff)) .ToListAsync(ct); if (oldTokens.Count > 0) { db.RefreshTokens.RemoveRange(oldTokens); await db.SaveChangesAsync(ct); } } }