summaryrefslogtreecommitdiff
path: root/lib/users/auth/partners-auth.ts
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-06-27 01:16:20 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-06-27 01:16:20 +0000
commite9897d416b3e7327bbd4d4aef887eee37751ae82 (patch)
treebd20ce6eadf9b21755bd7425492d2d31c7700a0e /lib/users/auth/partners-auth.ts
parent3bf1952c1dad9d479bb8b22031b06a7434d37c37 (diff)
(대표님) 20250627 오전 10시 작업사항
Diffstat (limited to 'lib/users/auth/partners-auth.ts')
-rw-r--r--lib/users/auth/partners-auth.ts374
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
+ };
+}
+