로컬 환경 분리와 회원가입 중복 검증 보강

로컬 개발 환경은 .env.local을 우선 읽고 운영은 .env.production만 읽도록 분리해 인증 우회 설정이 운영에 섞이지 않게 정리했다. 회원가입/로그인 이메일 정규화와 중복 예외 처리를 보강해 대소문자·공백 변형 중복 가입을 안정적으로 차단했다.

Made-with: Cursor
This commit is contained in:
2026-04-27 11:33:10 +09:00
committed by DUCK JIN
parent 069eda3544
commit 73d198f5a9
12 changed files with 105 additions and 25 deletions

View File

@@ -6,7 +6,22 @@ const cors = require('cors')
const session = require('express-session')
const FileStoreFactory = require('session-file-store')
dotenv.config({ path: path.join(__dirname, '..', '.env.production') })
function loadEnvFileIfExists(fileName) {
const envPath = path.join(__dirname, '..', fileName)
if (!fs.existsSync(envPath)) return false
dotenv.config({ path: envPath })
return true
}
loadEnvFileIfExists('.env')
const nodeEnv = String(process.env.NODE_ENV || 'development').trim().toLowerCase()
if (nodeEnv === 'production') {
loadEnvFileIfExists('.env.production')
} else {
const loadedLocal = loadEnvFileIfExists('.env.local')
if (!loadedLocal) loadEnvFileIfExists('.env.production')
}
const { ensureData } = require('./src/db')
const authRoutes = require('./src/routes/auth')

View File

@@ -4,8 +4,8 @@
"description": "",
"main": "index.js",
"scripts": {
"dev": "APP_ORIGIN=http://localhost:5173 DB_HOST=127.0.0.1 DB_PORT=3307 DB_USER=tier_cursor DB_PASSWORD=tier_cursor1234 DB_NAME=tier_cursor nodemon --legacy-watch --watch index.js --watch src index.js",
"start": "APP_ORIGIN=http://localhost:5173 DB_HOST=127.0.0.1 DB_PORT=3307 DB_USER=tier_cursor DB_PASSWORD=tier_cursor1234 DB_NAME=tier_cursor node index.js",
"dev": "NODE_ENV=development nodemon --legacy-watch --watch index.js --watch src index.js",
"start": "NODE_ENV=production node index.js",
"images:backfill": "DB_HOST=127.0.0.1 DB_PORT=3307 DB_USER=tier_cursor DB_PASSWORD=tier_cursor1234 DB_NAME=tier_cursor node scripts/backfill-legacy-image-assets.js",
"images:migrate-legacy": "DB_HOST=127.0.0.1 DB_PORT=3307 DB_USER=tier_cursor DB_PASSWORD=tier_cursor1234 DB_NAME=tier_cursor node scripts/migrate-legacy-uploads-to-assets.js",
"images:shard-assets": "DB_HOST=127.0.0.1 DB_PORT=3307 DB_USER=tier_cursor DB_PASSWORD=tier_cursor1234 DB_NAME=tier_cursor node scripts/migrate-flat-assets-to-sharded.js",

View File

@@ -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)
}

View File

@@ -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 {