feat(backend+frontend): edit-person-Berechtigung fuer Personen-Edit in Personen-Ansicht + Gruppen-Mitglieder #195

Closed
opened 2026-07-02 22:27:44 +00:00 by niboer · 0 comments
Owner

Einordnung

  • Position: 6 (Phase C — letzter Issue, nach allen anderen)
  • Benötigt: #193 (ActionMenu-Komponente als Trigger), #198 (zentrale Spalten fuer groups-Tabelle)
  • Wird benötigt von: — (keiner)

Beschreibung

User mit edit-person-Realm-Rolle sollen Personen aus der Personen-Ansicht (/app/persons) und aus der Gruppen-Detailseite (/app/groups/{id}) editieren koennen.

Das Editieren erfolgt via PersonEditModal — einem generischen Modal fuer Stammdaten + Kontaktdaten, das von beiden Views genutzt wird.

Wichtig: Der "Bearbeiten"-Button wird NICHT als Inline-Button realisiert, sondern als Eintrag im ActionMenu (siehe #193). Dies setzt #193 voraus.


Teil 1: Backend — Auth-Lockerung

Ist-Zustand

Endpunkt Auth Wer darf?
PUT /api/v1/persons/:person_id (Stammdaten) requireAuth + requireOwnData('person_id') Nur die Person selbst
PUT /api/v1/persons/:person_id/contacts (Kontaktdaten) requireAuth + requireOwnDataOrGroupMember('person_id') Selbst oder Haushaltsmitglieder

Soll-Zustand

Endpunkt Auth Wer darf?
PUT /api/v1/persons/:person_id requireAuth + requireOwnDataOrCompositeRole('edit-person', 'person_id') Selbst ODER edit-person-Rolle
PUT /api/v1/persons/:person_id/contacts requireAuth + requireOwnDataOrGroupMemberOrCompositeRole('edit-person', 'person_id') Selbst, Haushalt ODER edit-person-Rolle

Die Account-Seite (/app/account) nutzt denselben PUT /api/v1/persons/:person_id-Endpunkt. Da sie immer locals.user.personId sendet, ist sie von der Auth-Aenderung nicht betroffen (der requireOwnData-Check laeuft durch).

Aenderungen

  1. backend/src/lib/auth.ts — Neuer Hook requireOwnDataOrGroupMemberOrCompositeRole(permission, personIdParam):

    • Prueft in Reihe: (a) request.user.personId === targetPersonId, (b) GroupMember-Lookup gleiche groupId, (c) request.user.roles.includes(permission)
    • Gibt 403 wenn keiner der Checks durchlaeuft
  2. backend/src/routes/persons.ts:333 — preHandler aendern:

    // ALT: requireAuth, requireOwnData('person_id')
    // NEU: requireAuth, requireOwnDataOrCompositeRole('edit-person', 'person_id')
    
  3. backend/src/routes/contacts.ts:83 — preHandler aendern:

    // ALT: requireAuth, requireOwnDataOrGroupMember('person_id')
    // NEU: requireAuth, requireOwnDataOrGroupMemberOrCompositeRole('edit-person', 'person_id')
    
  4. docs/design/03-autorisierungsmatrix.md — Zeilen 30+32 aktualisieren


Teil 2: Backend-Tests

  • backend/src/__tests__/persons.test.ts — Testfall ergaenzen: User mit edit-person-Realm-Rolle darf fremde Person editieren (200)
  • backend/src/__tests__/contacts.test.ts — Testfall ergaenzen: User mit edit-person-Realm-Rolle darf fremde Kontaktdaten editieren (200)
  • Test: User OHNE edit-person-Rolle darf fremde Person NICHT editieren (403)

Teil 3: Frontend — PersonEditModal (generisch)

frontend/src/lib/components/person-edit-modal.svelte (neu)

Selbstaendiges Modal, das von jeder Seite eingebunden werden kann:

let { person, open, onClose }: {
  person: Person;        // mind. personId, firstName, lastName
  open: boolean;
  onClose: () => void;
} = $props();

Verhalten:

  • Bei open === true: $effect ruft GET /api/v1/persons/{personId}/contacts auf
  • Formular mit zwei Sektionen (identisch zur Account-Seite):
    • Stammdaten: Vorname, Nachname
    • Kontaktdaten: E-Mail, Telefon, Strasse, PLZ, Ort
  • Submit via use:enhance auf ?/editPerson
  • Nach Erfolg: onClose() + window.location.reload()

Layout (vorgesehen):

┌─ Person bearbeiten ──────────────────────────┐
│                                               │
│  Stammdaten                                   │
│  ┌─ Vorname ───────────────────────────────┐ │
│  │ [___________]                           │ │
│  ├─ Nachname ──────────────────────────────┤ │
│  │ [___________]                           │ │
│  └─────────────────────────────────────────┘ │
│                                               │
│  Kontaktdaten                                 │
│  ┌─ E-Mail ───────────────────────────────┐  │
│  │ [___________]                           │ │
│  ├─ Telefon ──────────────────────────────┤ │
│  │ [___________]                           │ │
│  ├─ Strasse ──────────────────────────────┤ │
│  │ [___________]                           │ │
│  ├── PLZ ──────┤ ┌── Ort ───────────────┤ │
│  │ [____]      │ │ [________]           │ │
│  └─────────────┘ └──────────────────────┘ │
│                                               │
│      [Abbrechen]      [Speichern]             │
└───────────────────────────────────────────────┘

Teil 4: Frontend — Personen-Ansicht

frontend/src/routes/app/persons/+page.svelte

import PersonEditModal from '$lib/components/person-edit-modal.svelte';
let editingPerson = $state<Person | null>(null);
  • Actions-Spalte: Im ActionMenu (#193) ein Eintrag "Bearbeiten" der editingPerson = row setzt
  • Am Ende der Seite:
{#if editingPerson}
  <PersonEditModal person={editingPerson} open={true} onClose={() => (editingPerson = null)} />
{/if}

frontend/src/routes/app/persons/+page.server.ts

Neue Action editPerson (identisch zu account ?/contacts + ?/person, aber mit beliebiger personId):

editPerson: async ({ request, fetch }) => {
  const data = await request.formData();
  const personId = data.get('personId') as string;
  if (!personId) return fail(400, { error: 'Person muss angegeben sein', formType: 'editPerson' });

  // Stammdaten
  const personRes = await fetch(`/api/v1/persons/${personId}`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      firstName: data.get('firstName'),
      lastName: data.get('lastName'),
    }),
  });
  if (!personRes.ok) return fail(...);

  // Kontaktdaten
  const contactsRes = await fetch(`/api/v1/persons/${personId}/contacts`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      email: (data.get('email') as string) || null,
      phone: (data.get('phone') as string) || null,
      addressStreet: (data.get('addressStreet') as string) || null,
      addressZip: (data.get('addressZip') as string) || null,
      addressCity: (data.get('addressCity') as string) || null,
    }),
  });
  if (!contactsRes.ok) return fail(...);

  return { success: true, formType: 'editPerson' };
};

