mega-plant/app/actions/auth.ts
Dave 2594dead84 feat: add user access and improve plant dashboard
Add PocketBase login, first-sign-in password changes, account settings,
and user activation, reactivation and deletion with self-removal guards.
Protect application pages and APIs and keep private pages out of PWA
caches.

Resolve plant locations from site assignments and improve map controls,
profit chart views, navigation and mobile table layouts.

Validation: production build and TypeScript passed; 28 unit tests passed.
Backend access integration tests and mobile verification are documented.
2026-10-06 04:02:08 +02:00

91 lines
4.1 KiB
TypeScript

"use server";
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, setUserActive } from "@/lib/auth-service";
import { SESSION_COOKIE, authDestination, 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 = 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();
if (!session) redirect("/login");
if (session.passwordChanged) redirect("/");
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("/");
}
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 user = await addUser(name, email);
revalidatePath("/settings");
return { success: `${user.name} has been added. They can sign in 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"].includes(operation)) {
return { error: "Choose a valid user and action." };
}
try {
if (operation === "delete") await deleteUser(actor.id, userId);
else await setUserActive(actor.id, userId, operation === "activate");
revalidatePath("/settings");
return { success: 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." };
}
}