From 9d9698186cbb1775724f60615451bc2352dff6c9 Mon Sep 17 00:00:00 2001 From: Sandro Date: Thu, 29 Oct 2026 19:43:23 +0100 Subject: [PATCH] fix(security): demo sends nothing, hidden SMTP password, body limits (FLAW-33..38) - Demo: senders for mail, Discord and webhooks refuse, no alarm ticker; test alarm, SMTP test, server settings, backups, account and session changes of the shared demo accounts answer 403; the session list only shows the own device. The UI shows the notices and disables the controls. - SMTP password is never sent back (smtpPassSet), empty keeps it. - ?token= on webhook ingest accepts only API keys. - Request bodies capped at 2 MB (sync 8 MB, imports 101 MB). - Deleting an account removes its server backups. - Backup downloads, exports and import uploads get 30 minutes. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 8 + Doku/api/rest.md | 6 +- Doku/architecture.md | 2 +- Doku/flaws.md | 18 +- .../features/settings/AdminSettingsView.tsx | 540 +++++++++--------- .../features/settings/UserSettingsView.tsx | 46 +- frontend/src/index.css | 6 + frontend/src/lib/types.ts | 3 +- internal/api/alarms.go | 9 + internal/api/backups.go | 14 + internal/api/bodylimit.go | 42 ++ internal/api/bodylimit_test.go | 30 + internal/api/demo.go | 16 + internal/api/handlers_account.go | 2 + internal/api/handlers_admin.go | 22 +- internal/api/handlers_auth.go | 6 + internal/api/handlers_integrations.go | 6 + internal/api/s4_test.go | 113 ++++ internal/api/server.go | 25 +- internal/api/sessions.go | 10 + internal/domain/models.go | 11 +- 21 files changed, 631 insertions(+), 304 deletions(-) create mode 100644 internal/api/bodylimit.go create mode 100644 internal/api/bodylimit_test.go create mode 100644 internal/api/s4_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 591650d..bb24018 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,14 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V - IDs neuer Datensätze haben 96 statt 40 Zufallsbits, Einladungscodes eigene 100 Bit (`INV-ABCDE-FGHIJ-KLMNO-PQRST`); vorhandene IDs und Codes bleiben gültig (FLAW-32). - Registrierung und „Passwort vergessen“ sind pro Adresse begrenzt (20 bzw. 10 Anfragen in 15 Minuten), Einladungscodes lassen sich so nicht durchprobieren (FLAW-39). - Ein Einladungscode lässt sich auch bei gleichzeitigen Registrierungen nur einmal einlösen (FLAW-40). +- Öffentliche Demo: verschickt nichts mehr (Alarme, Testalarme, SMTP-Test, Hinweismails), Server-Einstellungen sind schreibgeschützt, Passwort, E-Mail, Sitzungen und Konten der gemeinsamen Demo-Konten lassen sich nicht ändern; die Sitzungsliste zeigt nur das eigene Gerät (FLAW-33). +- Das SMTP-Passwort wird nie mehr an den Browser geschickt; leer lassen behält das gespeicherte (FLAW-34). +- `?token=` beim Webhook-Ingest nimmt nur noch API-Keys an, keine Sitzungs-Tokens (FLAW-35). +- Anfragekörper sind auf 2 MB begrenzt, Notiz-Sync auf 8 MB, Importe auf 101 MB (FLAW-36). +- Konto löschen entfernt auch die Server-Backups des Kontos (FLAW-37). ### Fixed +- Große Downloads (Backups, Exporte) brachen nach 30 Sekunden ab, große Importe nach 15 Sekunden Upload (FLAW-38). - Der Offline-Start der App funktioniert zuverlässig: Ein altes Script in der Startseite hat bei jedem Aufruf alle Caches des Service Workers gelöscht. ### Added @@ -24,6 +30,8 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V ### Changed - **Achtung:** Nach dem Update sind alle Benutzer einmal abgemeldet (neue Sitzungsverwaltung, die alte Sperrliste `revoked_tokens` entfällt). +- **Achtung:** `?token=` in der URL von `/webhooks/ingest` nimmt nur noch API-Keys (`mindsift_sec_…`) an. +- **Achtung:** `GET /admin/settings` liefert `smtpPass` nicht mehr; neu ist `smtpPassSet`. - **Achtung:** `PATCH /auth/settings` verlangt für `password` und `email` das Feld `currentPassword`, sonst `403`. - Versionsnummern folgen jetzt Semantic Versioning mit dreiteiligen Tags (`v0.6.0`); die Kopfzeile zeigt die volle Version (`v0.6.0-dev` für Entwicklungsstände). Vorabversionen wie `v1.0.0-rc.1` verschieben das Image `:stable` nicht. - Das Mengenfeld in Listen öffnet auf dem Handy die Zahlentastatur. diff --git a/Doku/api/rest.md b/Doku/api/rest.md index e1566b1..6b2560a 100644 --- a/Doku/api/rest.md +++ b/Doku/api/rest.md @@ -1,6 +1,6 @@ # REST-API -Basis: `/api/v1`. Antworten sind JSON; Fehler haben die Form `{"error": "Text"}`. Authentifizierung über `Authorization: Bearer `. **API-Keys** (`Bearer mindsift_sec_…` bzw. Header `X-API-Key`) liefern nur Daten ein, über `POST /inbox` und `POST /webhooks/ingest`, und nur für die Ziele ihrer Scopes: `webhook:triage` (Inbox), `webhook:task` (Aufgaben), `webhook:list` (Listeneinträge). Alles andere, auch Live-Streams, antwortet mit `403` bzw. `401`. Zeitstempel sind ISO-8601 (UTC). Quelle der Wahrheit sind die Routen in [server.go](../../internal/api/server.go). +Basis: `/api/v1`. Antworten sind JSON; Fehler haben die Form `{"error": "Text"}`. Authentifizierung über `Authorization: Bearer `. **API-Keys** (`Bearer mindsift_sec_…` bzw. Header `X-API-Key`) liefern nur Daten ein, über `POST /inbox` und `POST /webhooks/ingest`, und nur für die Ziele ihrer Scopes: `webhook:triage` (Inbox), `webhook:task` (Aufgaben), `webhook:list` (Listeneinträge). Alles andere, auch Live-Streams, antwortet mit `403` bzw. `401`. Zeitstempel sind ISO-8601 (UTC). Anfragekörper sind auf 2 MB begrenzt (Notiz-Sync 8 MB, Importe 101 MB). Im Demo-Modus antworten Endpunkte, die etwas verschicken, Server-Einstellungen ändern oder die gemeinsamen Demo-Konten verändern würden, mit `403` „In der Demo deaktiviert“. Quelle der Wahrheit sind die Routen in [server.go](../../internal/api/server.go). ## Öffentlich @@ -15,7 +15,7 @@ Basis: `/api/v1`. Antworten sind JSON; Fehler haben die Form `{"error": "Text"}` | POST | `/auth/forgot-password` | `{email}` → immer `{ok: true}`; Mail mit Link nur, wenn das Konto existiert. `503`, wenn SMTP oder `BASE_URL` fehlen; `429` nach 10 Anfragen pro Adresse in 15 Minuten | | POST | `/auth/reset-password` | `{token, password}` (mind. 8 Zeichen) → neues Passwort, alle Sitzungen beendet; `400` bei ungültigem oder abgelaufenem Link | | POST | `/auth/quickstart` | Immer `403` (Gastzugang deaktiviert) | -| POST | `/webhooks/ingest` | Einliefern per Webhook, Key als `X-API-Key` (empfohlen), Bearer oder `?token=`. Body: `title`/`content`/`text`/`message`, `priority`, `source`, `tags`, `dueDate`; `target` (oder `type`): `triage` (Standard, Inbox), `task` (optional `taskListId`), `list_item` (`listId`, `category`, `quantity`). Klartext-Body wird als Inhalt genommen. `403` ohne passenden Scope oder ohne Schreibrecht auf die Liste | +| POST | `/webhooks/ingest` | Einliefern per Webhook, Key als `X-API-Key` (empfohlen), Bearer oder `?token=` (in der URL nur API-Keys, keine Sitzungs-Tokens). Body: `title`/`content`/`text`/`message`, `priority`, `source`, `tags`, `dueDate`; `target` (oder `type`): `triage` (Standard, Inbox), `task` (optional `taskListId`), `list_item` (`listId`, `category`, `quantity`). Klartext-Body wird als Inhalt genommen. `403` ohne passenden Scope oder ohne Schreibrecht auf die Liste | | POST | `/auth/stream-ticket` | Einmal-Ticket (60 s) für WebSocket/SSE | | GET | `/ws`, `/ws/spaces/{id}`, `/ws/lists/{id}` | WebSocket, siehe [websocket.md](websocket.md); `?ticket=` oder `Authorization`-Header | | GET | `/events` | SSE-Fallback; `?ticket=` oder `Authorization`-Header | @@ -190,7 +190,7 @@ Wiederholte Operationen (gleiche `opId`) werden nicht erneut angewendet; die Ant | PATCH/DELETE | `/admin/users/{id}` | `displayName`, `email`, `role`, `isActive`, `discordWebhookUrl`; löschen | | PUT | `/admin/users/{id}/password` | Passwort zurücksetzen | | GET/POST/DELETE | `/admin/invites[/{id}]` | Einladungen (`email`, `role`, `organizationId`, `validDays`) | -| GET/PUT | `/admin/settings` | `instanceName`, `allowRegistration`, `sharingMode` (`private`/`organizations`/`open`), SMTP-Felder; ein Moduswechsel bereinigt Freigaben | +| GET/PUT | `/admin/settings` | `instanceName`, `allowRegistration`, `sharingMode` (`private`/`organizations`/`open`), SMTP-Felder; ein Moduswechsel bereinigt Freigaben. `smtpPass` kommt nie zurück (`smtpPassSet` zeigt, ob eines gespeichert ist); leer = unverändert | | GET | `/admin/settings/sharing-impact?mode=` | Probelauf: wie viele Freigaben und Gruppen-Mitgliedschaften ein Wechsel entfernen würde (`affected`) | | POST | `/admin/smtp/test` | Testmail (`toEmail`) | | GET/POST | `/admin/backups` | Datenbank-Abzüge auflisten bzw. sofort sichern (Abzug + alle Benutzer-Backups) | diff --git a/Doku/architecture.md b/Doku/architecture.md index 291226d..f890dbf 100644 --- a/Doku/architecture.md +++ b/Doku/architecture.md @@ -65,7 +65,7 @@ Das Frontend liegt per `go:embed` im Binary ([cmd/server/main.go](../cmd/server/ | `TZ` | `Europe/Berlin` | Zeitzone für Fälligkeiten und Alarme; die Zeitzonendaten sind ins Binary eingebettet (`time/tzdata`) | | `ALLOW_REGISTRATION` | `false` | Startwert; die Einstellung im Admin-UI (`system_settings`) hat Vorrang | | `LOG_LEVEL` / `DEBUG` | `info` / `false` | `debug`, `info`, `warn`, `error`; `DEBUG=true` erzwingt `debug` | -| `MINDA_DEMO` (auch `MINDSIFT_DEMO`, `DEMO_MODE`) | `false` | Demo-Konten anlegen | +| `MINDA_DEMO` (auch `MINDSIFT_DEMO`, `DEMO_MODE`) | `false` | Demo-Konten anlegen. Die Demo verschickt nichts (keine Alarme, Test- oder Hinweismails, Webhooks), Server-Einstellungen sind schreibgeschützt, Passwort, E-Mail und Konten der gemeinsam genutzten Demo-Konten lassen sich nicht ändern (`notInDemo` in `internal/api/demo.go`) | | `DEMO_RESET_MINUTES` | `20` | Demo-Modus: periodischer Reset auf die Beispieldaten (`0` = aus). Feste IDs der Demo-Konten (`usr_demo_admin`, `usr_demo_user`) halten Sitzungen gültig; laufende Anfragen sperren den Reset (`internal/api/demo.go`, Streams ausgenommen); danach `DEMO_RESET` an alle Verbindungen, `/auth/status` liefert `demoNextReset`/`demoResetInSeconds`. | | `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASS`, `SMTP_FROM`, `SMTP_TLS` | leer / `587` / `true` | Mailversand, auch im Admin-UI einstellbar | | `SMTP_SKIP_VERIFY` | `false` | Zertifikat des Mailservers nicht prüfen (nur interne Server mit selbstsigniertem Zertifikat) | diff --git a/Doku/flaws.md b/Doku/flaws.md index b1b69f4..96526f2 100644 --- a/Doku/flaws.md +++ b/Doku/flaws.md @@ -15,12 +15,12 @@ Schweregrade: **Hoch** (Kontoübernahme oder Datenabfluss möglich), **Mittel** | FLAW-30 | Konto | ~~Passwort und E-Mail lassen sich ohne das aktuelle Passwort ändern~~ | **Behoben** | | FLAW-31 | Auth | ~~Sitzungs-Token 30 Tage gültig und im `localStorage`~~ | **Teilweise behoben** | | FLAW-32 | Daten | ~~IDs und Einladungscodes haben nur 40 Bit Zufall~~ | **Behoben** | -| FLAW-33 | Demo | Öffentliche Demo als Absender missbrauchbar (Alarm-Webhooks, Testmails, Server-Einstellungen) | Mittel | -| FLAW-34 | Admin | SMTP-Passwort im Klartext an die Oberfläche, in DB und Datenbank-Backups | Niedrig | -| FLAW-35 | Netzwerk | Webhook-Ingest nimmt Sitzungs-Token und Keys auch als `?token=` | Niedrig | -| FLAW-36 | Backend | Kein allgemeines Größenlimit für JSON-Anfragen | Niedrig | -| FLAW-37 | Datenschutz | Konto löschen entfernt die Server-Backups des Kontos nicht | Niedrig | -| FLAW-38 | Betrieb | `WriteTimeout` von 30 s bricht große Downloads ab | Niedrig | +| FLAW-33 | Demo | ~~Öffentliche Demo als Absender missbrauchbar (Alarm-Webhooks, Testmails, Server-Einstellungen)~~ | **Behoben** | +| FLAW-34 | Admin | ~~SMTP-Passwort im Klartext an die Oberfläche~~, in DB und Datenbank-Backups | **Teilweise behoben** | +| FLAW-35 | Netzwerk | ~~Webhook-Ingest nimmt Sitzungs-Token und Keys auch als `?token=`~~ | **Behoben** | +| FLAW-36 | Backend | ~~Kein allgemeines Größenlimit für JSON-Anfragen~~ | **Behoben** | +| FLAW-37 | Datenschutz | ~~Konto löschen entfernt die Server-Backups des Kontos nicht~~ | **Behoben** | +| FLAW-38 | Betrieb | ~~`WriteTimeout` von 30 s bricht große Downloads ab~~ | **Behoben** | | FLAW-39 | Auth | ~~Kein Rate-Limit für Registrierung, Einladungscodes und „Passwort vergessen“ je Adresse~~ | **Behoben** | | FLAW-40 | Auth | ~~Einladungscode bei gleichzeitiger Nutzung zweimal einlösbar~~ | **Behoben** | | FLAW-41 | Qualität | Sehr große Frontend-Dateien | Niedrig | @@ -64,31 +64,37 @@ Schweregrade: **Hoch** (Kontoübernahme oder Datenabfluss möglich), **Mittel** - **Problem:** Auf einer öffentlichen Demo meldet sich jeder als Admin an. Er kann „Alarm-Webhooks ins lokale Netz erlauben“ einschalten und Alarme an Adressen im Netz des Demo-Hosts schicken lassen (SSRF), SMTP-Zugangsdaten lesen und ändern, Organisationen und Konten umbauen. Der 20-Minuten-Reset setzt das zurück, aber nicht rechtzeitig. - **Ausgehende Anfragen:** Auch ohne Admin-Rechte kann jeder Demo-Besucher eine Discord- oder eigene Webhook-URL hinterlegen und über „Alarm testen“ beliebig oft Anfragen an fremde öffentliche Adressen schicken lassen; Fälligkeits-Alarme tun dasselbe zeitgesteuert. Die Demo wird so zum Absender für Spam (z. B. in fremde Discord-Kanäle) oder für Anfragen an Dritte, und zwar von der IP-Adresse des Demo-Servers. Ist in der Demo SMTP eingerichtet, gilt das ebenso für Mails an beliebige Adressen. - **Maßnahme:** Im Demo-Modus alle ausgehenden Kanäle aus: keine Alarm-Webhooks, kein Discord, keine Alarm- und Testmails, „Alarm testen“ und der SMTP-Test antworten mit einem Hinweis; die Oberfläche zeigt „In der Demo deaktiviert“. Server-Einstellungen (SMTP, Webhook-Freigabe, Backups) schreibgeschützt. Eingehende Webhooks (API-Keys) bleiben erlaubt, sie schreiben nur in die Demo-Datenbank. +- **Status: Behoben.** Im Demo-Modus verweigern die Sender für Mail, Discord und Webhooks (`errDemoOutgoing`), der Alarm-Ticker läuft nicht. `notInDemo` ([demo.go](../internal/api/demo.go)) beantwortet mit `403` „In der Demo deaktiviert“: Test-Alarm, SMTP-Test, Server-Einstellungen speichern, Backup starten, Konten anlegen/ändern/löschen, Passwort-Reset durch Admins, „Alle Sitzungen beenden“, einzelne Geräte abmelden, eigenes Konto löschen; Passwort und E-Mail sind im Profil gesperrt. Weil sich alle Besucher die Demo-Konten teilen, zeigt die Sitzungsliste nur das eigene Gerät. Die Oberfläche zeigt die Hinweise und deaktiviert die Schalter. Test: `TestDemoSendsNothingAndKeepsAccounts`. ### FLAW-34: SMTP-Passwort im Klartext - **Fundstelle:** `handleAdminGetSettings` ([handlers_admin.go](../internal/api/handlers_admin.go)) liefert `smtpPass` aus; Speicherung in `system_settings`. - **Problem:** Das Passwort erscheint in der Admin-Oberfläche, im Browser-Cache der Antwort (Offline-Lese-Cache) und in jedem Datenbank-Backup. - **Maßnahme:** Nie zurückliefern (nur „gesetzt“ anzeigen, leer = unverändert); optional per `SMTP_PASS`-Umgebungsvariable statt Datenbank. +- **Status: Teilweise behoben.** `GET`/`PUT /admin/settings` liefern `smtpPass` immer leer und `smtpPassSet`; ein leeres Feld behält das gespeicherte Passwort, ohne SMTP-Benutzer wird es entfernt. In der Datenbank und ihren Abzügen steht es weiter im Klartext (der Server braucht es zum Anmelden); wer das nicht will, setzt `SMTP_HOST`/`SMTP_PASS` per Umgebung und lässt die Felder im Admin-UI leer. Test: `TestSMTPPasswordNeverReturned`. ### FLAW-35: `?token=` beim Webhook-Ingest - **Fundstelle:** `handleWebhookIngest` in [handlers_integrations.go](../internal/api/handlers_integrations.go). - **Problem:** Neben `X-API-Key` wird das Token weiterhin aus der URL gelesen, auch ein Sitzungs-Token. Es landet in Proxy- und Zugriffslogs; eine Sitzung in der URL ist ein vollwertiges Login. - **Maßnahme:** In der URL nur noch API-Keys annehmen (für Geräte, die keine Header setzen können), keine Sitzungs-Tokens; Doku weist bereits auf den Header hin. +- **Status: Behoben.** `?token=` nimmt nur noch `mindsift_sec_…` an, sonst `401`. Test: `TestWebhookTokenInURLOnlyAPIKey`. ### FLAW-36: Kein allgemeines Größenlimit für Anfragen - **Fundstelle:** 46 Stellen `json.NewDecoder(r.Body)` ohne `http.MaxBytesReader` in `internal/api/` (Ausnahmen: Import, Themes, Sync, Passwort-Reset). - **Problem:** Ein angemeldeter Nutzer oder ein API-Key kann sehr große Körper schicken und Speicher binden. - **Maßnahme:** Middleware mit Standardlimit (z. B. 2 MB), größere Grenzen nur für Import/Sync. +- **Status: Behoben.** `bodyLimit` ([bodylimit.go](../internal/api/bodylimit.go)) begrenzt jeden Anfragekörper auf 2 MB; Notiz-Sync 8 MB, Importe 101 MB. Test: `TestRequestBodyLimited`. ### FLAW-37: Server-Backups überleben das gelöschte Konto - **Fundstelle:** `DeleteOwnAccount` / `handleAdminDeleteUser` und `backups/users//` ([backups.go](../internal/api/backups.go)). - **Problem:** Nach dem Löschen eines Kontos bleiben seine Backups bis zum Ablauf der Aufbewahrung liegen (Datenschutz, „Recht auf Löschung“). Datenbank-Abzüge enthalten das Konto ebenfalls bis zum Ablauf. - **Maßnahme:** Beim Löschen eines Kontos `backups/users//` mitlöschen; Aufbewahrung der DB-Abzüge in der Datenschutzerklärung nennen. +- **Status: Behoben.** Konto löschen (selbst oder durch Admins) entfernt `backups/users//` (`removeUserBackups`, nur dieser eine Ordner). Die Datenbank-Abzüge enthalten das Konto bis zum Ablauf der Aufbewahrung; das gehört in die Datenschutzerklärung der jeweiligen Instanz. Test: `TestDeletedAccountLosesServerBackups`. ### FLAW-38: Große Downloads brechen nach 30 s ab - **Fundstelle:** `WriteTimeout: 30 * time.Second` in [cmd/server/main.go](../cmd/server/main.go); betroffen: Markdown-Archiv, ZIP-Backup, Datenbank-Abzug (`http.ServeFile`). - **Problem:** Bei langsamer Verbindung oder großem Abzug endet der Download unvollständig (Streams heben die Grenze bereits pro Antwort auf, Downloads nicht). - **Maßnahme:** Für Download-Handler die Schreibfrist per `http.ResponseController` verlängern, wie bei SSE. +- **Status: Behoben.** `allowLongTransfer` gibt Backup-Downloads, Exporten (JSON/ZIP, Markdown-Archiv) und Import-Uploads 30 Minuten Lese- und Schreibzeit; vorher brachen auch große Importe nach 15 s Lesezeit ab. ### FLAW-39: Kein Rate-Limit außerhalb des Logins - **Fundstelle:** `handleRegister`, `handleForgotPassword` (Sperre nur je Konto), Einladungscode-Prüfung. diff --git a/frontend/src/features/settings/AdminSettingsView.tsx b/frontend/src/features/settings/AdminSettingsView.tsx index 65de29f..ddc9bc1 100644 --- a/frontend/src/features/settings/AdminSettingsView.tsx +++ b/frontend/src/features/settings/AdminSettingsView.tsx @@ -146,8 +146,14 @@ export const AdminSettingsView: React.FC = ({ } }; + // Read-only in the public demo (FLAW-33) + const [demoMode, setDemoMode] = useState(false); useEffect(() => { loadAll(); + api + .getAuthStatus() + .then((st) => setDemoMode(Boolean(st.demoMode))) + .catch(() => {}); }, []); // ============================================================================ @@ -519,6 +525,15 @@ export const AdminSettingsView: React.FC = ({ {/* Right Content Area */}
+ {demoMode && ( +
+ + + In der Demo deaktiviert: Server-Einstellungen, E-Mail und Konten lassen sich hier nur + ansehen. + +
+ )} {/* ================================================================= */} {/* SECTION 1: BENUTZER, ROLLEN & EINLADUNGEN */} {/* ================================================================= */} @@ -1013,300 +1028,305 @@ export const AdminSettingsView: React.FC = ({ {/* SECTION 3: SERVER & SHARING-ISOLATION (SERVER-ADMIN ONLY) */} {/* ================================================================= */} {section === 'server' && isServerAdmin && ( -
-
-

- Server-Grundeinstellungen & Benutzer-Sichtbarkeit -

-

- Als Server-Admin bestimmst du zentral, ob Nutzer komplett isoliert arbeiten, sich nur - innerhalb ihrer Organisation sehen oder ob alle Nutzer für alle sichtbar sind. -

-
- -
-
- -
- - setSettings((prev) => ({ ...prev, instanceName: e.target.value })) - } - className="flex-1 h-9 px-3 input-field" - /> - -
+
+
+
+

+ Server-Grundeinstellungen & Benutzer-Sichtbarkeit +

+

+ Als Server-Admin bestimmst du zentral, ob Nutzer komplett isoliert arbeiten, sich nur + innerhalb ihrer Organisation sehen oder ob alle Nutzer für alle sichtbar sind. +

- {/* Sharing Isolation Cards */} -
- - -
- {SHARING_MODES.map((m) => ( +
+
+ +
+ + setSettings((prev) => ({ ...prev, instanceName: e.target.value })) + } + className="flex-1 h-9 px-3 input-field" + /> - ))} -
- - {pendingMode && ( -
- - - Beim Wechsel zu „{SHARING_MODES.find((m) => m.id === pendingMode.mode)?.title.replace(/^\d\.\s*/, '')}“ - fallen {pendingMode.affected} Freigaben und Gruppen-Mitgliedschaften weg, die danach nicht mehr erlaubt sind. - - -
- )} -
- - handleSaveSystemSettings(patch)} /> - - {/* Alarm webhooks into the local network (SSRF protection) */} -
-
-

Alarm-Webhooks ins lokale Netz erlauben

-

- Standard: Discord- und Webhook-Alarme gehen nur an öffentliche https-Adressen. Eingeschaltet sind auch Ziele - im Heimnetz erlaubt (z. B. Home Assistant unter 192.168.x.x oder über Tailscale), dort auch per http. - Der Server selbst (localhost) und Cloud-Metadaten bleiben immer gesperrt. -

-
- handleSaveSystemSettings({ allowPrivateWebhooks })} - /> -
- - {/* Public Registration Toggle Switch */} -
-
-

