헤더 검색 중앙 정렬·Resend 이메일 OTP·비밀번호 찾기 (v0.0.99)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
43
server/utils/email-otp.js
Normal file
43
server/utils/email-otp.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import { createHash, randomInt, timingSafeEqual } from 'node:crypto'
|
||||
|
||||
/**
|
||||
* OTP용 이메일 정규화(소문자·trim)
|
||||
* @param {string} email - 원본
|
||||
* @returns {string}
|
||||
*/
|
||||
export const normalizeOtpEmail = (email) => String(email || '').trim().toLowerCase()
|
||||
|
||||
/**
|
||||
* 6자리 숫자 인증 코드 생성
|
||||
* @returns {string}
|
||||
*/
|
||||
export const generateSixDigitOtp = () => String(randomInt(0, 1_000_000)).padStart(6, '0')
|
||||
|
||||
/**
|
||||
* OTP 코드 해시(hex)
|
||||
* @param {{ pepper: string, email: string, purpose: string, code: string }} input - 입력
|
||||
* @returns {string} sha256 hex
|
||||
*/
|
||||
export const hashOtpCode = (input) => {
|
||||
const payload = `${String(input.pepper || '')}|${normalizeOtpEmail(input.email)}|${String(input.purpose || '')}|${String(input.code || '')}`
|
||||
return createHash('sha256').update(payload, 'utf8').digest('hex')
|
||||
}
|
||||
|
||||
/**
|
||||
* 두 sha256 hex 문자열을 상수 시간으로 비교한다.
|
||||
* @param {string} a - hex
|
||||
* @param {string} b - hex
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const timingSafeEqualHex = (a, b) => {
|
||||
try {
|
||||
const ba = Buffer.from(String(a || ''), 'hex')
|
||||
const bb = Buffer.from(String(b || ''), 'hex')
|
||||
if (ba.length !== bb.length || ba.length === 0) {
|
||||
return false
|
||||
}
|
||||
return timingSafeEqual(ba, bb)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
42
server/utils/resend-mail.js
Normal file
42
server/utils/resend-mail.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import { createError } from 'h3'
|
||||
|
||||
/**
|
||||
* Resend가 서버 설정으로 사용 가능한지
|
||||
* @param {{ resendApiKey?: string, resendFromEmail?: string }} config - 런타임 설정
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isResendConfigured = (config) => {
|
||||
const key = String(config?.resendApiKey || '').trim()
|
||||
const from = String(config?.resendFromEmail || '').trim()
|
||||
return Boolean(key && from)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resend REST API로 이메일을 발송한다.
|
||||
* @param {{ apiKey: string, from: string, to: string, subject: string, html: string }} input - 발송 입력
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const sendResendEmail = async (input) => {
|
||||
const res = await fetch('https://api.resend.com/emails', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${input.apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
from: input.from,
|
||||
to: [input.to],
|
||||
subject: input.subject,
|
||||
html: input.html
|
||||
})
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const detail = await res.text().catch(() => '')
|
||||
throw createError({
|
||||
statusCode: 502,
|
||||
message: '이메일 발송에 실패했습니다. Resend 발신 주소·도메인 설정을 확인해 주세요.',
|
||||
data: process.env.NODE_ENV === 'development' && detail ? { detail } : undefined
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user