120 lines
3.5 KiB
TypeScript
120 lines
3.5 KiB
TypeScript
"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<LoginState> {
|
|
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<SignupState> {
|
|
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" });
|
|
}
|