Zugriffsverwaltung wurde hinzugefügt #issue/26 #36

Merged
lrsauvette merged 11 commits from issue/26 into develop 2026-08-02 14:17:17 +00:00
Owner
No description provided.
Foundations for the multi-user / role-based auth (issue/26).

- Benutzer model with BenutzerRolle enum (admin, coSsp, svr, kw,
  kp, keine) and a compact label extension.
- Permission enum + Permissions.hat(rolle, p) static map encoding
  the issue's permission table (5-role matrix with KP-only Kasse
  verify/delete, two separate kasse/schueler CSV permissions).
- BenutzerStore persisted in the JsonStore: add / update / remove,
  case-insensitive findByName, verifyCredentials (rejects users
  with role 'keine'), changeOwnPassword, adminSetPassword,
  adminResetPassword. Runs a one-time migration that replaces the
  old 'SVR-V' / 'stv. SVR-V' amtKürzel with 'Co-SVR-V' on load.
- AdminRecoveryStore holds the sha256 hash of the global recovery
  code used to reset the admin password. Code generator stays
  identical to the old PasswordStore (10 chars, no ambiguous
  glyphs).
- 20 new tests covering CRUD, lookup, password flows, role
  rejection, amtKürzel migration and JsonStore roundtrip.
Replaces the single-password AuthNotifier (login returned bool)
with a user-aware one. AuthState now carries the currently logged
in Benutzer; login returns the Benutzer or null depending on
whether the credentials check (and role-non-keine check) pass.

- login(name, password) -> Benutzer? validates through
  BenutzerStore.verifyCredentials, starts the 30-minute inactivity
  timer, sets currentUser.
- logout() cancels the timer and clears currentUser.
- resetAdminPasswordWithRecoveryCode(code, newPassword) uses
  AdminRecoveryStore.verify and BenutzerStore.adminResetPassword
  to reset the admin password.
- recordActivity clears the sessionWarning flag.

11 new tests cover initial state, login success/failure, role
rejection, logout, recordActivity, and the recovery flow's
success / wrong-code / no-admin paths.
Two small helpers for the upcoming permission gates:

- usePermission(ref, p) -> bool: true if the logged-in user has
  the given permission. Returns false when nobody is logged in.
- PermissionGate({permission, child, fallback}): renders [child]
  only when the user has the permission; otherwise renders
  [fallback] (default SizedBox.shrink).

Strategy throughout the rest of the issue: hide unauthorized
actions entirely (no snackbar fallback). Test covers the full
role x permission matrix for admin / coSsp / svr / kw / kp /
keine and asserts eigenesPasswortAendern is granted to every
logged-in role.
Updates the canonical Amt.standard list to a single Co-SVR-V
entry instead of the two former variants, and adds one-time
migrations in AmtStore and StudentStore so existing data is
mapped to the new kürzel on load.

- Amt.standard: drops SVR-V and stv. SVR-V, adds
  Co-SVR-V (bezeichnung: Co-SVR-Vorsitzende:r). Other entries
  reordered so MdSVR/MdBDK/SVL/KW/KP sit right after.
- AmtStore._load: when a persisted list contains the old kürzel,
  it is replaced with Co-SVR-V (idempotent).
- StudentStore._load: same replacement for each student's
  aemter list, with deduplication (a student with both SVR-V
  and stv. SVR-V in aemter ends up with a single Co-SVR-V).
- BenutzerStore migration (added in the previous commit) does
  the same for user.amtKuerzel.

Tests cover the migration in both stores plus idempotency.
Replaces the single-password SetPasswordPage with a multi-user
FirstAdminSetupPage that creates the first admin account and shows
the global recovery code. The LoginPage now shows a user dropdown
when more than one user exists (single-user shortcut remains).

AuthGate routing:
- empty benutzerStore → FirstAdminSetupPage
- non-empty & not logged in → LoginPage
- logged in → AppShell (unchanged listener pattern)

AuthGate also fires a one-time cleanup of the legacy
sv_helfer_pw_hash / sv_helfer_pw_recovery keys so the data dir
stays clean after the migration.

LoginPage: when the user clicks 'Admin-Passwort vergessen?'
the dialog now asks for the recovery code + a new password, and
calls AuthNotifier.resetAdminPasswordWithRecoveryCode.
Non-admin passwords are no longer recoverable via the dialog
(only Admin can reset, per issue spec).
New BenutzerPage (admin only) accessible from Einstellungen →
Sicherheit. Provides:

- List of all Benutzer with name, role label and amt
  bezeichnung.
- Add user dialog: name, initial password, role dropdown
  (keine hidden, since it has no permissions), amt dropdown.
- Edit user dialog: change name, role, amt (password not edited
  here).
