{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "admin-auth",
  "type": "registry:file",
  "title": "NextAuth v5 Admin Auth + Prisma",
  "description": "Full admin authentication for Next.js App Router: NextAuth v5 credentials, Prisma 7 (PostgreSQL via driver adapter), JWT sessions with isAdmin, guarded /admin routes, login page and admin seed.",
  "author": "degaina-store",
  "dependencies": [
    "next-auth@beta",
    "@prisma/client",
    "@prisma/adapter-pg",
    "pg",
    "bcryptjs"
  ],
  "devDependencies": [
    "prisma",
    "tsx",
    "dotenv",
    "@types/pg",
    "@types/bcryptjs"
  ],
  "files": [
    {
      "path": "auth.ts",
      "type": "registry:file",
      "target": "~/auth.ts",
      "content": "import NextAuth from \"next-auth\";\nimport Credentials from \"next-auth/providers/credentials\";\nimport bcrypt from \"bcryptjs\";\nimport { prisma } from \"@/lib/prisma\";\n\nexport const { handlers, signIn, signOut, auth } = NextAuth({\n  session: { strategy: \"jwt\" },\n  providers: [\n    Credentials({\n      credentials: {\n        email: { label: \"Email\", type: \"email\" },\n        password: { label: \"Password\", type: \"password\" },\n      },\n      authorize: async (credentials) => {\n        const email = credentials?.email as string | undefined;\n        const password = credentials?.password as string | undefined;\n\n        if (!email || !password) return null;\n\n        const user = await prisma.user.findUnique({ where: { email } });\n        if (!user) return null;\n\n        const valid = await bcrypt.compare(password, user.passwordHash);\n        if (!valid) return null;\n\n        return {\n          id: user.id,\n          email: user.email,\n          name: user.name,\n          isAdmin: user.isAdmin,\n        };\n      },\n    }),\n  ],\n  callbacks: {\n    jwt({ token, user }) {\n      if (user) {\n        token.isAdmin = (user as { isAdmin: boolean }).isAdmin;\n      }\n      return token;\n    },\n    session({ session, token }) {\n      if (token.isAdmin) {\n        session.user.isAdmin = token.isAdmin;\n      }\n      return session;\n    },\n  },\n  pages: {\n    signIn: \"/login\",\n  },\n});\n"
    },
    {
      "path": "lib/prisma.ts",
      "type": "registry:file",
      "target": "~/lib/prisma.ts",
      "content": "import \"dotenv/config\";\nimport { PrismaPg } from \"@prisma/adapter-pg\";\nimport { PrismaClient } from \"@/app/generated/prisma/client\";\n\nconst globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };\n\nfunction createPrismaClient() {\n  const adapter = new PrismaPg({\n    connectionString: process.env.DATABASE_URL!,\n  });\n  return new PrismaClient({ adapter });\n}\n\nexport const prisma = globalForPrisma.prisma ?? createPrismaClient();\n\nif (process.env.NODE_ENV !== \"production\") globalForPrisma.prisma = prisma;\n"
    },
    {
      "path": "types/next-auth.d.ts",
      "type": "registry:file",
      "target": "~/types/next-auth.d.ts",
      "content": "import { DefaultSession } from \"next-auth\";\n\ndeclare module \"next-auth\" {\n  interface Session {\n    user: {\n      isAdmin?: boolean;\n    } & DefaultSession[\"user\"];\n  }\n\n  interface User {\n    isAdmin?: boolean;\n  }\n}\n\ndeclare module \"@auth/core/jwt\" {\n  interface JWT {\n    isAdmin?: boolean;\n  }\n}\n"
    },
    {
      "path": "app/api/auth/[...nextauth]/route.ts",
      "type": "registry:file",
      "target": "~/app/api/auth/[...nextauth]/route.ts",
      "content": "import { handlers } from \"@/auth\";\n\nexport const { GET, POST } = handlers;\n"
    },
    {
      "path": "app/login/page.tsx",
      "type": "registry:file",
      "target": "~/app/login/page.tsx",
      "content": "\"use client\";\n\nimport { Suspense, useState } from \"react\";\nimport { signIn } from \"next-auth/react\";\nimport { useRouter, useSearchParams } from \"next/navigation\";\n\nexport default function LoginPage() {\n  return (\n    <Suspense>\n      <LoginForm />\n    </Suspense>\n  );\n}\n\nfunction LoginForm() {\n  const router = useRouter();\n  const searchParams = useSearchParams();\n  const callbackUrl = searchParams.get(\"callbackUrl\") ?? \"/admin\";\n\n  const [error, setError] = useState<string | null>(null);\n  const [pending, setPending] = useState(false);\n  const [showPassword, setShowPassword] = useState(false);\n\n  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {\n    e.preventDefault();\n    setError(null);\n    setPending(true);\n\n    const formData = new FormData(e.currentTarget);\n    const result = await signIn(\"credentials\", {\n      email: formData.get(\"email\"),\n      password: formData.get(\"password\"),\n      redirect: false,\n    });\n\n    setPending(false);\n\n    if (result?.error) {\n      setError(\"Invalid email or password.\");\n      return;\n    }\n\n    router.push(callbackUrl);\n    router.refresh();\n  }\n\n  return (\n    <div className=\"flex min-h-full flex-1 flex-col items-center justify-center bg-zinc-50 px-6 py-12\">\n      <div className=\"w-full max-w-sm\">\n        <h1 className=\"text-2xl font-semibold tracking-tight text-zinc-900\">\n          Degaina Store\n        </h1>\n        <p className=\"mt-1 text-sm text-zinc-500\">Sign in to your account</p>\n\n        <form onSubmit={handleSubmit} className=\"mt-8 space-y-4\">\n          <div>\n            <label\n              htmlFor=\"email\"\n              className=\"block text-sm font-medium text-zinc-700\"\n            >\n              Email\n            </label>\n            <input\n              id=\"email\"\n              name=\"email\"\n              type=\"email\"\n              required\n              autoComplete=\"email\"\n              className=\"mt-1 w-full rounded-md border border-zinc-300 px-3 py-2 text-sm text-zinc-900 placeholder-zinc-400 outline-none focus:border-zinc-500 focus:ring-2 focus:ring-zinc-200\"\n              placeholder=\"admin@degaina.com\"\n            />\n          </div>\n\n          <div>\n            <label\n              htmlFor=\"password\"\n              className=\"block text-sm font-medium text-zinc-700\"\n            >\n              Password\n            </label>\n            <div className=\"relative mt-1\">\n              <input\n                id=\"password\"\n                name=\"password\"\n                type={showPassword ? \"text\" : \"password\"}\n                required\n                autoComplete=\"current-password\"\n                className=\"w-full rounded-md border border-zinc-300 px-3 py-2 pr-10 text-sm text-zinc-900 placeholder-zinc-400 outline-none focus:border-zinc-500 focus:ring-2 focus:ring-zinc-200\"\n                placeholder=\"••••••••\"\n              />\n              <button\n                type=\"button\"\n                onClick={() => setShowPassword((v) => !v)}\n                aria-label={showPassword ? \"Hide password\" : \"Show password\"}\n                className=\"absolute inset-y-0 right-0 flex items-center px-3 text-zinc-400 transition-colors hover:text-zinc-600\"\n              >\n                {showPassword ? (\n                  <svg\n                    xmlns=\"http://www.w3.org/2000/svg\"\n                    fill=\"none\"\n                    viewBox=\"0 0 24 24\"\n                    strokeWidth={1.5}\n                    stroke=\"currentColor\"\n                    className=\"h-5 w-5\"\n                  >\n                    <path\n                      strokeLinecap=\"round\"\n                      strokeLinejoin=\"round\"\n                      d=\"M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88\"\n                    />\n                  </svg>\n                ) : (\n                  <svg\n                    xmlns=\"http://www.w3.org/2000/svg\"\n                    fill=\"none\"\n                    viewBox=\"0 0 24 24\"\n                    strokeWidth={1.5}\n                    stroke=\"currentColor\"\n                    className=\"h-5 w-5\"\n                  >\n                    <path\n                      strokeLinecap=\"round\"\n                      strokeLinejoin=\"round\"\n                      d=\"M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z\"\n                    />\n                    <path\n                      strokeLinecap=\"round\"\n                      strokeLinejoin=\"round\"\n                      d=\"M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z\"\n                    />\n                  </svg>\n                )}\n              </button>\n            </div>\n          </div>\n\n          {error && (\n            <p className=\"rounded-md bg-red-50 px-3 py-2 text-sm text-red-600\">\n              {error}\n            </p>\n          )}\n\n          <button\n            type=\"submit\"\n            disabled={pending}\n            className=\"w-full rounded-md bg-zinc-900 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-zinc-700 disabled:cursor-not-allowed disabled:opacity-50\"\n          >\n            {pending ? \"Signing in…\" : \"Sign in\"}\n          </button>\n        </form>\n      </div>\n    </div>\n  );\n}\n"
    },
    {
      "path": "app/(admin)/layout.tsx",
      "type": "registry:file",
      "target": "~/app/(admin)/layout.tsx",
      "content": "import { redirect } from \"next/navigation\";\nimport { auth } from \"@/auth\";\nimport AdminNav from \"./admin/AdminNav\";\n\nexport default async function AdminLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  const session = await auth();\n\n  if (!session?.user) {\n    redirect(\"/login\");\n  }\n\n  if (!session.user.isAdmin) {\n    redirect(\"/\");\n  }\n\n  return (\n    <div className=\"min-h-screen bg-zinc-50\">\n      <AdminNav />\n      <main className=\"mx-auto max-w-6xl px-6 py-8\">{children}</main>\n    </div>\n  );\n}\n"
    },
    {
      "path": "app/(admin)/admin/page.tsx",
      "type": "registry:file",
      "target": "~/app/(admin)/admin/page.tsx",
      "content": "import { auth } from \"@/auth\";\n\nexport const metadata = {\n  title: \"Admin\",\n};\n\nexport default async function AdminPage() {\n  const session = await auth();\n\n  return (\n    <div>\n      <h1 className=\"text-2xl font-semibold tracking-tight text-zinc-900\">\n        Welcome, {session?.user?.name ?? session?.user?.email}\n      </h1>\n      <p className=\"mt-1 text-sm text-zinc-500\">\n        Admin dashboard. More sections coming soon.\n      </p>\n    </div>\n  );\n}\n"
    },
    {
      "path": "app/(admin)/admin/AdminNav.tsx",
      "type": "registry:file",
      "target": "~/app/(admin)/admin/AdminNav.tsx",
      "content": "\"use client\";\n\nimport { signOut } from \"next-auth/react\";\nimport Link from \"next/link\";\n\nexport default function AdminNav() {\n  return (\n    <header className=\"border-b border-zinc-200 bg-white\">\n      <div className=\"mx-auto flex h-14 max-w-6xl items-center justify-between px-6\">\n        <Link href=\"/admin\" className=\"text-sm font-semibold text-zinc-900\">\n          Degaina Store Admin\n        </Link>\n        <div className=\"flex items-center gap-4\">\n          <Link\n            href=\"/\"\n            className=\"text-sm text-zinc-500 transition-colors hover:text-zinc-900\"\n          >\n            View store\n          </Link>\n          <button\n            onClick={() => signOut({ callbackUrl: \"/login\" })}\n            className=\"rounded-md border border-zinc-300 px-3 py-1.5 text-sm font-medium text-zinc-700 transition-colors hover:bg-zinc-100\"\n          >\n            Sign out\n          </button>\n        </div>\n      </div>\n    </header>\n  );\n}\n"
    },
    {
      "path": "app/page.tsx",
      "type": "registry:file",
      "target": "~/app/page.tsx",
      "content": "import Link from \"next/link\";\n\nexport default function Home() {\n  return (\n    <div className=\"flex min-h-full flex-1 flex-col items-center justify-center bg-white px-6 py-24\">\n      <div className=\"flex flex-col items-center text-center\">\n        <h1 className=\"max-w-2xl text-4xl font-semibold tracking-tight text-zinc-900 sm:text-5xl\">\n          Degaina Store\n        </h1>\n        <p className=\"mt-4 max-w-md text-lg text-zinc-500\">\n          A modern store, coming soon.\n        </p>\n        <Link\n          href=\"/login\"\n          className=\"mt-8 rounded-full bg-zinc-900 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-zinc-700\"\n        >\n          Sign in\n        </Link>\n      </div>\n    </div>\n  );\n}\n"
    },
    {
      "path": "prisma/schema.prisma",
      "type": "registry:file",
      "target": "~/prisma/schema.prisma",
      "content": "generator client {\n  provider = \"prisma-client\"\n  output   = \"../app/generated/prisma\"\n}\n\ndatasource db {\n  provider = \"postgresql\"\n}\n\nmodel User {\n  id           String   @id @default(cuid())\n  email        String   @unique\n  name         String?\n  passwordHash String\n  isAdmin      Boolean  @default(false)\n  createdAt    DateTime @default(now())\n  updatedAt    DateTime @updatedAt\n}\n"
    },
    {
      "path": "prisma/seed.ts",
      "type": "registry:file",
      "target": "~/prisma/seed.ts",
      "content": "import \"dotenv/config\";\nimport { PrismaPg } from \"@prisma/adapter-pg\";\nimport { PrismaClient } from \"@/app/generated/prisma/client\";\nimport bcrypt from \"bcryptjs\";\n\nconst adapter = new PrismaPg({\n  connectionString: process.env.DATABASE_URL!,\n});\nconst prisma = new PrismaClient({ adapter });\n\nasync function main() {\n  const email = process.env.ADMIN_EMAIL;\n  const password = process.env.ADMIN_PASSWORD;\n  const name = process.env.ADMIN_NAME ?? \"Admin\";\n\n  if (!email || !password) {\n    throw new Error(\"ADMIN_EMAIL and ADMIN_PASSWORD must be set in .env\");\n  }\n\n  const passwordHash = await bcrypt.hash(password, 10);\n\n  const admin = await prisma.user.upsert({\n    where: { email },\n    update: {\n      name,\n      passwordHash,\n      isAdmin: true,\n    },\n    create: {\n      email,\n      name,\n      passwordHash,\n      isAdmin: true,\n    },\n  });\n\n  console.log(`Seeded admin user: ${admin.email} (isAdmin: ${admin.isAdmin})`);\n}\n\nmain()\n  .catch((e) => {\n    console.error(e);\n    process.exit(1);\n  })\n  .finally(async () => {\n    await prisma.$disconnect();\n  });\n"
    },
    {
      "path": "prisma.config.ts",
      "type": "registry:file",
      "target": "~/prisma.config.ts",
      "content": "// This file was generated by Prisma, and assumes you have installed the following:\n// npm install --save-dev prisma dotenv\nimport \"dotenv/config\";\nimport { defineConfig } from \"prisma/config\";\n\nexport default defineConfig({\n  schema: \"prisma/schema.prisma\",\n  migrations: {\n    path: \"prisma/migrations\",\n    seed: \"tsx prisma/seed.ts\",\n  },\n  datasource: {\n    url: process.env[\"DATABASE_URL\"],\n  },\n});\n"
    },
    {
      "path": ".env",
      "type": "registry:file",
      "target": "~/.env",
      "content": "# Database\nDATABASE_URL=\"postgresql://postgres:StrongPassword%40123@localhost:5432/mydb?schema=public\"\n\n# Auth.js (session signing key - regenerate with: npx auth secret)\nAUTH_SECRET=\"oFMPvlGkAeboJ0zZ69vBFZRTfRw1gbEjkFfaMl5CA2U=\"\n\n# Admin seed user\nADMIN_EMAIL=\"admin@degaina.com\"\nADMIN_PASSWORD=\"admin12345\"\nADMIN_NAME=\"Admin\"\n"
    }
  ],
  "docs": "Next steps after installation:\n\n1. Review .env (created for you) and update DATABASE_URL, AUTH_SECRET, ADMIN_EMAIL and ADMIN_PASSWORD.\n2. npx prisma generate\n3. npx prisma migrate dev --name init\n4. npx prisma db seed\n5. npm run dev\n\nOpen http://localhost:3000/admin and sign in with ADMIN_EMAIL / ADMIN_PASSWORD.\nRoutes: /login (sign in), /admin (guarded dashboard)."
}
