summaryrefslogtreecommitdiff
path: root/lib/users/auth/validataions-password.ts
blob: ab73751c509e5fedcbef92420e9afb71c114216c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
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>;