Organize/lib/actions/auth.ts

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" });
}