diff --git a/src/actions/setup.ts b/src/actions/setup.ts new file mode 100644 index 0000000..8026d48 --- /dev/null +++ b/src/actions/setup.ts @@ -0,0 +1,45 @@ +"use server"; + +import bcrypt from "bcryptjs"; +import { prisma } from "@/lib/prisma"; + +export interface CreateFirstUserState { + error?: string; + success?: boolean; +} + +// Bootstrap-only: only works while the users table is empty, so this can't +// be used to create accounts once the real admin user exists. +export async function createFirstUser( + _prevState: CreateFirstUserState, + formData: FormData +): Promise { + const email = String(formData.get("email") || "").toLowerCase().trim(); + const password = String(formData.get("password") || ""); + const name = String(formData.get("name") || "").trim(); + + if (!email || !password) { + return { error: "Email and password are required." }; + } + if (password.length < 8) { + return { error: "Password must be at least 8 characters long." }; + } + + const existingCount = await prisma.user.count(); + if (existingCount > 0) { + return { error: "An account already exists. This setup page is disabled." }; + } + + const hashedPassword = await bcrypt.hash(password, 12); + + await prisma.user.create({ + data: { + email, + password: hashedPassword, + name: name || null, + role: "ADMIN", + }, + }); + + return { success: true }; +} diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx new file mode 100644 index 0000000..c158973 --- /dev/null +++ b/src/app/signup/page.tsx @@ -0,0 +1,45 @@ +import { prisma } from "@/lib/prisma"; +import { SignupForm } from "@/components/auth/signup-form"; +import { Film } from "lucide-react"; + +export const dynamic = "force-dynamic"; + +export const metadata = { + title: "Set up account — xBid", +}; + +export default async function SignupPage() { + const userCount = await prisma.user.count(); + + return ( +
+
+
+
+ +
+
+

+ xBid +

+

+ {userCount > 0 ? "Setup already complete" : "Create your account"} +

+
+
+ + {userCount > 0 ? ( +

+ An account already exists, so this setup page is disabled.{" "} + + Go to sign in + + . +

+ ) : ( + + )} +
+
+ ); +} diff --git a/src/components/auth/signup-form.tsx b/src/components/auth/signup-form.tsx new file mode 100644 index 0000000..49e7ed5 --- /dev/null +++ b/src/components/auth/signup-form.tsx @@ -0,0 +1,62 @@ +"use client"; + +import { useActionState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Card, CardContent } from "@/components/ui/card"; +import { createFirstUser, type CreateFirstUserState } from "@/actions/setup"; + +const initialState: CreateFirstUserState = {}; + +export function SignupForm() { + const [state, action, pending] = useActionState(createFirstUser, initialState); + + if (state.success) { + return ( + + +

+ Account created. You can delete the signup page now — sign in at{" "} + + /login + + . +

+
+
+ ); + } + + return ( + + +
+
+ + +
+
+ + +
+
+ + +
+ {state.error &&

{state.error}

} + +
+
+
+ ); +} diff --git a/src/proxy.ts b/src/proxy.ts index 8f1b757..37e68e5 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -2,7 +2,7 @@ import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; import { getToken } from "next-auth/jwt"; -const PUBLIC_PATHS = ["/login"]; +const PUBLIC_PATHS = ["/login", "/signup"]; export default async function proxy(request: NextRequest) { const { pathname } = request.nextUrl;