diff options
Diffstat (limited to 'lib/users/auth/validataions-password.ts')
| -rw-r--r-- | lib/users/auth/validataions-password.ts | 230 |
1 files changed, 230 insertions, 0 deletions
diff --git a/lib/users/auth/validataions-password.ts b/lib/users/auth/validataions-password.ts new file mode 100644 index 00000000..ab73751c --- /dev/null +++ b/lib/users/auth/validataions-password.ts @@ -0,0 +1,230 @@ +// lib/validations/password.ts + +import { z } from 'zod'; +import { createInsertSchema, createSelectSchema } from 'drizzle-zod'; +import { passwords, passwordHistory, loginAttempts, mfaTokens, users } from '@/db/schema'; + +// Drizzle 테이블에서 자동 생성된 Zod 스키마 +export const insertPasswordSchema = createInsertSchema(passwords); +export const selectPasswordSchema = createSelectSchema(passwords); + +export const insertPasswordHistorySchema = createInsertSchema(passwordHistory); +export const selectPasswordHistorySchema = createSelectSchema(passwordHistory); + +export const insertLoginAttemptSchema = createInsertSchema(loginAttempts); +export const selectLoginAttemptSchema = createSelectSchema(loginAttempts); + +export const insertMfaTokenSchema = createInsertSchema(mfaTokens); +export const selectMfaTokenSchema = createSelectSchema(mfaTokens); + +// 커스텀 검증 스키마들 + +// 패스워드 생성 시 검증 +export const createPasswordSchema = z.object({ + userId: z.number().int().positive(), + password: z.string() + .min(8, "패스워드는 최소 8자 이상이어야 합니다") + .max(128, "패스워드는 최대 128자까지 가능합니다") + .regex(/[A-Z]/, "대문자를 포함해야 합니다") + .regex(/[a-z]/, "소문자를 포함해야 합니다") + .regex(/\d/, "숫자를 포함해야 합니다") + .regex(/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/, "특수문자를 포함해야 합니다") + .refine( + (password) => !/(.)\\1{2,}/.test(password), + "같은 문자가 3번 이상 연속될 수 없습니다" + ) + .refine( + (password) => !/123|abc|qwe|password|admin|login/i.test(password), + "일반적인 패턴이나 단어는 사용할 수 없습니다" + ), + expiresAt: z.date().optional(), +}); + +// 로그인 검증 +export const loginCredentialsSchema = z.object({ + email: z.string() + .email("올바른 이메일 형식이 아닙니다") + .max(255, "이메일이 너무 깁니다"), + password: z.string() + .min(1, "패스워드를 입력해주세요") + .max(128, "패스워드가 너무 깁니다"), +}); + +// MFA SMS 토큰 검증 +export const smsTokenSchema = z.object({ + userId: z.string().or(z.number()).transform(val => Number(val)), + token: z.string() + .length(6, "인증번호는 6자리여야 합니다") + .regex(/^\d{6}$/, "인증번호는 숫자 6자리여야 합니다"), +}); + +// 전화번호 등록 +export const phoneRegistrationSchema = z.object({ + phoneNumber: z.string() + .regex(/^\+?[1-9]\d{1,14}$/, "올바른 전화번호 형식이 아닙니다") + .transform(phone => phone.replace(/[\s-]/g, '')), // 공백, 하이픈 제거 +}); + +// 패스워드 재설정 요청 +export const passwordResetRequestSchema = z.object({ + email: z.string() + .email("올바른 이메일 형식이 아닙니다") + .max(255, "이메일이 너무 깁니다"), +}); + +// 패스워드 재설정 실행 +export const passwordResetSchema = z.object({ + token: z.string() + .min(1, "토큰이 필요합니다") + .max(255, "토큰이 너무 깁니다"), + newPassword: z.string() + .min(8, "패스워드는 최소 8자 이상이어야 합니다") + .max(128, "패스워드는 최대 128자까지 가능합니다") + .regex(/[A-Z]/, "대문자를 포함해야 합니다") + .regex(/[a-z]/, "소문자를 포함해야 합니다") + .regex(/\d/, "숫자를 포함해야 합니다") + .regex(/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/, "특수문자를 포함해야 합니다"), + confirmPassword: z.string(), +}).refine( + (data) => data.newPassword === data.confirmPassword, + { + message: "패스워드가 일치하지 않습니다", + path: ["confirmPassword"], + } +); + +// 보안 설정 업데이트 +export const securitySettingsSchema = z.object({ + minPasswordLength: z.number().int().min(6).max(32).default(8), + requireUppercase: z.boolean().default(true), + requireLowercase: z.boolean().default(true), + requireNumbers: z.boolean().default(true), + requireSymbols: z.boolean().default(true), + passwordExpiryDays: z.number().int().min(0).max(365).nullable().default(90), + passwordHistoryCount: z.number().int().min(1).max(24).default(5), + maxFailedAttempts: z.number().int().min(3).max(20).default(5), + lockoutDurationMinutes: z.number().int().min(5).max(1440).default(30), // 최대 24시간 + requireMfaForPartners: z.boolean().default(true), + smsTokenExpiryMinutes: z.number().int().min(1).max(60).default(5), + maxSmsAttemptsPerDay: z.number().int().min(5).max(50).default(10), + sessionTimeoutMinutes: z.number().int().min(30).max(1440).default(480), // 30분 ~ 24시간 +}); + +// 사용자 등록 (partners용 - 패스워드 포함) +export const userRegistrationSchema = z.object({ + name: z.string() + .min(1, "이름을 입력해주세요") + .max(255, "이름이 너무 깁니다"), + email: z.string() + .email("올바른 이메일 형식이 아닙니다") + .max(255, "이메일이 너무 깁니다"), + password: z.string() + .min(8, "패스워드는 최소 8자 이상이어야 합니다") + .max(128, "패스워드는 최대 128자까지 가능합니다") + .regex(/[A-Z]/, "대문자를 포함해야 합니다") + .regex(/[a-z]/, "소문자를 포함해야 합니다") + .regex(/\d/, "숫자를 포함해야 합니다") + .regex(/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/, "특수문자를 포함해야 합니다"), + confirmPassword: z.string(), + phone: z.string() + .regex(/^\+?[1-9]\d{1,14}$/, "올바른 전화번호 형식이 아닙니다") + .optional(), + domain: z.enum(['partners', 'clients', 'internal']).default('partners'), + companyId: z.number().int().positive().optional(), + techCompanyId: z.number().int().positive().optional(), +}).refine( + (data) => data.password === data.confirmPassword, + { + message: "패스워드가 일치하지 않습니다", + path: ["confirmPassword"], + } +); + +// 패스워드 변경 (기존 패스워드 필요) +export const changePasswordSchema = z.object({ + currentPassword: z.string() + .min(1, "현재 패스워드를 입력해주세요"), + newPassword: z.string() + .min(8, "패스워드는 최소 8자 이상이어야 합니다") + .max(128, "패스워드는 최대 128자까지 가능합니다") + .regex(/[A-Z]/, "대문자를 포함해야 합니다") + .regex(/[a-z]/, "소문자를 포함해야 합니다") + .regex(/\d/, "숫자를 포함해야 합니다") + .regex(/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/, "특수문자를 포함해야 합니다"), + confirmPassword: z.string(), +}).refine( + (data) => data.newPassword === data.confirmPassword, + { + message: "새 패스워드가 일치하지 않습니다", + path: ["confirmPassword"], + } +).refine( + (data) => data.currentPassword !== data.newPassword, + { + message: "새 패스워드는 현재 패스워드와 달라야 합니다", + path: ["newPassword"], + } +); + +// 로그인 이력 조회 필터 +export const loginHistoryFilterSchema = z.object({ + userId: z.string().or(z.number()).transform(val => Number(val)), + limit: z.number().int().min(1).max(100).default(10), + offset: z.number().int().min(0).default(0), + success: z.boolean().optional(), // 성공/실패 필터 + dateFrom: z.date().optional(), + dateTo: z.date().optional(), + ipAddress: z.string().optional(), +}); + +// API 응답 타입들 +export type LoginCredentials = z.infer<typeof loginCredentialsSchema>; +export type SmsToken = z.infer<typeof smsTokenSchema>; +export type PhoneRegistration = z.infer<typeof phoneRegistrationSchema>; +export type PasswordResetRequest = z.infer<typeof passwordResetRequestSchema>; +export type PasswordReset = z.infer<typeof passwordResetSchema>; +export type UserRegistration = z.infer<typeof userRegistrationSchema>; +export type ChangePassword = z.infer<typeof changePasswordSchema>; +export type SecuritySettings = z.infer<typeof securitySettingsSchema>; +export type LoginHistoryFilter = z.infer<typeof loginHistoryFilterSchema>; +export type CreatePassword = z.infer<typeof createPasswordSchema>; + +// 패스워드 강도 결과 타입 +export const passwordStrengthSchema = z.object({ + score: z.number().int().min(1).max(5), + hasUppercase: z.boolean(), + hasLowercase: z.boolean(), + hasNumbers: z.boolean(), + hasSymbols: z.boolean(), + length: z.number().int(), + feedback: z.array(z.string()), +}); + +export type PasswordStrength = z.infer<typeof passwordStrengthSchema>; + +// 인증 결과 타입 +export const authResultSchema = z.object({ + success: z.boolean(), + user: z.object({ + id: z.string(), + name: z.string(), + email: z.string(), + imageUrl: z.string().nullable().optional(), + companyId: z.number().nullable().optional(), + techCompanyId: z.number().nullable().optional(), + domain: z.string().optional(), + }).optional(), + error: z.enum([ + 'INVALID_CREDENTIALS', + 'ACCOUNT_LOCKED', + 'PASSWORD_EXPIRED', + 'ACCOUNT_DISABLED', + 'RATE_LIMITED', + 'MFA_REQUIRED', + 'SYSTEM_ERROR' + ]).optional(), + requiresMfa: z.boolean().optional(), + mfaToken: z.string().optional(), +}); + +export type AuthResult = z.infer<typeof authResultSchema>;
\ No newline at end of file |