Öffentliche Selbstregistrierung erlauben

-

- Wenn deaktiviert, können sich fremde Personen auf dem Login-Bildschirm nicht selbst registrieren. - Neue Konten erfordern dann zwingend einen Einladungscode oder müssen von einem Admin angelegt werden. -

-
- + {/* Sharing Isolation Cards */} +
+ + +
+ {SHARING_MODES.map((m) => ( + + ))} +
+ + {pendingMode && ( +
+ + + Beim Wechsel zu „{SHARING_MODES.find((m) => m.id === pendingMode.mode)?.title.replace(/^\d\.\s*/, '')}“ + fallen {pendingMode.affected} Freigaben und Gruppen-Mitgliedschaften weg, die danach nicht mehr erlaubt sind. + + + +
+ )} +
+ + handleSaveSystemSettings(patch)} /> + + {/* Alarm webhooks into the local network (SSRF protection) */} +
+
+

Alarm-Webhooks ins lokale Netz erlauben

+

+ Standard: Discord- und Webhook-Alarme gehen nur an öffentliche https-Adressen. Eingeschaltet sind auch Ziele + im Heimnetz erlaubt (z. B. Home Assistant unter 192.168.x.x oder über Tailscale), dort auch per http. + Der Server selbst (localhost) und Cloud-Metadaten bleiben immer gesperrt. +

