using System.Security.Cryptography; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using Tiku.Application.Auth; using Tiku.Application.Security; using Tiku.Domain.Tenancy; using Tiku.Infrastructure.Persistence; namespace Tiku.Infrastructure.Auth; public sealed class SessionService( TikuDbContext dbContext, ITokenService tokenService, IOptions options) : ISessionService { private readonly JwtOptions options = options.Value; public string GenerateRefreshToken(Guid tenantId, Guid sessionId) { return $"v1.{tenantId:N}.{sessionId:N}.{Base64UrlEncoder.Encode(RandomNumberGenerator.GetBytes(64))}"; } public bool TryParseRefreshToken(string refreshToken, out RefreshTokenLocator locator) { locator = default; if (string.IsNullOrWhiteSpace(refreshToken)) { return false; } var parts = refreshToken.Split('.', 4, StringSplitOptions.None); if (parts.Length != 4 || parts[0] != "v1" || parts[3].Length < 32 || !Guid.TryParseExact(parts[1], "N", out var tenantId) || !Guid.TryParseExact(parts[2], "N", out var sessionId)) { return false; } locator = new RefreshTokenLocator(tenantId, sessionId); return true; } public string HashRefreshToken(string refreshToken) { var hash = SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(refreshToken)); return Convert.ToHexString(hash).ToLowerInvariant(); } public async Task IssueAsync( Guid userId, string? phone, string? email, TenantMembership membership, string provider, string? ipAddress, string? userAgent, CancellationToken cancellationToken = default) { var session = new AuthSession { Id = Guid.NewGuid(), TenantId = membership.TenantId, UserId = userId, TokenHash = string.Empty, Provider = provider, ExpiresAt = DateTimeOffset.UtcNow.AddDays(options.RefreshTokenDays), IpAddress = ipAddress, UserAgent = userAgent }; var refreshToken = GenerateRefreshToken(session.TenantId, session.Id); session.TokenHash = HashRefreshToken(refreshToken); dbContext.AuthSessions.Add(session); await dbContext.SaveChangesAsync(cancellationToken); var accessToken = tokenService.CreateAccessToken( userId, session.Id, phone, email, membership); return new AuthTokenPair( accessToken.Token, refreshToken, accessToken.ExpiresAt, session.ExpiresAt); } }