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.
80 lines
2.3 KiB
TypeScript
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 });
|
|
}
|