diff --git a/package-lock.json b/package-lock.json index a04d587a..e39e13b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "1.5.0", "license": "ISC", "dependencies": { - "@sendgrid/mail": "^8.1.5", "@supabase/supabase-js": "^2.78.0", "ajv": "^8.17.1", "apns2": "^12.2.0", @@ -41,9 +40,7 @@ "node-cache": "^5.1.2", "node-cron": "^3.0.3", "node-fetch": "^2.7.0", - "nodemailer": "^6.10.0", "pg": "^8.16.3", - "resend": "^6.8.0", "sequelize": "^6.37.5", "slug": "^10.0.0", "solapi": "^5.5.4", @@ -1747,44 +1744,6 @@ "hasInstallScript": true, "license": "Apache-2.0" }, - "node_modules/@sendgrid/client": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-8.1.5.tgz", - "integrity": "sha512-Jqt8aAuGIpWGa15ZorTWI46q9gbaIdQFA21HIPQQl60rCjzAko75l3D1z7EyjFrNr4MfQ0StusivWh8Rjh10Cg==", - "license": "MIT", - "dependencies": { - "@sendgrid/helpers": "^8.0.0", - "axios": "^1.8.2" - }, - "engines": { - "node": ">=12.*" - } - }, - "node_modules/@sendgrid/helpers": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-8.0.0.tgz", - "integrity": "sha512-Ze7WuW2Xzy5GT5WRx+yEv89fsg/pgy3T1E3FS0QEx0/VvRmigMZ5qyVGhJz4SxomegDkzXv/i0aFPpHKN8qdAA==", - "license": "MIT", - "dependencies": { - "deepmerge": "^4.2.2" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/@sendgrid/mail": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-8.1.5.tgz", - "integrity": "sha512-W+YuMnkVs4+HA/bgfto4VHKcPKLc7NiZ50/NH2pzO6UHCCFuq8/GNB98YJlLEr/ESDyzAaDr7lVE7hoBwFTT3Q==", - "license": "MIT", - "dependencies": { - "@sendgrid/client": "^8.1.5", - "@sendgrid/helpers": "^8.0.0" - }, - "engines": { - "node": ">=12.*" - } - }, "node_modules/@sideway/address": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", @@ -1833,12 +1792,6 @@ "@sinonjs/commons": "^3.0.0" } }, - "node_modules/@stablelib/base64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", - "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", - "license": "MIT" - }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -3423,6 +3376,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4278,12 +4232,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-sha256": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", - "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", - "license": "Unlicense" - }, "node_modules/fast-uri": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", @@ -6833,15 +6781,6 @@ "dev": true, "license": "MIT" }, - "node_modules/nodemailer": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.0.tgz", - "integrity": "sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA==", - "license": "MIT-0", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/nodemon": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", @@ -7664,26 +7603,6 @@ "node": ">=0.10.0" } }, - "node_modules/resend": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/resend/-/resend-6.8.0.tgz", - "integrity": "sha512-fDOXGqafQfQXl8nXe93wr93pus8tW7YPpowenE3SmG7dJJf0hH3xUWm3xqacnPvhqjCQTJH9xETg07rmUeSuqQ==", - "license": "MIT", - "dependencies": { - "svix": "1.84.1" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@react-email/render": "*" - }, - "peerDependenciesMeta": { - "@react-email/render": { - "optional": true - } - } - }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -8298,16 +8217,6 @@ "node": ">=8" } }, - "node_modules/standardwebhooks": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", - "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", - "license": "MIT", - "dependencies": { - "@stablelib/base64": "^1.0.0", - "fast-sha256": "^1.3.0" - } - }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -8532,29 +8441,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/svix": { - "version": "1.84.1", - "resolved": "https://registry.npmjs.org/svix/-/svix-1.84.1.tgz", - "integrity": "sha512-K8DPPSZaW/XqXiz1kEyzSHYgmGLnhB43nQCMeKjWGCUpLIpAMMM8kx3rVVOSm6Bo6EHyK1RQLPT4R06skM/MlQ==", - "license": "MIT", - "dependencies": { - "standardwebhooks": "1.0.0", - "uuid": "^10.0.0" - } - }, - "node_modules/svix/node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/swagger-ui-dist": { "version": "5.20.1", "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.20.1.tgz", diff --git a/package.json b/package.json index abd29e04..1888925c 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,6 @@ "license": "ISC", "type": "commonjs", "dependencies": { - "@sendgrid/mail": "^8.1.5", "@supabase/supabase-js": "^2.78.0", "ajv": "^8.17.1", "apns2": "^12.2.0", @@ -83,9 +82,7 @@ "node-cache": "^5.1.2", "node-cron": "^3.0.3", "node-fetch": "^2.7.0", - "nodemailer": "^6.10.0", "pg": "^8.16.3", - "resend": "^6.8.0", "sequelize": "^6.37.5", "slug": "^10.0.0", "solapi": "^5.5.4", diff --git a/src/app.js b/src/app.js index 54635450..f9c98682 100644 --- a/src/app.js +++ b/src/app.js @@ -25,7 +25,8 @@ const applicationRoutes = require("./routes/applicationRoutes"); const dashboardRoutes = require("./routes/dashboardRoutes"); const uploadRoutes = require("./routes/uploadRoutes"); const notificationRoutes = require("./routes/notificationRoutes"); -const verificationRoutes = require("./routes/verificationRoutes"); +// [REMOVED] 학교 이메일 인증이 Supabase OTP로 전환되어 제거됨 +// const verificationRoutes = require("./routes/verificationRoutes"); const smsRoutes = require("./routes/smsRoutes"); const findIdRoutes = require("./routes/findIdRoutes"); const typeTestRoutes = require("./routes/typeTestRoutes"); @@ -95,7 +96,8 @@ app.use("/api/applications", applicationRoutes); app.use("/api/dashboard", dashboardRoutes); app.use("/api/upload", uploadRoutes); app.use("/api/notifications", notificationRoutes); -app.use("/api/auth", verificationRoutes); +// [REMOVED] 학교 이메일 인증이 Supabase OTP로 전환되어 제거됨 +// app.use("/api/auth", verificationRoutes); app.use("/api/auth/sms", smsRoutes); app.use("/api/auth/find-id", findIdRoutes); app.use("/api/type-test", typeTestRoutes); diff --git a/src/config/emailConfig.js b/src/config/emailConfig.js deleted file mode 100644 index d8b48732..00000000 --- a/src/config/emailConfig.js +++ /dev/null @@ -1,146 +0,0 @@ -const nodemailer = require('nodemailer'); -const sgMail = require('@sendgrid/mail'); -const { Resend } = require('resend'); -const fs = require('fs'); -const path = require('path'); - -// SendGrid API 키 설정 (레거시 - 체험판 만료) -if (process.env.SENDGRID_API_KEY) { - sgMail.setApiKey(process.env.SENDGRID_API_KEY); -} - -// Resend 클라이언트 초기화 -const resend = process.env.RESEND_API_KEY ? new Resend(process.env.RESEND_API_KEY) : null; - -// 이메일 서비스 설정 -const emailConfig = { - // Gmail SMTP 설정 - gmail: { - service: 'gmail', - auth: { - user: process.env.EMAIL_USER, - pass: process.env.EMAIL_APP_PASSWORD // Gmail 앱 비밀번호 사용 - } - }, - - // SendGrid 설정 (SMTP) - sendgrid: { - host: 'smtp.sendgrid.net', - port: 587, - secure: false, - auth: { - user: 'apikey', - pass: process.env.SENDGRID_API_KEY - } - }, - - // 기본 설정 - default: { - host: process.env.SMTP_HOST || 'smtp.gmail.com', - port: process.env.SMTP_PORT || 587, - secure: false, - auth: { - user: process.env.EMAIL_USER, - pass: process.env.EMAIL_PASSWORD - } - } -}; - -// 현재 환경에 맞는 설정 반환 -const getEmailConfig = () => { - const emailService = process.env.EMAIL_SERVICE || 'default'; - return emailConfig[emailService] || emailConfig.default; -}; - -// 이메일 전송기 생성 -const createTransporter = () => { - const config = getEmailConfig(); - return nodemailer.createTransport(config); -}; - -// Resend를 사용한 이메일 전송 (권장) -const sendEmailWithResend = async (mailOptions) => { - try { - if (!resend) { - throw new Error('RESEND_API_KEY가 설정되지 않았습니다.'); - } - - const { data, error } = await resend.emails.send({ - from: mailOptions.from || process.env.EMAIL_FROM || 'TeamItaka ', - to: [mailOptions.to], - subject: mailOptions.subject, - html: mailOptions.html, - text: mailOptions.text - }); - - if (error) { - console.error('Resend 이메일 전송 실패:', error); - throw new Error(error.message); - } - - console.log('Resend 이메일 전송 성공:', data); - return data; - } catch (error) { - console.error('Resend 이메일 전송 실패:', error); - throw error; - } -}; - -// SendGrid를 사용한 이메일 전송 (레거시 - 체험판 만료) -const sendEmailWithSendGrid = async (mailOptions) => { - try { - const msg = { - to: mailOptions.to, - from: mailOptions.from || getFromEmail(), - subject: mailOptions.subject, - text: mailOptions.text, - html: mailOptions.html - }; - - // 배너 이미지 첨부 (첨부파일과 인라인 이미지로 사용) - if (mailOptions.attachments) { - msg.attachments = mailOptions.attachments; - } else { - // 기본 배너 이미지 첨부 - const bannerPath = path.join(__dirname, '../img/teamitaka_banner.png'); - if (fs.existsSync(bannerPath)) { - const bannerContent = fs.readFileSync(bannerPath).toString('base64'); - msg.attachments = [ - { - content: bannerContent, - filename: 'teamitaka_banner.png', - type: 'image/png', - disposition: 'inline', - content_id: 'banner' - } - ]; - } - } - - const result = await sgMail.send(msg); - return result; - } catch (error) { - console.error('SendGrid 이메일 전송 실패:', error); - throw error; - } -}; - -// 발신자 정보 -const getFromEmail = () => { - return process.env.EMAIL_FROM || 'noreply@teamitaka.com'; -}; - -// 도메인 인증 확인 -const isDomainAuthenticated = () => { - const fromEmail = getFromEmail(); - return fromEmail.includes('teamitaka.com'); -}; - -module.exports = { - createTransporter, - getFromEmail, - getEmailConfig, - isDomainAuthenticated, - sendEmailWithSendGrid, - sendEmailWithResend -}; diff --git a/src/config/supabase.js b/src/config/supabase.js index b639994c..918d78fc 100644 --- a/src/config/supabase.js +++ b/src/config/supabase.js @@ -1,12 +1,15 @@ const { createClient } = require('@supabase/supabase-js'); const supabaseUrl = process.env.SUPABASE_URL; -const supabaseKey = process.env.SUPABASE_ANON_KEY; +const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY; -if (!supabaseUrl || !supabaseKey) { - throw new Error('❌ SUPABASE_URL 또는 SUPABASE_ANON_KEY 환경 변수가 설정되지 않았습니다.'); -} +let supabase = null; -const supabase = createClient(supabaseUrl, supabaseKey); +if (supabaseUrl && supabaseServiceRoleKey) { + supabase = createClient(supabaseUrl, supabaseServiceRoleKey); + console.log('✅ Supabase client initialized'); +} else { + console.warn('⚠️ SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY is not set - Supabase email verification will not work'); +} -module.exports = { supabase }; +module.exports = supabase; diff --git a/src/controllers/authController.js b/src/controllers/authController.js index 9f24ac31..53874625 100644 --- a/src/controllers/authController.js +++ b/src/controllers/authController.js @@ -8,6 +8,7 @@ const { v4: uuidv4 } = require("uuid"); // ✅ UUID 생성 모듈 추가 const { jwtSecret } = require("../config/authConfig"); const { verifyGoogleIdToken } = require("../utils/googleTokenVerifier"); const { parseResidentNumber, formatPhoneNumber } = require("../utils/registrationUtils"); +const supabase = require("../config/supabase"); exports.register = async (req, res) => { try { @@ -26,6 +27,8 @@ exports.register = async (req, res) => { // 인증 상태 isSmsVerified, isEmailVerified, + // Supabase 학교 이메일 인증 토큰 + supabaseAccessToken, // 기존 필드 (호환성 유지) university, department, @@ -40,7 +43,30 @@ exports.register = async (req, res) => { return res.status(400).json({ error: "❌ 이메일과 비밀번호를 입력해주세요." }); } - // SMS 인증 상태 검증 (SMS 인증 우선) + // Supabase 학교 이메일 인증 검증 (이메일 인증 시) + if (isEmailVerified && supabaseAccessToken) { + if (!supabase) { + console.log('❌ Supabase client not initialized - cannot verify email'); + return res.status(500).json({ error: "❌ 서버 설정 오류: 이메일 인증을 처리할 수 없습니다." }); + } + + const { data: { user: supabaseUser }, error: supabaseError } = await supabase.auth.getUser(supabaseAccessToken); + + if (supabaseError || !supabaseUser?.email_confirmed_at) { + console.log(`❌ Supabase 이메일 인증 실패: ${supabaseError?.message || '이메일 미인증'}`); + return res.status(401).json({ error: "❌ 학교 이메일 인증이 완료되지 않았습니다." }); + } + + // 인증된 이메일과 요청 이메일 일치 확인 + if (supabaseUser.email !== userEmail) { + console.log(`❌ 이메일 불일치: Supabase(${supabaseUser.email}) vs Request(${userEmail})`); + return res.status(400).json({ error: "❌ 인증된 이메일이 일치하지 않습니다." }); + } + + console.log(`✅ Supabase 이메일 인증 확인 완료: ${userEmail}`); + } + + // SMS 인증 또는 이메일 인증 상태 검증 if (!isSmsVerified && !isEmailVerified) { return res.status(400).json({ error: "❌ SMS 인증 또는 이메일 인증을 완료해주세요." }); } diff --git a/src/controllers/uploadController.js b/src/controllers/uploadController.js index 02c4d020..6328e61f 100644 --- a/src/controllers/uploadController.js +++ b/src/controllers/uploadController.js @@ -1,4 +1,4 @@ -const { supabase } = require('../config/supabase'); +const supabase = require('../config/supabase'); const multer = require('multer'); const path = require('path'); const { v4: uuidv4 } = require('uuid'); @@ -47,6 +47,13 @@ const uploadRecruitmentImage = async (req, res) => { } try { + if (!supabase) { + return res.status(500).json({ + success: false, + message: 'Supabase가 설정되지 않았습니다.', + }); + } + const file = req.file; const fileExt = path.extname(file.originalname); const fileName = `${uuidv4()}${fileExt}`; @@ -125,6 +132,13 @@ const uploadProfileImage = async (req, res) => { } try { + if (!supabase) { + return res.status(500).json({ + success: false, + message: 'Supabase가 설정되지 않았습니다.', + }); + } + const file = req.file; const fileExt = path.extname(file.originalname); const fileName = `${uuidv4()}${fileExt}`; diff --git a/src/controllers/verificationController.js b/src/controllers/verificationController.js deleted file mode 100644 index e5f5b91c..00000000 --- a/src/controllers/verificationController.js +++ /dev/null @@ -1,269 +0,0 @@ -const verificationService = require('../services/verificationService'); -const { - logVerificationAttempt, - logEmailSent, - logEmailFailed, - logSecurityEvent -} = require('../utils/logger'); - -// 인증번호 전송 -exports.sendVerification = async (req, res) => { - let email, clientIP, userAgent; - - try { - email = req.body.email; - clientIP = req.ip || req.connection.remoteAddress; - userAgent = req.get('User-Agent'); - - console.log(`[VERIFICATION] 인증 요청 시작: ${email}, IP: ${clientIP}`); - - // 1. 이메일 형식 검증 - if (!email) { - console.log('[VERIFICATION] 이메일 누락'); - return res.status(400).json({ - error: 'MISSING_EMAIL', - message: '이메일을 입력해주세요.' - }); - } - - if (!verificationService.validateEmailFormat(email)) { - console.log(`[VERIFICATION] 잘못된 이메일 형식: ${email}`); - return res.status(400).json({ - error: 'INVALID_EMAIL_FORMAT', - message: '올바른 이메일 형식이 아닙니다.' - }); - } - - console.log(`[VERIFICATION] 이메일 형식 검증 통과: ${email}`); - - // 2. 중복 이메일 확인 - console.log(`[VERIFICATION] 중복 이메일 확인 중: ${email}`); - const isDuplicate = await verificationService.checkDuplicateEmail(email); - if (isDuplicate) { - console.log(`[VERIFICATION] 중복 이메일 발견: ${email}`); - return res.status(409).json({ - error: 'DUPLICATE_EMAIL', - message: '이미 등록된 이메일입니다.' - }); - } - - console.log(`[VERIFICATION] 중복 이메일 확인 통과: ${email}`); - - // 3. 인증번호 생성 - const verificationCode = verificationService.generateVerificationCode(); - console.log(`[VERIFICATION] 인증번호 생성 완료: ${email}, 코드: ${verificationCode.substring(0, 2)}****`); - - // 4. 이전 인증번호 무효화 - console.log(`[VERIFICATION] 이전 인증번호 무효화 중: ${email}`); - await verificationService.invalidatePreviousCodes(email); - console.log(`[VERIFICATION] 이전 인증번호 무효화 완료: ${email}`); - - // 5. 인증번호 저장 - console.log(`[VERIFICATION] 인증번호 저장 중: ${email}`); - await verificationService.saveVerification(email, verificationCode, clientIP, userAgent); - console.log(`[VERIFICATION] 인증번호 저장 완료: ${email}`); - - // 6. 이메일 발송 - console.log(`[VERIFICATION] 이메일 발송 시작: ${email}`); - const emailResult = await verificationService.sendVerificationEmail(email, verificationCode); - console.log(`[VERIFICATION] 이메일 발송 완료: ${email}, Message ID: ${emailResult.messageId || 'N/A'}`); - - // 7. 성공 응답 - logEmailSent(email, emailResult.messageId, 'sendgrid', { ip: clientIP }); - - console.log(`[VERIFICATION] 인증 요청 성공: ${email}`); - res.status(200).json({ - success: true, - message: '인증번호가 이메일로 전송되었습니다.', - data: { - email: email, - expiresIn: 180 // 3분 (초 단위) - } - }); - - } catch (error) { - // 에러 로깅 시 변수가 정의되지 않았을 수 있으므로 안전하게 처리 - const errorEmail = email || 'unknown'; - const errorIP = clientIP || 'unknown'; - - console.error(`[VERIFICATION] 오류 발생: ${errorEmail}`); - console.error(`[VERIFICATION] 오류 상세:`, error); - console.error(`[VERIFICATION] 오류 스택:`, error.stack); - console.error(`[VERIFICATION] 환경 정보: NODE_ENV=${process.env.NODE_ENV}, DB_HOST=${process.env.GCP_DB_HOST || 'N/A'}`); - - logEmailFailed(errorEmail, error, 'sendgrid', { ip: errorIP }); - - res.status(500).json({ - error: 'SERVER_ERROR', - message: '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.' - }); - } -}; - -// 인증번호 확인 -exports.verifyCode = async (req, res) => { - const clientIP = req.ip || req.connection.remoteAddress; - let email = ''; - - try { - const { email: requestEmail, code } = req.body; - email = requestEmail; - - // 1. 입력값 검증 - if (!email || !code) { - return res.status(400).json({ - error: 'MISSING_PARAMETERS', - message: '이메일과 인증번호를 모두 입력해주세요.' - }); - } - - if (!verificationService.validateEmailFormat(email)) { - return res.status(400).json({ - error: 'INVALID_EMAIL_FORMAT', - message: '올바른 이메일 형식이 아닙니다.' - }); - } - - if (code.length !== 6 || !/^\d{6}$/.test(code)) { - return res.status(400).json({ - error: 'INVALID_CODE_FORMAT', - message: '6자리 숫자 인증번호를 입력해주세요.' - }); - } - - // 2. 인증번호 검증 - const verificationResult = await verificationService.verifyCode(email, code); - - if (!verificationResult.valid) { - logVerificationAttempt(email, false, clientIP, req.get('User-Agent'), { - code: code.substring(0, 2) + '****' - }); - - return res.status(400).json({ - error: 'VERIFICATION_FAILED', - message: verificationResult.message - }); - } - - // 3. 성공 응답 - logVerificationAttempt(email, true, clientIP, req.get('User-Agent')); - - res.status(200).json({ - success: true, - message: '이메일 인증이 완료되었습니다.', - data: { - email: email, - verifiedAt: new Date().toISOString() - } - }); - - } catch (error) { - logSecurityEvent('인증번호 확인 오류', { email, error: error.message }, { ip: clientIP }); - - res.status(500).json({ - error: 'SERVER_ERROR', - message: '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.' - }); - } -}; - -// 인증 상태 확인 -exports.getVerificationStatus = async (req, res) => { - try { - const { email } = req.query; - - if (!email) { - return res.status(400).json({ - error: 'MISSING_EMAIL', - message: '이메일을 입력해주세요.' - }); - } - - if (!verificationService.validateEmailFormat(email)) { - return res.status(400).json({ - error: 'INVALID_EMAIL_FORMAT', - message: '올바른 이메일 형식이 아닙니다.' - }); - } - - const status = await verificationService.getVerificationStatus(email); - - res.status(200).json({ - success: true, - data: status - }); - - } catch (error) { - logger.error('인증 상태 확인 중 오류:', error); - - res.status(500).json({ - error: 'SERVER_ERROR', - message: '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.' - }); - } -}; - -// 인증번호 재전송 -exports.resendVerification = async (req, res) => { - try { - const { email } = req.body; - const clientIP = req.ip || req.connection.remoteAddress; - const userAgent = req.get('User-Agent'); - - // 1. 이메일 형식 검증 - if (!email) { - return res.status(400).json({ - error: 'MISSING_EMAIL', - message: '이메일을 입력해주세요.' - }); - } - - if (!verificationService.validateEmailFormat(email)) { - return res.status(400).json({ - error: 'INVALID_EMAIL_FORMAT', - message: '올바른 이메일 형식이 아닙니다.' - }); - } - - // 2. 중복 이메일 확인 - const isDuplicate = await verificationService.checkDuplicateEmail(email); - if (isDuplicate) { - return res.status(409).json({ - error: 'DUPLICATE_EMAIL', - message: '이미 등록된 이메일입니다.' - }); - } - - // 3. 새로운 인증번호 생성 - const verificationCode = verificationService.generateVerificationCode(); - - // 4. 이전 인증번호 무효화 - await verificationService.invalidatePreviousCodes(email); - - // 5. 새 인증번호 저장 - await verificationService.saveVerification(email, verificationCode, clientIP, userAgent); - - // 6. 이메일 발송 - await verificationService.sendVerificationEmail(email, verificationCode); - - // 7. 성공 응답 - logger.info(`인증번호 재전송 성공: ${email}`, { ip: clientIP }); - - res.status(200).json({ - success: true, - message: '새로운 인증번호가 이메일로 전송되었습니다.', - data: { - email: email, - expiresIn: 180 // 3분 (초 단위) - } - }); - - } catch (error) { - logger.error('인증번호 재전송 중 오류:', error); - - res.status(500).json({ - error: 'SERVER_ERROR', - message: '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.' - }); - } -}; diff --git a/src/middlewares/validationMiddleware.js b/src/middlewares/validationMiddleware.js index 5db479dc..c848081e 100644 --- a/src/middlewares/validationMiddleware.js +++ b/src/middlewares/validationMiddleware.js @@ -1,9 +1,4 @@ -const { - validateSendVerification, - validateVerifyCode, - validateVerificationStatus, - validateResendVerification -} = require('../validations/verificationValidation'); +// [REMOVED] 이메일 인증 관련 validation - Supabase OTP로 전환됨 const { validateSendSmsVerification, validateVerifySmsCode @@ -15,7 +10,7 @@ const createValidationError = (details) => { field: detail.path.join('.'), message: detail.message })); - + return { error: 'VALIDATION_ERROR', message: '입력값 검증에 실패했습니다.', @@ -23,58 +18,6 @@ const createValidationError = (details) => { }; }; -// 인증번호 전송 검증 미들웨어 -const validateSendVerificationInput = (req, res, next) => { - const { error, value } = validateSendVerification(req.body); - - if (error) { - return res.status(400).json(createValidationError(error.details)); - } - - // 검증된 데이터를 req.body에 할당 - req.body = value; - next(); -}; - -// 인증번호 확인 검증 미들웨어 -const validateVerifyCodeInput = (req, res, next) => { - const { error, value } = validateVerifyCode(req.body); - - if (error) { - return res.status(400).json(createValidationError(error.details)); - } - - // 검증된 데이터를 req.body에 할당 - req.body = value; - next(); -}; - -// 인증 상태 확인 검증 미들웨어 -const validateVerificationStatusInput = (req, res, next) => { - const { error, value } = validateVerificationStatus(req.query); - - if (error) { - return res.status(400).json(createValidationError(error.details)); - } - - // 검증된 데이터를 req.query에 할당 - req.query = value; - next(); -}; - -// 인증번호 재전송 검증 미들웨어 -const validateResendVerificationInput = (req, res, next) => { - const { error, value } = validateResendVerification(req.body); - - if (error) { - return res.status(400).json(createValidationError(error.details)); - } - - // 검증된 데이터를 req.body에 할당 - req.body = value; - next(); -}; - // 범용 검증 미들웨어 (스키마를 매개변수로 받음) const validateInput = (schema) => { return (req, res, next) => { @@ -122,10 +65,6 @@ const validateVerifySmsCodeInput = (req, res, next) => { }; module.exports = { - validateSendVerificationInput, - validateVerifyCodeInput, - validateVerificationStatusInput, - validateResendVerificationInput, validateInput, validateSendSmsVerificationInput, validateVerifySmsCodeInput diff --git a/src/middlewares/verificationRateLimit.js b/src/middlewares/verificationRateLimit.js deleted file mode 100644 index b44cded3..00000000 --- a/src/middlewares/verificationRateLimit.js +++ /dev/null @@ -1,137 +0,0 @@ -const rateLimit = require('express-rate-limit'); - -// 이메일 전송 제한: 같은 이메일로 1분에 1회만 전송 가능 -const emailSendLimit = rateLimit({ - windowMs: 60 * 1000, // 1분 - max: 1, // 같은 이메일로 1회 - keyGenerator: (req) => req.body.email, - message: { - error: 'RATE_LIMIT_EXCEEDED', - message: '1분에 1회만 전송 가능합니다.', - retryAfter: Math.ceil(60 / 1000) // 60초 - }, - standardHeaders: true, - legacyHeaders: false, - handler: (req, res) => { - res.status(429).json({ - error: 'RATE_LIMIT_EXCEEDED', - message: '1분에 1회만 전송 가능합니다.', - retryAfter: 60 - }); - } -}); - -// 일일 이메일 제한: 같은 이메일로 하루 최대 5회 전송 (프로덕션용) -const dailyEmailLimit = rateLimit({ - windowMs: 24 * 60 * 60 * 1000, // 24시간 - max: 5, // 같은 이메일로 5회 - keyGenerator: (req) => req.body.email, - message: { - error: 'DAILY_LIMIT_EXCEEDED', - message: '하루 최대 5회 전송 가능합니다.', - retryAfter: Math.ceil(24 * 60 * 60 / 1000) // 24시간 - }, - standardHeaders: true, - legacyHeaders: false, - handler: (req, res) => { - res.status(429).json({ - error: 'DAILY_LIMIT_EXCEEDED', - message: '하루 최대 5회 전송 가능합니다.', - retryAfter: 24 * 60 * 60 - }); - } -}); - -// 테스트 환경용 일일 이메일 제한: 같은 이메일로 하루 최대 100회 전송 -const testDailyEmailLimit = rateLimit({ - windowMs: 24 * 60 * 60 * 1000, // 24시간 - max: 100, // 같은 이메일로 100회 (테스트용) - keyGenerator: (req) => req.body.email, - message: { - error: 'DAILY_LIMIT_EXCEEDED', - message: '하루 최대 100회 전송 가능합니다.', - retryAfter: Math.ceil(24 * 60 * 60 / 1000) // 24시간 - }, - standardHeaders: true, - legacyHeaders: false, - handler: (req, res) => { - res.status(429).json({ - error: 'DAILY_LIMIT_EXCEEDED', - message: '하루 최대 100회 전송 가능합니다.', - retryAfter: 24 * 60 * 60 - }); - } -}); - -// IP 기반 일일 제한: 같은 IP에서 하루 최대 20회 전송 (프로덕션용) -const dailyIPLimit = rateLimit({ - windowMs: 24 * 60 * 60 * 1000, // 24시간 - max: 20, // 같은 IP에서 20회 - keyGenerator: (req) => req.ip || req.connection.remoteAddress, - message: { - error: 'IP_LIMIT_EXCEEDED', - message: '하루 최대 20회 전송 가능합니다.', - retryAfter: Math.ceil(24 * 60 * 60 / 1000) // 24시간 - }, - standardHeaders: true, - legacyHeaders: false, - handler: (req, res) => { - res.status(429).json({ - error: 'IP_LIMIT_EXCEEDED', - message: '하루 최대 20회 전송 가능합니다.', - retryAfter: 24 * 60 * 60 - }); - } -}); - -// 테스트 환경용 IP 기반 일일 제한: 같은 IP에서 하루 최대 500회 전송 -const testDailyIPLimit = rateLimit({ - windowMs: 24 * 60 * 60 * 1000, // 24시간 - max: 500, // 같은 IP에서 500회 (테스트용) - keyGenerator: (req) => req.ip || req.connection.remoteAddress, - message: { - error: 'IP_LIMIT_EXCEEDED', - message: '하루 최대 500회 전송 가능합니다.', - retryAfter: Math.ceil(24 * 60 * 60 / 1000) // 24시간 - }, - standardHeaders: true, - legacyHeaders: false, - handler: (req, res) => { - res.status(429).json({ - error: 'IP_LIMIT_EXCEEDED', - message: '하루 최대 500회 전송 가능합니다.', - retryAfter: 24 * 60 * 60 - }); - } -}); - -// 인증번호 확인 제한: 같은 이메일로 1분에 최대 5회 시도 -const verificationAttemptLimit = rateLimit({ - windowMs: 60 * 1000, // 1분 - max: 5, // 같은 이메일로 5회 - keyGenerator: (req) => req.body.email, - message: { - error: 'VERIFICATION_ATTEMPT_LIMIT_EXCEEDED', - message: '1분에 최대 5회 시도 가능합니다.', - retryAfter: Math.ceil(60 / 1000) // 60초 - }, - standardHeaders: true, - legacyHeaders: false, - handler: (req, res) => { - res.status(429).json({ - error: 'VERIFICATION_ATTEMPT_LIMIT_EXCEEDED', - message: '1분에 최대 5회 시도 가능합니다.', - retryAfter: 60 - }); - } -}); - -module.exports = { - emailSendLimit, - dailyEmailLimit, - dailyIPLimit, - verificationAttemptLimit, - // 테스트 환경용 제한 - testDailyEmailLimit, - testDailyIPLimit -}; diff --git a/src/routes/devRoutes.js b/src/routes/devRoutes.js index 37c475dc..dc230f0d 100644 --- a/src/routes/devRoutes.js +++ b/src/routes/devRoutes.js @@ -1,7 +1,6 @@ const express = require("express"); const router = express.Router(); const devController = require("../controllers/devController"); -const { createTransporter, getFromEmail, sendEmailWithSendGrid } = require("../config/emailConfig"); const jwt = require("jsonwebtoken"); const { jwtSecret } = require("../config/authConfig"); const { User } = require("../models"); @@ -46,43 +45,6 @@ router.post("/test-token", async (req, res) => { } }); -// SendGrid 연결 테스트 엔드포인트 (개발 환경에서만 사용) -router.post("/test-sendgrid", async (req, res) => { - try { - const { testEmail = 'test@example.com' } = req.body; - - const mailOptions = { - from: getFromEmail(), - to: testEmail, - subject: 'SendGrid 연결 테스트 - TEAMITAKA', - text: 'SendGrid 연결이 성공했습니다!', - html: ` -

