관리자 레이아웃과 네비게이션 정리

This commit is contained in:
2026-05-13 10:23:18 +09:00
parent ec9f9ea57f
commit b490d5b90f
17 changed files with 484 additions and 164 deletions

View File

@@ -1,7 +1,8 @@
import bcrypt from 'bcrypt'
import { createError, readBody } from 'h3'
import { z } from 'zod'
import { deleteMember, getUserByIdWithPassword } from '../../repositories/member-repository'
import { countOwnerMembers, deleteMember, getUserByIdWithPassword, MEMBER_ROLE } from '../../repositories/member-repository'
import { clearAdminSession } from '../../utils/admin-auth'
import { clearMemberSession, requireMemberSession } from '../../utils/member-auth'
import { removeManagedAvatarAsset } from '../../utils/member-avatar'
@@ -41,13 +42,20 @@ export default defineEventHandler(async (event) => {
})
}
if (user.role === MEMBER_ROLE.OWNER && (await countOwnerMembers()) <= 1) {
throw createError({
statusCode: 400,
message: '최소 1명의 소유자 권한은 유지되어야 합니다.'
})
}
if (user.avatarUrl) {
await removeManagedAvatarAsset(user.avatarUrl)
}
await deleteMember(session.userId)
clearMemberSession(event)
clearAdminSession(event)
return { ok: true }
})

View File

@@ -7,7 +7,9 @@ import {
countOtpSendsLastHour,
hasRecentOtpSend,
insertOtpChallenge,
invalidatePendingOtpChallenges
deleteOtpChallengeById,
invalidatePendingOtpChallenges,
invalidatePendingOtpChallengesExcept
} from '../../../repositories/email-otp-repository'
import { generateSixDigitOtp, hashOtpCode, normalizeOtpEmail } from '../../../utils/email-otp'
import { isResendConfigured, sendResendEmail } from '../../../utils/resend-mail'
@@ -129,8 +131,7 @@ export default defineEventHandler(async (event) => {
const expiresAt = new Date(Date.now() + OTP_TTL_MS)
const createdIp = String(getRequestIP(event) || '')
await invalidatePendingOtpChallenges(sql, email, purpose)
await insertOtpChallenge(sql, {
const challengeId = await insertOtpChallenge(sql, {
email,
purpose,
codeHash,
@@ -147,13 +148,20 @@ export default defineEventHandler(async (event) => {
? `<p>회원가입을 위한 인증번호입니다.</p><p style="font-size:24px;font-weight:700;letter-spacing:0.2em">${code}</p><p>15분 이내에 입력해 주세요.</p>`
: `<p>비밀번호 재설정을 위한 인증번호입니다.</p><p style="font-size:24px;font-weight:700;letter-spacing:0.2em">${code}</p><p>15분 이내에 입력해 주세요.</p>`
await sendResendEmail({
apiKey: String(config.resendApiKey).trim(),
from: String(config.resendFromEmail).trim(),
to: email,
subject,
html
})
try {
await sendResendEmail({
apiKey: String(config.resendApiKey).trim(),
from: String(config.resendFromEmail).trim(),
to: email,
subject,
html
})
} catch (error) {
await deleteOtpChallengeById(sql, challengeId)
throw error
}
await invalidatePendingOtpChallengesExcept(sql, email, purpose, challengeId)
return {
ok: true,