Files
Curio/src/app/api/auth/register/route.ts
T
arnaudne 4e38b5a791 feat: initial commit — Curio mastery tutor
Full Next.js App Router application with:
- AI-graded lesson checkpoints (BKT/Elo mastery tracking)
- Auth.js v5: credentials, Google, GitHub, generic OIDC
- Anonymous-session-first with migrate-on-signin
- Admin panel: users, blueprints, reports, site settings
- Password reset + email verification (nodemailer/SMTP)
- Site config: require_auth + signups_enabled flags
- server-only guards on all DB/generation/verification modules
- PostgreSQL 16 + pgvector, Redis cache, Drizzle ORM
- 271 unit tests (Vitest), golden-eval harness, Playwright e2e stubs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 22:25:43 +02:00

64 lines
2.2 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import bcrypt from 'bcryptjs';
import { randomBytes } from 'crypto';
import { eq, count } from 'drizzle-orm';
import { db } from '@/lib/db';
import { users, verificationTokens } from '@/lib/db/schema';
import { sendEmail } from '@/lib/email';
import { verificationEmail } from '@/lib/email/templates';
import { isSignupsEnabled } from '@/lib/site-config';
const RegisterSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(255),
password: z.string().min(8).max(128),
});
export async function POST(req: NextRequest): Promise<NextResponse> {
let body: unknown;
try {
body = await req.json();
} catch {
return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 });
}
const signupsEnabled = await isSignupsEnabled();
if (!signupsEnabled) {
return NextResponse.json({ error: 'Signups are currently disabled' }, { status: 403 });
}
const parsed = RegisterSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: parsed.error.issues.map((i) => i.message).join('; ') },
{ status: 400 },
);
}
const { email, name, password } = parsed.data;
const normalizedEmail = email.toLowerCase();
const [existing] = await db.select({ id: users.id }).from(users).where(eq(users.email, normalizedEmail)).limit(1);
if (existing) {
return NextResponse.json({ error: 'Email already registered' }, { status: 409 });
}
const [{ total }] = await db.select({ total: count() }).from(users);
const role = total === 0 ? 'admin' : 'learner';
const passwordHash = await bcrypt.hash(password, 12);
const [user] = await db
.insert(users)
.values({ email: normalizedEmail, name, passwordHash, role })
.returning({ id: users.id });
const token = randomBytes(32).toString('hex');
const expires = new Date(Date.now() + 86_400_000); // 24 hours
await db.insert(verificationTokens).values({ identifier: normalizedEmail, token, expires });
const { subject, html, text } = verificationEmail(token);
void sendEmail({ to: normalizedEmail, subject, html, text }).catch(() => {});
return NextResponse.json({ userId: user.id }, { status: 201 });
}