Add Phase 0: sign-up, login, and profile
- Next.js (App Router) + TypeScript + Tailwind - Prisma + SQLite for the User model - Sign-up with 18+ age gate, email/password validation (zod), bcrypt hashing - Login via NextAuth credentials provider, JWT sessions - Protected /profile route (view + edit name/bio) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VL5YUEf2B9LG4xdVUQwcSg
This commit is contained in:
27 files changed
+3196
No files matched your search
@@ -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 };
|
||||
@@ -0,0 +1,36 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { z } from "zod";
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const updateSchema = z.object({
|
||||
bio: z.string().trim().max(500).optional(),
|
||||
name: z.string().trim().min(1).max(80).optional(),
|
||||
});
|
||||
|
||||
export async function PATCH(request: Request) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: "Not signed in" }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json().catch(() => null);
|
||||
if (!body) {
|
||||
return NextResponse.json({ error: "Invalid request body" }, { status: 400 });
|
||||
}
|
||||
|
||||
const parsed = updateSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
const message = parsed.error.issues[0]?.message ?? "Invalid input";
|
||||
return NextResponse.json({ error: message }, { status: 400 });
|
||||
}
|
||||
|
||||
const user = await prisma.user.update({
|
||||
where: { id: session.user.id },
|
||||
data: parsed.data,
|
||||
select: { id: true, name: true, email: true, age: true, bio: true },
|
||||
});
|
||||
|
||||
return NextResponse.json({ user });
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const signupSchema = z.object({
|
||||
name: z.string().trim().min(1, "Name is required").max(80),
|
||||
email: z.string().trim().toLowerCase().email("Enter a valid email"),
|
||||
password: z.string().min(8, "Password must be at least 8 characters"),
|
||||
age: z.number().int().min(18, "You must be 18 or older to join").max(120),
|
||||
});
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body = await request.json().catch(() => null);
|
||||
if (!body) {
|
||||
return NextResponse.json({ error: "Invalid request body" }, { status: 400 });
|
||||
}
|
||||
|
||||
const parsed = signupSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
const message = parsed.error.issues[0]?.message ?? "Invalid input";
|
||||
return NextResponse.json({ error: message }, { status: 400 });
|
||||
}
|
||||
|
||||
const { name, email, password, age } = parsed.data;
|
||||
|
||||
const existing = await prisma.user.findUnique({ where: { email } });
|
||||
if (existing) {
|
||||
return NextResponse.json({ error: "An account with that email already exists" }, { status: 409 });
|
||||
}
|
||||
|
||||
const passwordHash = await bcrypt.hash(password, 12);
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: { name, email, age, passwordHash },
|
||||
select: { id: true, email: true, name: true },
|
||||
});
|
||||
|
||||
return NextResponse.json({ user }, { status: 201 });
|
||||
}
|
||||
Reference in new issue
Block a user