diff --git a/.env.example b/.env.example index ebc9827..4cccaf2 100644 --- a/.env.example +++ b/.env.example @@ -27,6 +27,8 @@ TZ=Europe/Berlin # open jeder, sofort aktiv # REGISTRATION_MODE=approval # REGISTRATION_DOMAINS=firma.de, familie.de +# Neue Konten ohne Einladung bestätigen zuerst ihre E-Mail-Adresse per Link (braucht SMTP und BASE_URL): +# REGISTRATION_VERIFY_EMAIL=true # Alt: true entspricht REGISTRATION_MODE=open; false lässt die Wahl dem Admin. ALLOW_REGISTRATION=false diff --git a/CHANGELOG.md b/CHANGELOG.md index f81f697..af59e21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V ## [Unreleased] +## [0.24.0] - 2026-10-31 + +### Added +- **E-Mail-Adresse bestätigen:** Neue Einstellung (Administration → Server & Isolation, oder `REGISTRATION_VERIFY_EMAIL=true`). Wer sich ohne Einladung registriert, bekommt einen Link per E-Mail und ist erst danach aktiv; in den Freigabe-Modi geht das Konto danach an die Admins, die auch erst dann benachrichtigt werden. Beim Anmelden vor der Bestätigung gibt es „Link erneut senden“. Unbestätigte Konten werden nach 48 Stunden gelöscht, die Adresse ist dann wieder frei. Wirkt nur mit Mail-Server und `BASE_URL`; Einladungen brauchen keine Bestätigung. + +### Changed +- Einladung erzeugen: Die E-Mail-Adresse steht vorne und hat mehr Platz, „Wie oft verwendbar“ ist schmal. + ## [0.23.0] - 2026-10-31 ### Added @@ -484,7 +492,8 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V - Geteilte Listen (Einkaufs-, Pack- und Checklisten) mit `#Kategorie`-Gruppierung, Mengenangaben und Echtzeit-Abhaken. - Benutzer-, Gruppen- und Admin-Verwaltung (Ersteinrichtungs-Assistent, Einladungscodes, Profilbilder, persönliche API-Keys & Webhook-Ingest `/api/v1/webhooks/ingest`, JSON-Datenexport) sowie CSS-Theme-Engine mit 4 Themes (`Dark`, `Light`, `High Contrast`, `Soft`) und PWA-Unterstützung. -[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.23.0...dev +[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.24.0...dev +[0.24.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.23.0...v0.24.0 [0.23.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.22.2...v0.23.0 [0.22.2]: https://git.dsit.me/sandrodach/MindSift/compare/v0.22.1...v0.22.2 [0.22.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.22.0...v0.22.1 diff --git a/Doku/api/rest.md b/Doku/api/rest.md index dbefd8d..6b1f75e 100644 --- a/Doku/api/rest.md +++ b/Doku/api/rest.md @@ -11,7 +11,9 @@ Basis: `/api/v1`. Antworten sind JSON; Fehler haben die Form `{"error": "Text"}` | GET | `/legal/{imprint\|privacy}` | Hinterlegter Markdown-Text (`content`), ohne Anmeldung; `404`, wenn keiner gesetzt ist | | POST | `/share-target` | PWA Web Share Target Fallback: Weiterleitung auf `/` (`303 See Other`), falls Service Worker noch inaktiv | | POST | `/auth/setup` | Erstes Administratorkonto (`email`, `password`, `displayName`, `instanceName`, `allowRegistration`, `sharingMode`, `seedSampleData`); nur solange es keine Konten gibt | -| POST | `/auth/register` | Registrierung (`email`, `password`, `displayName`, `inviteCode`); mit Code immer, sonst je nach Registrierungsmodus. `201` mit Token oder `202 {"pending":true,"message"}`, wenn ein Admin erst freigeben muss (Login bis dahin `403`); `429` nach 20 Versuchen pro Adresse in 15 Minuten oder bei mehr als 50 wartenden Konten | +| POST | `/auth/verify-email` | Link aus der Bestätigungs-E-Mail (`token`): schaltet das Konto frei oder reicht es an die Freigabe weiter (`{"verified","pending","message"}`) | +| POST | `/auth/resend-verification` | Bestätigungslink erneut schicken (`email`); antwortet immer gleich, höchstens alle 2 Minuten pro Konto | +| POST | `/auth/register` | Registrierung (`email`, `password`, `displayName`, `inviteCode`); mit Code immer, sonst je nach Registrierungsmodus. `201` mit Token oder `202 {"pending":true,"verify","message"}`, wenn erst die E-Mail-Adresse bestätigt (`verify`) und/oder ein Admin freigeben muss (Login bis dahin `403`); `429` nach 20 Versuchen pro Adresse in 15 Minuten oder bei mehr als 50 wartenden Konten | | POST | `/auth/login` | Anmeldung (`login`/`email`/`username`, `password`) → `{token, user}` | | 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 | diff --git a/Doku/architecture.md b/Doku/architecture.md index 2319998..49d1c6a 100644 --- a/Doku/architecture.md +++ b/Doku/architecture.md @@ -64,6 +64,7 @@ Das Frontend liegt per `go:embed` im Binary ([cmd/server/main.go](../cmd/server/ | `BASE_URL` | `http://localhost:` | Basis-URL der Instanz; explizit gesetzt Voraussetzung für das Passwort-Zurücksetzen per Mail | | `TZ` | `Europe/Berlin` | Zeitzone für Fälligkeiten und Alarme; die Zeitzonendaten sind ins Binary eingebettet (`time/tzdata`) | | `REGISTRATION_MODE`, `REGISTRATION_DOMAINS` | leer | Registrierung ohne Einladung (`closed`, `approval`, `domain`, `domain+approval`, `open`) und erlaubte E-Mail-Endungen. Gesetzt gewinnen sie über `system_settings` und sind im Admin-UI gesperrt (`registrationPolicy` in `internal/api/registration.go`). Wartende Konten: `is_active = 0` plus Zeile in `pending_registrations` (Migration 10) | +| `REGISTRATION_VERIFY_EMAIL` | leer | E-Mail-Bestätigung neuer Konten ohne Einladung (nur mit SMTP und `BASE_URL`). Token wie beim Passwort-Link nur als SHA-256 in `email_verifications` (Migration 11), 48 Stunden gültig; danach löscht die Verdichtung das unbestätigte Konto. Bei Freigabe-Modi erfahren Admins erst nach der Bestätigung davon | | `ALLOW_REGISTRATION` | `false` | `true` = `REGISTRATION_MODE=open`; `false` (Standard der Vorlagen) lässt die Wahl dem Admin-UI | | `LOG_LEVEL` / `DEBUG` | `info` / `false` | `debug`, `info`, `warn`, `error`; `DEBUG=true` erzwingt `debug` | | `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`) | diff --git a/Doku/plans/registrierung-und-login.md b/Doku/plans/registrierung-und-login.md index 2c0d73c..052ab22 100644 --- a/Doku/plans/registrierung-und-login.md +++ b/Doku/plans/registrierung-und-login.md @@ -1,6 +1,6 @@ # Plan: Registrierung und Anmeldung (Entwurf) -Stand 2026-10-31. **Umgesetzt in v0.23.0:** Abschnitte 1 (Registrierungsmodus und Freigabe), 2 (Umgebungsvariablen) und 3 (Einladungslinks, per E-Mail einladen, mehrfach verwendbare Links). Offen: E-Mail-Bestätigung per Link, OpenID Connect. Ziel: ein Zwischending zwischen „jeder darf sich registrieren“ und „für jede Person einen Einladungscode erzeugen“, dazu bequemere Anmeldung. +Stand 2026-10-31. **Umgesetzt in v0.23.0:** Abschnitte 1 (Registrierungsmodus und Freigabe), 2 (Umgebungsvariablen) und 3 (Einladungslinks, per E-Mail einladen, mehrfach verwendbare Links). E-Mail-Bestätigung per Link in v0.24.0. Offen: OpenID Connect. Ziel: ein Zwischending zwischen „jeder darf sich registrieren“ und „für jede Person einen Einladungscode erzeugen“, dazu bequemere Anmeldung. ## Heute diff --git a/README.md b/README.md index c5877dc..60a1d7a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Self-Hosted](https://img.shields.io/badge/100%25-Self--Hosted-2563eb?style=flat-square)](#) [![Docker](https://img.shields.io/badge/Deployment-Single%20Container-2496ed?style=flat-square&logo=docker&logoColor=white)](#-schnellstart) [![SQLite](https://img.shields.io/badge/Daten-SQLite-003b57?style=flat-square&logo=sqlite&logoColor=white)](#-deine-daten-gehören-dir) -[![Version](https://img.shields.io/badge/Version-v0.23.0-16a34a?style=flat-square)](CHANGELOG.md) +[![Version](https://img.shields.io/badge/Version-v0.24.0-16a34a?style=flat-square)](CHANGELOG.md) **MindSift** ist eine selbst gehostete Schaltstelle für spontane Gedanken, Aufgaben mit Kanban-Board, hierarchische Outliner-Notizen, die mehrere Personen gleichzeitig bearbeiten können, und gemeinsame Listen. Alles läuft als ein einzelner Container mit eingebetteter SQLite-Datenbank, auf deinem eigenen Server. @@ -106,7 +106,7 @@ Technik dahinter: [Doku/modules/collaboration.md](Doku/modules/collaboration.md) ### 🏢 Benutzer, Gruppen, Administration -* Geführte Ersteinrichtung, Rollen *Admin* und *Mitglied*. Einladungen als Code oder Link (auch für mehrere Personen, auf Wunsch per E-Mail verschickt). Registrierung ohne Einladung wahlweise geschlossen, mit Freigabe durch einen Admin, nur für bestimmte E-Mail-Endungen oder offen – auch fest per Umgebungsvariable. +* Geführte Ersteinrichtung, Rollen *Admin* und *Mitglied*. Einladungen als Code oder Link (auch für mehrere Personen, auf Wunsch per E-Mail verschickt). Registrierung ohne Einladung wahlweise geschlossen, mit Freigabe durch einen Admin, nur für bestimmte E-Mail-Endungen oder offen, auf Wunsch mit Bestätigung der E-Mail-Adresse per Link – auch fest per Umgebungsvariable. * Gruppen mit Gruppen-Admins für Freigaben. * Admin-Bereich: Benutzer, Einladungen, Instanzname, SMTP, Freigabe-Modus, Sicherungen, Spracherkennung. @@ -193,6 +193,7 @@ Rufe `http://localhost:8080` auf. Beim ersten Start führt ein Assistent durch d | `TZ` | `Europe/Berlin` | Zeitzone für Fälligkeiten und Alarme (ganztägige Aufgaben melden sich um 9:00) | | `REGISTRATION_MODE` | leer (Admin-UI entscheidet) | Registrierung ohne Einladung: `closed`, `approval` (Admin gibt frei), `domain` (nur erlaubte E-Mail-Endungen), `domain+approval` (Endungen sofort, andere nach Freigabe), `open`. Gesetzt ist es im Admin-UI gesperrt | | `REGISTRATION_DOMAINS` | leer | Erlaubte E-Mail-Endungen für die `domain`-Modi, z. B. `firma.de, *.firma.de`; gesetzt im Admin-UI gesperrt | +| `REGISTRATION_VERIFY_EMAIL` | leer (Admin-UI entscheidet) | `true`: Wer sich ohne Einladung registriert, bestätigt zuerst seine E-Mail-Adresse per Link (braucht SMTP und `BASE_URL`); unbestätigte Konten werden nach 48 Stunden gelöscht | | `ALLOW_REGISTRATION` | `false` | `true` = wie `REGISTRATION_MODE=open` (gesperrt); `false` lässt die Wahl dem Admin-UI | | `LOG_LEVEL` / `DEBUG` | `info` / `false` | Logging | | `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASS`, `SMTP_FROM`, `SMTP_TLS` | leer | E-Mail-Alarme (auch im Admin-UI) | diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d326fdf..99f9442 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "mindsift-frontend", - "version": "0.23.0", + "version": "0.24.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mindsift-frontend", - "version": "0.23.0", + "version": "0.24.0", "license": "Unlicense", "dependencies": { "@fontsource-variable/inter": "5.3.0", diff --git a/frontend/package.json b/frontend/package.json index d5f1070..586169b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "mindsift-frontend", "private": true, - "version": "0.23.0", + "version": "0.24.0", "license": "Unlicense", "type": "module", "scripts": { diff --git a/frontend/src/features/auth/SetupAndAuthView.tsx b/frontend/src/features/auth/SetupAndAuthView.tsx index a74b58c..ab43865 100644 --- a/frontend/src/features/auth/SetupAndAuthView.tsx +++ b/frontend/src/features/auth/SetupAndAuthView.tsx @@ -52,6 +52,34 @@ export const SetupAndAuthView: React.FC = ({ const [password, setPassword] = useState(''); const [displayName, setDisplayName] = useState(''); const [inviteCode, setInviteCode] = useState(linkInviteCode); + // Link from the confirmation mail (?verifyToken=…): confirm right away and say what happens next + useEffect(() => { + const url = new URL(window.location.href); + const token = url.searchParams.get('verifyToken'); + if (!token) return; + url.searchParams.delete('verifyToken'); + window.history.replaceState({}, '', url.pathname + url.search + url.hash); + setAuthTab('login'); + api + .verifyEmail(token) + .then((res) => setNotice(res.message)) + .catch((err) => setError(err.message || 'Der Bestätigungslink ist ungültig oder abgelaufen.')); + }, []); + // Login refused because the address is not confirmed: offer to send the link again + const [resendState, setResendState] = useState<'idle' | 'sending' | 'sent'>('idle'); + const resendVerification = async () => { + setResendState('sending'); + try { + await api.resendVerification(email.trim()); + setResendState('sent'); + setError(null); + setNotice('Falls die Adresse noch auf Bestätigung wartet, haben wir dir den Link erneut geschickt.'); + } catch (err: any) { + setResendState('idle'); + setError(err.message || 'Senden fehlgeschlagen'); + } + }; + // The code is in the form now; it should not stay in the address bar (history, screenshots) useEffect(() => { if (!linkInviteCode) return; @@ -65,6 +93,8 @@ export const SetupAndAuthView: React.FC = ({ switch (authStatus.registrationMode) { case 'approval': return 'Nach der Registrierung gibt ein Administrator dein Konto frei.'; + case 'open': + return authStatus.emailVerification ? 'Du bekommst eine E-Mail, um deine Adresse zu bestätigen.' : ''; case 'domain': return domains ? `Registrierung mit E-Mail-Adressen von ${domains}.` : ''; case 'domain+approval': @@ -74,6 +104,10 @@ export const SetupAndAuthView: React.FC = ({ } return ''; })(); + const registrationHintFull = + registrationHint && authStatus.emailVerification && authStatus.registrationMode !== 'open' + ? `${registrationHint} Vorher bestätigst du deine Adresse per E-Mail.` + : registrationHint; const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); @@ -512,7 +546,17 @@ export const SetupAndAuthView: React.FC = ({ {error && (
- {error} + {error} + {/bestätige zuerst deine E-Mail/i.test(error) && email.trim() && ( + + )}
)} {notice && !error && ( @@ -718,8 +762,8 @@ export const SetupAndAuthView: React.FC = ({ {(authTab === 'invite' || authTab === 'register') && (
- {authTab === 'register' && registrationHint && ( -

{registrationHint}

+ {authTab === 'register' && registrationHintFull && ( +

{registrationHintFull}

)} {authTab === 'invite' && (
diff --git a/frontend/src/features/settings/AdminSettingsView.tsx b/frontend/src/features/settings/AdminSettingsView.tsx index bf6d066..cfdd4e4 100644 --- a/frontend/src/features/settings/AdminSettingsView.tsx +++ b/frontend/src/features/settings/AdminSettingsView.tsx @@ -141,6 +141,9 @@ export const AdminSettingsView: React.FC = ({ registrationMode: sys.registrationMode || (sys.allowRegistration ? 'open' : 'closed'), registrationDomains: sys.registrationDomains || '', registrationConfiguredViaEnv: Boolean(sys.registrationConfiguredViaEnv), + registrationVerifyEmail: Boolean(sys.registrationVerifyEmail), + registrationVerifyConfiguredViaEnv: Boolean(sys.registrationVerifyConfiguredViaEnv), + emailLinksAvailable: Boolean(sys.emailLinksAvailable), instanceName: sys.instanceName || 'MindSift', smtpHost: sys.smtpHost || '', smtpPort: sys.smtpPort ? String(sys.smtpPort) : '587', @@ -698,20 +701,12 @@ export const AdminSettingsView: React.FC = ({ -
-
- - setInviteMaxUses(Math.max(1, Math.min(1000, parseInt(e.target.value, 10) || 1)))} - title="1 = ein Code für eine Person; mehr = ein Link für eine ganze Gruppe" - className="w-full min-w-0 h-8 px-2.5 input-field" - /> -
- + {/* E-mail gets the room, "how often" only needs a few digits */} +
- + + setInviteMaxUses(Math.max(1, Math.min(1000, parseInt(e.target.value, 10) || 1)))} + title="1 = ein Code für eine Person; mehr = ein Link für eine ganze Gruppe" + className="w-full min-w-0 h-8 px-2.5 input-field" + /> +
+ +
+