+
handleSaveSystemSettings({ allowRegistration })} - title={settings.allowRegistration ? 'Selbstregistrierung deaktivieren' : 'Selbstregistrierung aktivieren'} + checked={Boolean(settings.allowPrivateWebhooks)} + onChange={(allowPrivateWebhooks) => handleSaveSystemSettings({ allowPrivateWebhooks })} />
+ + {/* Public Registration Toggle Switch */} +
+
+

Öffentliche Selbstregistrierung erlauben

+

+ Wenn deaktiviert, können sich fremde Personen auf dem Login-Bildschirm nicht selbst registrieren. + Neue Konten erfordern dann zwingend einen Einladungscode oder müssen von einem Admin angelegt werden. +

+
+ +
+ + handleSaveSystemSettings({ allowRegistration })} + title={settings.allowRegistration ? 'Selbstregistrierung deaktivieren' : 'Selbstregistrierung aktivieren'} + /> +
+
-
+
)} {/* ================================================================= */} {/* SECTION 4: E-MAIL / SMTP-SERVER (SERVER-ADMIN ONLY) */} {/* ================================================================= */} {section === 'smtp' && isServerAdmin && ( -
-
-
-

- E-Mail / SMTP-Server für Aufgaben-Alarme -

-

- Konfiguriere den Postausgangsserver per Docker-Compose Environment-Variablen oder - direkt hier über die Oberfläche (wird persistent in der SQLite-Datenbank gespeichert). -

+
+
+
+
+

+ E-Mail / SMTP-Server für Aufgaben-Alarme +

+

+ Konfiguriere den Postausgangsserver per Docker-Compose Environment-Variablen oder + direkt hier über die Oberfläche (wird persistent in der SQLite-Datenbank gespeichert). +

+
+ + {settings.smtpConfiguredViaEnv && ( + + Docker Env erkannt + + )}
- {settings.smtpConfiguredViaEnv && ( - - Docker Env erkannt - - )} -
- -
-
- - - setSettings((prev) => ({ ...prev, smtpHost: e.target.value })) - } - placeholder="z.B. smtp.example.com oder mail.deinedomain.de" - className="w-full h-9 px-3 input-field font-mono" - /> -
- -
- - - // Digits only; an empty field stays empty while typing (saved as 587) - setSettings((prev) => ({ ...prev, smtpPort: e.target.value.replace(/\D/g, '').slice(0, 5) })) - } - placeholder="587" - className="w-full h-9 px-3 input-field font-mono" - /> -
- -
- - - setSettings((prev) => ({ ...prev, smtpUser: e.target.value })) - } - placeholder="alerts@deinedomain.de" - className="w-full h-9 px-3 input-field font-mono" - /> -
- -
- - - setSettings((prev) => ({ ...prev, smtpPass: e.target.value })) - } - placeholder="••••••••••••" - className="w-full h-9 px-3 input-field font-mono" - /> -
- -
- - - setSettings((prev) => ({ ...prev, smtpFrom: e.target.value })) - } - placeholder="MindSift " - className="w-full h-9 px-3 input-field" - /> -
- -
-
-
-

