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
text/typescript
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);
}