Files
alpenwerk-hr/actions/positions.ts
Maximilian Stubhan f96773da0f Reports/Export builder (CSV/XLSX), plus a security fix pass
Adds the Berichte export pipeline (/api/export/{report,events,employees})
with shared CSV/XLSX writers in lib/export.ts and lib/reports-data.ts.

Security pass alongside it: sanitize .or() search terms against PostgREST
filter injection, sanitize spreadsheet cells against CSV/Excel formula
injection, stop leaking raw DB error messages to clients, harden the
service-role client with server-only, add baseline security headers, and
bump the vulnerable nested postcss via an override.
2026-07-15 20:34:27 +02:00

59 lines
2.1 KiB
TypeScript

"use server";
import { revalidatePath } from "next/cache";
import { sanitizeIlikeTerm } from "@/lib/supabase/query";
import { createClient } from "@/lib/supabase/server";
type ActionResult = { success: boolean; error?: string };
export async function createPosition(payload: {
title: string;
superior_employee_id: string;
is_lead: boolean;
team_id?: string;
}): Promise<ActionResult> {
const supabase = await createClient();
const { error } = await supabase.rpc("create_position", { payload });
if (error) return { success: false, error: error.message };
revalidatePath("/positions");
revalidatePath("/orgchart");
revalidatePath("/");
return { success: true };
}
export async function staffPositionInternally(payload: {
position_id: string;
employee_id: string;
}): Promise<ActionResult> {
const supabase = await createClient();
const { error } = await supabase.rpc("staff_position_internally", { payload });
if (error) return { success: false, error: error.message };
revalidatePath("/positions");
revalidatePath("/orgchart");
revalidatePath("/employees");
revalidatePath(`/employees/${payload.employee_id}`);
revalidatePath("/");
return { success: true };
}
export type SuperiorSearchResult = { id: string; first_name: string; last_name: string; job_title: string; division_id: string };
// For "Position ausschreiben": superior lookup, filtered to team-leads when
// the new position is an IC role, or to division-heads/CEO when the new
// position is itself a team lead (§2).
export async function searchSuperiors(query: string, forLeadPosition: boolean): Promise<SuperiorSearchResult[]> {
const supabase = await createClient();
let q = supabase
.from("employees")
.select("id, first_name, last_name, job_title, division_id")
.eq("status", "Aktiv")
.limit(20);
q = forLeadPosition ? q.lte("org_level", 1) : q.eq("is_lead", true).eq("org_level", 2);
if (query.trim()) {
const term = sanitizeIlikeTerm(query.trim());
q = q.or(`first_name.ilike.%${term}%,last_name.ilike.%${term}%,job_title.ilike.%${term}%`);
}
const { data } = await q;
return data ?? [];
}