Rollenstruktur und englische Fachbegriffe festgelegt
Der Discord-Server hat drei Sorten Rollen, die Dokumente kannten nur eine: Zugehörigkeit (AfiS, Personalabteilung) als Türsteher, Rangrollen für die Rechte, Zugangsrollen für die Freigabestufe. Das bestehende RoleMapping trägt das fast, es fehlt nur der Türsteher. Die beiden Felder dafür kommen mit Phase 1, die Struktur ist in docs/13-entscheidungen.md festgehalten. Die Freigabestufen heißen jetzt wie die Zugangsrollen: Unclassified, Restricted Access, Confidential Access, Sensitive Access, Critical Operations, Executive Oversight. Fachbegriffe der Domäne bleiben englisch, auch in einer deutschen Oberfläche. Die Bezeichnungen sind damit Daten in ClearanceLevelConfig, nicht mehr Oberflächentext in lib/i18n/de.ts: sie sind im Adminbereich änderbar, und was der Leiter ändern kann, gehört nicht in eine Übersetzungsdatei. StampMark bekommt den Namen hereingereicht. Vier neue Tests prüfen den Zustand, den der Seed herstellt, gegen die Datenbank statt gegen eine Konstante im Code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
dfc47f91c4
commit
dbca57cedf
@@ -65,6 +65,11 @@ Vor dem Bau eines Bereichs immer das passende Dokument lesen:
|
|||||||
- **Code, Datenbank, API**: Englisch. `Case`, `CaseEntry`, `PersonnelFile`, `ClearanceLevel`.
|
- **Code, Datenbank, API**: Englisch. `Case`, `CaseEntry`, `PersonnelFile`, `ClearanceLevel`.
|
||||||
- **Oberfläche und Dokumentation**: Deutsch. Alle UI-Texte zentral in `lib/i18n/de.ts`,
|
- **Oberfläche und Dokumentation**: Deutsch. Alle UI-Texte zentral in `lib/i18n/de.ts`,
|
||||||
nie hart im JSX. Das Glossar in `docs/01-produkt.md` legt die Übersetzungen fest.
|
nie hart im JSX. Das Glossar in `docs/01-produkt.md` legt die Übersetzungen fest.
|
||||||
|
- **Fachbegriffe der Domäne bleiben englisch**, so wie sie auf dem Discord heißen:
|
||||||
|
Rollennamen, Dienstgrade und die Bezeichnungen der Freigabestufen (`Sensitive Access`,
|
||||||
|
nicht `Geheim`). Die Oberfläche ist deutsch, die Begriffe darin sind es nicht. Nicht
|
||||||
|
eindeutschen. Diese Namen sind Daten, keine Oberflächentexte: sie stehen in der
|
||||||
|
Datenbank und sind im Adminbereich änderbar, nicht in `lib/i18n/de.ts`.
|
||||||
|
|
||||||
## Kommandos
|
## Kommandos
|
||||||
|
|
||||||
|
|||||||
+14
-10
@@ -46,17 +46,21 @@ Verbindliche Zuordnung zwischen Fachbegriff und Code-Bezeichner.
|
|||||||
|
|
||||||
## Freigabestufen
|
## Freigabestufen
|
||||||
|
|
||||||
| Stufe | Bezeichnung | Existenz für niedrigere Stufen |
|
Die Bezeichnungen entsprechen den Zugangsrollen auf dem Discord-Server und bleiben deshalb
|
||||||
|---|---|---|
|
englisch. Stufe 0 hat keine eigene Zugangsrolle, sie gilt für jedes AfiS-Mitglied ohne eine
|
||||||
| 0 | Offen | sichtbar |
|
solche Rolle.
|
||||||
| 1 | Intern | sichtbar |
|
|
||||||
| 2 | Vertraulich | sichtbar, Inhalt gesperrt |
|
|
||||||
| 3 | Geheim | unsichtbar |
|
|
||||||
| 4 | Streng geheim | unsichtbar |
|
|
||||||
| 5 | Leitungssache | unsichtbar |
|
|
||||||
|
|
||||||
Die Spalte ganz rechts ist konfigurierbar (`ClearanceLevelConfig.hiddenBelow`), die Werte
|
| Stufe | Bezeichnung | Zugangsrolle | Existenz für niedrigere Stufen |
|
||||||
oben sind der Auslieferungszustand.
|
|---|---|---|---|
|
||||||
|
| 0 | Unclassified | — | sichtbar |
|
||||||
|
| 1 | Restricted Access | Restricted Access | sichtbar |
|
||||||
|
| 2 | Confidential Access | Confidential Access | sichtbar, Inhalt gesperrt |
|
||||||
|
| 3 | Sensitive Access | Sensitive Access | unsichtbar |
|
||||||
|
| 4 | Critical Operations | Critical Operations | unsichtbar |
|
||||||
|
| 5 | Executive Oversight | Executive Oversight | unsichtbar |
|
||||||
|
|
||||||
|
Die Spalte ganz rechts ist konfigurierbar (`ClearanceLevelConfig.hiddenBelow`), ebenso die
|
||||||
|
Bezeichnung. Die Werte oben sind der Auslieferungszustand, den `pnpm db:seed` herstellt.
|
||||||
|
|
||||||
## Nicht im Umfang
|
## Nicht im Umfang
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,53 @@ nicht über die Volltextsuche. **Phase 7 muss das auflösen**, sinnvollerweise
|
|||||||
Vektorspalten je Stufe. Diese Entscheidung ist bewusst konservativ getroffen, weil ein Leck
|
Vektorspalten je Stufe. Diese Entscheidung ist bewusst konservativ getroffen, weil ein Leck
|
||||||
hier schwerer zu bemerken ist als eine Lücke.
|
hier schwerer zu bemerken ist als eine Lücke.
|
||||||
|
|
||||||
|
## Rollen auf dem Discord
|
||||||
|
|
||||||
|
Die Dokumente kennen genau eine Sorte Rolle: `RoleMapping` trägt Stufe **und** Rechte, die
|
||||||
|
Auflösung nimmt das Maximum der Stufen und die Vereinigung der Rechte. Auf dem Server gibt es
|
||||||
|
aber drei Sorten mit verschiedener Bedeutung:
|
||||||
|
|
||||||
|
| Sorte | Beispiele | Wirkung |
|
||||||
|
|---|---|---|
|
||||||
|
| Zugehörigkeit | `AfiS`, `Personalabteilung` | Türsteher. Ohne eine davon kein Login |
|
||||||
|
| Rang | `Director of internal Security` … `Probationary Agent` | Bestimmt die **Rechte** |
|
||||||
|
| Zugang | `Executive Oversight` … `Restricted Access` | Bestimmt die **Freigabestufe** |
|
||||||
|
|
||||||
|
Das bestehende Modell trägt das fast: Zugangsrollen bekommen eine Stufe und keine Rechte,
|
||||||
|
Rangrollen Rechte und Stufe 0, Maximum und Vereinigung ergeben das Richtige. Was fehlt, ist
|
||||||
|
der Türsteher. `RoleMapping` bekommt deshalb in Phase 1 zwei Felder:
|
||||||
|
|
||||||
|
- `grantsMembership` — diese Rolle berechtigt zur Anmeldung
|
||||||
|
- `category` — nur für die Gruppierung im Adminbereich, damit dort drei Abschnitte stehen
|
||||||
|
statt einer Liste aus achtzehn Rollen
|
||||||
|
|
||||||
|
Anmeldeprüfung damit: Mitglied der Guild **und** (eine Rolle mit `grantsMembership` **oder**
|
||||||
|
in `BOOTSTRAP_ADMIN_IDS`). Notfall-Admins umgehen die AfiS-Rolle, müssen aber Mitglied der
|
||||||
|
Guild bleiben — sonst bliebe die Hintertür auch nach einem Rauswurf offen.
|
||||||
|
|
||||||
|
Rangrollen bekommen im Auslieferungszustand **keine** Rechte. Das Rollen-Mapping entsteht
|
||||||
|
vollständig im Adminbereich, nichts wird nach Rollennamen geraten.
|
||||||
|
|
||||||
|
### Umzug vom Test- auf den echten Server
|
||||||
|
|
||||||
|
Entwickelt wird gegen einen Test-Discord mit nachgebauten Rollen. Rollen-IDs sind pro Server
|
||||||
|
eindeutig, beim Umzug ist das gesamte `RoleMapping` deshalb wertlos. Das ist kein Fehler,
|
||||||
|
sondern eine Anforderung an Phase 1: die Adminseite muss ein neu erkanntes `DISCORD_GUILD_ID`
|
||||||
|
verkraften und beim Übernehmen anbieten, die alte Zuordnung **anhand der Rollennamen**
|
||||||
|
vorzuschlagen. `RoleMapping.label` wird dafür ohnehin aus Discord gespiegelt.
|
||||||
|
|
||||||
|
## Sprache der Fachbegriffe
|
||||||
|
|
||||||
|
`CLAUDE.md` verlangt eine deutsche Oberfläche. Die Begriffe der Domäne heißen auf dem Discord
|
||||||
|
aber englisch, und genau so werden sie im Rollenspiel gesprochen. Sie bleiben deshalb
|
||||||
|
englisch: Rollennamen, Dienstgrade und die Bezeichnungen der Freigabestufen.
|
||||||
|
|
||||||
|
Aus `docs/01-produkt.md` wird damit `Sensitive Access` statt `Geheim`. Die Bezeichnungen sind
|
||||||
|
Daten in `ClearanceLevelConfig`, nicht Oberflächentexte in `lib/i18n/de.ts` — sie sind im
|
||||||
|
Adminbereich änderbar, und ein Text, den der Leiter ändern kann, gehört nicht in eine
|
||||||
|
Übersetzungsdatei. `lib/i18n/de.ts` behält nur das Gerüst („Freigabestufe", „Geschwärzt,
|
||||||
|
ab Stufe 4 sichtbar").
|
||||||
|
|
||||||
## Prisma 7
|
## Prisma 7
|
||||||
|
|
||||||
Die Dokumente nennen nur „Prisma + PostgreSQL 16". Eingesetzt wird Prisma 7, die
|
Die Dokumente nennen nur „Prisma + PostgreSQL 16". Eingesetzt wird Prisma 7, die
|
||||||
|
|||||||
@@ -11,17 +11,21 @@ import { createPrismaClient } from '../src/index.js'
|
|||||||
const db = createPrismaClient()
|
const db = createPrismaClient()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auslieferungszustand nach docs/01-produkt.md. Ab Stufe 3 ist die Existenz
|
* Auslieferungszustand. Ab Stufe 3 ist die Existenz einer Akte für niedrigere
|
||||||
* einer Akte für niedrigere Stufen verborgen, darunter nur der Inhalt gesperrt.
|
* Stufen verborgen, darunter ist nur der Inhalt gesperrt. Der Leiter kann das
|
||||||
* Der Leiter kann das im Adminbereich ändern.
|
* im Adminbereich ändern.
|
||||||
|
*
|
||||||
|
* Die Namen entsprechen den Zugangsrollen auf dem Discord und bleiben deshalb
|
||||||
|
* englisch, siehe docs/13-entscheidungen.md. Stufe 0 hat keine eigene
|
||||||
|
* Zugangsrolle: sie gilt für jedes AfiS-Mitglied ohne eine solche Rolle.
|
||||||
*/
|
*/
|
||||||
const CLEARANCE_LEVELS = [
|
const CLEARANCE_LEVELS = [
|
||||||
{ level: 0, name: 'Offen', hiddenBelow: false, colorToken: 'stamp-0' },
|
{ level: 0, name: 'Unclassified', hiddenBelow: false, colorToken: 'stamp-0' },
|
||||||
{ level: 1, name: 'Intern', hiddenBelow: false, colorToken: 'stamp-1' },
|
{ level: 1, name: 'Restricted Access', hiddenBelow: false, colorToken: 'stamp-1' },
|
||||||
{ level: 2, name: 'Vertraulich', hiddenBelow: false, colorToken: 'stamp-2' },
|
{ level: 2, name: 'Confidential Access', hiddenBelow: false, colorToken: 'stamp-2' },
|
||||||
{ level: 3, name: 'Geheim', hiddenBelow: true, colorToken: 'stamp-3' },
|
{ level: 3, name: 'Sensitive Access', hiddenBelow: true, colorToken: 'stamp-3' },
|
||||||
{ level: 4, name: 'Streng geheim', hiddenBelow: true, colorToken: 'stamp-4' },
|
{ level: 4, name: 'Critical Operations', hiddenBelow: true, colorToken: 'stamp-4' },
|
||||||
{ level: 5, name: 'Leitungssache', hiddenBelow: true, colorToken: 'stamp-5' },
|
{ level: 5, name: 'Executive Oversight', hiddenBelow: true, colorToken: 'stamp-5' },
|
||||||
]
|
]
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
||||||
|
import { createPrismaClient, type PrismaClient } from '../src/index.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft den Zustand, den `pnpm db:seed` herstellt. Das ist kein Testinhalt,
|
||||||
|
* sondern Konfiguration, ohne die die Anwendung nicht sinnvoll läuft.
|
||||||
|
*/
|
||||||
|
const url = process.env.DIRECT_DATABASE_URL ?? process.env.DATABASE_URL
|
||||||
|
const suite = url ? describe : describe.skip
|
||||||
|
|
||||||
|
let db: PrismaClient
|
||||||
|
|
||||||
|
suite('Auslieferungszustand', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
db = createPrismaClient(url)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await db.$disconnect()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('kennt sechs Freigabestufen, benannt nach den Zugangsrollen', async () => {
|
||||||
|
const levels = await db.clearanceLevelConfig.findMany({ orderBy: { level: 'asc' } })
|
||||||
|
expect(levels.map((l) => l.level)).toEqual([0, 1, 2, 3, 4, 5])
|
||||||
|
expect(levels.map((l) => l.name)).toEqual([
|
||||||
|
'Unclassified',
|
||||||
|
'Restricted Access',
|
||||||
|
'Confidential Access',
|
||||||
|
'Sensitive Access',
|
||||||
|
'Critical Operations',
|
||||||
|
'Executive Oversight',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('verbirgt die Existenz ab Stufe 3, darunter nicht', async () => {
|
||||||
|
const levels = await db.clearanceLevelConfig.findMany({ orderBy: { level: 'asc' } })
|
||||||
|
expect(levels.map((l) => l.hiddenBelow)).toEqual([false, false, false, true, true, true])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('liefert die Änderungsrichtlinie mit Karenzzeit und ohne Markierung aus', async () => {
|
||||||
|
const setting = await db.appSetting.findUnique({ where: { id: 1 } })
|
||||||
|
expect(setting?.entryEditPolicy).toBe('GRACE_WINDOW')
|
||||||
|
expect(setting?.entryEditGraceMinutes).toBe(15)
|
||||||
|
expect(setting?.entryEditMarkGrace).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ordnet im Auslieferungszustand keiner Rolle Rechte zu', async () => {
|
||||||
|
// entry.edit_own, entry.edit_any und entry.retract schaltet der Leiter
|
||||||
|
// bewusst frei. Siehe docs/03-auth-und-rechte.md.
|
||||||
|
expect(await db.roleMapping.count()).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import { de } from '@/lib/i18n/de'
|
|
||||||
|
|
||||||
const STAMP_COLOR: Record<number, string> = {
|
const STAMP_COLOR: Record<number, string> = {
|
||||||
0: 'var(--color-stamp-0)',
|
0: 'var(--color-stamp-0)',
|
||||||
1: 'var(--color-stamp-1)',
|
1: 'var(--color-stamp-1)',
|
||||||
@@ -17,8 +15,11 @@ export function stampColor(level: number): string {
|
|||||||
* Stempelmarke oben rechts am Objekt. Der Stempelcharakter kommt aus dem
|
* Stempelmarke oben rechts am Objekt. Der Stempelcharakter kommt aus dem
|
||||||
* schmalen Schnitt, der Rahmenlinie und der Farbe, nicht aus Grossbuchstaben.
|
* schmalen Schnitt, der Rahmenlinie und der Farbe, nicht aus Grossbuchstaben.
|
||||||
* Die Stufe steht zusaetzlich als Text, nicht nur als Farbe.
|
* Die Stufe steht zusaetzlich als Text, nicht nur als Farbe.
|
||||||
|
*
|
||||||
|
* `name` kommt aus ClearanceLevelConfig und ist im Adminbereich aenderbar,
|
||||||
|
* deshalb wird er hereingereicht statt aus lib/i18n/de.ts gelesen.
|
||||||
*/
|
*/
|
||||||
export function StampMark({ level }: { level: number }) {
|
export function StampMark({ level, name }: { level: number; name: string }) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className="inline-flex items-center gap-1.5 border px-1.5 py-0.5 font-condensed text-2xs"
|
className="inline-flex items-center gap-1.5 border px-1.5 py-0.5 font-condensed text-2xs"
|
||||||
@@ -27,7 +28,7 @@ export function StampMark({ level }: { level: number }) {
|
|||||||
<span aria-hidden className="font-mono">
|
<span aria-hidden className="font-mono">
|
||||||
{level}
|
{level}
|
||||||
</span>
|
</span>
|
||||||
{de.clearance.names[level] ?? de.common.unknown}
|
{name}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-8
@@ -47,14 +47,9 @@ export const de = {
|
|||||||
label: 'Freigabestufe',
|
label: 'Freigabestufe',
|
||||||
classification: 'Klassifizierung',
|
classification: 'Klassifizierung',
|
||||||
level: (n: number) => `Stufe ${n}`,
|
level: (n: number) => `Stufe ${n}`,
|
||||||
names: {
|
// Die Bezeichnungen der Stufen stehen nicht hier, sondern in
|
||||||
0: 'Offen',
|
// ClearanceLevelConfig: sie sind im Adminbereich änderbar und folgen den
|
||||||
1: 'Intern',
|
// Zugangsrollen des Discord-Servers. Siehe docs/13-entscheidungen.md.
|
||||||
2: 'Vertraulich',
|
|
||||||
3: 'Geheim',
|
|
||||||
4: 'Streng geheim',
|
|
||||||
5: 'Leitungssache',
|
|
||||||
} as Record<number, string>,
|
|
||||||
locked: 'Inhalt gesperrt',
|
locked: 'Inhalt gesperrt',
|
||||||
lockedHint: (n: number) => `Sichtbar ab Stufe ${n}`,
|
lockedHint: (n: number) => `Sichtbar ab Stufe ${n}`,
|
||||||
redacted: 'Geschwärzt',
|
redacted: 'Geschwärzt',
|
||||||
|
|||||||
@@ -4,11 +4,10 @@ import { join } from 'node:path'
|
|||||||
import { de } from '@/lib/i18n/de'
|
import { de } from '@/lib/i18n/de'
|
||||||
|
|
||||||
describe('Sprachkonvention', () => {
|
describe('Sprachkonvention', () => {
|
||||||
it('kennt alle sechs Freigabestufen', () => {
|
it('haelt die Stufenbezeichnungen aus den Texten heraus', () => {
|
||||||
for (let level = 0; level <= 5; level++) {
|
// Sie stehen in ClearanceLevelConfig, sind im Adminbereich aenderbar und
|
||||||
expect(de.clearance.names[level]).toBeTypeOf('string')
|
// folgen den Zugangsrollen des Discord-Servers.
|
||||||
}
|
expect(de.clearance).not.toHaveProperty('names')
|
||||||
expect(de.clearance.names[3]).toBe('Geheim')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('verzichtet auf Ausrufezeichen und Entschuldigungen', () => {
|
it('verzichtet auf Ausrufezeichen und Entschuldigungen', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user