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);
}