Workstream 7: Wehr-Bereich — Fuhrpark & Benutzer (Phase 4)
Implementiert den auf die eigene brigadeId beschränkten Wehr-Bereich: Profil (inkl. Inline-Geocoding via geocodeAddress), Fuhrpark (Fahrzeug per Vorlage oder frei, typisierter Merkmal-Editor), Geräte (Kategorie, Werte, Zuordnung Fahrzeug/„im Gerätehaus") und Benutzerkonten (wehr_admin/wehr_read). - Schema importiert (nicht neu definiert); ASCII-Property wehrfuehrer. - Default-deny dreifach: Layout-Guard requireWehrAdmin() + jede Server Action beginnt mit requireWehrAdmin(); fremde Entities -> notFound() (404). - Validierung an der Grenze (Zod): buildMerkmalValuesSchema validiert Werte typgerecht gegen die serverseitig aufgelösten Definitionen; Rolle auf wehr_admin|wehr_read beschränkt (platform_admin abgelehnt). - upsertMerkmalValues delete-then-insert mit typisierter Drizzle-Tx (kein any); boolean false/num 0 gelten als gesetzt. - argon2id-Einmalpasswort beim Benutzeranlegen; Selbst-Deaktivierung verhindert. - Audit vollständig: brigade.profile_update, vehicle.create/update/delete/status, equipment.create/update/delete/status, user.create/deactivate. - Vorgabewerte aus drei typisierten Spalten (vorgabewert_num/_text/_bool). - i18n via zentraler de.ts; loading/empty/error-konforme Listen. Tests: 22 neue Unit-Tests (vehicle/equipment/brigade-user-Validierung, upsertMerkmalValues) grün; Playwright-Specs verwaltung-fuhrpark + -scoping geschrieben (deferred: kein Server/DB in der Sandbox). Verifikation offline: tsc --noEmit clean, eslint clean, vitest 147 passed, next build exit 0 (alle /verwaltung/*-Routen), drizzle-kit check ohne Drift. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
66
src/server/actions/brigade.ts
Normal file
66
src/server/actions/brigade.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
"use server";
|
||||
|
||||
import { eq } from "drizzle-orm";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { db } from "@/db";
|
||||
import { brigades } from "@/db/schema";
|
||||
import { requireWehrAdmin } from "@/lib/auth/guards";
|
||||
import { brigadeProfileSchema } from "@/lib/validation/brigade";
|
||||
import { geocodeAddress } from "@/lib/geo/nominatim";
|
||||
import { writeAudit } from "@/lib/audit";
|
||||
|
||||
export type ProfileActionResult =
|
||||
| { ok: true; geocodeWarnung: boolean }
|
||||
| { ok: false; error: string };
|
||||
|
||||
/**
|
||||
* Aktualisiert das Profil der EIGENEN Wehr (Default-deny: Guard zuerst).
|
||||
* `brigadeId` kommt IMMER aus der Session. Geocoding inline via `geocodeAddress`
|
||||
* (lat/lng selbst geschrieben; kein zweiter Geo-Pfad). Audit
|
||||
* `brigade.profile_update`. Nicht geokodierbar => Speichern trotzdem, Warnung
|
||||
* an den Aufrufer (Querschnittsstandard 4/6).
|
||||
*/
|
||||
export async function updateBrigadeProfile(
|
||||
input: unknown,
|
||||
): Promise<ProfileActionResult> {
|
||||
const s = await requireWehrAdmin();
|
||||
const parsed = brigadeProfileSchema.safeParse(input);
|
||||
if (!parsed.success) {
|
||||
return {
|
||||
ok: false,
|
||||
error: parsed.error.issues[0]?.message ?? "Ungültige Eingabe.",
|
||||
};
|
||||
}
|
||||
const d = parsed.data;
|
||||
const query = `${d.strasse}, ${d.plz} ${d.ort}, Österreich`;
|
||||
const geo = await geocodeAddress(query);
|
||||
const geocodeWarnung = geo.status !== "ok";
|
||||
|
||||
await db
|
||||
.update(brigades)
|
||||
.set({
|
||||
strasse: d.strasse,
|
||||
plz: d.plz,
|
||||
ort: d.ort,
|
||||
telefon: d.telefon ?? null,
|
||||
email: d.email ?? null,
|
||||
wehrfuehrer: d.wehrfuehrer ?? null,
|
||||
funkrufnameSchema: d.funkrufnameSchema ?? null,
|
||||
geocodeQuery: query,
|
||||
geocodedAt: new Date(),
|
||||
...(geo.status === "ok"
|
||||
? { lat: geo.coords.lat, lng: geo.coords.lng, geocodeStatus: "ok" }
|
||||
: { geocodeStatus: geo.status }),
|
||||
})
|
||||
.where(eq(brigades.id, s.user.brigadeId));
|
||||
|
||||
await writeAudit(
|
||||
s.user.id,
|
||||
"brigade.profile_update",
|
||||
"brigade",
|
||||
s.user.brigadeId,
|
||||
{ geocodeWarnung },
|
||||
);
|
||||
revalidatePath("/verwaltung/profil");
|
||||
return { ok: true, geocodeWarnung };
|
||||
}
|
||||
Reference in New Issue
Block a user