"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 { 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 { 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 { 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 { 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." }; } }