Initial commit

This commit is contained in:
2026-03-27 19:35:14 +01:00
commit 38581b88a4
68 changed files with 12137 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
import NextAuth from "next-auth";
import { authOptions } from "@/lib/auth";
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };

87
app/api/register/route.ts Normal file
View File

@@ -0,0 +1,87 @@
import { hash } from "bcryptjs";
import { NextResponse } from "next/server";
import { Prisma } from "@prisma/client";
import { DEFAULT_THEME_ID } from "@/lib/constants";
import { db } from "@/lib/db";
import { normalizeUsername, sanitizePlainText } from "@/lib/sanitize";
import { registerSchema } from "@/lib/validators/auth";
export async function POST(request: Request) {
// Rate limiting should be applied here (IP/email key) at proxy or middleware level in production.
const body = await request.json().catch(() => null);
if (!body) {
return NextResponse.json({ message: "Invalid JSON payload" }, { status: 400 });
}
const parsed = registerSchema.safeParse({
email: sanitizePlainText(String(body.email ?? "")).toLowerCase(),
password: String(body.password ?? ""),
username: normalizeUsername(String(body.username ?? "")),
displayName: sanitizePlainText(String(body.displayName ?? ""))
});
if (!parsed.success) {
return NextResponse.json({ message: parsed.error.issues[0]?.message ?? "Invalid input" }, { status: 400 });
}
try {
const existingEmail = await db.user.findUnique({
where: { email: parsed.data.email },
select: { id: true }
});
if (existingEmail) {
return NextResponse.json({ message: "Email is already registered" }, { status: 409 });
}
const existingUsername = await db.profile.findUnique({
where: { username: parsed.data.username },
select: { id: true }
});
if (existingUsername) {
return NextResponse.json({ message: "Username is already taken" }, { status: 409 });
}
const hashedPassword = await hash(parsed.data.password, 12);
await db.$transaction(async (tx) => {
const theme = await tx.theme.findUnique({ where: { id: DEFAULT_THEME_ID } });
if (!theme) {
throw new Error(`Missing default theme: ${DEFAULT_THEME_ID}`);
}
const user = await tx.user.create({
data: {
email: parsed.data.email,
hashedPassword
}
});
await tx.profile.create({
data: {
userId: user.id,
username: parsed.data.username,
displayName: parsed.data.displayName,
themeId: DEFAULT_THEME_ID,
isPublic: true
}
});
});
} catch (error) {
if (error instanceof Prisma.PrismaClientInitializationError) {
return NextResponse.json(
{ message: "Database is not reachable. Start PostgreSQL and run migrations first." },
{ status: 503 }
);
}
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
return NextResponse.json({ message: "Account already exists" }, { status: 409 });
}
return NextResponse.json({ message: "Could not create account" }, { status: 500 });
}
return NextResponse.json({ message: "Account created" }, { status: 201 });
}