mega-plant/app/actions/auth.ts
David 1ec52961ee Cash float card, Office role + Slips tab, job card PDF, manual close/delete
- Cash Floats: ledger card (opening balance, top-ups, slips), manual entries, slip photos
- Office access level; clock app Slips tab (photograph a slip, popup for supplier/cost
  code/comments/amount); only Office sees it; per-role route table in lib/clock-access
- Job cards: Machine Service PDF (with Oil block), JC numbers, manual Close job, Delete
- Hide Expenses > Vehicle Costs and Finance (Bastion) from the menu

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 15:09:57 +02:00

95 lines
5 KiB
TypeScript

"use server";
import { clockAuthDestination } from "@/lib/clock-pwa";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
import { ClientResponseError } from "pocketbase";
import { getSession, requireUser } from "@/lib/auth";
import { addUser, authenticateUser, changeUserPassword, deleteUser, InactiveUserError, setUserAccess, setUserActive } from "@/lib/auth-service";
import { SESSION_COOKIE, authDestination, parseAccess, validateLogin, validateNewPassword, validateNewUser } from "@/lib/auth-policy";
export type FormState = { error?: string; success?: string };
const value = (form: FormData, key: string) => String(form.get(key) ?? "");
async function saveSession(token: string) {
(await cookies()).set(SESSION_COOKIE, token, {
httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "lax",
path: "/", maxAge: 60 * 60 * 24 * 7,
});
}
export async function loginAction(_state: FormState, form: FormData): Promise<FormState> {
const email = value(form, "email");
const password = value(form, "password");
const error = validateLogin(email, password);
if (error) return { error };
let destination: string;
try {
const session = await authenticateUser(email, password);
await saveSession(session.pb.authStore.token);
destination = value(form,"clock")==="1"?clockAuthDestination(session):authDestination(session);
} catch (error) {
return { error: error instanceof InactiveUserError ? error.message : error instanceof ClientResponseError && error.status === 400
? "The email or password is incorrect. Please try again."
: "We couldn't connect to your account. Please try again shortly." };
}
redirect(destination);
}
export async function changePasswordAction(_state: FormState, form: FormData): Promise<FormState> {
const session = await getSession();
const clock=value(form,"clock")==="1";
if (!session) redirect(clock?"/clock/login":"/login");
if (session.passwordChanged) redirect(clock?"/clock":"/");
const password = value(form, "password");
const error = validateNewPassword(password, value(form, "confirmation"));
if (error) return { error };
try {
const changed = await changeUserPassword(session, password);
await saveSession(changed.pb.authStore.token);
} catch {
return { error: "We couldn't finish changing your password. Try again, or sign out and sign in with your new password if it was saved." };
}
redirect(clock?"/clock":"/");
}
export async function addUserAction(_state: FormState, form: FormData): Promise<FormState> {
await requireUser();
const name = value(form, "name");
const email = value(form, "email");
const error = validateNewUser(name, email);
if (error) return { error };
try {
const access = parseAccess(value(form, "access"));
const user = await addUser(name, email, access);
revalidatePath("/settings");
return { success: `${user.name} has been added${access === "clock" ? " as a clock-in user" : access === "mechanic" ? " as a mechanic" : access === "office" ? " as an office user" : ""}. They can sign in${access === "full" ? "" : " at /clock"} with ${user.email} and the starter password.` };
} catch (error) {
const details = error instanceof ClientResponseError ? error.response?.data : null;
return { error: details?.email?.code === "validation_not_unique"
? "A user with that email already exists."
: "We couldn't add this user. Check the details and try again." };
}
}
export async function manageUserAction(_state: FormState, form: FormData): Promise<FormState> {
const { user: actor } = await requireUser();
const userId = value(form, "userId");
const operation = value(form, "operation");
if (userId === actor.id) return { error: "You can't disable or delete your own account." };
if (!/^[a-z0-9]{15}$/.test(userId) || !["activate", "deactivate", "delete", "full", "clock", "mechanic", "office"].includes(operation)) {
return { error: "Choose a valid user and action." };
}
try {
if (operation === "delete") await deleteUser(actor.id, userId);
else if (operation === "full" || operation === "clock" || operation === "mechanic" || operation === "office") await setUserAccess(actor.id, userId, operation);
else await setUserActive(actor.id, userId, operation === "activate");
revalidatePath("/settings");
return { success: operation === "full" ? "User now has full access." : operation === "clock" ? "User is now a clock-in user (clock app only)." : operation === "mechanic" ? "User is now a mechanic (Fuel and Job cards only)." : operation === "office" ? "User is now an office user (Slips only)." : operation === "delete" ? "User deleted." : operation === "activate" ? "User is active again." : "User is inactive and can no longer access the app." };
} catch (error) {
return { error: error instanceof ClientResponseError && error.status === 404
? "This user no longer exists. Refresh the list."
: "We couldn't update this user. Please try again." };
}
}