Durchgesehen, was mit Text passiert, der nicht als Text ankommt. Die Grundlage
war schon in Ordnung: genau ein dangerouslySetInnerHTML mit einer Konstanten,
kein eval, kein $queryRawUnsafe, keine Weiterleitung aus einem Adressparameter,
Dateien mit nosniff und sandbox-CSP als Anhang statt inline.
Drei Ränder waren offen.
**Vorlagen.** Das zod-Schema hatte einen festen Satz Feldarten und speicherte
das Geprüfte, verwarf unbekannte Schlüssel aber stillschweigend. Mit
strictObject fällt ein `typ` statt `type` auf, statt eine Vorlage zu
hinterlassen, die nicht tut, was dasteht.
**Platzhalter lasen über die Prototypkette.** Beim Durchprobieren gefunden:
`constructor` ist ein gültiger Feldschlüssel, die Schlüsselregel erlaubt
Kleinbuchstaben. Ein Baustein mit {{constructor}} bekam den Quelltext der
geerbten Funktion in den Text geschrieben — kein ausgeführter Code, aber
Innenleben in einem Dokument, das weitergereicht wird. Jetzt Object.hasOwn.
**Farbmarken aus der Datenbank** landen als Name einer CSS-Variablen in einem
style-Attribut. Heute setzt das nur der Seed, und eine zweite Deklaration liesse
sich darüber ohnehin nicht einschleusen — aber der Kommentar sagt „im
Adminbereich pflegbar", und was aus der Datenbank in ein Stilattribut geht,
wird geprüft.
Dreizehn Prüfungen, die es mit echten Versuchen probieren. Bösartiger Text
bleibt dabei ausdrücklich erlaubt: eine Feldbeschriftung darf
<script>alert(1)</script> heissen und sieht dann genau so aus. Verboten ist,
dass daraus ein Element wird.
Nebenbei: eine Vorlagenänderung stand im Zugriffsprotokoll als roles.update.
216 lines
8.4 KiB
TypeScript
216 lines
8.4 KiB
TypeScript
export const auditText = {
|
|
heading: 'Protokoll',
|
|
intro:
|
|
'Wer was wo getan hat. Jede schreibende Aktion steht hier, dazu jeder Lesezugriff ab Stufe 3. Einträge lassen sich nicht ändern und nicht entfernen.',
|
|
when: 'Zeitpunkt',
|
|
who: 'Wer',
|
|
what: 'Was',
|
|
where: 'Wo',
|
|
clearance: 'Stufe',
|
|
filterActor: 'Person',
|
|
filterAction: 'Aktion',
|
|
filterTarget: 'Art',
|
|
filterFrom: 'Ab',
|
|
filterTo: 'Bis',
|
|
apply: 'Filtern',
|
|
reset: 'Alles zeigen',
|
|
all: 'Alle',
|
|
empty: 'Für diesen Ausschnitt steht nichts im Protokoll.',
|
|
older: 'Ältere anzeigen',
|
|
gone: 'nicht mehr auffindbar',
|
|
ofUser: 'Nur von dieser Person',
|
|
ofTarget: 'Nur zu diesem Vorgang',
|
|
actions: {
|
|
'case.read': 'Fallakte gelesen',
|
|
'case.create': 'Fallakte angelegt',
|
|
'case.update': 'Fallakte geändert',
|
|
'case.reclassify': 'Fallakte umklassifiziert',
|
|
'case.status': 'Status geändert',
|
|
'entry.create': 'Vermerk angelegt',
|
|
'entry.edit': 'Vermerk geändert',
|
|
'entry.retract': 'Vermerk zurückgezogen',
|
|
'entry.restore': 'Vermerk wiederhergestellt',
|
|
'case.participant.add': 'Beteiligten eingetragen',
|
|
'case.participant.remove': 'Beteiligten ausgetragen',
|
|
'case.link': 'Akten verknüpft',
|
|
'case.unlink': 'Verknüpfung gelöst',
|
|
'personnel.read': 'Personalakte gelesen',
|
|
'personnel.create': 'Personalakte angelegt',
|
|
'personnel.update': 'Personalakte geändert',
|
|
'personnel.reclassify': 'Personalakte umklassifiziert',
|
|
'personnel.assign': 'Person zugeordnet',
|
|
'personnel.unassign': 'Zuordnung aufgehoben',
|
|
'document.read': 'Dokument gelesen',
|
|
'document.create': 'Dokument angelegt',
|
|
'document.update': 'Dokument geändert',
|
|
'document.finalize': 'Dokument abgeschlossen',
|
|
'document.retract': 'Dokument zurückgezogen',
|
|
'document.reclassify': 'Dokument umklassifiziert',
|
|
'attachment.upload': 'Datei hochgeladen',
|
|
'attachment.read': 'Datei abgerufen',
|
|
'attachment.release': 'Datei freigegeben',
|
|
'chat.adopt': 'Nachricht als Vermerk übernommen',
|
|
'department.create': 'Abteilung angelegt',
|
|
'department.update': 'Abteilung geändert',
|
|
'department.assign': 'Abteilung zugeordnet',
|
|
'department.entry.create': 'Abteilungsvermerk angelegt',
|
|
'department.entry.edit': 'Abteilungsvermerk geändert',
|
|
'department.entry.retract': 'Abteilungsvermerk zurückgezogen',
|
|
'roles.update': 'Rollenzuordnung geändert',
|
|
'template.update': 'Vorlage geändert',
|
|
'export.pdf': 'PDF erzeugt',
|
|
'export.approve': 'Export freigegeben',
|
|
'export.reject': 'Export abgelehnt',
|
|
'export.download': 'Export heruntergeladen',
|
|
'export.database': 'Datenbankabzug erzeugt',
|
|
} as Record<string, string>,
|
|
targets: {
|
|
Case: 'Fallakte',
|
|
CaseEntry: 'Vermerk',
|
|
CaseSection: 'Abschnitt',
|
|
PersonnelFile: 'Personalakte',
|
|
Document: 'Dokument',
|
|
DocumentSection: 'Dokumentabschnitt',
|
|
Attachment: 'Datei',
|
|
ChatMessage: 'Nachricht',
|
|
ExportJob: 'Export',
|
|
RoleMapping: 'Rollenzuordnung',
|
|
DocumentTemplate: 'Vorlage',
|
|
ChangePolicy: 'Änderungsrichtlinie',
|
|
Database: 'Datenbank',
|
|
Department: 'Abteilung',
|
|
DepartmentEntry: 'Abteilungsvermerk',
|
|
} as Record<string, string>,
|
|
} as const
|
|
|
|
export const adminText = {
|
|
heading: 'Admin',
|
|
intro: 'Rollen, Vorlagen, Regeln und das Protokoll der Abteilung.',
|
|
sections: {
|
|
roles: 'Rollen',
|
|
departments: 'Abteilungen',
|
|
templates: 'Vorlagen',
|
|
policy: 'Änderungsrichtlinie',
|
|
log: 'Protokoll',
|
|
system: 'System',
|
|
} as Record<string, string>,
|
|
descriptions: {
|
|
roles: 'Welche Discord-Rolle welche Freigabestufe und welche Rechte trägt.',
|
|
departments: 'Die Abteilungen der Foundation. Zugeordnet wird in der Personalakte.',
|
|
templates: 'Aufbau der Fallakten und Dokumente, Bausteine und Fassungen.',
|
|
policy: 'Ob und wie lange Verlaufseinträge nachträglich geändert werden dürfen.',
|
|
log: 'Wer was wo getan hat. Vollständig und nicht änderbar.',
|
|
system: 'Zustand der Dienste, Ladewerte, Nummernkreise.',
|
|
} as Record<string, string>,
|
|
empty: 'Für die Verwaltung fehlen dir die Rechte. Die Abteilungsleitung vergibt sie.',
|
|
} as const
|
|
|
|
/**
|
|
* Der Adminbereich: Rollen, Vorlagen, Einstellungen.
|
|
*
|
|
* Nach Bereichen getrennt, weil eine **Client-Komponente die ganze Datei ins
|
|
* Bündel zieht**, in der ihr Text steht. Lägen alle Texte in einer, trüge jede
|
|
* Ansicht die der ganzen Anwendung mit sich herum — das Budget aus
|
|
* docs/09-performance-und-ux.md wäre nach wenigen Phasen aufgebraucht.
|
|
*
|
|
* Serverkomponenten nehmen weiter `de` aus `lib/i18n/de.ts`: was dorthin
|
|
* gezogen wird, verlässt den Server nicht.
|
|
*/
|
|
|
|
export const roleAdminText = {
|
|
heading: 'Rollenverwaltung',
|
|
intro:
|
|
'Jede Rolle des Discord-Servers steht hier. Zugangsrollen bestimmen die Freigabestufe, Rangrollen die Rechte, und eine Zugehörigkeitsrolle entscheidet, wer sich überhaupt anmelden darf.',
|
|
empty:
|
|
'Noch keine Rollen eingelesen. Der Worker spiegelt sie, sobald er am Discord-Gateway hängt.',
|
|
role: 'Rolle',
|
|
category: 'Sorte',
|
|
clearance: 'Freigabestufe',
|
|
grantsMembership: 'Darf sich anmelden',
|
|
permissions: 'Rechte',
|
|
missingFromGuild: 'Auf dem Server nicht mehr vorhanden',
|
|
save: 'Zuordnung speichern',
|
|
invalidInput: 'Eingabe nicht gültig',
|
|
roleGone: 'Rolle nicht gefunden, sie wurde zwischenzeitlich gelöscht',
|
|
saving: 'Wird gespeichert',
|
|
saved: 'Zuordnung gespeichert',
|
|
confirm: 'Rollen-Mapping ändern? Das wirkt sofort für alle angemeldeten Personen.',
|
|
categories: {
|
|
MEMBERSHIP: 'Zugehörigkeit',
|
|
RANK: 'Rang',
|
|
CLEARANCE: 'Zugang',
|
|
UNSORTED: 'Nicht eingeordnet',
|
|
} as Record<string, string>,
|
|
optIn:
|
|
'Dieses Recht ist im Auslieferungszustand keiner Rolle zugeordnet. Vergib es bewusst.',
|
|
preview: {
|
|
heading: 'Vorschau',
|
|
intro: 'Was jemand mit dieser Stufe sieht, wenn er keine weiteren Rollen hat.',
|
|
level: 'Stufe',
|
|
sees: 'Sieht Akten bis einschliesslich',
|
|
hidden: 'Höhere Akten sind unsichtbar',
|
|
lockedOnly: 'Höhere Akten sind sichtbar, aber gesperrt',
|
|
nobody: 'Keine Rolle führt zu dieser Stufe',
|
|
viaRoles: 'Erreichbar über',
|
|
},
|
|
} as const
|
|
|
|
export const templateAdminText = {
|
|
heading: 'Vorlagen',
|
|
intro:
|
|
'Eine Vorlage bestimmt, aus welchen Bausteinen eine Fallart besteht. Speichern legt eine neue Version an; bestehende Akten behalten die Version, mit der sie angelegt wurden.',
|
|
empty: 'Noch keine Vorlage.',
|
|
version: (v: number) => `Version ${v}`,
|
|
fieldsLabel: 'Bausteine als JSON',
|
|
fieldsHelp:
|
|
'Liste von Bausteinen. Eingabefelder: shorttext, longtext, select, multiselect, date, person, number, checklist, table. Feste Bausteine: heading, paragraph, signature, pagebreak.',
|
|
numberPrefix: 'Präfix des Aktenzeichens',
|
|
numberPadding: 'Stellen der laufenden Nummer',
|
|
save: 'Als neue Version speichern',
|
|
saved: (v: number) => `Gespeichert als Version ${v}`,
|
|
preview: 'Vorschau',
|
|
previewIntro: 'So sieht eine neue Akte mit dieser Vorlage aus.',
|
|
invalid: 'Die Bausteine sind kein gültiges JSON.',
|
|
fieldsInvalid: 'Die Bausteine sind nicht gültig.',
|
|
inputField: 'Eingabe',
|
|
staticBlock: 'Fester Baustein',
|
|
} as const
|
|
|
|
export const settingsText = {
|
|
heading: 'Änderungsrichtlinie',
|
|
intro:
|
|
'Regelt, ob und wie lange Verlaufseinträge nachträglich geändert werden dürfen. Gilt für die ganze Abteilung.',
|
|
policy: 'Richtlinie',
|
|
graceMinutes: 'Karenzzeit in Minuten',
|
|
markGrace: 'Auch Änderungen in der Karenzzeit sichtbar markieren',
|
|
save: 'Richtlinie speichern',
|
|
saved: 'Richtlinie gespeichert',
|
|
confirm: 'Änderungsrichtlinie ändern? Das gilt sofort für alle Akten.',
|
|
policies: {
|
|
APPEND_ONLY: 'Keine Änderung möglich, nur Korrektureinträge',
|
|
GRACE_WINDOW: 'Eigene Einträge in der Karenzzeit frei änderbar, danach nur mit Recht',
|
|
OPEN: 'Jederzeit änderbar, sofern das Recht vorliegt',
|
|
} as Record<string, string>,
|
|
} as const
|
|
|
|
export const vitalsText = {
|
|
heading: 'Ladewerte',
|
|
intro:
|
|
'Gemessen in den Browsern der Nutzer, letzte sieben Tage. Nur Zahlen und das Routenmuster — wer wann welche Akte geöffnet hat, steht im Zugriffsprotokoll.',
|
|
empty: 'Noch nichts gemessen.',
|
|
metric: 'Wert',
|
|
median: 'Median',
|
|
p75: '75. Perzentil',
|
|
count: 'Messungen',
|
|
target: 'Ziel',
|
|
worstRoutes: 'Langsamste Routen',
|
|
route: 'Route',
|
|
targets: {
|
|
LCP: 'unter 1,5 s',
|
|
INP: 'unter 200 ms',
|
|
CLS: 'unter 0,05',
|
|
TTFB: 'unter 500 ms',
|
|
FCP: 'unter 1,0 s',
|
|
} as Record<string, string>,
|
|
} as const
|