설정 화면 정리

This commit is contained in:
2026-04-07 15:03:30 +09:00
parent 6fdd780859
commit 923a9af83d
10 changed files with 588 additions and 222 deletions

View File

@@ -30,6 +30,7 @@ const { isReservedNickname } = require('../lib/user-validation')
const router = express.Router()
const EMAIL_VERIFICATION_TTL_MS = 24 * 60 * 60 * 1000
const PASSWORD_RESET_TTL_MS = 60 * 60 * 1000
const NICKNAME_CHANGE_INTERVAL_MS = 14 * 24 * 60 * 60 * 1000
const signupSchema = z.object({
email: z.string().email(),
@@ -60,7 +61,7 @@ const changePasswordSchema = z.object({
})
const profileSchema = z.object({
nickname: z.string().trim().min(1).max(40),
nickname: z.string().trim().min(2).max(40),
removeAvatar: z.union([z.string(), z.undefined()]).optional(),
})
@@ -87,6 +88,8 @@ async function serializeUser(user) {
id: user.id,
email: user.email,
nickname: user.nickname || '',
nicknameUpdatedAt: user.nicknameUpdatedAt || 0,
nicknameChangeAvailableAt: (user.nicknameUpdatedAt || 0) + NICKNAME_CHANGE_INTERVAL_MS,
isAdmin: !!user.isAdmin,
isPrimaryAdmin,
isOperator: !!user.isAdmin && !isPrimaryAdmin,
@@ -358,8 +361,18 @@ router.post('/profile', requireAuth, upload.single('avatar'), async (req, res) =
const user = await findUserById(req.session.userId)
if (!user) return res.status(404).json({ error: 'not_found' })
if (isReservedNickname(parsed.data.nickname)) return res.status(400).json({ error: 'nickname_reserved' })
const nicknameExists = await findUserByNickname(parsed.data.nickname, user.id)
const normalizedNickname = parsed.data.nickname.trim()
const nicknameChanged = normalizedNickname !== (user.nickname || '').trim()
if (isReservedNickname(normalizedNickname)) return res.status(400).json({ error: 'nickname_reserved' })
if (nicknameChanged && user.nicknameUpdatedAt && Date.now() < user.nicknameUpdatedAt + NICKNAME_CHANGE_INTERVAL_MS) {
return res.status(429).json({
error: 'nickname_change_locked',
nicknameChangeAvailableAt: user.nicknameUpdatedAt + NICKNAME_CHANGE_INTERVAL_MS,
})
}
const nicknameExists = await findUserByNickname(normalizedNickname, user.id)
if (nicknameExists) return res.status(409).json({ error: 'nickname_taken' })
const optimized = req.file
@@ -377,8 +390,9 @@ router.post('/profile', requireAuth, upload.single('avatar'), async (req, res) =
const nextAvatarSrc = shouldRemoveAvatar ? '' : optimized?.src || user.avatarSrc || ''
const updated = await updateUserProfile({
id: user.id,
nickname: parsed.data.nickname,
nickname: normalizedNickname,
avatarSrc: nextAvatarSrc,
touchNicknameUpdatedAt: nicknameChanged,
})
res.json({ user: await serializeUser(updated) })