using System.Data.Common; using GB5Shared.DTO.Framework.Login; namespace EntitlementBLL.Auth; /// Result of IssueAndPersistTokenPairAsync — carries the new refresh token row's PK /// alongside the pair, since a caller mid-rotation needs that id to set the OLD row's /// ReplacedByTokenId in the same transaction. public class ClientIssuedTokenPair { public ClientTokenPair TokenPair { get; set; } = null!; public int ClientRefreshTokenId { get; set; } } public enum ClientRefreshResultStatus : byte { Success = 0, /// Token hash not found, expired, or the owning ClientUser is no longer Active — /// a plain, generic rejection. Nothing was ever issued from this token, so there is no chain /// to cascade-revoke. Invalid = 1, /// The presented token hash matched a row that was ALREADY revoked — meaning it was /// already exchanged for a new pair once before, and someone is now presenting a stale copy. /// This is a theft signal, distinct from a plain Invalid rejection: the entire forward chain /// of still-active descendants has been cascade-revoked as a side effect of this result. ReuseDetected = 2 } public class ClientRefreshResult { public ClientRefreshResultStatus Status { get; set; } public ClientTokenPair? TokenPair { get; set; } } public enum ClientLoginResultStatus : byte { Success = 0, /// Unknown email, wrong password, or a non-Active account (Locked/Deleted) — all /// collapse to this single status so a caller can never distinguish which reason applied. Invalid = 1 } public class ClientLoginResult { public ClientLoginResultStatus Status { get; set; } public ClientTokenPair? TokenPair { get; set; } /// The individual-user first-login gate (tracker §50.3) — AgreementVersionIds this /// user has not yet accepted for any currently-Published, RequiresIndividualAcceptance=1 /// agreement type (e.g. an Enterprise Clickwrap EULA). Empty on a Success login with nothing /// pending. The token is still issued either way — this is advisory for the FE to gate /// navigation on, not a second authentication failure mode. public int[] PendingAgreementVersionIds { get; set; } = Array.Empty(); } /// Shared result of CreateClientAdminAsync/CreateClientUserAsync — both funnel through /// the same internal creation logic, differing only in the Role persisted. public class CreateClientUserResult { public bool AlreadyExists { get; set; } public int ClientUserId { get; set; } /// Plaintext temporary password — populated only when a new user was actually /// created. This is the one and only time the plaintext value exists outside this process's /// memory; only its PasswordHasher hash is ever persisted. public string? TemporaryPassword { get; set; } } public enum ClientChangePasswordResultStatus : byte { Success = 0, /// Covers both "current password didn't match" and "ClientUserId not found" — never /// distinguished to the caller. CurrentPasswordInvalid = 1 } public class ClientChangePasswordResult { public ClientChangePasswordResultStatus Status { get; set; } public string Message { get; set; } = string.Empty; } public enum ClientPasswordResetResultStatus : byte { Success = 0, /// Bad signature, wrong action code, already-used token, ClientUserId not found, or /// a tenant mismatch — every rejection reason collapses to this single status. InvalidOrExpiredToken = 1 } public class ClientPasswordResetResult { public ClientPasswordResetResultStatus Status { get; set; } public string Message { get; set; } = string.Empty; } /// /// Client-facing auth orchestration for gb-ent-client Surface B — the real implementation behind /// the currently-placeholder /lic/Auth.svc/GetClientContext. Established now so later items /// (initial login, GOODBOOKS_ADMIN-provisioned first CLIENT_ADMIN, CLIENT_ADMIN-provisioned /// CLIENT_USER accounts) extend the same interface rather than growing a second, parallel auth /// service. /// public interface IClientAuthBLL { /// Issues a token pair via IClientJwtService and persists the refresh-token row in /// the caller-supplied transaction (caller commits/rolls back). Reusable both by a future /// login flow and internally by RefreshAsync's rotation — the single place a refresh-token /// row is ever inserted. Task IssueAndPersistTokenPairAsync( ClientAccessTokenClaims claims, LoginDTO login, DbTransaction tx, CancellationToken ct); /// /// Validates a presented raw refresh token and, if valid and not yet used, rotates it: issues /// a new token pair, links the presented token's ReplacedByTokenId to the new one, and marks /// the presented token revoked — all in one transaction. If the presented token was already /// revoked (i.e. already rotated once before), that is a reuse/theft signal: every /// still-active descendant in the forward ReplacedByTokenId chain is cascade-revoked and the /// request is rejected with ReuseDetected (not a generic Invalid), so a future audit/alerting /// item has a distinct condition to hook into. /// Task RefreshAsync(string rawRefreshToken, LoginDTO login, CancellationToken ct); /// Validates email+password against the caller-supplied ClientId, enforces the /// account-lockout threshold on failure, and issues a token pair on success. Task LoginAsync(int clientId, string email, string password, LoginDTO login, CancellationToken ct); /// Plain, idempotent revoke of the presented refresh token (no ReplacedByTokenId) — /// a voluntary logout is never routed through the reuse-detection cascade. Task LogoutAsync(string rawRefreshToken, LoginDTO login, CancellationToken ct); /// Verifies the caller's current password before persisting a new hash. Task ChangePasswordAsync( int clientUserId, string currentPassword, string newPassword, LoginDTO login, CancellationToken ct); /// GOODBOOKS_ADMIN-only: creates the first CLIENT_ADMIN for a client. Task CreateClientAdminAsync(int clientId, string email, string fullName, LoginDTO login, CancellationToken ct); /// CLIENT_ADMIN-only: creates a CLIENT_USER scoped to the caller's own ClientId. Task CreateClientUserAsync(int clientId, string email, string fullName, LoginDTO login, CancellationToken ct); /// Admin sets a client user's status directly (Active/Locked/Deleted). Reactivating /// TO Active also resets FailedLoginCount to 0. Task SetClientUserStatusAsync(int clientUserId, byte status, LoginDTO login, CancellationToken ct); /// Queues a password-reset email via the existing signed-link-email mechanism. /// Never reveals whether an account was found — an unknown email or a Deleted account is a /// silent no-op, indistinguishable from a queued send to the caller. Task ForgotPasswordAsync(int clientId, string email, LoginDTO login, CancellationToken ct); /// Validates a forgot-password token and, on success, updates the password hash and /// reactivates/clears any lockout state. Task ResetPasswordAsync(string rawToken, string newPassword, LoginDTO login, CancellationToken ct); /// The individual-user first-login gate's acceptance side (tracker §50.3) — records /// that the caller (identified by clientUserId/role from their own verified JWT claims, never /// client-supplied beyond that) accepted the given agreement versions. Re-validates each is /// still Published (via IAgreementConsentProvider.RecordAcceptanceAsync) before writing. Task AcceptPendingAgreementsAsync( int clientUserId, string role, int[] agreementVersionIds, string? ipAddress, string? userAgent, LoginDTO login, CancellationToken ct); }