diff options
Diffstat (limited to 'lib/users/auth/partners-auth.ts')
| -rw-r--r-- | lib/users/auth/partners-auth.ts | 374 |
1 files changed, 374 insertions, 0 deletions
diff --git a/lib/users/auth/partners-auth.ts b/lib/users/auth/partners-auth.ts new file mode 100644 index 00000000..5418e2a8 --- /dev/null +++ b/lib/users/auth/partners-auth.ts @@ -0,0 +1,374 @@ +'use server'; + +import { z } from 'zod'; +import { eq ,and} from 'drizzle-orm'; +import db from '@/db/db'; +import { users, mfaTokens } from '@/db/schema'; +import crypto from 'crypto'; +import { PasswordStrength, passwordResetRequestSchema, passwordResetSchema } from './validataions-password'; +import { sendEmail } from '@/lib/mail/sendEmail'; +import { analyzePasswordStrength, checkPasswordHistory, validatePasswordPolicy } from '@/lib/users/auth/passwordUtil'; + + +export interface PasswordValidationResult { + strength: PasswordStrength; + policyValid: boolean; + policyErrors: string[]; + historyValid?: boolean; +} + +// 비밀번호 재설정 요청 (서버 액션) +export async function requestPasswordResetAction( + prevState: any, + formData: FormData +): Promise<{ success: boolean; error?: string; message?: string }> { + try { + const rawData = { + email: formData.get('email') as string, + }; + + const validatedData = passwordResetRequestSchema.parse(rawData); + + // 사용자 존재 확인 + const user = await db + .select({ + id: users.id, + name: users.name, + email: users.email, + language: users.language + }) + .from(users) + .where(eq(users.email, validatedData.email)) + .limit(1); + + if (!user[0]) { + // 보안상 사용자가 없어도 성공한 것처럼 응답 + return { + success: true, + message: '해당 이메일로 재설정 링크를 전송했습니다. (가입된 이메일인 경우)' + }; + } + + // 기존 재설정 토큰 비활성화 + await db + .update(mfaTokens) + .set({ isActive: false }) + .where(and( + eq(mfaTokens.userId, user[0].id) , + eq(mfaTokens.type, 'password_reset') , + eq(mfaTokens.isActive, true)) + ); + + // 새 토큰 생성 (32바이트 랜덤) + const resetToken = crypto.randomBytes(32).toString('hex'); + const expiresAt = new Date(); + expiresAt.setHours(expiresAt.getHours() + 1); // 1시간 후 만료 + + await db.insert(mfaTokens).values({ + userId: user[0].id, + token: resetToken, + type: 'password_reset', + expiresAt, + isActive: true, + }); + + // 재설정 링크 생성 + const resetLink = `${process.env.NEXTAUTH_URL}/auth/reset-password?token=${resetToken}`; + + // 이메일 전송 + await sendEmail({ + to: user[0].email, + subject: user[0].language === 'ko' ? '비밀번호 재설정 요청' : 'Password Reset Request', + template: 'password-reset', + context: { + language: user[0].language || 'ko', + userName: user[0].name, + resetLink: resetLink, + expiryTime: '1시간', + supportEmail: process.env.SUPPORT_EMAIL || 'support@evcp.com', + }, + }); + + return { + success: true, + message: '비밀번호 재설정 링크를 이메일로 전송했습니다.' + }; + + } catch (error) { + if (error instanceof z.ZodError) { + return { + success: false, + error: error.errors[0]?.message || '입력값이 올바르지 않습니다' + }; + } + + console.error('Password reset request error:', error); + return { + success: false, + error: '재설정 요청 처리 중 오류가 발생했습니다' + }; + } +} + +// 비밀번호 재설정 실행 (서버 액션) +export async function resetPasswordAction( + prevState: any, + formData: FormData +): Promise<{ success: boolean; error?: string; message?: string }> { + try { + const rawData = { + token: formData.get('token') as string, + newPassword: formData.get('newPassword') as string, + confirmPassword: formData.get('confirmPassword') as string, + }; + + const validatedData = passwordResetSchema.parse(rawData); + + // 토큰 검증 + const resetToken = await db + .select({ + id: mfaTokens.id, + userId: mfaTokens.userId, + expiresAt: mfaTokens.expiresAt, + }) + .from(mfaTokens) + .where(and( + eq(mfaTokens.token, validatedData.token) , + eq(mfaTokens.type, 'password_reset') , + eq(mfaTokens.isActive, true) + ) + ) + .limit(1); + + if (!resetToken[0]) { + return { + success: false, + error: '유효하지 않은 재설정 링크입니다' + }; + } + + if (resetToken[0].expiresAt < new Date()) { + // 만료된 토큰 비활성화 + await db + .update(mfaTokens) + .set({ isActive: false }) + .where(eq(mfaTokens.id, resetToken[0].id)); + + return { + success: false, + error: '재설정 링크가 만료되었습니다. 다시 요청해주세요' + }; + } + + // 패스워드 변경 (기존 setPassword 함수 사용) + const { setPassword } = await import('@/lib/users/auth/passwordUtil'); + const result = await setPassword(resetToken[0].userId, validatedData.newPassword); + + if (result.success) { + // 토큰 비활성화 + await db + .update(mfaTokens) + .set({ + isActive: false, + usedAt: new Date(), + }) + .where(eq(mfaTokens.id, resetToken[0].id)); + + // 계정 잠금 해제 (패스워드 재설정 시) + await db + .update(users) + .set({ + isLocked: false, + lockoutUntil: null, + failedLoginAttempts: 0, + }) + .where(eq(users.id, resetToken[0].userId)); + + return { + success: true, + message: '비밀번호가 성공적으로 변경되었습니다. 새 비밀번호로 로그인해주세요.' + }; + } + + return { + success: false, + error: result.errors.join(', ') + }; + + } catch (error) { + if (error instanceof z.ZodError) { + return { + success: false, + error: error.errors[0]?.message || '입력값이 올바르지 않습니다' + }; + } + + console.error('Password reset error:', error); + return { + success: false, + error: '비밀번호 재설정 중 오류가 발생했습니다' + }; + } +} + +// 토큰 유효성 검증 (페이지 접근 시) +export async function validateResetTokenAction( + token: string +): Promise<{ valid: boolean; error?: string; expired?: boolean, userId?: number }> { + try { + if (!token) { + return { valid: false, error: '토큰이 제공되지 않았습니다' }; + } + + const resetToken = await db + .select({ + id: mfaTokens.id, + expiresAt: mfaTokens.expiresAt, + token: mfaTokens.token, + isActive: mfaTokens.isActive, + userId: mfaTokens.userId, + }) + .from(mfaTokens) + .where( + and(eq(mfaTokens.token, token), + eq(mfaTokens.type, 'password_reset') + ) + ) + .limit(1); + + + console.log(token) + console.log(resetToken[0], "resetToken") + + if (!resetToken[0]) { + return { valid: false, error: '유효하지 않은 토큰입니다' }; + } + + if (!resetToken[0].isActive) { + return { valid: false, error: '이미 사용된 토큰입니다' }; + } + + if (resetToken[0].expiresAt < new Date()) { + return { valid: false, expired: true, error: '토큰이 만료되었습니다' }; + } + + return { valid: true, userId: resetToken[0].userId }; + + } catch (error) { + console.error('Token validation error:', error); + return { valid: false, error: '토큰 검증 중 오류가 발생했습니다' }; + } +} + + +export async function validatePasswordAction( + password: string, + userId?: number +): Promise<PasswordValidationResult> { + try { + // 패스워드 강도 분석 + const strength = analyzePasswordStrength(password); + + // 정책 검증 + const policyResult = await validatePasswordPolicy(password); + + // 히스토리 검증 (userId가 있는 경우에만) + let historyValid: boolean | undefined = undefined; + if (userId) { + historyValid = await checkPasswordHistory(userId, password); + } + + return { + strength, + policyValid: policyResult.valid, + policyErrors: policyResult.errors, + historyValid, + }; + } catch (error) { + console.error('Password validation error:', error); + + // 에러 발생 시 기본값 반환 + return { + strength: { + score: 1, + hasUppercase: false, + hasLowercase: false, + hasNumbers: false, + hasSymbols: false, + length: 0, + feedback: ['검증 중 오류가 발생했습니다'], + }, + policyValid: false, + policyErrors: ['검증 중 오류가 발생했습니다'], + }; + } +} + + +// 비활성 유저 탐지 +export async function findInactiveUsers(inactiveDays: number = 90) { + const cutoffDate = new Date(); + cutoffDate.setDate(cutoffDate.getDate() - inactiveDays); + + return await db + .select({ + id: users.id, + email: users.email, + name: users.name, + lastLoginAt: users.lastLoginAt, + }) + .from(users) + .where( + and( + eq(users.isActive, true), + or( + lt(users.lastLoginAt, cutoffDate), + isNull(users.lastLoginAt) // 한번도 로그인하지 않은 유저 + ) + ) + ); +} + +// 유저 비활성화 (Soft Delete) +export async function deactivateUser( + userId: number, + reason: 'INACTIVE' | 'ADMIN' | 'GDPR' = 'INACTIVE' +) { + return await db + .update(users) + .set({ + isActive: false, + deactivatedAt: new Date(), + deactivationReason: reason, + }) + .where(eq(users.id, userId)); +} + +// 배치 비활성화 +export async function deactivateInactiveUsers(inactiveDays: number = 90) { + const inactiveUsers = await findInactiveUsers(inactiveDays); + + if (inactiveUsers.length === 0) { + return { deactivatedCount: 0, users: [] }; + } + + // 로그 기록 + console.log(`Deactivating ${inactiveUsers.length} inactive users`); + + await db + .update(users) + .set({ + isActive: false, + deactivatedAt: new Date(), + deactivationReason: 'INACTIVE', + }) + .where( + inArray(users.id, inactiveUsers.map(u => u.id)) + ); + + return { + deactivatedCount: inactiveUsers.length, + users: inactiveUsers + }; +} + |
