feat(api,web): eigene Module — Tabelle, API, Seitenleiste, Rahmen-Seite

- Tabelle CustomModule mit Zeilenschutz (tenant_isolation_policy), Migration 20260929120000
- API /custom-modules: Lesen für jeden Angemeldeten, Schreiben nur Administrator, nur https ohne Zugangsdaten
- Seitenleiste zeigt eigene Module unter ihrer Kategorie, Rahmen-Seite mit Sandbox und „In neuem Tab öffnen“
- MODULE_CATEGORIES als gemeinsame Liste, Zugriffsklassifikation nachgemessen fortgeschrieben

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-29 07:25:33 +02:00
parent 643b1a2caa
commit b9d87be360
23 changed files with 1270 additions and 40 deletions
+7
View File
@@ -1346,6 +1346,13 @@
"close": "Fenster schließen",
"contentLabel": "Änderungen"
},
"customModules": {
"openInNewTab": "In neuem Tab öffnen",
"embedHint": "Manche Seiten lassen sich nicht einbetten. Öffnen Sie die Seite dann in einem neuen Tab.",
"notFound": "Dieses Modul gibt es nicht mehr.",
"invalidUrl": "Die Adresse dieses Moduls ist keine gültige https-Adresse und wird deshalb nicht angezeigt.",
"loading": "Wird geladen …"
},
"moduleCategories": {
"domain-tools": "Domains",
"security-tools": "Sicherheit",
+7
View File
@@ -1346,6 +1346,13 @@
"close": "Close window",
"contentLabel": "Changes"
},
"customModules": {
"openInNewTab": "Open in new tab",
"embedHint": "Some pages cannot be embedded. If this one stays blank, open it in a new tab.",
"notFound": "This module no longer exists.",
"invalidUrl": "This module's address is not a valid https address and is therefore not shown.",
"loading": "Loading …"
},
"moduleCategories": {
"domain-tools": "Domains",
"security-tools": "Security",
@@ -0,0 +1,22 @@
import { MODULE_CATEGORIES } from '@tessera/shared';
import { describe, expect, it } from 'vitest';
import de from './de.json';
import en from './en.json';
/**
* Gleichlauf der Modulkategorien (quick-260929-9wc): jede Kennung aus
* `MODULE_CATEGORIES` (Auswahl im Formular „Eigene Module“, Pruefung in der
* API) braucht einen Anzeigenamen in `moduleCategories` beider Sprachen —
* sonst zeigte die Seitenleiste die rohe Kennung.
*/
describe('MODULE_CATEGORIES', () => {
it.each([
['de', de],
['en', en],
] as const)('hat je Kennung einen Schluessel in moduleCategories (%s)', (_lang, messages) => {
const labels = messages.moduleCategories as Record<string, string>;
for (const category of MODULE_CATEGORIES) {
expect(labels[category], `moduleCategories.${category}`).toBeTruthy();
}
});
});