로컬 환경 분리와 회원가입 중복 검증 보강
로컬 개발 환경은 .env.local을 우선 읽고 운영은 .env.production만 읽도록 분리해 인증 우회 설정이 운영에 섞이지 않게 정리했다. 회원가입/로그인 이메일 정규화와 중복 예외 처리를 보강해 대소문자·공백 변형 중복 가입을 안정적으로 차단했다. Made-with: Cursor
This commit is contained in:
@@ -699,9 +699,16 @@ async function countUsers() {
|
||||
}
|
||||
|
||||
async function findUserByEmail(email) {
|
||||
const normalized = String(email || '').trim().toLowerCase()
|
||||
if (!normalized) return null
|
||||
const rows = await query(
|
||||
'SELECT id, email, nickname, nickname_updated_at, password_hash, email_verified, is_admin, avatar_src, last_login_at, created_at FROM users WHERE email = ? LIMIT 1',
|
||||
[email]
|
||||
`
|
||||
SELECT id, email, nickname, nickname_updated_at, password_hash, email_verified, is_admin, avatar_src, last_login_at, created_at
|
||||
FROM users
|
||||
WHERE TRIM(LOWER(email)) = TRIM(LOWER(?))
|
||||
LIMIT 1
|
||||
`,
|
||||
[normalized]
|
||||
)
|
||||
const row = rows[0]
|
||||
if (!row) return null
|
||||
@@ -745,12 +752,14 @@ async function findUserById(id) {
|
||||
|
||||
async function createUser({ id, email, nickname, passwordHash, emailVerified = true, isAdmin }) {
|
||||
const createdAt = now()
|
||||
const normalizedEmail = String(email || '').trim().toLowerCase()
|
||||
const normalizedNickname = String(nickname || '').trim()
|
||||
await query(
|
||||
`
|
||||
INSERT INTO users (id, email, nickname, nickname_updated_at, password_hash, email_verified, is_admin, avatar_src, last_login_at, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
[id, email, nickname || '', createdAt, passwordHash, emailVerified ? 1 : 0, isAdmin ? 1 : 0, '', isAdmin ? createdAt : 0, createdAt]
|
||||
[id, normalizedEmail, normalizedNickname, createdAt, passwordHash, emailVerified ? 1 : 0, isAdmin ? 1 : 0, '', isAdmin ? createdAt : 0, createdAt]
|
||||
)
|
||||
return findUserById(id)
|
||||
}
|
||||
|
||||
@@ -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