UNPKG

codalware-auth

Version:

Complete authentication system with enterprise security, attack protection, team workspaces, waitlist, billing, UI components, 2FA, and account recovery - production-ready in 5 minutes. Enhanced CLI with verification, rollback, and App Router scaffolding.

500 lines (433 loc) 14.6 kB
import { z } from 'zod'; // Use type-only imports to avoid runtime dependency on @prisma/client during // module evaluation (prevents errors when package is used without Prisma installed). import type { UserRole as PrismaUserRole, UserStatus as PrismaUserStatus } from '@prisma/client'; // Define runtime enum-like objects that match Prisma's generated enums. // This allows validation schemas to reference enum values without requiring // @prisma/client to be installed at runtime in consuming applications. export const UserRole = { SUPER_ADMIN: 'SUPER_ADMIN', TENANT_ADMIN: 'TENANT_ADMIN', USER: 'USER', } as const; export const UserStatus = { PENDING: 'PENDING', APPROVED: 'APPROVED', SUSPENDED: 'SUSPENDED', REJECTED: 'REJECTED', } as const; // Type aliases that reference Prisma types when available, fallback to our const objects export type UserRoleType = PrismaUserRole | typeof UserRole[keyof typeof UserRole]; export type UserStatusType = PrismaUserStatus | typeof UserStatus[keyof typeof UserStatus]; // ============================================================================ // Authentication Schemas // ============================================================================ /** * Email validation schema * - Must be a valid email format * - Maximum length: 255 characters */ export const emailSchema = z .string() .email('Invalid email address') .max(255, 'Email must be less than 255 characters') .toLowerCase() .trim(); /** * Password validation schema * - Minimum length: 8 characters * - Must contain at least one uppercase letter * - Must contain at least one lowercase letter * - Must contain at least one number * - Must contain at least two special characters */ export const passwordSchema = z .string() .min(8, 'Password must be at least 8 characters') .max(128, 'Password must be less than 128 characters') .regex(/[A-Z]/, 'Password must contain at least one uppercase letter') .regex(/[a-z]/, 'Password must contain at least one lowercase letter') .regex(/[0-9]/, 'Password must contain at least one number') .refine( (password) => (password.match(/[^A-Za-z0-9]/g) || []).length >= 2, 'Password must contain at least two special characters' ); /** * Login credentials schema */ export const loginSchema = z.object({ email: emailSchema, password: z.string().min(1, 'Password is required'), tenantDomain: z.string().optional(), totpCode: z.string().length(6, 'TOTP code must be 6 digits').optional(), }); export type LoginInput = z.infer<typeof loginSchema>; /** * Registration schema */ export const registerSchema = z.object({ email: emailSchema, password: passwordSchema, name: z .string() .min(2, 'Name must be at least 2 characters') .max(100, 'Name must be less than 100 characters') .optional(), tenantId: z.string().uuid('Invalid tenant ID').optional(), tenantDomain: z.string().optional(), }); export type RegisterInput = z.infer<typeof registerSchema>; /** * Password reset request schema */ export const passwordResetRequestSchema = z.object({ email: emailSchema, tenantDomain: z.string().optional(), }); export type PasswordResetRequestInput = z.infer<typeof passwordResetRequestSchema>; /** * Password reset schema */ export const passwordResetSchema = z.object({ token: z.string().min(1, 'Reset token is required'), password: passwordSchema, }); export type PasswordResetInput = z.infer<typeof passwordResetSchema>; /** * Password change schema */ export const passwordChangeSchema = z.object({ currentPassword: z.string().min(1, 'Current password is required'), newPassword: passwordSchema, }); export type PasswordChangeInput = z.infer<typeof passwordChangeSchema>; /** * Email verification schema */ export const emailVerificationSchema = z.object({ token: z.string().min(1, 'Verification token is required'), email: emailSchema, }); export type EmailVerificationInput = z.infer<typeof emailVerificationSchema>; // ============================================================================ // Two-Factor Authentication Schemas // ============================================================================ /** * TOTP code validation */ export const totpCodeSchema = z .string() .length(6, 'TOTP code must be 6 digits') .regex(/^\d{6}$/, 'TOTP code must contain only digits'); /** * 2FA setup verification schema */ export const twoFactorSetupSchema = z.object({ secret: z.string().min(1, 'Secret is required'), code: totpCodeSchema, }); export type TwoFactorSetupInput = z.infer<typeof twoFactorSetupSchema>; /** * 2FA verification schema */ export const twoFactorVerificationSchema = z.object({ userId: z.string().uuid('Invalid user ID'), code: totpCodeSchema, }); export type TwoFactorVerificationInput = z.infer<typeof twoFactorVerificationSchema>; /** * Backup code schema */ export const backupCodeSchema = z .string() .length(8, 'Backup code must be 8 characters') .regex(/^[A-Z0-9]{8}$/, 'Invalid backup code format'); // ============================================================================ // Tenant/Organization Schemas // ============================================================================ /** * Domain validation schema */ export const domainSchema = z .string() .min(3, 'Domain must be at least 3 characters') .max(255, 'Domain must be less than 255 characters') .regex( /^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9](?:\.[a-zA-Z]{2,})+$/, 'Invalid domain format' ) .toLowerCase() .trim(); /** * Subdomain validation schema */ export const subdomainSchema = z .string() .min(3, 'Subdomain must be at least 3 characters') .max(63, 'Subdomain must be less than 63 characters') .regex( /^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$/, 'Subdomain can only contain letters, numbers, and hyphens' ) .toLowerCase() .trim(); /** * Tenant creation schema */ export const createTenantSchema = z.object({ name: z .string() .min(2, 'Organization name must be at least 2 characters') .max(100, 'Organization name must be less than 100 characters') .trim(), domain: domainSchema, subdomain: subdomainSchema.optional(), requireAccountApproval: z.boolean().optional().default(false), autoApproveEmails: z.array(emailSchema).optional().default([]), }); export type CreateTenantInput = z.infer<typeof createTenantSchema>; /** * Tenant update schema */ export const updateTenantSchema = createTenantSchema.partial(); export type UpdateTenantInput = z.infer<typeof updateTenantSchema>; // ============================================================================ // User Management Schemas // ============================================================================ /** * User role enum schema * Uses local enum object to avoid runtime Prisma dependency */ export const userRoleSchema = z.enum(['SUPER_ADMIN', 'TENANT_ADMIN', 'USER']); /** * User status enum schema * Uses local enum object to avoid runtime Prisma dependency */ export const userStatusSchema = z.enum(['PENDING', 'APPROVED', 'SUSPENDED', 'REJECTED']); /** * User profile update schema */ export const updateProfileSchema = z.object({ name: z .string() .min(2, 'Name must be at least 2 characters') .max(100, 'Name must be less than 100 characters') .optional(), email: emailSchema.optional(), phoneNumber: z .string() .regex(/^\+?[1-9]\d{1,14}$/, 'Invalid phone number format') .optional(), bio: z .string() .max(500, 'Bio must be less than 500 characters') .optional(), }); export type UpdateProfileInput = z.infer<typeof updateProfileSchema>; /** * User invite schema */ export const inviteUserSchema = z.object({ email: emailSchema, role: userRoleSchema.optional().default(UserRole.USER), tenantId: z.string().uuid('Invalid tenant ID'), }); export type InviteUserInput = z.infer<typeof inviteUserSchema>; // ============================================================================ // Security Settings Schemas // ============================================================================ /** * Account lockout settings schema */ export const accountLockoutSchema = z.object({ enabled: z.boolean().default(true), maxAttempts: z .number() .int() .min(1, 'Max attempts must be at least 1') .max(100, 'Max attempts must be less than 100') .default(5), lockoutDuration: z .number() .int() .min(1, 'Lockout duration must be at least 1 minute') .max(1440, 'Lockout duration must be less than 24 hours') .default(15), resetAfter: z .number() .int() .min(1, 'Reset period must be at least 1 minute') .default(60), }); export type AccountLockoutSettings = z.infer<typeof accountLockoutSchema>; /** * Email restriction settings schema */ export const emailRestrictionSchema = z.object({ mode: z.enum(['OPEN', 'ALLOWLIST', 'BLOCKLIST']).default('OPEN'), allowedDomains: z.array(domainSchema).optional().default([]), blockedDomains: z.array(domainSchema).optional().default([]), allowedEmails: z.array(emailSchema).optional().default([]), blockedEmails: z.array(emailSchema).optional().default([]), }); export type EmailRestrictionSettings = z.infer<typeof emailRestrictionSchema>; /** * Security settings schema */ export const securitySettingsSchema = z.object({ tenantId: z.string().uuid('Invalid tenant ID'), accountLockout: accountLockoutSchema.optional(), emailRestrictions: emailRestrictionSchema.optional(), requireEmailVerification: z.boolean().optional().default(true), require2FA: z.boolean().optional().default(false), sessionTimeout: z .number() .int() .min(5, 'Session timeout must be at least 5 minutes') .max(43200, 'Session timeout must be less than 30 days') .optional() .default(1440), passwordExpiry: z .number() .int() .min(1, 'Password expiry must be at least 1 day') .max(365, 'Password expiry must be less than 365 days') .optional(), }); export type SecuritySettingsInput = z.infer<typeof securitySettingsSchema>; // ============================================================================ // Waitlist Schemas // ============================================================================ /** * Waitlist entry schema */ export const waitlistSchema = z.object({ email: emailSchema, name: z .string() .min(2, 'Name must be at least 2 characters') .max(100, 'Name must be less than 100 characters') .optional(), company: z .string() .max(100, 'Company name must be less than 100 characters') .optional(), role: z .string() .max(100, 'Role must be less than 100 characters') .optional(), notes: z .string() .max(1000, 'Notes must be less than 1000 characters') .optional(), metadata: z.record(z.string(), z.unknown()).optional(), }); export type WaitlistInput = z.infer<typeof waitlistSchema>; // ============================================================================ // Audit Log Schemas // ============================================================================ /** * Audit log action types */ export const auditActionSchema = z.enum([ 'LOGIN', 'LOGOUT', 'REGISTER', 'PASSWORD_CHANGE', 'PASSWORD_RESET', 'EMAIL_VERIFIED', '2FA_ENABLED', '2FA_DISABLED', 'ACCOUNT_LOCKED', 'ACCOUNT_UNLOCKED', 'PROFILE_UPDATED', 'ROLE_CHANGED', 'TENANT_CREATED', 'TENANT_UPDATED', 'USER_INVITED', 'INVITATION_ACCEPTED', ]); export type AuditAction = z.infer<typeof auditActionSchema>; // ============================================================================ // Pagination Schemas // ============================================================================ /** * Pagination query schema */ export const paginationSchema = z.object({ page: z .number() .int() .min(1, 'Page must be at least 1') .optional() .default(1), limit: z .number() .int() .min(1, 'Limit must be at least 1') .max(100, 'Limit must be less than 100') .optional() .default(10), sortBy: z.string().optional(), sortOrder: z.enum(['asc', 'desc']).optional().default('desc'), }); export type PaginationInput = z.infer<typeof paginationSchema>; // ============================================================================ // Webhook Schemas // ============================================================================ /** * Webhook event types */ export const webhookEventSchema = z.enum([ 'user.created', 'user.updated', 'user.deleted', 'tenant.created', 'tenant.updated', 'session.created', 'payment.succeeded', 'payment.failed', 'subscription.created', 'subscription.updated', 'subscription.cancelled', ]); export type WebhookEvent = z.infer<typeof webhookEventSchema>; /** * Webhook configuration schema */ export const webhookConfigSchema = z.object({ url: z.string().url('Invalid webhook URL'), events: z.array(webhookEventSchema).min(1, 'At least one event is required'), secret: z.string().min(32, 'Webhook secret must be at least 32 characters').optional(), enabled: z.boolean().default(true), }); export type WebhookConfig = z.infer<typeof webhookConfigSchema>; // ============================================================================ // Helper Functions // ============================================================================ /** * Safe parse with custom error handling */ export function validateData<T>( schema: z.ZodSchema<T>, data: unknown ): { success: true; data: T } | { success: false; errors: string[] } { const result = schema.safeParse(data); if (result.success) { return { success: true, data: result.data }; } const errors = result.error.issues.map((err) => { const path = err.path.join('.'); return path ? `${path}: ${err.message}` : err.message; }); return { success: false, errors }; } /** * Async validation wrapper */ export async function validateDataAsync<T>( schema: z.ZodSchema<T>, data: unknown ): Promise<T> { return schema.parseAsync(data); }