Wenn ein Formular einen Fremdschluessel mitschickt, kommt der aus einer Auswahlliste im Browser. Der Server darf sich darauf nicht verlassen - eine veraenderte Anfrage kann jede beliebige Id enthalten. Alle 22 Dateien unter src/server/actions/ durchgesehen. Das Ergebnis ist besser als befuerchtet: Bis auf eine Stelle war jeder Verweis geprueft. buchung.vorgangId ging ungeprueft durch. Konto, Beleg und Vertrag wurden geprueft, der Vorgang nicht. Die Pruefung liegt jetzt in lib/auth/verweise.ts statt verstreut in den Aktionen. Die acht erlaubten Ziele sind eine ausgeschriebene Aufzaehlung und kein dynamischer Tabellenzugriff: So sieht der Typpruefer jede Abfrage, und welche Tabellen erreichbar sind, ist eine Entscheidung und keine Nebenwirkung. Mehrere Verweise laufen nebeneinander - bei vier Verweisen eine Wartezeit statt vier. Umgestellt: die vier Einzelpruefungen der Buchung, die drei des Vermoegenspostens, die eigene Hilfsfunktion fuer den Ansprechpartner. Nicht angefasst wurden die Stellen, die ihre eigene, passendere Form haben - etwa das Kontaktprotokoll, das beide Verweise parallel laedt und feldgenaue Meldungen zurueckgibt. Zum Test: Der erste Entwurf rief die Server Action per fetch nach. Der waere auch ohne jede Pruefung gruen gewesen, weil Next einen Aufruf ohne gueltige Action-Id ohnehin ablehnt - er haette nichts bewiesen. Der jetzige schiebt dem echten Auswahlfeld einen Eintrag mit fremder Id unter und schickt das Formular normal ab. Gegenprobe mit ausgebauter Pruefung: faellt um, wie er soll. Refs #23
122 lines
4.3 KiB
TypeScript
122 lines
4.3 KiB
TypeScript
import 'server-only'
|
|
import { db, nichtGeloescht } from '@/lib/db'
|
|
|
|
/**
|
|
* Gehoert ein verknuepfter Datensatz zu dieser Akte?
|
|
*
|
|
* Wenn ein Formular einen Fremdschluessel mitschickt - Konto, Vertrag,
|
|
* Immobilie, Dokument, Vorgang, Kontakt -, dann kommt der aus einer
|
|
* Auswahlliste im Browser. Der Server darf sich darauf nicht verlassen:
|
|
* Eine veraenderte Anfrage kann jede beliebige Id enthalten.
|
|
*
|
|
* Ohne die Pruefung entstuende eine Verknuepfung ueber Aktengrenzen
|
|
* hinweg. Was daran gefaehrlich ist, sieht man erst an den Folgen: Der
|
|
* Rueckkaufswert der einen Akte haengt am Vertrag der anderen, ein
|
|
* fremder Name steht im Briefkopf, ein fremder Beschluss in der
|
|
* Genehmigungskette.
|
|
*
|
|
* Solange nur eine betreute Person angelegt ist, ist all das folgenlos.
|
|
* Die Pruefung gehoert trotzdem hin - sie kostet eine Abfrage, und die
|
|
* zweite betreute Person kommt frueher, als man denkt.
|
|
*
|
|
* Bewusst KEINE Ausnahme geworfen: Formularaktionen geben Fehler zurueck,
|
|
* damit die Eingaben des Benutzers nicht verlorengehen.
|
|
*/
|
|
|
|
/** Die Tabellen, auf die ein Formular verweisen darf. */
|
|
export type Verweisziel =
|
|
| 'konto'
|
|
| 'vertrag'
|
|
| 'immobilie'
|
|
| 'dokument'
|
|
| 'vorgang'
|
|
| 'kontakt'
|
|
| 'vermoegensposten'
|
|
| 'buchung'
|
|
|
|
type Sucher = (id: string, betreuteId: string) => Promise<{ id: string } | null>
|
|
|
|
/**
|
|
* Je Ziel eine Abfrage, ausgeschrieben statt dynamisch zusammengesetzt.
|
|
*
|
|
* Das ist ein paar Zeilen laenger als ein Zugriff ueber den Tabellennamen -
|
|
* dafuer ist die Liste der erlaubten Ziele eine bewusste Aufzaehlung und
|
|
* keine Nebenwirkung, und der Typpruefer sieht jede Abfrage.
|
|
*/
|
|
const SUCHER: Record<Verweisziel, Sucher> = {
|
|
konto: (id, betreuteId) =>
|
|
db.konto.findFirst({ where: { id, betreuteId, ...nichtGeloescht }, select: { id: true } }),
|
|
vertrag: (id, betreuteId) =>
|
|
db.vertrag.findFirst({ where: { id, betreuteId, ...nichtGeloescht }, select: { id: true } }),
|
|
immobilie: (id, betreuteId) =>
|
|
db.immobilie.findFirst({ where: { id, betreuteId, ...nichtGeloescht }, select: { id: true } }),
|
|
dokument: (id, betreuteId) =>
|
|
db.dokument.findFirst({ where: { id, betreuteId, ...nichtGeloescht }, select: { id: true } }),
|
|
vorgang: (id, betreuteId) =>
|
|
db.vorgang.findFirst({ where: { id, betreuteId, ...nichtGeloescht }, select: { id: true } }),
|
|
kontakt: (id, betreuteId) =>
|
|
db.kontakt.findFirst({ where: { id, betreuteId, ...nichtGeloescht }, select: { id: true } }),
|
|
vermoegensposten: (id, betreuteId) =>
|
|
db.vermoegensposten.findFirst({
|
|
where: { id, betreuteId, ...nichtGeloescht },
|
|
select: { id: true },
|
|
}),
|
|
buchung: (id, betreuteId) =>
|
|
db.buchung.findFirst({ where: { id, betreuteId, ...nichtGeloescht }, select: { id: true } }),
|
|
}
|
|
|
|
const BESCHRIFTUNG: Record<Verweisziel, string> = {
|
|
konto: 'Dieses Konto',
|
|
vertrag: 'Dieser Vertrag',
|
|
immobilie: 'Diese Immobilie',
|
|
dokument: 'Dieses Dokument',
|
|
vorgang: 'Dieser Vorgang',
|
|
kontakt: 'Dieser Kontakt',
|
|
vermoegensposten: 'Dieser Vermögensposten',
|
|
buchung: 'Diese Buchung',
|
|
}
|
|
|
|
/**
|
|
* Prueft einen einzelnen Verweis.
|
|
*
|
|
* Ein leerer Verweis ist in Ordnung - "keine Verknuepfung" ist ein
|
|
* gueltiger Zustand und keine Verletzung.
|
|
*
|
|
* @returns null, wenn alles stimmt; sonst die fertige Fehlermeldung.
|
|
*/
|
|
export async function verweisPruefen(
|
|
ziel: Verweisziel,
|
|
id: string | null | undefined,
|
|
betreuteId: string,
|
|
): Promise<string | null> {
|
|
if (!id) return null
|
|
|
|
const treffer = await SUCHER[ziel](id, betreuteId)
|
|
|
|
return treffer ? null : `${BESCHRIFTUNG[ziel]} gehört nicht zu dieser betreuten Person.`
|
|
}
|
|
|
|
/**
|
|
* Prueft mehrere Verweise auf einmal und meldet den ersten Verstoss.
|
|
*
|
|
* Die Abfragen laufen nebeneinander: Bei vier Verweisen ist das eine
|
|
* Wartezeit statt vier.
|
|
*
|
|
* @example
|
|
* const fehler = await verweisePruefen(betreuteId, {
|
|
* konto: werte.kontoId,
|
|
* vertrag: werte.vertragId,
|
|
* })
|
|
* if (fehler) return { fehler }
|
|
*/
|
|
export async function verweisePruefen(
|
|
betreuteId: string,
|
|
verweise: Partial<Record<Verweisziel, string | null | undefined>>,
|
|
): Promise<string | null> {
|
|
const eintraege = Object.entries(verweise) as [Verweisziel, string | null | undefined][]
|
|
const ergebnisse = await Promise.all(
|
|
eintraege.map(([ziel, id]) => verweisPruefen(ziel, id, betreuteId)),
|
|
)
|
|
return ergebnisse.find((meldung) => meldung !== null) ?? null
|
|
}
|