🎉 SendGrid 연결 성공!

-

TEAMITAKA 백엔드에서 SendGrid를 통해 이메일을 성공적으로 발송했습니다.

-

발송 시간: ${new Date().toLocaleString('ko-KR')}

-

API: SendGrid Web API

-

도메인: teamitaka.com

- ` - }; - - // SendGrid로 이메일 발송 - const result = await sendEmailWithSendGrid(mailOptions); - - res.json({ - success: true, - messageId: result[0]?.headers['x-message-id'] || 'N/A', - message: 'SendGrid 테스트 이메일 발송 성공', - timestamp: new Date().toISOString(), - provider: 'SendGrid Web API' - }); - } catch (error) { - console.error('SendGrid 테스트 실패:', error); - res.status(500).json({ - error: 'SENDGRID_TEST_FAILED', - message: error.message, - details: process.env.NODE_ENV === 'development' ? error.stack : undefined - }); - } -}); +// [REMOVED] SendGrid 테스트 엔드포인트 - 학교 이메일 인증이 Supabase OTP로 전환됨 module.exports = router; diff --git a/src/routes/verificationRoutes.js b/src/routes/verificationRoutes.js deleted file mode 100644 index 4d8a5398..00000000 --- a/src/routes/verificationRoutes.js +++ /dev/null @@ -1,50 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const verificationController = require('../controllers/verificationController'); -const { - emailSendLimit, - dailyEmailLimit, - dailyIPLimit, - verificationAttemptLimit, - testDailyEmailLimit, - testDailyIPLimit -} = require('../middlewares/verificationRateLimit'); -const { - validateSendVerificationInput, - validateVerifyCodeInput, - validateVerificationStatusInput, - validateResendVerificationInput -} = require('../middlewares/validationMiddleware'); - -// 인증번호 전송 (속도 제한 적용) -router.post('/send-verification', - validateSendVerificationInput, // 입력값 검증 - emailSendLimit, // 1분에 1회 - process.env.NODE_ENV === 'production' ? dailyEmailLimit : testDailyEmailLimit, // 환경별 일일 제한 - process.env.NODE_ENV === 'production' ? dailyIPLimit : testDailyIPLimit, // 환경별 IP 제한 - verificationController.sendVerification -); - -// 인증번호 확인 (시도 횟수 제한) -router.post('/verify-code', - validateVerifyCodeInput, // 입력값 검증 - verificationAttemptLimit, // 1분에 5회 - verificationController.verifyCode -); - -// 인증 상태 확인 -router.get('/status', - validateVerificationStatusInput, // 입력값 검증 - verificationController.getVerificationStatus -); - -// 인증번호 재전송 (속도 제한 적용) -router.post('/resend-verification', - validateResendVerificationInput, // 입력값 검증 - emailSendLimit, // 1분에 1회 - process.env.NODE_ENV === 'production' ? dailyEmailLimit : testDailyEmailLimit, // 환경별 일일 제한 - process.env.NODE_ENV === 'production' ? dailyIPLimit : testDailyIPLimit, // 환경별 IP 제한 - verificationController.resendVerification -); - -module.exports = router; diff --git a/src/services/emailSender.js b/src/services/emailSender.js deleted file mode 100644 index c4ab5625..00000000 --- a/src/services/emailSender.js +++ /dev/null @@ -1,13 +0,0 @@ -const { v4: uuidv4 } = require('uuid'); - -module.exports = { - async sendVerification({ to, magicLink, code, locale = 'ko' }) { - if (!to || (!magicLink && !code)) throw new Error('to and (magicLink or code) are required'); - const requestId = uuidv4(); - // Stub: integrate SES/SMTP later. For now, just log. - console.log(`[EmailSender][stub] to=${to} locale=${locale} requestId=${requestId}`); - return { provider: 'stub', requestId }; - }, -}; - - diff --git a/src/services/verificationService.js b/src/services/verificationService.js deleted file mode 100644 index bb3a1ebd..00000000 --- a/src/services/verificationService.js +++ /dev/null @@ -1,227 +0,0 @@ -const crypto = require('crypto'); -const { getFromEmail, sendEmailWithResend } = require('../config/emailConfig'); -const { generateVerificationEmail, generateVerificationEmailText } = require('../templates/verificationEmail'); -const { EmailVerification, User } = require('../models'); -const { Op } = require('sequelize'); - -class VerificationService { - constructor() { - this.EXPIRATION_TIME = 3 * 60 * 1000; // 3분 - this.MAX_ATTEMPTS = 5; - this.CODE_LENGTH = 6; - } - - // 6자리 인증번호 생성 - generateVerificationCode() { - return Math.floor(100000 + Math.random() * 900000).toString(); - } - - // 이메일 형식 검증 - validateEmailFormat(email) { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - return emailRegex.test(email); - } - - // 대학교 이메일 도메인 검증 (선택사항) - validateUniversityDomain(email) { - const domain = email.split('@')[1]; - // 주요 대학교 도메인 목록 (필요시 확장) - const universityDomains = [ - 'korea.ac.kr', 'g.hongik.ac.kr', 'snu.ac.kr', 'yonsei.ac.kr', - 'kaist.ac.kr', 'postech.ac.kr', 'skku.edu', 'hanyang.ac.kr' - ]; - return universityDomains.includes(domain); - } - - // 중복 이메일 확인 - async checkDuplicateEmail(email) { - try { - const existingUser = await User.findOne({ - where: { email: email } - }); - return !!existingUser; - } catch (error) { - console.error('중복 이메일 확인 중 오류:', error); - throw new Error('이메일 중복 확인 중 오류가 발생했습니다.'); - } - } - - // 이전 인증번호 무효화 - async invalidatePreviousCodes(email) { - try { - await EmailVerification.update( - { consumed_at: new Date() }, - { - where: { - email: email, - consumed_at: null, - expires_at: { [Op.gt]: new Date() } - } - } - ); - } catch (error) { - console.error('이전 인증번호 무효화 중 오류:', error); - // 오류가 발생해도 계속 진행 - } - } - - // 인증번호 저장 - async saveVerification(email, code, ip, userAgent) { - try { - console.log(`[SERVICE] 인증번호 저장 시작: ${email}`); - - const expiresAt = new Date(Date.now() + this.EXPIRATION_TIME); - console.log(`[SERVICE] 만료 시간 설정: ${expiresAt.toISOString()}`); - - const verificationData = { - email: email, - purpose: 'signup', - jti: crypto.randomUUID(), - code_hash: this.hashCode(code), - expires_at: expiresAt, - created_ip: ip, - ua: userAgent, - attempt_count: 0 - }; - - console.log(`[SERVICE] 저장할 데이터 준비 완료: ${email}`); - - const result = await EmailVerification.create(verificationData); - console.log(`[SERVICE] 인증번호 저장 성공: ${email}, ID: ${result.id}`); - - return true; - } catch (error) { - console.error(`[SERVICE] 인증번호 저장 중 오류: ${email}`); - console.error(`[SERVICE] 오류 상세:`, error); - console.error(`[SERVICE] 오류 스택:`, error.stack); - console.error(`[SERVICE] 데이터베이스 연결 상태 확인 필요`); - throw new Error('인증번호 저장 중 오류가 발생했습니다.'); - } - } - - // 인증번호 해시화 - hashCode(code) { - return crypto.createHash('sha256').update(code).digest('hex'); - } - - // 이메일 발송 - async sendVerificationEmail(email, code) { - try { - console.log(`[SERVICE] 이메일 발송 시작: ${email}`); - - const fromEmail = getFromEmail(); - console.log(`[SERVICE] 발신자 이메일: ${fromEmail}`); - - const mailOptions = { - from: fromEmail, - to: email, - subject: '[티미타카] 인증번호를 입력해주세요', - html: generateVerificationEmail(code, email), - text: generateVerificationEmailText(code) - }; - - console.log(`[SERVICE] 이메일 옵션 준비 완료: ${email}`); - - // Resend API로 이메일 발송 - console.log(`[SERVICE] Resend API로 이메일 발송 시도: ${email}`); - const result = await sendEmailWithResend(mailOptions); - console.log(`[SERVICE] Resend 이메일 발송 성공: ${email}, Response: ${JSON.stringify(result)}`); - return { messageId: result?.id || 'sent' }; - } catch (error) { - console.error(`[SERVICE] 이메일 발송 중 오류: ${email}`); - console.error(`[SERVICE] 오류 상세:`, error); - console.error(`[SERVICE] 오류 스택:`, error.stack); - console.error(`[SERVICE] 환경 변수 확인: RESEND_API_KEY=${process.env.RESEND_API_KEY ? 'SET' : 'NOT_SET'}, EMAIL_FROM=${process.env.EMAIL_FROM || 'NOT_SET'}`); - throw new Error('이메일 발송에 실패했습니다.'); - } - } - - // 인증번호 검증 - async verifyCode(email, code) { - try { - const verification = await EmailVerification.findOne({ - where: { - email: email, - code_hash: this.hashCode(code), - consumed_at: null, - expires_at: { [Op.gt]: new Date() } - }, - order: [['created_at', 'DESC']] - }); - - if (!verification) { - return { valid: false, message: '유효하지 않은 인증번호입니다.' }; - } - - // 시도 횟수 확인 - if (verification.attempt_count >= this.MAX_ATTEMPTS) { - return { valid: false, message: '최대 시도 횟수를 초과했습니다.' }; - } - - // 시도 횟수 증가 - await verification.increment('attempt_count'); - - // 인증 성공 시 사용 완료 처리 - if (verification.attempt_count <= this.MAX_ATTEMPTS) { - await verification.update({ consumed_at: new Date() }); - return { valid: true, message: '인증이 완료되었습니다.' }; - } - - return { valid: false, message: '인증에 실패했습니다.' }; - } catch (error) { - console.error('인증번호 검증 중 오류:', error); - throw new Error('인증번호 검증 중 오류가 발생했습니다.'); - } - } - - // 만료된 인증번호 정리 (크론 작업용) - async cleanupExpiredCodes() { - try { - const result = await EmailVerification.update( - { consumed_at: new Date() }, - { - where: { - expires_at: { [Op.lt]: new Date() }, - consumed_at: null - } - } - ); - - console.log(`만료된 인증번호 ${result[0]}개 정리 완료`); - return result[0]; - } catch (error) { - console.error('만료된 인증번호 정리 중 오류:', error); - return 0; - } - } - - // 인증 상태 확인 - async getVerificationStatus(email) { - try { - const verification = await EmailVerification.findOne({ - where: { - email: email, - consumed_at: null, - expires_at: { [Op.gt]: new Date() } - }, - order: [['created_at', 'DESC']] - }); - - if (!verification) { - return { hasActiveCode: false, remainingTime: 0 }; - } - - const remainingTime = Math.max(0, verification.expires_at.getTime() - Date.now()); - return { - hasActiveCode: true, - remainingTime: Math.ceil(remainingTime / 1000), // 초 단위 - attemptCount: verification.attempt_count - }; - } catch (error) { - console.error('인증 상태 확인 중 오류:', error); - return { hasActiveCode: false, remainingTime: 0 }; - } - } -} - -module.exports = new VerificationService(); diff --git a/src/templates/verificationEmail.js b/src/templates/verificationEmail.js deleted file mode 100644 index 4cad8ba2..00000000 --- a/src/templates/verificationEmail.js +++ /dev/null @@ -1,160 +0,0 @@ -const generateVerificationEmail = (code, email) => { - return ` - - - - - - 티미타카 이메일 인증 - - - -
- -
- - - `; -}; - -const generateVerificationEmailText = (code) => { - return ` -티미타카 이메일 인증 - -안녕하세요, 티미타카 가입을 진행해주셔서 감사합니다. -아래 인증번호를 입력하면 바로 서비스 이용을 시작할 수 있어요. - -인증번호: ${code} - -• 인증번호는 3분 후 만료됩니다 -• 본인이 요청하지 않은 경우 이 메일을 무시하세요 - -© 2025 티미타카. All rights reserved. - `; -}; - -module.exports = { - generateVerificationEmail, - generateVerificationEmailText -}; diff --git a/src/validations/verificationValidation.js b/src/validations/verificationValidation.js deleted file mode 100644 index 2ad00792..00000000 --- a/src/validations/verificationValidation.js +++ /dev/null @@ -1,94 +0,0 @@ -const Joi = require('joi'); - -// 인증번호 전송 검증 스키마 -const sendVerificationSchema = Joi.object({ - email: Joi.string() - .email() - .required() - .max(255) - .pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/) - .messages({ - 'string.email': '올바른 이메일 형식이 아닙니다.', - 'any.required': '이메일을 입력해주세요.', - 'string.max': '이메일은 255자를 초과할 수 없습니다.', - 'string.pattern': '올바른 이메일 형식이 아닙니다.' - }) -}); - -// 인증번호 확인 검증 스키마 -const verifyCodeSchema = Joi.object({ - email: Joi.string() - .email() - .required() - .max(255) - .messages({ - 'string.email': '올바른 이메일 형식이 아닙니다.', - 'any.required': '이메일을 입력해주세요.', - 'string.max': '이메일은 255자를 초과할 수 없습니다.' - }), - code: Joi.string() - .length(6) - .pattern(/^\d{6}$/) - .required() - .messages({ - 'string.length': '인증번호는 6자리여야 합니다.', - 'string.pattern': '인증번호는 6자리 숫자여야 합니다.', - 'any.required': '인증번호를 입력해주세요.' - }) -}); - -// 인증 상태 확인 검증 스키마 -const verificationStatusSchema = Joi.object({ - email: Joi.string() - .email() - .required() - .max(255) - .messages({ - 'string.email': '올바른 이메일 형식이 아닙니다.', - 'any.required': '이메일을 입력해주세요.', - 'string.max': '이메일은 255자를 초과할 수 없습니다.' - }) -}); - -// 인증번호 재전송 검증 스키마 -const resendVerificationSchema = Joi.object({ - email: Joi.string() - .email() - .required() - .max(255) - .pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/) - .messages({ - 'string.email': '올바른 이메일 형식이 아닙니다.', - 'any.required': '이메일을 입력해주세요.', - 'string.max': '이메일은 255자를 초과할 수 없습니다.', - 'string.pattern': '올바른 이메일 형식이 아닙니다.' - }) -}); - -// 검증 함수들 -const validateSendVerification = (data) => { - return sendVerificationSchema.validate(data, { abortEarly: false }); -}; - -const validateVerifyCode = (data) => { - return verifyCodeSchema.validate(data, { abortEarly: false }); -}; - -const validateVerificationStatus = (data) => { - return verificationStatusSchema.validate(data, { abortEarly: false }); -}; - -const validateResendVerification = (data) => { - return resendVerificationSchema.validate(data, { abortEarly: false }); -}; - -module.exports = { - sendVerificationSchema, - verifyCodeSchema, - verificationStatusSchema, - resendVerificationSchema, - validateSendVerification, - validateVerifyCode, - validateVerificationStatus, - validateResendVerification -};