auth.ValidatePassword checks every way a password is set: setup, registration, profile, admin create and reset, reset by mail. Same rule in the browser before sending (lib/password.ts), with our own German message instead of the browser popup. Existing shorter passwords still log in. Profile and admin reset no longer trim the password, which made passwords with surrounding spaces unusable for login. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
205 lines
15 KiB
Markdown
205 lines
15 KiB
Markdown
# REST-API
|
||
|
||
Basis: `/api/v1`. Antworten sind JSON; Fehler haben die Form `{"error": "Text"}`. Authentifizierung über `Authorization: Bearer <JWT>`. **API-Keys** (`Bearer mindsift_sec_…` bzw. Header `X-API-Key`) haben vorerst genau ein Recht: Daten einliefern über `POST /inbox` (Gedanke oder per `target` direkt Aufgabe) und `POST /webhooks/ingest`; alle anderen Endpunkte antworten mit `403`. Feinere Berechtigungen (Scopes) folgen. Zeitstempel sind ISO-8601 (UTC). Quelle der Wahrheit sind die Routen in [server.go](../../internal/api/server.go).
|
||
|
||
## Öffentlich
|
||
|
||
| Methode | Pfad | Zweck |
|
||
|---|---|---|
|
||
| GET | `/health` | Lebenszeichen |
|
||
| GET | `/auth/status` | `needsSetup`, `allowRegistration`, `instanceName`, `demoMode`, `sharingMode`, `legal` (`imprintUrl`, `hasImprintContent`, `privacyUrl`, `hasPrivacyContent`) |
|
||
| GET | `/legal/{imprint\|privacy}` | Hinterlegter Markdown-Text (`content`), ohne Anmeldung; `404`, wenn keiner gesetzt ist |
|
||
| 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`); nur mit Code oder wenn freigegeben |
|
||
| 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 |
|
||
| 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` | Aufgabe per Webhook anlegen, Token als `?token=`, `X-API-Key` oder Bearer. Body: `title`/`content`/`text`/`message`, `priority`, `source`, `tags`, `dueDate`; Klartext-Body wird als Inhalt genommen |
|
||
| GET | `/ws`, `/ws/spaces/{id}`, `/ws/lists/{id}` | WebSocket, siehe [websocket.md](websocket.md); Token als `?token=` nötig |
|
||
| GET | `/events` | SSE-Fallback; Token als `?token=` nötig |
|
||
|
||
## Konto und Allgemeines (angemeldet)
|
||
|
||
| Methode | Pfad | Zweck |
|
||
|---|---|---|
|
||
| GET | `/auth/me` | Benutzer und Kennzahlen |
|
||
| POST | `/events/rooms` | SSE-Raum beitreten/verlassen (`sseId`, `room`, `action`); nur eigene `sseId`, nur Räume mit Lesezugriff |
|
||
| POST | `/auth/logout` | Token sperren |
|
||
| POST | `/auth/revoke-all` | Alle Sitzungen beenden (`token_version` +1) |
|
||
| PATCH | `/auth/settings` | `displayName`, `email`, `password`, `avatarUrl`, `alarmChannels`, `alarmEmail`, `discordWebhookUrl`, `customWebhookUrl`, `notifyBeforeMinutes` |
|
||
| POST | `/auth/alarms/test` | Test-Alarm (`channel`: `email`/`discord`/`webhook`, optional `target`) |
|
||
| GET | `/users/directory`, `/collaborators` | Personen, die man sehen und mit denen man teilen kann (je nach `sharingMode`, [sharing-and-permissions.md](../sharing-and-permissions.md)) |
|
||
| GET | `/stats` | Zähler fürs Dashboard |
|
||
| GET | `/search?q=` | Globale FTS5-Suche in zugänglichen Notizen (Titel & Blöcke), Aufgaben und Listen → `{thoughts, documents, listItems, hits}` |
|
||
| GET | `/export` | Datenexport als JSON; mit `?format=zip` dieselben Daten als `mindsift-backup.json` in einer ZIP (so lädt die Oberfläche das Backup) |
|
||
| GET | `/export/markdown` | Obsidian-kompatibles ZIP (`mindsift-vault-DATUM.zip`), direkt gestreamt: Notizen als `.md` mit YAML-Frontmatter in der Ordnerstruktur des Nutzers (inkl. geteilter), `Aufgaben/<Liste>.md` im Format des Tasks-Plugins (`📅`, `⏫`/`🔺`), `Listen/<Liste>.md` |
|
||
| GET/POST | `/themes` | Eigene CSS-Themes; anlegen oder mit `id` ändern (`name`, `css`, max. 200 KB) |
|
||
| DELETE | `/themes/{id}` | Eigenes Theme löschen |
|
||
| POST | `/import/mindsift`, `/import/obsidian` | Import (Multipart-Feld `file`, max. 100 MB): MindSift-JSON/-Archiv bzw. Obsidian-Vault-ZIP; `?preview=1` liefert nur die Zusammenfassung (`folders`, `notes`, `taskLists`, `tasks`, `lists`, `listItems`, `inbox`, `skipped`) |
|
||
| — | `/import/mindsift?mode=restore` | Wiederherstellen: löscht vor dem Import alle eigenen Notizen, Ordner, Aufgaben, Gedanken und Listen (erst nachdem die Datei vollständig gelesen und geprüft ist) |
|
||
| DELETE | `/user/data` | Alle eigenen Daten löschen, Konto bleibt (`{"password"}` nötig) |
|
||
| DELETE | `/user/account` | Eigenes Konto mit allen Daten löschen (`{"password"}` nötig); `403` für den letzten Server-Admin. `/auth/me` liefert dafür `isLastAdmin` |
|
||
| GET/POST/DELETE | `/keys`, `/keys/{id}` | API-Keys verwalten (Klartext nur bei Erstellung) |
|
||
|
||
### Globale Suche (`GET /api/v1/search?q=`)
|
||
|
||
Führt eine Volltextsuche über SQLite FTS5 mit BM25-Relevanz-Ranking aus.
|
||
- Durchsucht Aufgaben/Gedanken (Inhalt, Notizen, Unteraufgaben), Notizen (Titel sowie Inhalte aktiver Blöcke) und Listeneinträge.
|
||
- Beachtet strikt alle Zugriffsregeln: Nur eigene Gedanken/Aufgaben und solche in zugänglichen Aufgabenlisten; nur eigene und geteilte Notizen (direkt, über Gruppen oder freigegebene Ordner); nur eigene und geteilte Listen.
|
||
- Titel-Treffer werden höher gewichtet als Treffer im Fließtext.
|
||
- Liefert rückwärtskompatible Collections (`thoughts`, `documents`, `listItems`) sowie strukturierte `hits` mit Snippet und `<mark>`-Hervorhebung:
|
||
```json
|
||
{
|
||
"thoughts": [...],
|
||
"documents": [...],
|
||
"listItems": [...],
|
||
"hits": [
|
||
{
|
||
"type": "document",
|
||
"id": "blk_123",
|
||
"title": "Projektplanung",
|
||
"snippet": "... wichtige Notiz zum <mark>Release</mark> ...",
|
||
"docId": "doc_abc",
|
||
"blockId": "blk_123"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
## Gedanken, Triage, Aufgaben
|
||
|
||
| Methode | Pfad | Zweck |
|
||
|---|---|---|
|
||
| POST | `/inbox` | Schnellerfassung. Body: `content`, `priority`, `source`, `dueDate`, `target` (`inbox`/`task`/`list_item`/`document`), `targetListId`, `taskListId` (snake_case-Varianten werden akzeptiert) |
|
||
| GET | `/thoughts?status=` | Gedanken (`inbox`, `kept`, `converted`, `discarded`, `all`) |
|
||
| GET/PATCH/DELETE | `/thoughts/{id}` | Einzelabruf; Teiländerung (`content`, `isCompleted`, `dueDate`, `priority`, `taskListId`, `kanbanStatus`, `notes`, `subtasks`, `tags`, `assignees`, `recurrence` = `daily`/`weekly`/`monthly`/`yearly`/leer); Löschen. `assignees` ersetzt die Zuweisungen; nur Personen mit Zugriff auf die Liste, sonst `403` |
|
||
| DELETE | `/thoughts/discarded/purge` | Papierkorb leeren |
|
||
| GET | `/triage/deck` | Posteingang (max. 100) |
|
||
| PATCH | `/triage/{id}` | `action`: `keep`/`discard`/`restore`; mit `convertTo`: `task`/`document`/`list_item` |
|
||
| GET | `/tasks?taskListId=` | Aufgaben (optional einer Liste) |
|
||
| POST | `/tasks/reorder`, `/task-lists/{id}/tasks/reorder` | Reihenfolge/Spalte ändern (`items[{id, sortOrder, kanbanStatus}]`) |
|
||
| POST | `/tasks/{id}/unlink` | Verknüpfung zu Notiz-Block lösen |
|
||
|
||
## Aufgabenlisten
|
||
|
||
| Methode | Pfad | Zweck |
|
||
|---|---|---|
|
||
| GET/POST | `/task-lists` | Listen (`permission` je Liste: `owner`/`editor`/`viewer`); anlegen (`title`, `icon`, `color`, `teamId`) |
|
||
| PUT/DELETE | `/task-lists/{id}` | Ändern (`title`, `icon`, `color`, `teamId`, `columns`) – nicht für Leser; Löschen nur Eigentümer |
|
||
| GET | `/task-lists/{id}/shares` | Freigaben |
|
||
| POST | `/task-lists/{id}/shares` | Nur Eigentümer. Body: `targetType` (`team`/`user`), `targetId`, `permission` (`editor`/`viewer`, Standard `editor`). Erneutes Senden für dasselbe Ziel ändert die Stufe |
|
||
| DELETE | `/task-lists/{id}/shares/{shareId}` | Nur Eigentümer |
|
||
|
||
Schreibzugriffe von Lesern auf Listen oder deren Aufgaben liefern `403` („Keine Schreibrechte für diese Aufgabenliste (nur Lesen)“).
|
||
|
||
## Workspace
|
||
|
||
| Methode | Pfad | Zweck |
|
||
|---|---|---|
|
||
| GET/POST/DELETE | `/folders` | Ordnerliste, anlegen (`path`), löschen |
|
||
| POST | `/folders/move` | `sourcePath`, `targetParentPath` |
|
||
| GET/POST | `/workspace/folders/shared` | Geteilte Ordner; anlegen (`name`, `mountPath`) |
|
||
| GET/DELETE | `/workspace/folders/shared/{id}` | Abruf; Löschen |
|
||
| PUT | `/workspace/folders/shared/{id}/mount` | Eigenen Einhängepfad ändern (`mountPath`) |
|
||
| GET/POST/DELETE | `/workspace/folders/shared/{id}/shares[/{shareId}]` | Freigaben (`permission`: `read`/`write`) |
|
||
| GET | `/workspace/tags`, `/workspace/tags/{tag}` | Tag-Übersicht und -Details |
|
||
| PUT | `/workspace/tags/{tag}/note` | Tag-Seite speichern (`content`) |
|
||
| GET/POST | `/documents` | Notizen auflisten; anlegen (`title`, `icon`, `folderPath`, `blocks`, `tags`, `isPinned`) |
|
||
| GET/PUT/DELETE | `/documents/{id}` | Abruf; Ändern von `title`, `icon`, `folderPath`, `isPinned` (Blöcke → `400`, sie laufen über `/docs/{id}/sync`); Löschen = in den Papierkorb (Eigentümer) bzw. Freigabe verlassen (andere) |
|
||
| GET/DELETE | `/documents/trash` | Papierkorb auflisten (`id`, `title`, `icon`, `folderPath`, `deletedAt`, `isOwner`) bzw. leeren |
|
||
| GET | `/documents/{id}/backlinks` | Notizen mit `[[Titel]]`-Link auf diese Notiz (nur sichtbare, ohne Papierkorb): `docId`, `title`, `icon`, `folderPath`, bis zu 5 `blocks` (`id`, `content`), `more` |
|
||
| POST | `/documents/{id}/restore` | Aus dem Papierkorb zurückholen (fehlender Ordner wird neu angelegt) |
|
||
| DELETE | `/documents/{id}/permanent` | Notiz im Papierkorb endgültig löschen |
|
||
| PUT | `/documents/{id}/move` | Ordner wechseln (`folderPath`) |
|
||
| POST | `/documents/{id}/blocks/{blockId}/enhanced-task` | Aus einem Block eine verknüpfte Aufgabe machen (`content`, `checked`) |
|
||
| GET/POST/DELETE | `/documents/{id}/shares[/{userId}]` | Notiz-Freigaben (`userId`/`email`/`teamId`, `permission`: `read`/`write`) |
|
||
|
||
Der Inhalt einer Notiz (Blöcke) wird ausschließlich über `POST /docs/{id}/sync` geändert. `POST /documents` nimmt beim Anlegen weiterhin eine Start-Blockliste an; sie wird intern in Operationen umgerechnet.
|
||
|
||
## Gemeinsames Bearbeiten von Notizen
|
||
|
||
Konzept und Regeln: [modules/collaboration.md](../modules/collaboration.md). Zugriff über die Rolle `owner`/`editor`/`viewer` (`Store.DocRole`); ohne Rolle `404`.
|
||
|
||
| Methode | Pfad | Zweck |
|
||
|---|---|---|
|
||
| GET | `/docs/{id}/snapshot` | Kompletter Blockbaum inkl. Grabsteinen, `title`, `titleVersion`, `headSeq`, `role`, `shared` |
|
||
| POST | `/docs/{id}/sync` | Ein Rundlauf: eigene Operationen senden, Leases halten/abgeben, Log ab `lastSeq` abholen |
|
||
| GET | `/docs/{id}/changes?since=N&wait=S` | Long-Polling: antwortet sofort, wenn `headSeq > N`, sonst bei der nächsten Änderung (auch Leases/Präsenz) oder nach höchstens 25 s. Antwort `{headSeq, changed}`; Daten holt der Client danach per `/sync` |
|
||
| POST | `/docs/{id}/leave` | Sitzung beenden, alle Leases des Clients freigeben (`{clientId}`) |
|
||
|
||
`/sync`-Anfrage:
|
||
|
||
```json
|
||
{
|
||
"clientId": "tab-7f3…",
|
||
"lastSeq": 412,
|
||
"ops": [
|
||
{ "opId": "c1a…", "kind": "set_content", "blockId": "b9…", "baseVersion": 7, "content": "Milch und Brot" },
|
||
{ "opId": "c1b…", "kind": "insert", "blockId": "bA…", "parentId": "b2…", "orderKey": "a0V", "type": "bullet", "content": "" }
|
||
],
|
||
"leases": { "hold": ["b9…", "bA…"], "release": [] }
|
||
}
|
||
```
|
||
|
||
- Operationsarten: `insert`, `set_content`, `set_props` (`patch`: `type`, `checked`, `collapsed`, `lang`, `clearConflict`), `move`, `delete`, `restore` (`ids`), `set_title` (`baseVersion`, `title`).
|
||
- `leases.hold` ist die vollständige Liste der Blöcke, die der Client halten will; alle anderen Leases des Clients werden freigegeben. Neu eingefügte Blöcke und Konfliktkopien bekommt der Autor automatisch.
|
||
- Höchstens 500 Operationen pro Anfrage. Leser dürfen keine Operationen senden (`403`), aber abfragen.
|
||
- `skipLog: true` liefert kein Log zurück; gedacht für einzelne Änderungen ohne geöffneten Editor (z. B. Abhaken in der To-Do-Übersicht).
|
||
|
||
Antwort: `results` (je Operation `status` `applied`/`conflict`/`rejected`, ggf. `conflictBlockId`, `version`, `seq`), `ops` (Log-Einträge `{seq, opId, userId, clientId, effect}` ab `lastSeq`, höchstens 1000; `hasMore` bei mehr), `headSeq`, `resync` (Snapshot neu laden: der Client ist dem Server voraus, oder sein `lastSeq` liegt vor der verdichteten Grenze `log_floor`; die gesendeten Operationen werden trotzdem angewendet), `role`, `shared`, `sessions`, `presence`, `leases` (`granted`, `denied`, `others`).
|
||
|
||
Wiederholte Operationen (gleiche `opId`) werden nicht erneut angewendet; die Antwort enthält das gespeicherte Ergebnis.
|
||
|
||
## Geteilte Listen
|
||
|
||
| Methode | Pfad | Zweck |
|
||
|---|---|---|
|
||
| GET/POST | `/lists` | Listen; anlegen (`title`, `icon`, `listType`) |
|
||
| GET/DELETE | `/lists/{id}` | Abruf mit Einträgen; Löschen |
|
||
| GET/POST/DELETE | `/lists/{id}/shares[/{shareId}]` | Freigaben (`targetType`, `targetId`, `role`: `editor`/`viewer`) |
|
||
| POST | `/lists/{id}/items` | Eintrag (`content`, `category`, `quantity`) |
|
||
| PATCH | `/lists/{id}/items/{itemId}` | Abhaken (`isCompleted`) |
|
||
| PUT | `/lists/{id}/items/{itemId}` | Ändern (`content`, `category`, `quantity`) |
|
||
| DELETE | `/lists/{id}/items/{itemId}` | Löschen |
|
||
| POST/PUT | `/lists/{id}/items/reorder` | Reihenfolge/Kategorie (`items[{id, sortOrder, category, content}]`) |
|
||
| POST | `/lists/{id}/clear-completed` | Erledigte entfernen |
|
||
|
||
## Gruppen
|
||
|
||
| Methode | Pfad | Zweck |
|
||
|---|---|---|
|
||
| GET/POST | `/teams` | Eigene Gruppen; anlegen (`name`, `description`) – Ersteller wird Gruppen-Admin, die Gruppe gehört zu seiner Organisation; im Modus `private` gesperrt |
|
||
| PUT/DELETE | `/teams/{id}` | Nur Gruppen-Admin |
|
||
| POST | `/teams/{id}/members` | Mitglied hinzufügen (`userId` oder `email`, `role`); nur sichtbare Personen, sonst `404` |
|
||
| PUT/DELETE | `/teams/{id}/members/{userId}` | Rolle ändern (`admin`/`member`); entfernen |
|
||
|
||
## Administration (`role = admin`)
|
||
|
||
| Methode | Pfad | Zweck |
|
||
|---|---|---|
|
||
| GET/POST | `/admin/users` | Benutzer auflisten, anlegen (`email`, `password`, `displayName`, `role`, optional `organizationId`) |
|
||
| PUT | `/admin/users/{id}/organization` | Organisation setzen (`organizationId`, leer = keine); entzieht Freigaben, Gruppen und Zuweisungen der alten |
|
||
| GET/POST | `/admin/organizations` | Organisationen mit `memberCount`; anlegen (`name`) |
|
||
| PUT/DELETE | `/admin/organizations/{id}` | Umbenennen (`name`); löschen (Mitglieder sind danach ohne Organisation) |
|
||
| 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 | `/admin/settings/sharing-impact?mode=` | Probelauf: wie viele Freigaben und Gruppen-Mitgliedschaften ein Wechsel entfernen würde (`affected`) |
|
||
| POST | `/admin/smtp/test` | Testmail (`toEmail`) |
|
||
|
||
## Beispiele
|
||
|
||
```bash
|
||
# Gedanken per API-Key erfassen
|
||
curl -X POST https://mindsift.example.com/api/v1/webhooks/ingest \
|
||
-H "X-API-Key: mindsift_sec_…" -H "Content-Type: application/json" \
|
||
-d '{"title":"Milch kaufen #einkauf","priority":"high","dueDate":"2026-11-01"}'
|
||
|
||
# Aufgabenliste nur lesend freigeben
|
||
curl -X POST https://mindsift.example.com/api/v1/task-lists/tkl_…/shares \
|
||
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
|
||
-d '{"targetType":"user","targetId":"usr_…","permission":"viewer"}'
|
||
```
|