Teil 5: Frontend — Gruppen-Mitglieder

frontend/src/routes/app/groups/[groupId]/+page.svelte

  • State editingPerson analog zur Personen-Ansicht
  • In der Mitglieder-Tabelle: ActionMenu pro Zeile mit "Bearbeiten"-Eintrag
  • PersonEditModal am Ende der Seite
  • Nutzt GROUP_MEMBER_COLUMNS aus #198 (zentrale Spalten + Consent)

frontend/src/routes/app/groups/[groupId]/+page.server.ts

Identische editPerson-Action wie in persons (gleiche API-Aufrufe).


Dokumentation

docs/design/04-frontend-architektur.md

PersonEditModal in den neuen "Modals/Dialoge"-Abschnitt eintragen (siehe #193).

frontend/src/tests/persons.test.ts

Test fuer editPerson-Action ergaenzen:

test('editPerson action updates person and contacts successfully', async () => { ... });

frontend/src/tests/groups.test.ts

Test fuer editPerson-Action ergaenzen.


Dateien (vollstaendig)

Datei Aenderung
backend/src/lib/auth.ts Neuer Hook requireOwnDataOrGroupMemberOrCompositeRole
backend/src/routes/persons.ts Auth requireOwnDatarequireOwnDataOrCompositeRole
backend/src/routes/contacts.ts Auth requireOwnDataOrGroupMember...OrCompositeRole
docs/design/03-autorisierungsmatrix.md Matrix-Zeilen 30+32
frontend/src/lib/components/person-edit-modal.svelte Neu
frontend/src/routes/app/persons/+page.svelte State + Modal + ActionMenu-Eintrag
frontend/src/routes/app/persons/+page.server.ts Action editPerson
frontend/src/routes/app/groups/[groupId]/+page.svelte State + Modal + ActionMenu-Eintrag
frontend/src/routes/app/groups/[groupId]/+page.server.ts Action editPerson
frontend/src/tests/persons.test.ts Test
frontend/src/tests/groups.test.ts Test
backend/src/__tests__/persons.test.ts Auth-Test
backend/src/__tests__/contacts.test.ts Auth-Test
## Einordnung - **Position:** 6 (Phase C — letzter Issue, nach allen anderen) - **Benötigt:** #193 (ActionMenu-Komponente als Trigger), #198 (zentrale Spalten fuer groups-Tabelle) - **Wird benötigt von:** — (keiner) ## Beschreibung User mit `edit-person`-Realm-Rolle sollen Personen aus der Personen-Ansicht (`/app/persons`) und aus der Gruppen-Detailseite (`/app/groups/{id}`) editieren koennen. Das Editieren erfolgt via `PersonEditModal` — einem generischen Modal fuer Stammdaten + Kontaktdaten, das von beiden Views genutzt wird. **Wichtig:** Der "Bearbeiten"-Button wird NICHT als Inline-Button realisiert, sondern als Eintrag im `ActionMenu` (siehe #193). Dies setzt #193 voraus. --- ## Teil 1: Backend — Auth-Lockerung ### Ist-Zustand | Endpunkt | Auth | Wer darf? | |----------|------|-----------| | `PUT /api/v1/persons/:person_id` (Stammdaten) | `requireAuth + requireOwnData('person_id')` | Nur die Person selbst | | `PUT /api/v1/persons/:person_id/contacts` (Kontaktdaten) | `requireAuth + requireOwnDataOrGroupMember('person_id')` | Selbst oder Haushaltsmitglieder | ### Soll-Zustand | Endpunkt | Auth | Wer darf? | |----------|------|-----------| | `PUT /api/v1/persons/:person_id` | `requireAuth + requireOwnDataOrCompositeRole('edit-person', 'person_id')` | Selbst ODER `edit-person`-Rolle | | `PUT /api/v1/persons/:person_id/contacts` | `requireAuth + requireOwnDataOrGroupMemberOrCompositeRole('edit-person', 'person_id')` | Selbst, Haushalt ODER `edit-person`-Rolle | Die Account-Seite (`/app/account`) nutzt denselben `PUT /api/v1/persons/:person_id`-Endpunkt. Da sie immer `locals.user.personId` sendet, ist sie von der Auth-Aenderung nicht betroffen (der `requireOwnData`-Check laeuft durch). ### Aenderungen 1. **`backend/src/lib/auth.ts`** — Neuer Hook `requireOwnDataOrGroupMemberOrCompositeRole(permission, personIdParam)`: - Prueft in Reihe: (a) `request.user.personId === targetPersonId`, (b) GroupMember-Lookup gleiche groupId, (c) `request.user.roles.includes(permission)` - Gibt 403 wenn keiner der Checks durchlaeuft 2. **`backend/src/routes/persons.ts:333`** — preHandler aendern: ```typescript // ALT: requireAuth, requireOwnData('person_id') // NEU: requireAuth, requireOwnDataOrCompositeRole('edit-person', 'person_id') ``` 3. **`backend/src/routes/contacts.ts:83`** — preHandler aendern: ```typescript // ALT: requireAuth, requireOwnDataOrGroupMember('person_id') // NEU: requireAuth, requireOwnDataOrGroupMemberOrCompositeRole('edit-person', 'person_id') ``` 4. **`docs/design/03-autorisierungsmatrix.md`** — Zeilen 30+32 aktualisieren --- ## Teil 2: Backend-Tests - **`backend/src/__tests__/persons.test.ts`** — Testfall ergaenzen: User mit `edit-person`-Realm-Rolle darf fremde Person editieren (200) - **`backend/src/__tests__/contacts.test.ts`** — Testfall ergaenzen: User mit `edit-person`-Realm-Rolle darf fremde Kontaktdaten editieren (200) - Test: User OHNE `edit-person`-Rolle darf fremde Person NICHT editieren (403) --- ## Teil 3: Frontend — PersonEditModal (generisch) ### `frontend/src/lib/components/person-edit-modal.svelte` (neu) Selbstaendiges Modal, das von jeder Seite eingebunden werden kann: ```typescript let { person, open, onClose }: { person: Person; // mind. personId, firstName, lastName open: boolean; onClose: () => void; } = $props(); ``` **Verhalten:** - Bei `open === true`: `$effect` ruft `GET /api/v1/persons/{personId}/contacts` auf - Formular mit zwei Sektionen (identisch zur Account-Seite): - **Stammdaten**: Vorname, Nachname - **Kontaktdaten**: E-Mail, Telefon, Strasse, PLZ, Ort - Submit via `use:enhance` auf `?/editPerson` - Nach Erfolg: `onClose()` + `window.location.reload()` ### Layout (vorgesehen): ``` ┌─ Person bearbeiten ──────────────────────────┐ │ │ │ Stammdaten │ │ ┌─ Vorname ───────────────────────────────┐ │ │ │ [___________] │ │ │ ├─ Nachname ──────────────────────────────┤ │ │ │ [___________] │ │ │ └─────────────────────────────────────────┘ │ │ │ │ Kontaktdaten │ │ ┌─ E-Mail ───────────────────────────────┐ │ │ │ [___________] │ │ │ ├─ Telefon ──────────────────────────────┤ │ │ │ [___________] │ │ │ ├─ Strasse ──────────────────────────────┤ │ │ │ [___________] │ │ │ ├── PLZ ──────┤ ┌── Ort ───────────────┤ │ │ │ [____] │ │ [________] │ │ │ └─────────────┘ └──────────────────────┘ │ │ │ │ [Abbrechen] [Speichern] │ └───────────────────────────────────────────────┘ ``` --- ## Teil 4: Frontend — Personen-Ansicht ### `frontend/src/routes/app/persons/+page.svelte` ```typescript import PersonEditModal from '$lib/components/person-edit-modal.svelte'; let editingPerson = $state<Person | null>(null); ``` - Actions-Spalte: Im ActionMenu (#193) ein Eintrag "Bearbeiten" der `editingPerson = row` setzt - Am Ende der Seite: ```svelte {#if editingPerson} <PersonEditModal person={editingPerson} open={true} onClose={() => (editingPerson = null)} /> {/if} ``` ### `frontend/src/routes/app/persons/+page.server.ts` Neue Action `editPerson` (identisch zu account `?/contacts` + `?/person`, aber mit beliebiger personId): ```typescript editPerson: async ({ request, fetch }) => { const data = await request.formData(); const personId = data.get('personId') as string; if (!personId) return fail(400, { error: 'Person muss angegeben sein', formType: 'editPerson' }); // Stammdaten const personRes = await fetch(`/api/v1/persons/${personId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ firstName: data.get('firstName'), lastName: data.get('lastName'), }), }); if (!personRes.ok) return fail(...); // Kontaktdaten const contactsRes = await fetch(`/api/v1/persons/${personId}/contacts`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: (data.get('email') as string) || null, phone: (data.get('phone') as string) || null, addressStreet: (data.get('addressStreet') as string) || null, addressZip: (data.get('addressZip') as string) || null, addressCity: (data.get('addressCity') as string) || null, }), }); if (!contactsRes.ok) return fail(...); return { success: true, formType: 'editPerson' }; }; ``` --- ## Teil 5: Frontend — Gruppen-Mitglieder ### `frontend/src/routes/app/groups/[groupId]/+page.svelte` - State `editingPerson` analog zur Personen-Ansicht - In der Mitglieder-Tabelle: ActionMenu pro Zeile mit "Bearbeiten"-Eintrag - `PersonEditModal` am Ende der Seite - Nutzt `GROUP_MEMBER_COLUMNS` aus #198 (zentrale Spalten + Consent) ### `frontend/src/routes/app/groups/[groupId]/+page.server.ts` Identische `editPerson`-Action wie in persons (gleiche API-Aufrufe). --- ## Dokumentation ### `docs/design/04-frontend-architektur.md` `PersonEditModal` in den neuen "Modals/Dialoge"-Abschnitt eintragen (siehe #193). ### `frontend/src/tests/persons.test.ts` Test fuer `editPerson`-Action ergaenzen: ```typescript test('editPerson action updates person and contacts successfully', async () => { ... }); ``` ### `frontend/src/tests/groups.test.ts` Test fuer `editPerson`-Action ergaenzen. --- ## Dateien (vollstaendig) | Datei | Aenderung | |---|---| | `backend/src/lib/auth.ts` | Neuer Hook `requireOwnDataOrGroupMemberOrCompositeRole` | | `backend/src/routes/persons.ts` | Auth `requireOwnData` → `requireOwnDataOrCompositeRole` | | `backend/src/routes/contacts.ts` | Auth `requireOwnDataOrGroupMember` → `...OrCompositeRole` | | `docs/design/03-autorisierungsmatrix.md` | Matrix-Zeilen 30+32 | | `frontend/src/lib/components/person-edit-modal.svelte` | **Neu** | | `frontend/src/routes/app/persons/+page.svelte` | State + Modal + ActionMenu-Eintrag | | `frontend/src/routes/app/persons/+page.server.ts` | Action `editPerson` | | `frontend/src/routes/app/groups/[groupId]/+page.svelte` | State + Modal + ActionMenu-Eintrag | | `frontend/src/routes/app/groups/[groupId]/+page.server.ts` | Action `editPerson` | | `frontend/src/tests/persons.test.ts` | Test | | `frontend/src/tests/groups.test.ts` | Test | | `backend/src/__tests__/persons.test.ts` | Auth-Test | | `backend/src/__tests__/contacts.test.ts` | Auth-Test |
niboer changed title from feat(backend+frontend): edit-person-Berechtigung fuer Personen-Edit in Personen-Ansicht to feat(backend+frontend): edit-person-Berechtigung fuer Personen-Edit in Personen-Ansicht + Gruppen-Mitglieder 2026-07-02 22:37:32 +00:00
Sign in to join this conversation.
No description provided.