docs: rewrite documentation and README from current codebase, add flaw analysis
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 8s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 8s
Remove outdated claims (CRDT, DocRooms, presence, .md files on disk) and document the actual architecture, API, permissions and known flaws. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4e9c4ffa8c
commit
bc13235da6
14 files changed
+733
-1618
No files matched your search
+119
-175
@@ -1,185 +1,129 @@
|
||||
# 🔌 REST-API Referenz
|
||||
# REST-API
|
||||
|
||||
Die MindSift REST-API ermöglicht den vollständigen Zugriff auf Gedanken, Aufgaben, Workspace-Notizen, Listen, Benutzerkonten und Smarthome-Automationen.
|
||||
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
|
||||
|
||||
## 🔐 1. Authentifizierung
|
||||
| Methode | Pfad | Zweck |
|
||||
|---|---|---|
|
||||
| GET | `/health` | Lebenszeichen |
|
||||
| GET | `/auth/status` | `needsSetup`, `allowRegistration`, `instanceName`, `demoMode` |
|
||||
| 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) |
|
||||
| GET | `/events` | SSE-Fallback |
|
||||
| POST | `/events/rooms` | SSE-Räume beitreten/verlassen ([FLAW-02](../flaws.md)) |
|
||||
|
||||
MindSift unterstützt zwei Authentifizierungsmethoden:
|
||||
## Konto und Allgemeines (angemeldet)
|
||||
|
||||
1. **JSON Web Token (JWT Bearer):**
|
||||
Wird beim Login oder Setup erzeugt und im HTTP-Header mitgesendet:
|
||||
`Authorization: Bearer <jwt-token>`
|
||||
2. **Persönliche API-Schlüssel (Smarthome / Automatisierung):**
|
||||
Werden unter *Einstellungen ➔ Private Webhooks* erzeugt (Präfix `mindsift_sec_...`):
|
||||
- Per HTTP-Header: `X-API-Key: mindsift_sec_xxxxxxxxxxxx`
|
||||
- Per Authorization-Header: `Authorization: Bearer mindsift_sec_xxxxxxxxxxxx`
|
||||
- Per Query-Parameter: `?token=mindsift_sec_xxxxxxxxxxxx`
|
||||
| Methode | Pfad | Zweck |
|
||||
|---|---|---|
|
||||
| GET | `/auth/me` | Benutzer und Kennzahlen |
|
||||
| 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` | Benutzerverzeichnis für Freigaben ([FLAW-10](../flaws.md)) |
|
||||
| GET | `/stats` | Zähler fürs Dashboard |
|
||||
| GET | `/search?q=` | Suche in eigenen Gedanken und Notizen |
|
||||
| GET | `/export` | Datenexport als JSON |
|
||||
| GET/POST/DELETE | `/keys`, `/keys/{id}` | API-Keys verwalten (Klartext nur bei Erstellung) |
|
||||
|
||||
---
|
||||
## Gedanken, Triage, Aufgaben
|
||||
|
||||
## 🚦 2. Status- & Fehler-Codes
|
||||
| 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`); Löschen |
|
||||
| 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 |
|
||||
|
||||
- `200 OK`: Erfolgreiche Abfrage oder Aktualisierung.
|
||||
- `201 Created`: Ressource erfolgreich angelegt.
|
||||
- `400 Bad Request`: Ungültiges JSON oder fehlende Pflichtfelder.
|
||||
- `401 Unauthorized`: Token ungültig, abgelaufen oder fehlend.
|
||||
- `403 Forbidden`: Unzureichende Berechtigungen (z.B. nur Server-Admin oder nur Lesezugriff).
|
||||
- `404 Not Found`: Ressource existiert nicht oder gehört einem anderen Benutzer.
|
||||
- `409 Conflict`: Ressource gesperrt (z.B. Dokument wird bereits von anderem Benutzer bearbeitet).
|
||||
## Aufgabenlisten
|
||||
|
||||
Fehlerrückgaben erfolgen im einheitlichen Format:
|
||||
```json
|
||||
{
|
||||
"error": "Fehlermeldung im Klartext"
|
||||
}
|
||||
| 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 (nur Lesen wirksam) |
|
||||
| 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 (`title`, `icon`, `folderPath`, `blocks`, `tags`, `isPinned`); Löschen |
|
||||
| PATCH | `/documents/{id}/blocks` | Teil-Update der Blöcke (`upserted`, `deletedIds`, Reihenfolge); löst `DOC_BLOCKS_PATCHED` aus |
|
||||
| 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`; wirksam nur Lesen) |
|
||||
|
||||
Nur der Eigentümer schreibt in eine Notiz; Empfänger erhalten `403`/„nur Lesezugriff“ ([FLAW-17](../flaws.md)).
|
||||
|
||||
## 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 |
|
||||
| PUT/DELETE | `/teams/{id}` | Nur Gruppen-Admin |
|
||||
| POST | `/teams/{id}/members` | Mitglied hinzufügen (`userId` oder `email`, `role`) |
|
||||
| PUT/DELETE | `/teams/{id}/members/{userId}` | Rolle ändern (`admin`/`member`); entfernen |
|
||||
| GET/POST/DELETE | `/organizations`, `/organizations/{id}[/teams]` | Veraltet, ungeschützt ([FLAW-24](../flaws.md)) |
|
||||
|
||||
## Administration (`role = admin`)
|
||||
|
||||
| Methode | Pfad | Zweck |
|
||||
|---|---|---|
|
||||
| GET/POST | `/admin/users` | Benutzer auflisten, anlegen (`email`, `password`, `displayName`, `role`) |
|
||||
| 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`, `teamId`, `validDays`) |
|
||||
| GET/PUT | `/admin/settings` | `instanceName`, `allowRegistration`, `sharingMode`, SMTP-Felder |
|
||||
| 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"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 3. Endpunkte nach Modulen
|
||||
|
||||
### 3.1 Authentifizierung & System-Status
|
||||
|
||||
| Methode | Pfad | Auth | Beschreibung |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `GET` | `/api/v1/health` | Öffentlich | Liveness-Check (`{"status":"ok"}`) |
|
||||
| `GET` | `/api/v1/auth/status` | Öffentlich | Prüft ob Ersteinrichtung nötig ist (`needsSetup: true/false`) |
|
||||
| `POST` | `/api/v1/auth/setup` | Öffentlich | Führt First-Boot Setup durch (legt ersten Admin an) |
|
||||
| `POST` | `/api/v1/auth/register` | Öffentlich | Registrierung über Einladungscode (`inviteCode`) |
|
||||
| `POST` | `/api/v1/auth/login` | Öffentlich | Login via E-Mail oder Benutzername + Passwort |
|
||||
| `GET` | `/api/v1/auth/me` | JWT | Gibt das aktuelle Benutzerprofil zurück |
|
||||
| `PATCH` | `/api/v1/auth/settings` | JWT | Aktualisiert Profil, Passwort, Theme oder Alarm-Kanäle |
|
||||
| `POST` | `/api/v1/auth/alarms/test` | JWT | Sendet Test-Alarm an E-Mail, Discord oder Custom Webhook |
|
||||
|
||||
#### Beispiel: `POST /api/v1/auth/login`
|
||||
```json
|
||||
// Request
|
||||
{
|
||||
"login": "sandro",
|
||||
"password": "geheimesPasswort123"
|
||||
}
|
||||
|
||||
// Response (200 OK)
|
||||
{
|
||||
"token": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"user": {
|
||||
"id": "usr_abc123",
|
||||
"email": "sandro@example.com",
|
||||
"username": "sandro",
|
||||
"displayName": "Sandro",
|
||||
"role": "admin",
|
||||
"alarmChannels": ["discord", "webhook"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.2 Triage & Instant Capture
|
||||
|
||||
| Methode | Pfad | Auth | Beschreibung |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `POST` | `/api/v1/inbox` | JWT / API-Key | Erfasst einen neuen Quick Dump oder leitet direkt weiter |
|
||||
| `GET` | `/api/v1/triage/deck` | JWT | Liefert den Stapel aller noch nicht triagierten Gedanken (`status: 'inbox'`) |
|
||||
| `PATCH` | `/api/v1/triage/{id}` | JWT | Führt Triage-Aktion aus (`keep`, `discard`, `restore`) |
|
||||
|
||||
#### Beispiel: `POST /api/v1/inbox`
|
||||
Ermöglicht flexible Direkt-Zuordnungen:
|
||||
```json
|
||||
// Request (Quick Dump in Triage-Deck)
|
||||
{
|
||||
"content": "Kellerfeuchtigkeit über 68% gestiegen #smarthome #alarm",
|
||||
"priority": "urgent"
|
||||
}
|
||||
|
||||
// Request (Direkt als Aufgabe anlegen)
|
||||
{
|
||||
"content": "Server Backup prüfen #devops",
|
||||
"priority": "high",
|
||||
"target": "task",
|
||||
"dueDate": "2026-10-30"
|
||||
}
|
||||
```
|
||||
*Hinweis:* Wenn `/api/v1/inbox` über einen API-Key aufgerufen wird und kein `source` definiert ist, wird die Quelle automatisch als `"webhook"` markiert!
|
||||
|
||||
---
|
||||
|
||||
### 3.3 Aufgaben & Kanban-System
|
||||
|
||||
| Methode | Pfad | Auth | Beschreibung |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `GET` | `/api/v1/tasks` | JWT | Liefert alle Aufgaben des Benutzers (inkl. geteilter Aufgaben) |
|
||||
| `POST` | `/api/v1/tasks/reorder` | JWT | Speichert Kanban-Status und Reihenfolge (`sort_order`) |
|
||||
| `GET` | `/api/v1/task-lists` | JWT | Liefert alle Aufgabenlisten (eigene & geteilte) |
|
||||
| `POST` | `/api/v1/task-lists` | JWT | Erstellt eine neue Aufgabenliste (mit Farbe & Spalten) |
|
||||
| `PUT` | `/api/v1/task-lists/{id}` | JWT | Ändert Titel, Farbe, Icon oder Kanban-Spalten |
|
||||
| `DELETE` | `/api/v1/task-lists/{id}` | JWT | Löscht Aufgabenliste |
|
||||
| `POST` | `/api/v1/task-lists/{id}/shares` | JWT | Teilt Liste mit Team oder Benutzer |
|
||||
|
||||
---
|
||||
|
||||
### 3.4 Workspace & Dokumente
|
||||
|
||||
| Methode | Pfad | Auth | Beschreibung |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `GET` | `/api/v1/documents` | JWT | Liefert Liste aller Dokumente des Benutzers (Metadaten) |
|
||||
| `POST` | `/api/v1/documents` | JWT | Erstellt neues Notiz-Dokument (mit Blocks oder Markdown) |
|
||||
| `GET` | `/api/v1/documents/{id}` | JWT | Lädt komplettes Dokument inklusive aller Blöcke |
|
||||
| `PUT` | `/api/v1/documents/{id}` | JWT | Aktualisiert Dokumenttitel, Icon oder Ordnerpfad |
|
||||
| `PATCH` | `/api/v1/documents/{id}/blocks` | JWT | Granulares Patching von Dokument-Blöcken (Database-First) |
|
||||
| `DELETE` | `/api/v1/documents/{id}` | JWT | Löscht Dokument (Datenbank & physische Datei) |
|
||||
| `POST` | `/api/v1/documents/{id}/shares` | JWT | Notiz mit anderem Benutzer teilen (`read` / `write`) |
|
||||
| `GET` | `/api/v1/documents/{id}/lock` | JWT | Fragt Bearbeitungs-Sperrstatus ab |
|
||||
| `POST` | `/api/v1/documents/{id}/lock` | JWT | Sperrt Dokument für Bearbeitung (30s Lease) |
|
||||
| `DELETE` | `/api/v1/documents/{id}/lock` | JWT | Gibt Bearbeitungssperre wieder frei |
|
||||
|
||||
---
|
||||
|
||||
### 3.5 Workspace Ordner & Geteilte Ordner
|
||||
|
||||
| Methode | Pfad | Auth | Beschreibung |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `GET` | `/api/v1/folders` | JWT | Liefert alle Ordnerpfade des Benutzers |
|
||||
| `POST` | `/api/v1/folders` | JWT | Legt neuen (auch leeren) Ordner an |
|
||||
| `POST` | `/api/v1/folders/move` | JWT | Benennt Ordner um oder verschiebt ihn hierarchisch |
|
||||
| `DELETE` | `/api/v1/folders` | JWT | Löscht Ordner und verschiebt Notizen |
|
||||
| `GET` | `/api/v1/workspace/folders/shared` | JWT | Liefert für den Benutzer freigegebene geteilte Ordner |
|
||||
| `POST` | `/api/v1/workspace/folders/shared` | JWT | Erstellt neuen geteilten Ordner |
|
||||
| `PUT` | `/api/v1/workspace/folders/shared/{id}/mount` | JWT | Ändert den lokalen Einhängepfad beim Empfänger |
|
||||
| `POST` | `/api/v1/workspace/folders/shared/{id}/shares` | JWT | Gibt Ordner für Teams oder Benutzer frei |
|
||||
|
||||
---
|
||||
|
||||
### 3.6 Geteilte Listen (Listen & Einkauf)
|
||||
|
||||
| Methode | Pfad | Auth | Beschreibung |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `GET` | `/api/v1/lists` | JWT | Liefert alle geteilten Listen |
|
||||
| `POST` | `/api/v1/lists` | JWT | Erstellt neue geteilte Liste |
|
||||
| `GET` | `/api/v1/lists/{id}` | JWT | Lädt Liste inklusive aller Einträge |
|
||||
| `POST` | `/api/v1/lists/{id}/items` | JWT | Fügt Eintrag hinzu |
|
||||
| `PATCH` | `/api/v1/lists/{id}/items/{itemId}` | JWT | Hakt Eintrag ab (`isCompleted: true/false`) |
|
||||
| `POST` | `/api/v1/lists/{id}/items/reorder` | JWT | Speichert sortierte Einträge |
|
||||
| `POST` | `/api/v1/lists/{id}/clear-completed`| JWT | Löscht alle abgehakten Einträge |
|
||||
| `POST` | `/api/v1/lists/{id}/shares` | JWT | Teilt Liste mit Team oder Benutzer |
|
||||
|
||||
---
|
||||
|
||||
### 3.7 Smarthome Ingestion & Externe Webhooks
|
||||
|
||||
| Methode | Pfad | Auth | Beschreibung |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `POST` | `/api/v1/webhooks/ingest` | Token / Key | Empfängt externe Webhook-Payloads von Home Assistant / n8n |
|
||||
|
||||
#### Beispiel: `POST /api/v1/webhooks/ingest?token=mindsift_sec_...`
|
||||
Unterstützt flexible Payloads (JSON oder Plain Text):
|
||||
```json
|
||||
{
|
||||
"title": "Waschmaschine Keller fertig",
|
||||
"priority": "high",
|
||||
"tags": ["smarthome", "keller"]
|
||||
}
|
||||
```
|
||||
*Live-Aktualisierung:* Sendet automatisch ein `TASK_CREATED` WebSocket-Event an alle Clients des Benutzers, sodass die neue Karte sofort ohne Neuladen im Triage-Deck erscheint!
|
||||
Reference in new issue
Block a user