7f59b94dcf
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.
109 lines
3.4 KiB
TypeScript
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');
|
|
}
|