- GET /export/markdown streams the workspace as ZIP (no temp files): notes as .md with YAML front matter in the user's folder structure (shared notes and mounted folders included, empty folders kept) - Clean Markdown: top-level headings/dividers/callouts/code as real blocks (their children move up a level), lists with tabs, todos as - [ ] - Aufgaben/<list>.md in Obsidian Tasks format (due date, priority, subtasks, notes), Listen/<list>.md for shopping and other lists - Safe, unique file names; download button in settings (mobile-safe) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
170 lines
12 KiB
Markdown
170 lines
12 KiB
Markdown
# REST-API
|
||
|
||
Basis: `/api/v1`. Antworten sind JSON; Fehler haben die Form `{"error": "Text"}`. Authentifizierung über `Authorization: Bearer <JWT>` oder `Bearer mindsift_sec_…` bzw. Header `X-API-Key`. 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/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=` | Suche in eigenen Gedanken und Notizen |
|
||
| GET | `/export` | Datenexport als JSON |
|
||
| 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 |
|
||
| GET/POST/DELETE | `/keys`, `/keys/{id}` | API-Keys verwalten (Klartext nur bei Erstellung) |
|
||
|
||
## 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`); 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 |
|
||
| 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"}'
|
||
```
|