using Avalonia_EFCore.Database; using Avalonia_EFCore.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using System.Security.Cryptography; using System.Text; namespace Avalonia_API.Authentication { public sealed class RefreshTokenService(AppDataContext db, IOptions options) { private readonly JwtOptions _options = options.Value; public async Task<(string Token, ApiRefreshTokenEntity Entity)> CreateAsync( int userId, string? device, string? ipAddress, CancellationToken cancellationToken = default) { var token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64)); var entity = new ApiRefreshTokenEntity { UserId = userId, TokenHash = HashToken(token), ExpiresAt = DateTime.UtcNow.AddDays(_options.RefreshTokenDays), Device = device, IpAddress = ipAddress, }; db.ApiRefreshTokens.Add(entity); await db.SaveChangesAsync(cancellationToken); return (token, entity); } public async Task FindActiveAsync(string? token, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(token)) { return null; } var hash = HashToken(token); var entity = await db.ApiRefreshTokens.FirstOrDefaultAsync(x => x.TokenHash == hash, cancellationToken); return entity?.IsActive == true ? entity : null; } public async Task RevokeAsync(string? token, CancellationToken cancellationToken = default) { var entity = await FindActiveAsync(token, cancellationToken); if (entity is null) { return; } entity.RevokedAt = DateTime.UtcNow; await db.SaveChangesAsync(cancellationToken); } public async Task<(string Token, ApiRefreshTokenEntity Entity)?> RotateAsync( string? token, string? device, string? ipAddress, CancellationToken cancellationToken = default) { var current = await FindActiveAsync(token, cancellationToken); if (current is null) { return null; } var next = await CreateAsync(current.UserId, device, ipAddress, cancellationToken); current.RevokedAt = DateTime.UtcNow; current.ReplacedByTokenHash = next.Entity.TokenHash; await db.SaveChangesAsync(cancellationToken); return next; } private static string HashToken(string token) { var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(token)); return Convert.ToHexString(bytes); } } }