/** * Auth Domain - Types * * Contains ONLY authentication mechanism types: * - Login, Signup, Password Management * - Token Management * - MFA, SSO * * User entity types are in customer domain. * Auth responses reference User from customer domain. */ import { z } from "zod"; import { emailSchema, nameSchema, passwordSchema, phoneSchema } from "../common/schema.js"; import { addressSchema, userSchema } from "../customer/schema.js"; // ============================================================================ // Authentication Request Schemas // ============================================================================ const genderEnum = z.enum(["male", "female", "other"]); const isoDateOnlySchema = z .string() .regex(/^\d{4}-\d{2}-\d{2}$/, "Enter a valid date (YYYY-MM-DD)") .refine(value => !Number.isNaN(Date.parse(value)), "Enter a valid date (YYYY-MM-DD)"); export const loginRequestSchema = z.object({ email: emailSchema, password: z.string().min(1, "Password is required"), }); /** * Base signup input schema (before API transformation) * Used by frontend forms - can be extended with UI-specific fields (e.g., confirmPassword) */ export const signupInputSchema = z.object({ email: emailSchema, password: passwordSchema, firstName: nameSchema, lastName: nameSchema, company: z.string().optional(), phone: phoneSchema, sfNumber: z.string().trim().min(6, "Customer number must be at least 6 characters").optional(), address: addressSchema.optional(), nationality: z.string().optional(), dateOfBirth: isoDateOnlySchema.optional(), gender: genderEnum.optional(), acceptTerms: z.boolean(), marketingConsent: z.boolean().optional(), }); /** * Signup request schema with API transformation * Transforms camelCase fields to match WHMCS API expectations */ export const signupRequestSchema = signupInputSchema.transform(data => ({ ...data, firstname: data.firstName, lastname: data.lastName, companyname: data.company, phonenumber: data.phone, })); export const passwordResetRequestSchema = z.object({ email: emailSchema }); export const passwordResetSchema = z.object({ token: z.string().min(1, "Reset token is required"), password: passwordSchema, }); export const setPasswordRequestSchema = z.object({ email: emailSchema, password: passwordSchema, }); export const changePasswordRequestSchema = z.object({ currentPassword: z.string().min(1, "Current password is required"), newPassword: passwordSchema, }); export const linkWhmcsRequestSchema = z.object({ email: emailSchema, password: z.string().min(1, "Password is required"), }); export const validateSignupRequestSchema = z.object({ sfNumber: z.string().trim().min(1, "Customer number is required").optional(), }); /** * Schema for updating customer profile in WHMCS (single source of truth) * All fields optional - only send what needs to be updated * Can update profile fields and/or address fields in a single request */ export const updateCustomerProfileRequestSchema = z.object({ // Basic profile email: emailSchema.optional(), firstname: nameSchema.optional(), lastname: nameSchema.optional(), companyname: z.string().max(100).optional(), phonenumber: phoneSchema.optional(), // Address (optional fields for partial updates) address1: z.string().max(200).optional(), address2: z.string().max(200).optional(), city: z.string().max(100).optional(), state: z.string().max(100).optional(), postcode: z.string().max(20).optional(), country: z.string().length(2).optional(), // ISO country code // Additional language: z.string().max(10).optional(), }); export const updateProfileRequestSchema = updateCustomerProfileRequestSchema; export const updateAddressRequestSchema = updateCustomerProfileRequestSchema; export const accountStatusRequestSchema = z.object({ email: emailSchema, }); export const ssoLinkRequestSchema = z.object({ destination: z.string().optional(), }); export const checkPasswordNeededRequestSchema = z.object({ email: emailSchema, }); export const refreshTokenRequestSchema = z.object({ refreshToken: z.string().min(1, "Refresh token is required").optional(), deviceId: z.string().optional(), }); // ============================================================================ // Token Schemas // ============================================================================ /** * Password reset token payload schema * Used for validating JWT payload structure in password reset tokens */ export const passwordResetTokenPayloadSchema = z.object({ /** User ID (standard JWT subject) */ sub: z.string().uuid(), /** Unique token identifier for single-use tracking */ tokenId: z.string().uuid(), /** Purpose claim to distinguish from other token types */ purpose: z.literal("password_reset"), }); export type PasswordResetTokenPayload = z.infer; export const authTokensSchema = z.object({ accessToken: z.string().min(1, "Access token is required"), refreshToken: z.string().min(1, "Refresh token is required"), expiresAt: z.string().min(1, "Access token expiry required"), refreshExpiresAt: z.string().min(1, "Refresh token expiry required"), tokenType: z.literal("Bearer"), }); /** * Auth session metadata returned to clients. * * Security note: * - Token strings are stored in httpOnly cookies and MUST NOT be returned to the browser. * - The client only needs expiry metadata for UX (timeout warnings, refresh scheduling). */ export const authSessionSchema = z.object({ expiresAt: z.string().min(1, "Access token expiry required"), refreshExpiresAt: z.string().min(1, "Refresh token expiry required"), tokenType: z.literal("Bearer"), }); // ============================================================================ // Authentication Response Schemas (Reference User from Customer Domain) // ============================================================================ /** * Auth response - returns User from customer domain */ export const authResponseSchema = z.object({ user: userSchema, // User from customer domain session: authSessionSchema, }); /** * Signup result - returns User from customer domain */ export const signupResultSchema = z.object({ user: userSchema, // User from customer domain session: authSessionSchema, }); /** * Password change result - returns User from customer domain */ export const passwordChangeResultSchema = z.object({ user: userSchema, // User from customer domain session: authSessionSchema, }); /** * SSO link response */ export const ssoLinkResponseSchema = z.object({ url: z.url(), expiresAt: z.string(), }); /** * Check password needed response */ export const checkPasswordNeededResponseSchema = z.object({ needsPasswordSet: z.boolean(), userExists: z.boolean(), email: z.email().optional(), }); /** * Link WHMCS response */ export const linkWhmcsResponseSchema = z.object({ user: userSchema, needsPasswordSet: z.boolean(), }); // ============================================================================ // Inferred Types (Schema-First Approach) // ============================================================================ // Request types export type LoginRequest = z.infer; export type SignupRequest = z.infer; export type PasswordResetRequest = z.infer; export type ResetPasswordRequest = z.infer; export type SetPasswordRequest = z.infer; export type ChangePasswordRequest = z.infer; export type LinkWhmcsRequest = z.infer; export type ValidateSignupRequest = z.infer; export type UpdateCustomerProfileRequest = z.infer; export type AccountStatusRequest = z.infer; export type SsoLinkRequest = z.infer; export type CheckPasswordNeededRequest = z.infer; export type RefreshTokenRequest = z.infer; // Token types export type AuthTokens = z.infer; export type AuthSession = z.infer; // Response types export type AuthResponse = z.infer; export type SignupResult = z.infer; export type PasswordChangeResult = z.infer; export type SsoLinkResponse = z.infer; export type CheckPasswordNeededResponse = z.infer; export type LinkWhmcsResponse = z.infer; // ============================================================================ // Error Types // ============================================================================ export interface AuthError { code: | "INVALID_CREDENTIALS" | "EMAIL_NOT_VERIFIED" | "ACCOUNT_LOCKED" | "MFA_REQUIRED" | "INVALID_TOKEN" | "TOKEN_EXPIRED" | "PASSWORD_TOO_WEAK" | "EMAIL_ALREADY_EXISTS" | "WHMCS_ACCOUNT_NOT_FOUND" | "SALESFORCE_ACCOUNT_NOT_FOUND" | "LINKING_FAILED"; message: string; details?: unknown; } // ============================================================================ // Login OTP Flow Schemas // ============================================================================ /** * Response when OTP verification is required after valid credentials * Returned by /api/auth/login when OTP is mandatory */ export const loginOtpRequiredResponseSchema = z.object({ requiresOtp: z.literal(true), sessionToken: z.string(), maskedEmail: z.string(), expiresAt: z.string().datetime(), }); /** * Request to verify OTP code during login * Sent to /api/auth/login/verify-otp */ export const loginVerifyOtpRequestSchema = z.object({ sessionToken: z.string().min(1, "Session token is required"), code: z .string() .length(6, "Code must be 6 digits") .regex(/^\d{6}$/, "Code must be 6 digits"), /** If true, remember this device and skip OTP on future logins for 7 days */ rememberDevice: z.boolean().optional(), }); /** * Union type for login response: either OTP required or full auth response */ export const loginResponseSchema = z.union([loginOtpRequiredResponseSchema, authResponseSchema]); export type LoginOtpRequiredResponse = z.infer; export type LoginVerifyOtpRequest = z.infer; export type LoginResponse = z.infer;