v0.1.43 - 인증 강제와 회원 탈퇴 흐름 정리

This commit is contained in:
2026-04-24 10:04:44 +09:00
parent 54f4b34e5e
commit a38714dfe4
9 changed files with 250 additions and 14 deletions

View File

@@ -61,5 +61,10 @@ export async function findAuthenticatedUser(request) {
.where(eq(users.id, session.userId))
.limit(1)
if (user && user.role !== 'admin' && !user.emailVerifiedAt) {
await db.delete(authSessions).where(eq(authSessions.id, session.id))
return null
}
return user ?? null
}

View File

@@ -28,6 +28,10 @@ const passwordSchema = z.object({
newPassword: z.string().min(8).max(72),
})
const deleteAccountSchema = z.object({
currentPassword: z.string().min(1).max(72),
})
const verificationRequestSchema = z.object({
email: z.string().trim().email().optional(),
})
@@ -110,7 +114,11 @@ function sanitizeUser(user) {
}
function withPreviewUrl(payload, key, previewUrl) {
if (!env.AUTH_PREVIEW_LINKS) {
const allowPreviewLinks =
env.AUTH_PREVIEW_LINKS &&
/localhost|127\.0\.0\.1/i.test(env.APP_BASE_URL)
if (!allowPreviewLinks) {
return payload
}
@@ -175,13 +183,12 @@ export async function registerAuthRoutes(app) {
nickname,
role: 'user',
emailVerifiedAt: null,
lastLoginAt: now,
lastLoginAt: null,
createdAt: now,
updatedAt: now,
})
.returning()
const { token } = await createSession(user.id)
const verification = await createEmailVerificationToken(user.id)
await sendVerificationEmail({
to: user.email,
@@ -189,9 +196,8 @@ export async function registerAuthRoutes(app) {
})
return reply.code(201).send(withPreviewUrl({
message: '회원가입이 완료되었습니다.',
token,
user: sanitizeUser(user),
message: '회원가입이 완료되었습니다. 이메일 인증 후 로그인해 주세요.',
requiresEmailVerification: true,
}, 'verificationPreviewUrl', verification.previewUrl))
})
@@ -233,6 +239,12 @@ export async function registerAuthRoutes(app) {
})
}
if (user.role !== 'admin' && !user.emailVerifiedAt) {
return reply.code(403).send({
message: '이메일 인증을 완료한 뒤 로그인해 주세요.',
})
}
const now = new Date()
const [updatedUser] = await db
@@ -364,6 +376,46 @@ export async function registerAuthRoutes(app) {
}
})
app.delete('/api/auth/account', async (request, reply) => {
const user = await findAuthenticatedUser(request)
if (!user) {
return reply.code(401).send({
message: '인증이 필요합니다.',
})
}
if (user.role === 'admin') {
return reply.code(403).send({
message: '기본 관리자 계정은 여기서 삭제할 수 없습니다.',
})
}
const payload = deleteAccountSchema.safeParse(request.body)
if (!payload.success) {
return reply.code(400).send({
message: '회원 탈퇴 확인 비밀번호를 입력해 주세요.',
issues: payload.error.flatten(),
})
}
const passwordMatches = await verifyPassword(payload.data.currentPassword, user.passwordHash)
if (!passwordMatches) {
return reply.code(401).send({
message: '현재 비밀번호가 올바르지 않습니다.',
})
}
await db.delete(authSessions).where(eq(authSessions.userId, user.id))
await db.delete(users).where(eq(users.id, user.id))
return {
message: '회원 탈퇴가 완료되었습니다.',
}
})
app.post('/api/auth/verification/request', async (request, reply) => {
const authenticatedUser = await findAuthenticatedUser(request)
const payload = verificationRequestSchema.safeParse(request.body ?? {})