Files
startover/apps/web/src/app/api/auth/invite/accept/route.ts
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

80 lines
2.3 KiB
TypeScript

import { NextResponse } from 'next/server';
import argon2 from 'argon2';
import { createPrismaClient } from '@startover/database';
const prisma = createPrismaClient();
export async function POST(request: Request) {
const { token, password, name } = await request.json();
if (!token || !password || !name) {
return NextResponse.json({ error: '필수 정보가 누락되었습니다' }, { status: 400 });
}
if (password.length < 8 || !/[a-zA-Z]/.test(password) || !/[0-9]/.test(password)) {
return NextResponse.json(
{ error: '비밀번호는 8자 이상, 영문+숫자를 포함해야 합니다' },
{ status: 400 },
);
}
const inviteToken = await prisma.inviteToken.findUnique({ where: { token } });
if (!inviteToken) {
return NextResponse.json({ error: '유효하지 않은 초대입니다' }, { status: 400 });
}
if (inviteToken.usedAt) {
return NextResponse.json({ error: '이미 사용된 초대입니다' }, { status: 400 });
}
if (inviteToken.expires < new Date()) {
return NextResponse.json({ error: '초대가 만료되었습니다' }, { status: 400 });
}
const emailNormalized = inviteToken.email.toLowerCase().trim();
const existing = await prisma.user.findFirst({ where: { emailNormalized } });
if (existing) {
return NextResponse.json({ error: '이미 가입된 이메일입니다' }, { status: 400 });
}
const passwordHash = await argon2.hash(password, { type: argon2.argon2id });
await prisma.$transaction(async (tx) => {
const user = await tx.user.create({
data: {
email: inviteToken.email,
emailNormalized,
name,
passwordHash,
primaryRole: inviteToken.role,
status: 'ACTIVE',
emailVerifiedAt: new Date(),
},
});
await tx.userProfile.create({
data: {
userId: user.id,
profileType: 'OPERATOR',
},
});
await tx.inviteToken.update({
where: { id: inviteToken.id },
data: { usedAt: new Date() },
});
await tx.auditLog.create({
data: {
actorUserId: user.id,
resourceType: 'User',
resourceId: user.id.toString(),
actionType: 'OPERATOR_INVITED_ACCEPTED',
afterJson: { role: inviteToken.role, invitedBy: inviteToken.createdBy.toString() },
},
});
});
return NextResponse.json({ success: true });
}