summaryrefslogtreecommitdiff
path: root/lib/users/auth/email-auth.ts
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-10-16 18:11:48 +0900
committerjoonhoekim <26rote@gmail.com>2025-10-16 18:11:48 +0900
commitc49d5a42a66d1d29d477cca2ad56f923313c3961 (patch)
tree5d243ae86daf172bd1817283f28d0b58d6c7c540 /lib/users/auth/email-auth.ts
parent5f29ee456fddfb85d1798094cf3e612f4b7647d8 (diff)
(김준회) 파트너 로그인 시, 이메일 화이트리스트 대상은 비밀번호 입력 안하도록 변경
Diffstat (limited to 'lib/users/auth/email-auth.ts')
-rw-r--r--lib/users/auth/email-auth.ts229
1 files changed, 229 insertions, 0 deletions
diff --git a/lib/users/auth/email-auth.ts b/lib/users/auth/email-auth.ts
new file mode 100644
index 00000000..edd4a634
--- /dev/null
+++ b/lib/users/auth/email-auth.ts
@@ -0,0 +1,229 @@
+'use server';
+
+import { eq, and } from 'drizzle-orm';
+import db from '@/db/db';
+import { users, mfaTokens } from '@/db/schema';
+import { isEmailWhitelisted } from '@/lib/email-whitelist/service';
+import { SessionRepository } from '@/lib/users/session/repository';
+import { sendEmail } from '@/lib/mail/sendEmail';
+
+/**
+ * 이메일 화이트리스트 확인 및 인증 시작
+ * 화이트리스트면 바로 Email MFA 시작, 아니면 패스워드 필요
+ */
+export async function checkEmailAndStartAuth(email: string): Promise<{
+ success: boolean;
+ isWhitelisted: boolean;
+ requiresPassword: boolean;
+ userId?: number;
+ userName?: string;
+ tempAuthKey?: string;
+ error?: string;
+ errorCode?: string;
+}> {
+ try {
+ if (!email) {
+ return {
+ success: false,
+ isWhitelisted: false,
+ requiresPassword: false,
+ error: '이메일을 입력해주세요.',
+ errorCode: 'INVALID_INPUT'
+ };
+ }
+
+ const normalizedEmail = email.toLowerCase().trim();
+
+ // 1. 사용자 존재 확인
+ const [user] = await db
+ .select({
+ id: users.id,
+ name: users.name,
+ email: users.email,
+ isActive: users.isActive,
+ isLocked: users.isLocked,
+ lockoutUntil: users.lockoutUntil,
+ })
+ .from(users)
+ .where(eq(users.email, normalizedEmail))
+ .limit(1);
+
+ if (!user) {
+ return {
+ success: false,
+ isWhitelisted: false,
+ requiresPassword: false,
+ error: '등록되지 않은 이메일입니다.',
+ errorCode: 'USER_NOT_FOUND'
+ };
+ }
+
+ // 2. 계정 상태 확인
+ if (!user.isActive) {
+ return {
+ success: false,
+ isWhitelisted: false,
+ requiresPassword: false,
+ error: '비활성화된 계정입니다.',
+ errorCode: 'ACCOUNT_DEACTIVATED'
+ };
+ }
+
+ if (user.isLocked) {
+ const now = new Date();
+ if (user.lockoutUntil && user.lockoutUntil > now) {
+ const remainingMinutes = Math.ceil((user.lockoutUntil.getTime() - now.getTime()) / 60000);
+ return {
+ success: false,
+ isWhitelisted: false,
+ requiresPassword: false,
+ error: `계정이 잠겼습니다. ${remainingMinutes}분 후 다시 시도해주세요.`,
+ errorCode: 'ACCOUNT_LOCKED'
+ };
+ }
+ }
+
+ // 3. 이메일 화이트리스트 확인
+ const whitelisted = await isEmailWhitelisted(normalizedEmail);
+
+ if (whitelisted) {
+ // 화이트리스트 이메일: 바로 Email MFA 시작
+ const expiresAt = new Date();
+ expiresAt.setMinutes(expiresAt.getMinutes() + 10); // 10분 유효
+
+ // 임시 인증 세션 생성 (tempAuthSessions 테이블 사용)
+ const tempAuthKey = await SessionRepository.createTempAuthSession({
+ userId: user.id,
+ email: user.email,
+ authMethod: 'email',
+ expiresAt,
+ });
+
+ // Email OTP 생성 및 전송
+ const otpCode = Math.floor(100000 + Math.random() * 900000).toString();
+ const otpExpiresAt = new Date();
+ otpExpiresAt.setMinutes(otpExpiresAt.getMinutes() + 5); // 5분 유효
+
+ // 기존 OTP 비활성화
+ await db
+ .update(mfaTokens)
+ .set({ isActive: false })
+ .where(
+ and(
+ eq(mfaTokens.userId, user.id),
+ eq(mfaTokens.type, 'email_otp'),
+ eq(mfaTokens.isActive, true)
+ )
+ );
+
+ // 새 OTP 생성
+ await db.insert(mfaTokens).values({
+ userId: user.id,
+ token: otpCode,
+ type: 'email_otp',
+ expiresAt: otpExpiresAt,
+ isActive: true,
+ });
+
+ // OTP 이메일 전송
+ await sendEmail({
+ to: user.email,
+ subject: '로그인 인증번호',
+ template: 'otp',
+ context: {
+ language: 'ko',
+ name: user.name,
+ otp: otpCode,
+ verificationUrl: '',
+ location: '',
+ },
+ });
+
+ return {
+ success: true,
+ isWhitelisted: true,
+ requiresPassword: false,
+ userId: user.id,
+ userName: user.name,
+ tempAuthKey,
+ };
+ } else {
+ // 일반 이메일: 패스워드 입력 필요
+ return {
+ success: true,
+ isWhitelisted: false,
+ requiresPassword: true,
+ userId: user.id,
+ userName: user.name,
+ };
+ }
+ } catch (error) {
+ console.error('Email auth check error:', error);
+ return {
+ success: false,
+ isWhitelisted: false,
+ requiresPassword: false,
+ error: '인증 처리 중 오류가 발생했습니다.',
+ errorCode: 'SYSTEM_ERROR'
+ };
+ }
+}
+
+/**
+ * Email OTP 재전송
+ */
+export async function resendEmailOtp(userId: number, email: string, userName: string): Promise<{
+ success: boolean;
+ error?: string;
+}> {
+ try {
+ // OTP 생성
+ const otpCode = Math.floor(100000 + Math.random() * 900000).toString();
+ const otpExpiresAt = new Date();
+ otpExpiresAt.setMinutes(otpExpiresAt.getMinutes() + 5);
+
+ // 기존 OTP 비활성화
+ await db
+ .update(mfaTokens)
+ .set({ isActive: false })
+ .where(
+ and(
+ eq(mfaTokens.userId, userId),
+ eq(mfaTokens.type, 'email_otp'),
+ eq(mfaTokens.isActive, true)
+ )
+ );
+
+ // 새 OTP 생성
+ await db.insert(mfaTokens).values({
+ userId,
+ token: otpCode,
+ type: 'email_otp',
+ expiresAt: otpExpiresAt,
+ isActive: true,
+ });
+
+ // OTP 이메일 전송
+ await sendEmail({
+ to: email,
+ subject: '로그인 인증번호',
+ template: 'otp',
+ context: {
+ language: 'ko',
+ name: userName,
+ otp: otpCode,
+ verificationUrl: '',
+ location: '',
+ },
+ });
+
+ return { success: true };
+ } catch (error) {
+ console.error('Email OTP resend error:', error);
+ return {
+ success: false,
+ error: '이메일 전송에 실패했습니다.',
+ };
+ }
+}
+