diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..643577d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,9 @@ + + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + + diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/src/app/api/profile/route.ts b/src/app/api/profile/route.ts index 6b23dcd..08c87e9 100644 --- a/src/app/api/profile/route.ts +++ b/src/app/api/profile/route.ts @@ -2,7 +2,7 @@ import { NextResponse } from "next/server"; import { getServerSession } from "next-auth"; import { z } from "zod"; import { authOptions } from "@/lib/auth"; -import { prisma } from "@/lib/prisma"; +import { prisma, ensureDb } from "@/lib/prisma"; const updateSchema = z.object({ bio: z.string().trim().max(500).optional(), @@ -10,6 +10,8 @@ const updateSchema = z.object({ }); export async function PATCH(request: Request) { + await ensureDb(); + const session = await getServerSession(authOptions); if (!session?.user?.id) { return NextResponse.json({ error: "Not signed in" }, { status: 401 }); diff --git a/src/app/api/signup/route.ts b/src/app/api/signup/route.ts index 8ed8085..64c6b34 100644 --- a/src/app/api/signup/route.ts +++ b/src/app/api/signup/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from "next/server"; import bcrypt from "bcryptjs"; import { z } from "zod"; -import { prisma } from "@/lib/prisma"; +import { prisma, ensureDb } from "@/lib/prisma"; const signupSchema = z.object({ name: z.string().trim().min(1, "Name is required").max(80), @@ -11,6 +11,8 @@ const signupSchema = z.object({ }); export async function POST(request: Request) { + await ensureDb(); + const body = await request.json().catch(() => null); if (!body) { return NextResponse.json({ error: "Invalid request body" }, { status: 400 }); diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index 23e2b30..8e15d40 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -1,7 +1,7 @@ import { getServerSession } from "next-auth"; import { redirect } from "next/navigation"; import { authOptions } from "@/lib/auth"; -import { prisma } from "@/lib/prisma"; +import { prisma, ensureDb } from "@/lib/prisma"; import ProfileEditor from "./profile-editor"; import SignOutButton from "./sign-out-button"; @@ -11,6 +11,8 @@ export default async function ProfilePage() { redirect("/login"); } + await ensureDb(); + const user = await prisma.user.findUnique({ where: { id: session.user.id }, select: { id: true, name: true, email: true, age: true, bio: true }, diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 6bf2a9a..38e2642 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,7 +1,7 @@ import type { AuthOptions } from "next-auth"; import CredentialsProvider from "next-auth/providers/credentials"; import bcrypt from "bcryptjs"; -import { prisma } from "@/lib/prisma"; +import { prisma, ensureDb } from "@/lib/prisma"; export const authOptions: AuthOptions = { session: { strategy: "jwt" }, @@ -18,6 +18,8 @@ export const authOptions: AuthOptions = { async authorize(credentials) { if (!credentials?.email || !credentials?.password) return null; + await ensureDb(); + const user = await prisma.user.findUnique({ where: { email: credentials.email.toLowerCase() }, }); diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index a309320..6022f84 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -1,9 +1,48 @@ import { PrismaClient } from "@prisma/client"; -const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient }; +const globalForPrisma = globalThis as unknown as { + prisma?: PrismaClient; + dbReady?: Promise; +}; -export const prisma = globalForPrisma.prisma ?? new PrismaClient(); +// On Vercel's serverless functions, only /tmp is writable, and it starts +// empty on every cold start. Point SQLite there instead of the repo-relative +// path used for local development. This is a demo-friendly stand-in until a +// real hosted database is wired up (data does not persist across cold starts). +const databaseUrl = process.env.VERCEL ? "file:/tmp/dev.db" : process.env.DATABASE_URL; + +export const prisma = + globalForPrisma.prisma ?? + new PrismaClient({ + datasources: { db: { url: databaseUrl } }, + }); if (process.env.NODE_ENV !== "production") { globalForPrisma.prisma = prisma; } + +const CREATE_USER_TABLE = ` +CREATE TABLE IF NOT EXISTS "User" ( + "id" TEXT NOT NULL PRIMARY KEY, + "email" TEXT NOT NULL, + "passwordHash" TEXT NOT NULL, + "name" TEXT NOT NULL, + "age" INTEGER NOT NULL, + "bio" TEXT NOT NULL DEFAULT '', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); +`; +const CREATE_USER_EMAIL_INDEX = `CREATE UNIQUE INDEX IF NOT EXISTS "User_email_key" ON "User"("email");`; + +// Idempotent, so it's safe to call before every request. Needed because on +// Vercel the /tmp SQLite file is missing entirely on a fresh cold start. +export async function ensureDb(): Promise { + if (!globalForPrisma.dbReady) { + globalForPrisma.dbReady = prisma + .$executeRawUnsafe(CREATE_USER_TABLE) + .then(() => prisma.$executeRawUnsafe(CREATE_USER_EMAIL_INDEX)) + .then(() => undefined); + } + await globalForPrisma.dbReady; +}