Drei Rollen: Administrator, Techniker, Kunde. Jede der 28 Aktionen nennt das Recht, das sie verlangt; die Rechte stehen an einer Stelle. Kundenkonten haengen an genau einem Kunden und sehen nur dessen Hosts und Rechnungen - ohne Einkaufspreise und Marge. Die Einschraenkung sitzt in den Abfragen selbst, damit keine Seite vergessen kann, sie weiterzugeben; sechs Tests pruefen das. Dabei gefunden: Neu ueber OIDC angelegte Konten haetten die Vorgabe des Datenmodells geerbt - ADMIN. Jetzt gibt es dafuer eine Einstellung mit der Vorgabe Techniker. Passkeys (#028) als gleichwertiger, passwortloser Anmeldeweg neben Passwort und OIDC. Mehrere je Konto, Aufgabe in einem kurzlebigen Cookie, rpID aus der oeffentlichen Adresse. Ohne HTTPS sagt die Seite es, statt still zu scheitern. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
107 lines
3.8 KiB
TypeScript
107 lines
3.8 KiB
TypeScript
"use server";
|
|
|
|
import { revalidatePath } from "next/cache";
|
|
import { prisma } from "@/lib/db";
|
|
import { protokolliere } from "@/server/audit";
|
|
import { sichtbarerKunde, sitzungErforderlich } from "@/server/auth/sitzungspflicht";
|
|
import { alertsAbgleichen } from "@/server/alerts/abgleich";
|
|
import { REGELN } from "@/server/alerts/regeln";
|
|
import type { AktionsErgebnis } from "./kunden";
|
|
|
|
function neuLaden() {
|
|
revalidatePath("/warnungen");
|
|
revalidatePath("/einstellungen/warnregeln");
|
|
revalidatePath("/");
|
|
}
|
|
|
|
/** Warnung quittieren: sie bleibt offen, ist aber als gesehen markiert. */
|
|
export async function alertQuittieren(formular: FormData) {
|
|
await sitzungErforderlich("mitarbeiten");
|
|
const id = formular.get("id")?.toString();
|
|
if (!id) return;
|
|
|
|
// Ein Kundenkonto darf nur eigene Warnungen quittieren. Bedingung und Schreiben in einem
|
|
// Schritt: So kann zwischen Prüfung und Änderung nichts dazwischenkommen.
|
|
const nurKunde = await sichtbarerKunde();
|
|
const ergebnis = await prisma.alert.updateMany({
|
|
where: { id, ...(nurKunde ? { host: { kundeId: nurKunde } } : {}) },
|
|
data: { quittiertAm: new Date() },
|
|
});
|
|
if (ergebnis.count === 0) return;
|
|
|
|
await protokolliere("alert.quittiert", "Alert", id, {});
|
|
neuLaden();
|
|
}
|
|
|
|
/** Warnung von Hand schließen; taucht der Fall weiter auf, öffnet der nächste Abgleich sie erneut. */
|
|
export async function alertSchliessen(formular: FormData) {
|
|
await sitzungErforderlich("fachlichSchreiben");
|
|
const id = formular.get("id")?.toString();
|
|
if (!id) return;
|
|
|
|
await prisma.alert.update({ where: { id }, data: { erledigtAm: new Date() } });
|
|
await protokolliere("alert.geschlossen", "Alert", id, {});
|
|
neuLaden();
|
|
}
|
|
|
|
/** Auswertung sofort anstoßen, ohne auf den Worker zu warten. */
|
|
export async function alertsJetztPruefen(): Promise<AktionsErgebnis> {
|
|
await sitzungErforderlich("betrieb");
|
|
const ergebnis = await alertsAbgleichen();
|
|
neuLaden();
|
|
return {
|
|
erfolg: `${ergebnis.geoeffnet.length} neu, ${ergebnis.erledigt.length} erledigt, ${ergebnis.unveraendert} unverändert.`,
|
|
};
|
|
}
|
|
|
|
/** Schwellwerte und Ein-/Ausschalter der Regeln speichern. */
|
|
export async function warnregelnSpeichern(
|
|
_vorher: AktionsErgebnis,
|
|
formular: FormData,
|
|
): Promise<AktionsErgebnis> {
|
|
await sitzungErforderlich("betrieb");
|
|
for (const regel of REGELN) {
|
|
const aktiv = formular.get(`aktiv:${regel.name}`) === "on";
|
|
const schwellwerte: Record<string, number | number[]> = {};
|
|
|
|
for (const [name, standard] of Object.entries(regel.standardSchwellwerte)) {
|
|
const eingabe = formular.get(`schwelle:${regel.name}:${name}`)?.toString().trim();
|
|
if (!eingabe) continue;
|
|
|
|
if (Array.isArray(standard)) {
|
|
const zahlen = eingabe
|
|
.split(",")
|
|
.map((teil) => Number(teil.trim()))
|
|
.filter((zahl) => Number.isFinite(zahl) && zahl > 0)
|
|
.sort((a, b) => b - a);
|
|
if (zahlen.length === 0) {
|
|
return { fehler: `Bei „${regel.beschreibung}“ bitte mindestens einen Wert angeben.` };
|
|
}
|
|
schwellwerte[name] = zahlen;
|
|
} else {
|
|
const zahl = Number(eingabe);
|
|
if (!Number.isFinite(zahl) || zahl <= 0) {
|
|
return { fehler: `Bei „${regel.beschreibung}“ bitte eine Zahl größer null angeben.` };
|
|
}
|
|
schwellwerte[name] = zahl;
|
|
}
|
|
}
|
|
|
|
await prisma.warnregelEinstellung.upsert({
|
|
where: { regel: regel.name },
|
|
update: { aktiv, schwellwerte },
|
|
create: { regel: regel.name, aktiv, schwellwerte },
|
|
});
|
|
}
|
|
|
|
await protokolliere("warnregeln.geaendert", "WarnregelEinstellung", null, {});
|
|
|
|
// Änderungen sollen sofort sichtbar sein.
|
|
const ergebnis = await alertsAbgleichen();
|
|
neuLaden();
|
|
|
|
return {
|
|
erfolg: `Gespeichert. Auswertung: ${ergebnis.geoeffnet.length} neu, ${ergebnis.erledigt.length} erledigt.`,
|
|
};
|
|
}
|