318 lines
10 KiB
TypeScript
Raw Normal View History

/**
* 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<typeof passwordResetTokenPayloadSchema>;
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<typeof loginRequestSchema>;
export type SignupRequest = z.infer<typeof signupRequestSchema>;
export type PasswordResetRequest = z.infer<typeof passwordResetRequestSchema>;
export type ResetPasswordRequest = z.infer<typeof passwordResetSchema>;
export type SetPasswordRequest = z.infer<typeof setPasswordRequestSchema>;
export type ChangePasswordRequest = z.infer<typeof changePasswordRequestSchema>;
export type LinkWhmcsRequest = z.infer<typeof linkWhmcsRequestSchema>;
export type ValidateSignupRequest = z.infer<typeof validateSignupRequestSchema>;
export type UpdateCustomerProfileRequest = z.infer<typeof updateCustomerProfileRequestSchema>;
export type AccountStatusRequest = z.infer<typeof accountStatusRequestSchema>;
export type SsoLinkRequest = z.infer<typeof ssoLinkRequestSchema>;
export type CheckPasswordNeededRequest = z.infer<typeof checkPasswordNeededRequestSchema>;
export type RefreshTokenRequest = z.infer<typeof refreshTokenRequestSchema>;
// Token types
export type AuthTokens = z.infer<typeof authTokensSchema>;
export type AuthSession = z.infer<typeof authSessionSchema>;
// Response types
export type AuthResponse = z.infer<typeof authResponseSchema>;
export type SignupResult = z.infer<typeof signupResultSchema>;
export type PasswordChangeResult = z.infer<typeof passwordChangeResultSchema>;
export type SsoLinkResponse = z.infer<typeof ssoLinkResponseSchema>;
export type CheckPasswordNeededResponse = z.infer<typeof checkPasswordNeededResponseSchema>;
export type LinkWhmcsResponse = z.infer<typeof linkWhmcsResponseSchema>;
// ============================================================================
// 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<typeof loginOtpRequiredResponseSchema>;
export type LoginVerifyOtpRequest = z.infer<typeof loginVerifyOtpRequestSchema>;
export type LoginResponse = z.infer<typeof loginResponseSchema>;