diff --git a/src/api/SIGCM2.Domain/Entities/RefreshToken.cs b/src/api/SIGCM2.Domain/Entities/RefreshToken.cs new file mode 100644 index 0000000..c62353e --- /dev/null +++ b/src/api/SIGCM2.Domain/Entities/RefreshToken.cs @@ -0,0 +1,72 @@ +namespace SIGCM2.Domain.Entities; + +public sealed class RefreshToken +{ + public int Id { get; init; } + public int UsuarioId { get; init; } + public string TokenHash { get; init; } = null!; + public Guid FamilyId { get; init; } + public DateTime IssuedAt { get; init; } + public DateTime ExpiresAt { get; init; } + public DateTime? RevokedAt { get; private set; } + public int? ReplacedById { get; private set; } + public string CreatedByIp { get; init; } = null!; + public string? UserAgent { get; init; } + + /// Factory for a brand-new session (login). Generates a new FamilyId. + public static RefreshToken IssueForNewFamily( + int usuarioId, + string tokenHash, + DateTime now, + TimeSpan ttl, + string createdByIp, + string? userAgent) + => new() + { + UsuarioId = usuarioId, + TokenHash = tokenHash, + FamilyId = Guid.NewGuid(), + IssuedAt = now, + ExpiresAt = now + ttl, + CreatedByIp = createdByIp, + UserAgent = userAgent, + }; + + /// Factory for a rotation. Inherits FamilyId and ExpiresAt (absolute TTL). + public static RefreshToken IssueRotation( + RefreshToken previous, + string newTokenHash, + DateTime now, + string createdByIp, + string? userAgent) + => new() + { + UsuarioId = previous.UsuarioId, + TokenHash = newTokenHash, + FamilyId = previous.FamilyId, + IssuedAt = now, + ExpiresAt = previous.ExpiresAt, // ABSOLUTE — inherited from original + CreatedByIp = createdByIp, + UserAgent = userAgent, + }; + + /// Returns true if ExpiresAt <= now (expired). + public bool IsExpired(DateTime now) => now >= ExpiresAt; + + /// Returns true if the token has been explicitly revoked. + public bool IsRevoked => RevokedAt.HasValue; + + /// Returns true if the token is neither revoked nor expired. + public bool IsActive(DateTime now) => !IsRevoked && !IsExpired(now); + + /// + /// Marks the token as revoked with the given timestamp and optional successor id. + /// Should only be called by the repository when reconstructing or persisting state. + /// This is NOT the way to revoke in business logic — use the repository SQL methods. + /// + public void MarkAsPersistedRevocation(DateTime revokedAt, int? replacedById) + { + RevokedAt = revokedAt; + ReplacedById = replacedById; + } +}