"use server"; import bcrypt from "bcryptjs"; import { AuthError } from "next-auth"; import { prisma } from "@/lib/db"; import { signIn, signOut } from "@/auth"; import { LoginSchema, SignupSchema } from "@/lib/validation/auth"; import { Role, SignupMode } from "@/lib/generated/prisma/enums"; import { getSignupMode } from "@/lib/settings"; import { PendingAccountSignin } from "@/lib/auth-errors"; export type SignupState = { error?: string; // Set instead of `error` once a PENDING account is created successfully // -- there's nothing to sign in yet, so the form has nothing to submit. pending?: boolean; } | undefined; export type LoginState = { error?: string; } | undefined; export async function login( _prevState: LoginState, formData: FormData ): Promise { const parsed = LoginSchema.safeParse({ email: formData.get("email"), password: formData.get("password"), }); if (!parsed.success) { return { error: parsed.error.issues[0]?.message ?? "Invalid input." }; } try { await signIn("credentials", { email: parsed.data.email, password: parsed.data.password, redirectTo: "/", }); } catch (error) { if (error instanceof PendingAccountSignin) { return { error: "Your account is awaiting administrator approval." }; } if (error instanceof AuthError) { return { error: "Incorrect email or password." }; } throw error; } } export async function signup( _prevState: SignupState, formData: FormData ): Promise { const parsed = SignupSchema.safeParse({ name: formData.get("name"), email: formData.get("email"), password: formData.get("password"), }); if (!parsed.success) { return { error: parsed.error.issues[0]?.message ?? "Invalid input." }; } const { name, email, password } = parsed.data; // Enforced here, not just hidden in the UI -- this is the actual // back-stop against sign-ups while closed, since a Server Action can // always be invoked directly regardless of what the page renders. const signupMode = await getSignupMode(); if (signupMode === SignupMode.CLOSED) { return { error: "Sign-ups are currently closed." }; } const existing = await prisma.user.findUnique({ where: { email } }); if (existing) { return { error: "An account with that email already exists." }; } const passwordHash = await bcrypt.hash(password, 12); // The very first account on the whole site becomes the administrator // regardless of signupMode -- the site needs at least one admin to // bootstrap. Everyone after that gets USER (mode OPEN) or PENDING // (APPROVED, and CONFIRMED once email confirmation exists). const isFirstUser = (await prisma.user.count()) === 0; const role = isFirstUser ? Role.ADMIN : signupMode === SignupMode.OPEN ? Role.USER : Role.PENDING; await prisma.user.create({ data: { name, email, passwordHash, role }, }); if (role === Role.PENDING) { // Nothing to sign in to yet -- authorize() rejects PENDING accounts. return { pending: true }; } try { // Signs the user in and redirects to "/" on success. NextAuth throws a // framework-handled redirect internally, so nothing after this runs. await signIn("credentials", { email, password, redirectTo: "/" }); } catch (error) { if (error instanceof AuthError) { return { error: "Account created, but sign-in failed. Try logging in." }; } throw error; } } export async function logout() { await signOut({ redirectTo: "/login" }); }