설정 제한 보정

This commit is contained in:
2026-04-07 15:10:43 +09:00
parent 923a9af83d
commit 51170b2ff7
7 changed files with 63 additions and 21 deletions

View File

@@ -30,7 +30,35 @@ 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
function resolveNicknameChangeIntervalMs() {
const rawMs = String(process.env.NICKNAME_CHANGE_INTERVAL_MS || '').trim()
if (rawMs) {
const parsed = Number(rawMs)
if (Number.isFinite(parsed) && parsed >= 0) return parsed
}
const rawDays = String(process.env.NICKNAME_CHANGE_INTERVAL_DAYS || '').trim()
if (rawDays) {
const parsed = Number(rawDays)
if (Number.isFinite(parsed) && parsed >= 0) return parsed * 24 * 60 * 60 * 1000
}
return 14 * 24 * 60 * 60 * 1000
}
function formatNicknameChangeIntervalLabel(intervalMs) {
if (!intervalMs || intervalMs <= 0) return '제한 없음'
const oneDayMs = 24 * 60 * 60 * 1000
const wholeDays = intervalMs / oneDayMs
if (Number.isInteger(wholeDays) && wholeDays >= 7 && wholeDays % 7 === 0) {
return `${wholeDays / 7}`
}
if (Number.isInteger(wholeDays)) {
return `${wholeDays}`
}
return `${Math.ceil(wholeDays)}`
}
const signupSchema = z.object({
email: z.string().email(),
@@ -83,13 +111,17 @@ async function serializeUser(user) {
if (!user) return null
const primaryAdmin = await findPrimaryAdminUser()
const isPrimaryAdmin = !!user.isAdmin && primaryAdmin?.id === user.id
const nicknameChangeIntervalMs = resolveNicknameChangeIntervalMs()
const nicknameChangeAvailableAt = nicknameChangeIntervalMs > 0 ? (user.nicknameUpdatedAt || 0) + nicknameChangeIntervalMs : 0
return {
id: user.id,
email: user.email,
nickname: user.nickname || '',
nicknameUpdatedAt: user.nicknameUpdatedAt || 0,
nicknameChangeAvailableAt: (user.nicknameUpdatedAt || 0) + NICKNAME_CHANGE_INTERVAL_MS,
nicknameChangeAvailableAt,
nicknameChangeIntervalMs,
nicknameChangeIntervalLabel: formatNicknameChangeIntervalLabel(nicknameChangeIntervalMs),
isAdmin: !!user.isAdmin,
isPrimaryAdmin,
isOperator: !!user.isAdmin && !isPrimaryAdmin,
@@ -363,12 +395,15 @@ router.post('/profile', requireAuth, upload.single('avatar'), async (req, res) =
if (!user) return res.status(404).json({ error: 'not_found' })
const normalizedNickname = parsed.data.nickname.trim()
const nicknameChanged = normalizedNickname !== (user.nickname || '').trim()
const nicknameChangeIntervalMs = resolveNicknameChangeIntervalMs()
if (isReservedNickname(normalizedNickname)) return res.status(400).json({ error: 'nickname_reserved' })
if (nicknameChanged && user.nicknameUpdatedAt && Date.now() < user.nicknameUpdatedAt + NICKNAME_CHANGE_INTERVAL_MS) {
if (nicknameChanged && nicknameChangeIntervalMs > 0 && user.nicknameUpdatedAt && Date.now() < user.nicknameUpdatedAt + nicknameChangeIntervalMs) {
return res.status(429).json({
error: 'nickname_change_locked',
nicknameChangeAvailableAt: user.nicknameUpdatedAt + NICKNAME_CHANGE_INTERVAL_MS,
nicknameChangeAvailableAt: user.nicknameUpdatedAt + nicknameChangeIntervalMs,
nicknameChangeIntervalMs,
nicknameChangeIntervalLabel: formatNicknameChangeIntervalLabel(nicknameChangeIntervalMs),
})
}