- Passwort zurücksetzen dialog: admin sets a new password for a
  user, with confirmation.
- Delete user with confirmation, blocked for the user themselves
  and for the last admin (both produce a snackbar explaining why).
- Admin-Wiederherstellungscode neu erzeugen action: regenerates
  the global recovery code and shows it once with confirmation
  checkbox.

The 'Benutzer verwalten' card in Einstellungen is wrapped in a
PermissionGate(permission: nutzerVerwalten), so non-admins never
see the entry.
- Extracts the password-change dialog into a standalone
  PasswortAendernDialog so it can be opened from anywhere
  (currently the AppBar). Uses BenutzerStore.changeOwnPassword.
- AppShell changes:
  - AppBar shows a compact chip with the current user's name
    and role label, plus a 'Passwort ändern' icon button next
    to the existing logout button.
  - Tabs are filtered by the user's role via the new
    _visibleTabs helper: Verwaltung needs schuelerSehen,
    Anträge antraegeErstellen, Wahlen wahlenErstellen,
    Anwesenheit anwesenheitslisteErzeugen, Kasse kasseLesen,
    Einstellungen always. Hidden tabs are not rendered at all
    (no empty body, no hidden IndexedStack child).
Wraps the action buttons in every feature page with the
PermissionGate widget so users only see what they're allowed to do.

EinstellungenPage:
- Schuljahr ändern + Datenspeicherort cards: blocked unless
  stammdatenAendern / speicherortAendern respectively.
- Ämter verwalten + Antragstypen verwalten: stammdatenAendern.
- Kassen-Kategorien verwalten: kasseKategorieVerwalten (note: KW
  and KP can manage kassen categories via this entry).
- CSV import / export (Schüler): schuelerCsvImportExport.
- Vollsicherung exportieren: backupExportieren.
- Sicherung importieren: backupImportieren.
- Passwort ändern: opens the new PasswortAendernDialog.

VerwaltungPage: Hinzufügen / Edit / Delete buttons all gated by
schuelerVerwalten. svLeitung still sees the table and student
list (read-only) via schuelerSehen on the tab.

WahlenPage: Wahl starten + Online-Wahl erstellen gated by
wahlenErstellen. Löschen-Button gated by datenLoeschen. Tap-on-row
to edit gated by wahlenErstellen.

AntraegePage: Live-Abstimmung gated by antraegeAbstimmen, Neuer
Antrag by antraegeErstellen, Löschen by datenLoeschen, Edit-on-tap
by antraegeErstellen.

ErzeugungPage: Anwesenheitsliste erzeugen gated by
anwesenheitslisteErzeugen.

KassePage: CSV import / export gated by kasseCsvImportExport,
Neuer Eintrag by kasseSchreiben, verify by kasseVerifizieren,
edit by kasseSchreiben, delete by kasseLoeschenVerifiziert.

The old _PasswortAendernDialog class is removed from
EinstellungenPage; the new shared PasswortAendernDialog replaces
it.
The old PasswordStore (single-password sha256 hash + global
recovery code) and AppConfig.adminPassword constant are no longer
referenced anywhere — credentials now live in BenutzerStore
(multi-user) and the global recovery code in AdminRecoveryStore.

AuthGate already cleans up the legacy sv_helfer_pw_hash and
sv_helfer_pw_recovery JsonStore keys on first launch after the
upgrade, so old installs lose their auth data cleanly.
The _EditBenutzerDialog used default-e?.rolle ?? BenutzerRolle.keine,
but the dropdown items filter out BenutzerRolle.keine, so the
initialValue had no matching item and the form threw an assertion
when adding a new user.

New users now default to BenutzerRolle.coSsp (always present in
the items list). Existing users with BenutzerRolle.keine also
fall back to coSsp, which is the right behaviour since such
records can never log in anyway.
EinstellungenPage tweaks:

- The 'Stammdaten' header and its 16px gap are now wrapped in a
  conditional that checks whether any of the three cards below
  (Ämter, Antragstypen, Kassen-Kategorien) would be visible for
  the current user. The same is done for 'Import / Export' (CSV
  im-/export, Vollsicherung im-/export). When the user has no
  permission for any of a section's cards, the entire section
  header is omitted so the page only shows relevant labels.

- The SVVV-Sitzungsstatistik card under 'Darstellung' is now
  hidden for users without schuelerSehen (KW, KP, etc.). Since
  those roles don't have a Verwaltung tab, the toggle had no
  effect for them anyway.
lrsauvette merged commit cf11d1a163 into develop 2026-08-02 14:17:17 +00:00
lrsauvette deleted branch issue/26 2026-08-02 14:17:18 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
lrsauvette/sv-helfer!36
No description provided.