diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..fed932c --- /dev/null +++ b/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,31 @@ +import NextAuth, { type NextAuthOptions } from "next-auth" +import Credentials from "next-auth/providers/credentials" +import { PrismaAdapter } from "@auth/prisma-adapter" +import prisma from "@/lib/prisma" +import { compare } from "bcryptjs" + +export const authOptions: NextAuthOptions = { + // Adapter from @auth works with next-auth v4 as well + adapter: PrismaAdapter(prisma) as any, + session: { strategy: "jwt" }, + providers: [ + Credentials({ + name: "Credentials", + credentials: { + email: { label: "Email", type: "text" }, + password: { label: "Password", type: "password" }, + }, + async authorize(credentials) { + if (!credentials?.email || !credentials?.password) return null + const user = await prisma.user.findUnique({ where: { email: credentials.email } }) + if (!user || !user.password) return null + const ok = await compare(credentials.password, user.password) + if (!ok) return null + return { id: user.id, email: user.email, name: user.name ?? undefined } + }, + }), + ], +} + +const handler = NextAuth(authOptions) +export { handler as GET, handler as POST } diff --git a/app/api/signup/route.ts b/app/api/signup/route.ts new file mode 100644 index 0000000..e55fa97 --- /dev/null +++ b/app/api/signup/route.ts @@ -0,0 +1,18 @@ +import { NextResponse } from "next/server" +import prisma from "@/lib/prisma" +import { hash } from "bcryptjs" + +export async function POST(req: Request) { + const body = await req.json().catch(() => null) as { email?: string; password?: string; name?: string } | null + if (!body?.email || !body?.password) return NextResponse.json({ error: "Missing fields" }, { status: 400 }) + + const existing = await prisma.user.findUnique({ where: { email: body.email } }) + if (existing) return NextResponse.json({ error: "Email in use" }, { status: 409 }) + + const password = await hash(body.password, 10) + const user = await prisma.user.create({ data: { email: body.email, name: body.name ?? null, password } }) + + return NextResponse.json({ id: user.id, email: user.email }) +} + + diff --git a/app/components/AuthPanel.tsx b/app/components/AuthPanel.tsx new file mode 100644 index 0000000..4cf1712 --- /dev/null +++ b/app/components/AuthPanel.tsx @@ -0,0 +1,67 @@ +"use client" + +import { useState } from "react" +import { signIn, signOut, useSession } from "next-auth/react" + +export default function AuthPanel() { + const { data: session } = useSession() + const [email, setEmail] = useState("") + const [password, setPassword] = useState("") + const [name, setName] = useState("") + const [loading, setLoading] = useState(false) + const [msg, setMsg] = useState("") + + const doSignup = async () => { + setLoading(true) + setMsg("") + try { + const res = await fetch("/api/signup", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password, name }), + }) + if (!res.ok) { + const j = await res.json().catch(() => ({})) + throw new Error(j?.error || `HTTP ${res.status}`) + } + setMsg("Signed up. Now log in.") + } catch (e: any) { + setMsg(e?.message || "Failed") + } finally { + setLoading(false) + } + } + + const doLogin = async () => { + setLoading(true) + setMsg("") + const res = await signIn("credentials", { email, password, redirect: false }) + if (res?.error) setMsg(res.error) + setLoading(false) + } + + if (session?.user) { + return ( +