From ffe2d450eb1e14a179d8b99bfbbec9e4ed17f927 Mon Sep 17 00:00:00 2001 From: patrick Date: Fri, 11 Sep 2026 18:46:34 +0200 Subject: [PATCH] fix: revoke org API access immediately on member suspend/leave, not just Matrix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit findMembership() and GET /api/organizations/mine ignored members.status — a suspended or left member kept full API access to the organization (member list, teams, roles, audit log) and still saw it in the org switcher, even though only their Matrix account was deactivated. Both now require status = "active". Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0143yYvjesjHndBACoweyK67 --- backend/src/lib/authz.ts | 10 +++++++++- backend/src/routes/organizations.ts | 4 ++-- features/PROJ-3-mitgliederverwaltung.md | 2 ++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/backend/src/lib/authz.ts b/backend/src/lib/authz.ts index 1f98178..3cdd121 100644 --- a/backend/src/lib/authz.ts +++ b/backend/src/lib/authz.ts @@ -5,10 +5,18 @@ import { members, organizationRoles } from "../db/schema.js"; const ADMIN_ROLES = ["ORG_ADMIN", "ORG_MANAGER"] as const; export async function findMembership(userId: string, organizationId: string) { + // Only "active" grants access — "suspended"/"left" must lose API access immediately, + // not just get their Matrix account deactivated. const [membership] = await db .select({ id: members.id }) .from(members) - .where(and(eq(members.userId, userId), eq(members.organizationId, organizationId))) + .where( + and( + eq(members.userId, userId), + eq(members.organizationId, organizationId), + eq(members.status, "active"), + ), + ) .limit(1); return membership ?? null; } diff --git a/backend/src/routes/organizations.ts b/backend/src/routes/organizations.ts index 678f7cc..61cea27 100644 --- a/backend/src/routes/organizations.ts +++ b/backend/src/routes/organizations.ts @@ -1,6 +1,6 @@ import type { FastifyInstance } from "fastify"; import { z } from "zod"; -import { eq } from "drizzle-orm"; +import { and, eq } from "drizzle-orm"; import { db } from "../db/index.js"; import { organizations, members, organizationRoles, users } from "../db/schema.js"; import { logAudit } from "../lib/audit.js"; @@ -21,7 +21,7 @@ export async function organizationRoutes(app: FastifyInstance) { .select({ id: organizations.id, name: organizations.name, slug: organizations.slug }) .from(members) .innerJoin(organizations, eq(members.organizationId, organizations.id)) - .where(eq(members.userId, userId)); + .where(and(eq(members.userId, userId), eq(members.status, "active"))); return { organizations: rows }; }); diff --git a/features/PROJ-3-mitgliederverwaltung.md b/features/PROJ-3-mitgliederverwaltung.md index 40e4a06..be27978 100644 --- a/features/PROJ-3-mitgliederverwaltung.md +++ b/features/PROJ-3-mitgliederverwaltung.md @@ -11,12 +11,14 @@ Mitglieder eines Vereins verwalten: Liste, Status-Aktionen (aktivieren/sperren/A - `frontend/src/pages/MembersPage.tsx` - Mitglied-Status-Aktionen + Deaktivierungs-Sync (commit b38a5a4) - **Nachtrag 2026-09-11 (commit bc4d749):** `DELETE /api/members/:memberId` — Admin-only, hartes Löschen. Audit-Log-Eintrag wird vor dem Delete geschrieben (`audit_logs.targetId` ist unconstrained text, überlebt das Löschen). Matrix-Account wird per `deactivateUser(matrixUserId, erase=true)` gelöscht. Abhängige Daten (`parent_links`, `team_members`, `org_roles`) laufen über bestehende `onDelete: cascade`-FKs mit. Frontend: Bestätigungsdialog (`AlertDialog`) im Dropdown-Menü der Mitgliederliste. +- **Nachtrag 2026-09-11 (Bugfix, ungetaggter Commit nach bc4d749):** `backend/src/lib/authz.ts` (`findMembership`) und `GET /api/organizations/mine` prüften `members.status` bisher **nicht** — ein Mitglied mit Status `suspended`/`left` behielt vollen API-Zugriff auf die Organisation (Mitgliederliste, Teams, Rollen, Audit-Log) und sah sie weiter im Organisation-Switcher, obwohl nur der Matrix-Account deaktiviert wurde. Beide Stellen filtern jetzt zusätzlich auf `status = "active"`. ## Acceptance Criteria - [x] Mitgliederliste pro Verein abrufbar (paginiert) - [x] Status ändern (aktiv/gesperrt/ausgetreten) durch berechtigte Rollen (`memberStatusEnum`: `active`/`suspended`/`left`) - [x] Austritt kann eigenständig vermerkt werden ("Austritt vermerken" im Dropdown, Status → `left`), getrennt von Sperren - [x] Sperren oder Austritt lösen Sync-Schritt Richtung Matrix aus (`deactivateUser`, ohne Erase — Account bleibt inaktiv wiederherstellbar) +- [x] Sperren oder Austritt entziehen sofort den TalkTime-API-Zugriff auf die Organisation (nicht nur den Matrix-Account) — durchgesetzt in `findMembership`/`requesterIsOrganizationMember`/`requesterIsOrganizationAdmin`, betrifft alle org-gescopten Routen - [x] Mitglied kann durch Org-Admin endgültig gelöscht werden (mit Bestätigungsdialog im Frontend) - [x] Löschen entfernt/erased den zugehörigen Matrix-Account und alle abhängigen Datensätze (Team-Zuordnungen, Rollen, Eltern-Verknüpfungen) - [x] Audit-Log-Eintrag bleibt nach dem Löschen erhalten