로컬 환경 분리와 회원가입 중복 검증 보강
로컬 개발 환경은 .env.local을 우선 읽고 운영은 .env.production만 읽도록 분리해 인증 우회 설정이 운영에 섞이지 않게 정리했다. 회원가입/로그인 이메일 정규화와 중복 예외 처리를 보강해 대소문자·공백 변형 중복 가입을 안정적으로 차단했다. Made-with: Cursor
This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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