Verschlüsselung (`SMTP_TLS`)

-

- Port 465: TLS ab Verbindungsbeginn, sonst STARTTLS. Ist es an, wird ohne Verschlüsselung nicht gesendet. -

-
- setSettings((prev) => ({ ...prev, smtpTls }))} +
+
+ + + setSettings((prev) => ({ ...prev, smtpHost: e.target.value })) + } + placeholder="z.B. smtp.example.com oder mail.deinedomain.de" + className="w-full h-9 px-3 input-field font-mono" />
-
-
-

Zertifikatsprüfung deaktivieren (`SMTP_SKIP_VERIFY`)

-

- Nur für interne Mailserver mit selbstsigniertem Zertifikat oder abweichendem Namen. Die Verbindung - bleibt verschlüsselt, der Server wird aber nicht mehr geprüft. -

-
- setSettings((prev) => ({ ...prev, smtpSkipVerify }))} + +
+ + + // Digits only; an empty field stays empty while typing (saved as 587) + setSettings((prev) => ({ ...prev, smtpPort: e.target.value.replace(/\D/g, '').slice(0, 5) })) + } + placeholder="587" + className="w-full h-9 px-3 input-field font-mono" />
+ +
+ + + setSettings((prev) => ({ ...prev, smtpUser: e.target.value })) + } + placeholder="alerts@deinedomain.de" + className="w-full h-9 px-3 input-field font-mono" + /> +
+ +
+ + + setSettings((prev) => ({ ...prev, smtpPass: e.target.value })) + } + placeholder={settings.smtpPassSet ? 'Gespeichert – leer lassen für keine Änderung' : '••••••••••••'} + autoComplete="new-password" + className="w-full h-9 px-3 input-field font-mono" + /> +
+ +
+ + + setSettings((prev) => ({ ...prev, smtpFrom: e.target.value })) + } + placeholder="MindSift " + className="w-full h-9 px-3 input-field" + /> +
+ +
+
+
+

