summaryrefslogtreecommitdiff
path: root/lib/users/auth/validataions-password.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/users/auth/validataions-password.ts')
-rw-r--r--lib/users/auth/validataions-password.ts230
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