로컬 환경 분리와 회원가입 중복 검증 보강
로컬 개발 환경은 .env.local을 우선 읽고 운영은 .env.production만 읽도록 분리해 인증 우회 설정이 운영에 섞이지 않게 정리했다. 회원가입/로그인 이메일 정규화와 중복 예외 처리를 보강해 대소문자·공백 변형 중복 가입을 안정적으로 차단했다. Made-with: Cursor
This commit is contained in:
@@ -145,6 +145,14 @@ function hashToken(token) {
|
||||
return crypto.createHash('sha256').update(String(token || '')).digest('hex')
|
||||
}
|
||||
|
||||
function normalizeEmail(value) {
|
||||
return String(value || '').trim().toLowerCase()
|
||||
}
|
||||
|
||||
function normalizeNickname(value) {
|
||||
return String(value || '').trim()
|
||||
}
|
||||
|
||||
function resolveAppOrigin(req) {
|
||||
const envOrigin = String(process.env.APP_ORIGIN || process.env.PUBLIC_APP_ORIGIN || '').trim()
|
||||
if (envOrigin) return envOrigin.replace(/\/+$/, '')
|
||||
@@ -207,7 +215,9 @@ router.post('/signup', async (req, res) => {
|
||||
const parsed = signupSchema.safeParse(req.body)
|
||||
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
||||
|
||||
const { email, nickname, password } = parsed.data
|
||||
const email = normalizeEmail(parsed.data.email)
|
||||
const nickname = normalizeNickname(parsed.data.nickname)
|
||||
const { password } = parsed.data
|
||||
const exists = await findUserByEmail(email)
|
||||
if (exists) return res.status(409).json({ error: 'email_taken' })
|
||||
if (isReservedNickname(nickname)) return res.status(400).json({ error: 'nickname_reserved' })
|
||||
@@ -221,14 +231,22 @@ router.post('/signup', async (req, res) => {
|
||||
return res.status(503).json({ error: 'mail_not_configured' })
|
||||
}
|
||||
|
||||
const user = await createUser({
|
||||
id: nanoid(),
|
||||
email,
|
||||
nickname,
|
||||
passwordHash,
|
||||
emailVerified: isAdmin || skipEmailVerification,
|
||||
isAdmin,
|
||||
})
|
||||
let user = null
|
||||
try {
|
||||
user = await createUser({
|
||||
id: nanoid(),
|
||||
email,
|
||||
nickname,
|
||||
passwordHash,
|
||||
emailVerified: isAdmin || skipEmailVerification,
|
||||
isAdmin,
|
||||
})
|
||||
} catch (err) {
|
||||
if (err?.code === 'ER_DUP_ENTRY') {
|
||||
return res.status(409).json({ error: 'email_or_nickname_taken' })
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
if (!isAdmin && !skipEmailVerification) {
|
||||
try {
|
||||
@@ -260,7 +278,8 @@ router.post('/login', async (req, res) => {
|
||||
}).safeParse(req.body)
|
||||
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
||||
|
||||
const { email, password } = parsed.data
|
||||
const email = normalizeEmail(parsed.data.email)
|
||||
const { password } = parsed.data
|
||||
const user = await findUserByEmail(email)
|
||||
if (!user) return res.status(401).json({ error: 'invalid_credentials' })
|
||||
|
||||
@@ -320,7 +339,7 @@ router.post('/email/resend', async (req, res) => {
|
||||
const parsed = resendVerificationSchema.safeParse(req.body)
|
||||
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
||||
|
||||
const user = await findUserByEmail(parsed.data.email)
|
||||
const user = await findUserByEmail(normalizeEmail(parsed.data.email))
|
||||
if (!user || user.emailVerified) return res.json({ ok: true })
|
||||
|
||||
try {
|
||||
@@ -336,7 +355,7 @@ router.post('/password-reset/request', async (req, res) => {
|
||||
const parsed = requestPasswordResetSchema.safeParse(req.body)
|
||||
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
|
||||
|
||||
const user = await findUserByEmail(parsed.data.email)
|
||||
const user = await findUserByEmail(normalizeEmail(parsed.data.email))
|
||||
if (!user) return res.json({ ok: true })
|
||||
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user