- 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>
95 lines
5 KiB
TypeScript
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." };
|
|
}
|
|
}
|