Verschlüsselung (`SMTP_TLS`)

+

+ Port 465: TLS ab Verbindungsbeginn, sonst STARTTLS. Ist es an, wird ohne Verschlüsselung nicht gesendet. +

+
+ setSettings((prev) => ({ ...prev, smtpTls }))} + /> +
+
+
+

Zertifikatsprüfung deaktivieren (`SMTP_SKIP_VERIFY`)

+

+ Nur für interne Mailserver mit selbstsigniertem Zertifikat oder abweichendem Namen. Die Verbindung + bleibt verschlüsselt, der Server wird aber nicht mehr geprüft. +

+
+ setSettings((prev) => ({ ...prev, smtpSkipVerify }))} + /> +
+
-
-
- -
- - {/* Test SMTP Delivery */} -
-

SMTP-Verbindung testen

-

- Sendet sofort eine Test-E-Mail mit den oben eingetragenen Daten. -

-
- setTestSmtpEmail(e.target.value)} - placeholder="empfaenger@beispiel.de" - className="flex-1 h-8 px-3 input-field" - /> +
+ + {/* Test SMTP Delivery */} +
+

SMTP-Verbindung testen

+

+ Sendet sofort eine Test-E-Mail mit den oben eingetragenen Daten. +

+
+ setTestSmtpEmail(e.target.value)} + placeholder="empfaenger@beispiel.de" + className="flex-1 h-8 px-3 input-field" + /> + +
+
-
+
)}
diff --git a/frontend/src/features/settings/UserSettingsView.tsx b/frontend/src/features/settings/UserSettingsView.tsx index 7b4ca83..22ae1a9 100644 --- a/frontend/src/features/settings/UserSettingsView.tsx +++ b/frontend/src/features/settings/UserSettingsView.tsx @@ -97,8 +97,16 @@ export const UserSettingsView: React.FC = ({ } }; const [showAbout, setShowAbout] = useState(false); + // The public demo sends nothing and keeps the shared accounts intact (FLAW-33) + const [demoMode, setDemoMode] = useState(false); useEffect(() => { - api.getAuthStatus().then((st) => setLegal(st.legal)).catch(() => {}); + api + .getAuthStatus() + .then((st) => { + setLegal(st.legal); + setDemoMode(Boolean(st.demoMode)); + }) + .catch(() => {}); }, []); // --- Custom Theme Editor State --- @@ -628,7 +636,9 @@ export const UserSettingsView: React.FC = ({ value={email} onChange={(e) => setEmail(e.target.value)} required - className="w-full h-9 px-3 input-field" + disabled={demoMode} + title={demoMode ? 'In der Demo deaktiviert' : undefined} + className="w-full h-9 px-3 input-field disabled:opacity-60" />
@@ -641,9 +651,10 @@ export const UserSettingsView: React.FC = ({ type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} - placeholder="Mindestens 8 Zeichen" + placeholder={demoMode ? 'In der Demo deaktiviert' : 'Mindestens 8 Zeichen'} autoComplete="new-password" - className="w-full h-9 px-3 input-field" + disabled={demoMode} + className="w-full h-9 px-3 input-field disabled:opacity-60" />
@@ -693,8 +704,9 @@ export const UserSettingsView: React.FC = ({ @@ -735,7 +747,8 @@ export const UserSettingsView: React.FC = ({
+ {demoMode && ( +
+ + In der Demo deaktiviert: Alarme und Tests werden nicht verschickt. +
+ )} + {alarmMsg && (
= ({