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>
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { db } from '@/lib/db';
|
||||
import { users, passwordResetTokens } from '@/lib/db/schema';
|
||||
import { sendEmail } from '@/lib/email';
|
||||
import { passwordResetEmail } from '@/lib/email/templates';
|
||||
|
||||
const Schema = z.object({ email: z.string().email() });
|
||||
|
||||
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 parsed = Schema.safeParse(body);
|
||||
if (!parsed.success) return NextResponse.json({ error: 'Valid email required' }, { status: 400 });
|
||||
|
||||
// Always return 200 — don't reveal whether email exists
|
||||
const [user] = await db
|
||||
.select({ id: users.id })
|
||||
.from(users)
|
||||
.where(eq(users.email, parsed.data.email.toLowerCase()))
|
||||
.limit(1);
|
||||
|
||||
if (user) {
|
||||
const token = randomBytes(32).toString('hex');
|
||||
const expires = new Date(Date.now() + 3_600_000); // 1 hour
|
||||
|
||||
await db
|
||||
.delete(passwordResetTokens)
|
||||
.where(eq(passwordResetTokens.userId, user.id));
|
||||
|
||||
await db.insert(passwordResetTokens).values({ token, userId: user.id, expires });
|
||||
|
||||
const { subject, html, text } = passwordResetEmail(token);
|
||||
void sendEmail({ to: parsed.data.email, subject, html, text }).catch(() => {});
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
Reference in New Issue
Block a user