Files
startover/apps/web/src/app/auth/verify/page.tsx
T
Johngreen 7f59b94dcf Rename project from Re:Link to Startover
Rebrand repository from "Re:Link" to "Startover" across the codebase. Updates include package names and scopes (@relink/* -> @startover/*), import paths, Next.js transpile settings, vitest name, UI text and docs, Dockerfile and CI/workflow names, deploy scripts and repo paths, and example/production env values. Also add auth-related env vars, an apps/web .env symlink, and small formatting/typing cleanups in several TSX/TS files and tests to accommodate the rename.
2026-03-08 20:22:08 +09:00

109 lines
3.4 KiB
TypeScript

import { redirect } from 'next/navigation';
import Link from 'next/link';
import { createPrismaClient } from '@startover/database';
const prisma = createPrismaClient();
export default async function VerifyPage({
searchParams,
}: {
searchParams: Promise<{ token?: string }>;
}) {
const { token } = await searchParams;
if (!token) {
return (
<div className="mx-auto max-w-md px-4 py-16 text-center">
<h1 className="mb-4 text-2xl font-bold text-red-600"> </h1>
<p className="text-gray-600"> .</p>
</div>
);
}
const verificationToken = await prisma.verificationToken.findUnique({
where: { token },
});
if (!verificationToken) {
return (
<div className="mx-auto max-w-md px-4 py-16 text-center">
<h1 className="mb-4 text-2xl font-bold text-red-600"> </h1>
<p className="mb-6 text-gray-600"> .</p>
<Link href="/auth/login" className="text-blue-600 hover:underline">
</Link>
</div>
);
}
if (verificationToken.expires < new Date()) {
await prisma.verificationToken.delete({
where: { identifier_token: { identifier: verificationToken.identifier, token } },
});
return (
<div className="mx-auto max-w-md px-4 py-16 text-center">
<h1 className="mb-4 text-2xl font-bold text-red-600"> </h1>
<p className="mb-6 text-gray-600">
. .
</p>
<Link href="/auth/login" className="text-blue-600 hover:underline">
</Link>
</div>
);
}
const user = await prisma.user.findFirst({
where: { emailNormalized: verificationToken.identifier },
});
if (!user) {
// 토큰은 유효하지만 사용자가 삭제된 경우
await prisma.verificationToken.delete({
where: { identifier_token: { identifier: verificationToken.identifier, token } },
});
return (
<div className="mx-auto max-w-md px-4 py-16 text-center">
<h1 className="mb-4 text-2xl font-bold text-red-600"> </h1>
<p className="mb-6 text-gray-600"> .</p>
<Link href="/auth/register" className="text-blue-600 hover:underline">
</Link>
</div>
);
}
// 이미 ACTIVE인 경우 중복 처리 방지
if (user.status !== 'PENDING_VERIFICATION') {
await prisma.verificationToken.delete({
where: { identifier_token: { identifier: verificationToken.identifier, token } },
});
redirect('/auth/login?verified=true');
}
// 인증 처리 (트랜잭션)
await prisma.$transaction(async (tx) => {
await tx.user.update({
where: { id: user.id },
data: { emailVerifiedAt: new Date(), status: 'ACTIVE' },
});
await tx.verificationToken.delete({
where: { identifier_token: { identifier: verificationToken.identifier, token } },
});
await tx.auditLog.create({
data: {
actorUserId: user.id,
resourceType: 'User',
resourceId: user.id.toString(),
actionType: 'EMAIL_VERIFIED',
},
});
});
redirect('/auth/login?verified=true');
}