UDT-002: Logout + Refresh Token con rotación y chain revocation #3
72
src/api/SIGCM2.Domain/Entities/RefreshToken.cs
Normal file
72
src/api/SIGCM2.Domain/Entities/RefreshToken.cs
Normal file
@@ -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; }
|
||||||
|
|
||||||
|
/// <summary>Factory for a brand-new session (login). Generates a new FamilyId.</summary>
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>Factory for a rotation. Inherits FamilyId and ExpiresAt (absolute TTL).</summary>
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>Returns true if ExpiresAt <= now (expired).</summary>
|
||||||
|
public bool IsExpired(DateTime now) => now >= ExpiresAt;
|
||||||
|
|
||||||
|
/// <summary>Returns true if the token has been explicitly revoked.</summary>
|
||||||
|
public bool IsRevoked => RevokedAt.HasValue;
|
||||||
|
|
||||||
|
/// <summary>Returns true if the token is neither revoked nor expired.</summary>
|
||||||
|
public bool IsActive(DateTime now) => !IsRevoked && !IsExpired(now);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public void MarkAsPersistedRevocation(DateTime revokedAt, int? replacedById)
|
||||||
|
{
|
||||||
|
RevokedAt = revokedAt;
|
||||||
|
ReplacedById = replacedById;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user