diff --git a/API/ROLAC.API/Entities/AppRole.cs b/API/ROLAC.API/Entities/AppRole.cs new file mode 100644 index 0000000..08a9cc0 --- /dev/null +++ b/API/ROLAC.API/Entities/AppRole.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Identity; + +namespace ROLAC.API.Entities; + +public class AppRole : IdentityRole +{ + public string? Description { get; set; } +} diff --git a/API/ROLAC.API/Entities/AppUser.cs b/API/ROLAC.API/Entities/AppUser.cs new file mode 100644 index 0000000..c0493d4 --- /dev/null +++ b/API/ROLAC.API/Entities/AppUser.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Identity; + +namespace ROLAC.API.Entities; + +public class AppUser : IdentityUser +{ + /// Links this login account to a church member record. Null for admin-only accounts. + public int? MemberId { get; set; } + + /// UI language preference: 'en' or 'zh-TW'. + public string LanguagePreference { get; set; } = "en"; + + /// False = account suspended (returns 403 even with correct password). + public bool IsActive { get; set; } = true; + + public DateTime? LastLoginAt { get; set; } + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + public ICollection RefreshTokens { get; set; } = new List(); +} diff --git a/API/ROLAC.API/Entities/RefreshToken.cs b/API/ROLAC.API/Entities/RefreshToken.cs new file mode 100644 index 0000000..328e3f9 --- /dev/null +++ b/API/ROLAC.API/Entities/RefreshToken.cs @@ -0,0 +1,29 @@ +namespace ROLAC.API.Entities; + +public class RefreshToken +{ + public int Id { get; set; } + + public string UserId { get; set; } = null!; + public AppUser User { get; set; } = null!; + + /// SHA-256 hex of the raw token sent to the client. Never store raw tokens. + public string TokenHash { get; set; } = null!; + + public DateTime ExpiresAt { get; set; } + public DateTime CreatedAt { get; set; } + + /// Set when this token is revoked (logout or rotation). + public DateTime? RevokedAt { get; set; } + + /// Points to the hash of the token that replaced this one during rotation. + public string? ReplacedByHash { get; set; } + + public string? DeviceInfo { get; set; } + public string? IpAddress { get; set; } + + // Computed helpers — NOT mapped to DB columns (ignored in OnModelCreating) + public bool IsExpired => DateTime.UtcNow >= ExpiresAt; + public bool IsRevoked => RevokedAt.HasValue; + public bool IsActive => !IsRevoked && !IsExpired; +}