From 69be1d6fccd9a8dd2cabd532615767ffa2aad430 Mon Sep 17 00:00:00 2001 From: JetSprow Date: Wed, 29 Apr 2026 16:52:04 +1000 Subject: [PATCH] fix: require email verification before activation --- prisma/schema.prisma | 1 + src/actions/auth/email.ts | 2 +- src/app/(admin)/admin/subscription-risk/risk-data.ts | 4 ++-- src/app/(admin)/admin/users/page.tsx | 1 + src/app/(admin)/admin/users/users-data.ts | 4 ++-- src/app/api/auth/register/route.ts | 1 + src/components/shared/domain-badges.tsx | 2 ++ src/lib/auth.ts | 9 +++++++-- src/services/email.ts | 5 ++++- 9 files changed, 21 insertions(+), 8 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b9feb26..9a52c07 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -13,6 +13,7 @@ enum Role { enum UserStatus { ACTIVE + PENDING_EMAIL DISABLED BANNED } diff --git a/src/actions/auth/email.ts b/src/actions/auth/email.ts index f0f1edc..68f30cc 100644 --- a/src/actions/auth/email.ts +++ b/src/actions/auth/email.ts @@ -77,7 +77,7 @@ export async function requestRegistrationVerification(formData: FormData) { select: { id: true, email: true, status: true, emailVerifiedAt: true }, }); - if (user?.status === "ACTIVE" && !user.emailVerifiedAt) { + if (user && ["ACTIVE", "PENDING_EMAIL"].includes(user.status) && !user.emailVerifiedAt) { await sendRegistrationVerificationEmail({ userId: user.id, email: user.email, diff --git a/src/app/(admin)/admin/subscription-risk/risk-data.ts b/src/app/(admin)/admin/subscription-risk/risk-data.ts index 0532ce9..2fdca78 100644 --- a/src/app/(admin)/admin/subscription-risk/risk-data.ts +++ b/src/app/(admin)/admin/subscription-risk/risk-data.ts @@ -1,4 +1,4 @@ -import type { Prisma, SubscriptionRiskEvent } from "@prisma/client"; +import type { Prisma, SubscriptionRiskEvent, UserStatus } from "@prisma/client"; import { prisma } from "@/lib/prisma"; import { parsePage } from "@/lib/utils"; import { @@ -11,7 +11,7 @@ type RiskUser = { id: string; email: string; name: string | null; - status: "ACTIVE" | "DISABLED" | "BANNED"; + status: UserStatus; }; type RiskSubscription = { diff --git a/src/app/(admin)/admin/users/page.tsx b/src/app/(admin)/admin/users/page.tsx index 946ee1b..a7a02bd 100644 --- a/src/app/(admin)/admin/users/page.tsx +++ b/src/app/(admin)/admin/users/page.tsx @@ -44,6 +44,7 @@ export default async function UsersPage({ options: [ { label: "全部状态", value: "" }, { label: "正常", value: "ACTIVE" }, + { label: "待邮箱验证", value: "PENDING_EMAIL" }, { label: "禁用", value: "DISABLED" }, { label: "封禁", value: "BANNED" }, ], diff --git a/src/app/(admin)/admin/users/users-data.ts b/src/app/(admin)/admin/users/users-data.ts index c44b971..6014163 100644 --- a/src/app/(admin)/admin/users/users-data.ts +++ b/src/app/(admin)/admin/users/users-data.ts @@ -1,4 +1,4 @@ -import type { Prisma } from "@prisma/client"; +import type { Prisma, UserStatus } from "@prisma/client"; import { prisma } from "@/lib/prisma"; import { parsePage } from "@/lib/utils"; @@ -31,7 +31,7 @@ export async function getAdminUsers( const where = { ...(role ? { role: role as "ADMIN" | "USER" } : {}), - ...(status ? { status: status as "ACTIVE" | "DISABLED" | "BANNED" } : {}), + ...(status ? { status: status as UserStatus } : {}), ...(q ? { OR: [ diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts index d5298be..e582897 100644 --- a/src/app/api/auth/register/route.ts +++ b/src/app/api/auth/register/route.ts @@ -90,6 +90,7 @@ export async function POST(req: Request) { data: { email, emailVerifiedAt: config.emailVerificationRequired ? null : new Date(), + status: config.emailVerificationRequired ? "PENDING_EMAIL" : "ACTIVE", password: hashedPassword, name: name || null, invitedById: inviterId, diff --git a/src/components/shared/domain-badges.tsx b/src/components/shared/domain-badges.tsx index 6bd548b..73b8cff 100644 --- a/src/components/shared/domain-badges.tsx +++ b/src/components/shared/domain-badges.tsx @@ -51,6 +51,7 @@ export const userRoleLabels: Record = { export const userStatusLabels: Record = { ACTIVE: "正常", + PENDING_EMAIL: "待邮箱验证", DISABLED: "禁用", BANNED: "封禁", }; @@ -104,6 +105,7 @@ export function getSubscriptionTypeTone(type: SubscriptionType): StatusTone { export function getUserStatusTone(status: UserStatus): StatusTone { if (status === "ACTIVE") return "success"; + if (status === "PENDING_EMAIL") return "info"; if (status === "DISABLED") return "warning"; return "danger"; } diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 7a8ec74..01f9fda 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -27,12 +27,17 @@ export const authOptions: NextAuthOptions = { const user = await prisma.user.findUnique({ where: { email: credentials.email.trim().toLowerCase() }, }); - if (!user || user.status !== "ACTIVE") return null; + if (!user) return null; const valid = await bcrypt.compare(credentials.password, user.password); if (!valid) return null; - if (config?.emailVerificationRequired && user.role !== "ADMIN" && !user.emailVerifiedAt) { + if ( + user.role !== "ADMIN" && + !user.emailVerifiedAt && + (config?.emailVerificationRequired || user.status === "PENDING_EMAIL") + ) { throw new Error("EMAIL_NOT_VERIFIED"); } + if (user.status !== "ACTIVE") return null; return { id: user.id, email: user.email, name: user.name, role: user.role }; }, }), diff --git a/src/services/email.ts b/src/services/email.ts index d0515d6..2d5513b 100644 --- a/src/services/email.ts +++ b/src/services/email.ts @@ -244,7 +244,10 @@ export async function verifyEmailToken(token: string) { if (!record.userId) return { ok: false as const, message: "验证链接缺少账户信息" }; await prisma.user.update({ where: { id: record.userId }, - data: { emailVerifiedAt: new Date() }, + data: { + emailVerifiedAt: new Date(), + status: "ACTIVE", + }, }); return { ok: true as const, message: "邮箱验证完成,现在可以登录账户。" }; }