Changeset 0.28.2.2 (#185)
This commit is contained in:
@@ -1,55 +1,205 @@
|
||||
# User Profile & Settings
|
||||
|
||||
### Profile
|
||||
User identity, preferences, and credentials.
|
||||
|
||||
**All endpoints require:** `Auth()` middleware (JWT bearer token).
|
||||
|
||||
---
|
||||
|
||||
### Get Profile
|
||||
|
||||
```
|
||||
GET /profile → profileResponse
|
||||
PUT /profile ← { "display_name", "email" }
|
||||
GET /profile
|
||||
```
|
||||
|
||||
**Auth:** Authenticated user
|
||||
|
||||
Returns the current user's profile.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"username": "jdoe",
|
||||
"email": "jdoe@example.com",
|
||||
"display_name": "Jane Doe",
|
||||
"role": "user|admin",
|
||||
"avatar_url": "...",
|
||||
"created_at": "...",
|
||||
"last_login": "..."
|
||||
"role": "user",
|
||||
"avatar": "data:image/png;base64,...",
|
||||
"settings": { "theme": "dark" },
|
||||
"created_at": "2025-06-15T14:30:00Z",
|
||||
"last_login_at": "2025-06-15T14:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Avatar
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| `id` | string | UUIDv4 |
|
||||
| `username` | string | Unique login name |
|
||||
| `email` | string | Unique, lowercased |
|
||||
| `display_name` | string \| null | Optional friendly name |
|
||||
| `role` | `"user"` \| `"admin"` | Platform role |
|
||||
| `avatar` | string \| null | Data URI (PNG, 128×128). Omitted if unset. |
|
||||
| `settings` | object | User preferences (theme, keybindings, etc.) |
|
||||
| `created_at` | string | ISO 8601 timestamp |
|
||||
| `last_login_at` | string \| null | ISO 8601 timestamp. Null if never logged in. |
|
||||
|
||||
> **Note:** The profile endpoint returns `avatar` (not `avatar_url`) as a
|
||||
> curated response shape. The `User` model uses `avatar_url` internally.
|
||||
|
||||
---
|
||||
|
||||
### Update Profile
|
||||
|
||||
```
|
||||
PUT /profile
|
||||
```
|
||||
|
||||
**Auth:** Authenticated user
|
||||
|
||||
```json
|
||||
{
|
||||
"display_name": "New Name",
|
||||
"email": "new@example.com"
|
||||
}
|
||||
```
|
||||
|
||||
Both fields are optional (partial update). Returns the full profile
|
||||
(same shape as `GET /profile`).
|
||||
|
||||
**Errors:**
|
||||
- `409` — email already taken
|
||||
|
||||
---
|
||||
|
||||
### Upload Avatar
|
||||
|
||||
```
|
||||
POST /profile/avatar
|
||||
```
|
||||
|
||||
**Auth:** Authenticated user
|
||||
|
||||
**Content-Type:** `application/json`
|
||||
|
||||
```json
|
||||
{
|
||||
"image": "data:image/png;base64,..."
|
||||
}
|
||||
```
|
||||
|
||||
Accepts a base64 data URI or raw base64 string. The server decodes,
|
||||
resizes to 128×128 PNG, and stores as a data URI. Max input: 2 MB.
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"avatar": "data:image/png;base64,..."
|
||||
}
|
||||
```
|
||||
|
||||
**Errors:**
|
||||
- `400` — missing `image` field, invalid base64, unsupported format, too large
|
||||
|
||||
---
|
||||
|
||||
### Delete Avatar
|
||||
|
||||
```
|
||||
POST /profile/avatar ← multipart/form-data (field: "avatar")
|
||||
DELETE /profile/avatar
|
||||
```
|
||||
|
||||
### Password
|
||||
**Auth:** Authenticated user
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "avatar removed"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Change Password
|
||||
|
||||
```
|
||||
POST /profile/password
|
||||
```
|
||||
|
||||
**Auth:** Authenticated user (builtin auth only)
|
||||
|
||||
```json
|
||||
{
|
||||
"current_password": "...",
|
||||
"new_password": "..."
|
||||
"current_password": "old-pass",
|
||||
"new_password": "new-pass-min-8"
|
||||
}
|
||||
```
|
||||
|
||||
On password change, the UEK is re-wrapped with the new password
|
||||
(BYOK keys remain accessible without re-encryption).
|
||||
Validates `current_password` against stored bcrypt hash. `new_password`
|
||||
must be 8–128 characters.
|
||||
|
||||
### App Settings
|
||||
On success, the UEK (User Encryption Key) is re-wrapped with the new
|
||||
password so BYOK-encrypted provider keys remain accessible.
|
||||
|
||||
```
|
||||
GET /settings → { "settings": {...} }
|
||||
PUT /settings ← { "theme", "editor_keybindings", ... }
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "password updated"
|
||||
}
|
||||
```
|
||||
|
||||
User-level preferences (theme, keybindings, default model, etc.).
|
||||
**Errors:**
|
||||
- `400` — missing fields, password too short/long
|
||||
- `401` — current password incorrect
|
||||
|
||||
---
|
||||
|
||||
### Get Settings
|
||||
|
||||
```
|
||||
GET /settings
|
||||
```
|
||||
|
||||
**Auth:** Authenticated user
|
||||
|
||||
Returns user-level preferences wrapped in a `settings` key.
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": {
|
||||
"theme": "dark",
|
||||
"editor_keybindings": "vim",
|
||||
"default_model": "claude-sonnet-4-5"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is a **composite response** (named key wrapping a single object),
|
||||
not a bare object. The `settings` key is always present; its value is
|
||||
`{}` if the user has never saved preferences.
|
||||
|
||||
---
|
||||
|
||||
### Update Settings
|
||||
|
||||
```
|
||||
PUT /settings
|
||||
```
|
||||
|
||||
**Auth:** Authenticated user
|
||||
|
||||
```json
|
||||
{
|
||||
"theme": "light",
|
||||
"new_key": "new_value"
|
||||
}
|
||||
```
|
||||
|
||||
Shallow merge: incoming keys overwrite existing, unmentioned keys are
|
||||
preserved. Handles edge cases: SQL NULL, JSON `null`, and corrupted
|
||||
array values are all normalized to `{}` before merge.
|
||||
|
||||
Returns the updated settings (same shape as `GET /settings`).
|
||||
|
||||
---
|
||||
|
||||
@@ -4,6 +4,13 @@ The **Workspace** is a virtual filesystem for the editor surface.
|
||||
Each workspace has a root directory, file operations, optional Git
|
||||
integration, and full-text indexing for code search.
|
||||
|
||||
**All endpoints require:** `Auth()` middleware (JWT bearer token).
|
||||
Workspace-scoped endpoints additionally verify ownership via
|
||||
`loadAndAuthorize` (user owns workspace, or is member of the
|
||||
owning team/project/channel).
|
||||
|
||||
---
|
||||
|
||||
### Workspace CRUD
|
||||
|
||||
```
|
||||
@@ -14,62 +21,175 @@ PATCH /workspaces/:id ← partial update
|
||||
DELETE /workspaces/:id
|
||||
```
|
||||
|
||||
**Auth:** Authenticated user. `GET /workspaces` returns user-owned
|
||||
workspaces plus those owned by the user's teams. All `:id` endpoints
|
||||
verify the caller can access the workspace's owner entity.
|
||||
|
||||
Workspace object:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"name": "my-project",
|
||||
"owner_type": "user|project",
|
||||
"owner_type": "user",
|
||||
"owner_id": "uuid",
|
||||
"status": "active|archived",
|
||||
"storage_bytes": 1048576,
|
||||
"status": "active",
|
||||
"max_bytes": 52428800,
|
||||
"indexing_enabled": false,
|
||||
"git_remote_url": "https://github.com/...",
|
||||
"git_branch": "main",
|
||||
"git_credential_id": "uuid",
|
||||
"git_last_sync": "2025-06-15T14:30:00Z",
|
||||
"file_count": 42,
|
||||
"created_at": "...",
|
||||
"updated_at": "..."
|
||||
"total_bytes": 1048576,
|
||||
"created_at": "2025-06-15T14:30:00Z",
|
||||
"updated_at": "2025-06-15T14:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### File Operations
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| `id` | string | UUIDv4 |
|
||||
| `name` | string | Max 200 chars |
|
||||
| `owner_type` | `"user"` \| `"project"` \| `"channel"` \| `"team"` | Owner entity type |
|
||||
| `owner_id` | string | UUID of owner entity |
|
||||
| `status` | `"active"` \| `"archived"` \| `"deleting"` | Lifecycle state |
|
||||
| `max_bytes` | int \| null | Quota ceiling. Omitted if unset. |
|
||||
| `indexing_enabled` | bool | Full-text search indexing |
|
||||
| `git_remote_url` | string \| null | Git remote. Omitted if unset. |
|
||||
| `git_branch` | string \| null | Tracked branch. Omitted if unset. |
|
||||
| `git_credential_id` | string \| null | FK to git credential. Omitted if unset. |
|
||||
| `git_last_sync` | string \| null | ISO 8601. Omitted if never synced. |
|
||||
| `file_count` | int | Computed. Enriched on `GET /:id` only. |
|
||||
| `total_bytes` | int | Computed. Enriched on `GET /:id` only. |
|
||||
| `created_at` | string | ISO 8601 |
|
||||
| `updated_at` | string | ISO 8601 |
|
||||
|
||||
All paths are relative to the workspace root.
|
||||
> **Note:** `root_path` is a server-internal filesystem path and is
|
||||
> never exposed in API responses (json tag `"-"`).
|
||||
|
||||
```
|
||||
GET /workspaces/:id/files?path=/ → { "files": [entries] }
|
||||
GET /workspaces/:id/files/read?path=/a.md → { "content": "...", "size": 123 }
|
||||
PUT /workspaces/:id/files/write ← { "path": "/a.md", "content": "..." }
|
||||
DELETE /workspaces/:id/files/delete ← { "path": "/a.md" }
|
||||
POST /workspaces/:id/files/mkdir ← { "path": "/src" }
|
||||
```
|
||||
|
||||
File entry:
|
||||
**Create request:**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "main.go",
|
||||
"path": "/src/main.go",
|
||||
"is_dir": false,
|
||||
"size": 4096,
|
||||
"modified": "..."
|
||||
"name": "my-project",
|
||||
"owner_type": "user",
|
||||
"owner_id": "uuid",
|
||||
"max_bytes": 52428800
|
||||
}
|
||||
```
|
||||
|
||||
Returns `201` with the workspace object.
|
||||
|
||||
**PATCH request** (all fields optional):
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "new-name",
|
||||
"status": "archived",
|
||||
"max_bytes": 104857600,
|
||||
"indexing_enabled": true,
|
||||
"git_remote_url": "https://...",
|
||||
"git_branch": "develop",
|
||||
"git_credential_id": "uuid"
|
||||
}
|
||||
```
|
||||
|
||||
Returns updated workspace object.
|
||||
|
||||
**DELETE** returns `{"ok": true}`. Deletion is async (status set to
|
||||
`"deleting"`, filesystem cleanup runs in background).
|
||||
|
||||
---
|
||||
|
||||
### File Operations
|
||||
|
||||
All paths are relative to the workspace root. File path is passed
|
||||
as `?path=` query parameter.
|
||||
|
||||
```
|
||||
GET /workspaces/:id/files?path=/ → { "data": [entries] }
|
||||
GET /workspaces/:id/files/read?path=/a.md → raw file content (binary stream)
|
||||
PUT /workspaces/:id/files/write?path=/a.md ← raw body (Content-Length required)
|
||||
DELETE /workspaces/:id/files/delete?path=/a.md
|
||||
POST /workspaces/:id/files/mkdir?path=/src
|
||||
```
|
||||
|
||||
**Auth:** Authenticated user with workspace access.
|
||||
|
||||
**`GET .../files`** accepts `?recursive=true` for deep listing.
|
||||
Returns `{"data": [...]}` (never bare array). Nil slice guarded.
|
||||
|
||||
File entry (from `WorkspaceFile` model):
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"workspace_id": "uuid",
|
||||
"path": "/src/main.go",
|
||||
"is_directory": false,
|
||||
"content_type": "text/x-go",
|
||||
"size_bytes": 4096,
|
||||
"sha256": "abc123...",
|
||||
"index_status": "ready",
|
||||
"chunk_count": 3,
|
||||
"created_at": "2025-06-15T14:30:00Z",
|
||||
"updated_at": "2025-06-15T14:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**`GET .../files/read`** returns raw file content with appropriate
|
||||
`Content-Type` header and `Content-Length`. This is a binary stream,
|
||||
not JSON.
|
||||
|
||||
**`PUT .../files/write`** reads the request body as raw content.
|
||||
Returns `{"ok": true, "path": "/a.md"}`. Returns `413` if the
|
||||
write would exceed the workspace quota.
|
||||
|
||||
**`DELETE .../files/delete`** accepts `?recursive=true` for
|
||||
directory removal. Returns `{"ok": true}`.
|
||||
|
||||
**`POST .../files/mkdir`** returns `201 {"ok": true, "path": "/src"}`.
|
||||
|
||||
---
|
||||
|
||||
### Archive
|
||||
|
||||
**Download** (tar.gz of entire workspace):
|
||||
**Download** (full workspace as archive):
|
||||
|
||||
```
|
||||
GET /workspaces/:id/archive/download
|
||||
GET /workspaces/:id/archive/download?format=zip
|
||||
```
|
||||
|
||||
**Upload** (restore from tar.gz):
|
||||
**Auth:** Authenticated user with workspace access.
|
||||
|
||||
`format` query param: `zip` (default), `tar.gz`, `tgz`.
|
||||
Returns binary stream with `Content-Disposition: attachment`.
|
||||
|
||||
**Upload** (restore from archive):
|
||||
|
||||
```
|
||||
POST /workspaces/:id/archive/upload
|
||||
Content-Type: multipart/form-data
|
||||
Content-Type: multipart/form-data (field: "file")
|
||||
```
|
||||
|
||||
### Reconcile, Stats, Index
|
||||
**Auth:** Authenticated user with workspace access.
|
||||
|
||||
Accepts `.zip`, `.tar.gz`, `.tgz` archives. Format detected from
|
||||
filename extension.
|
||||
|
||||
Returns:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"files_extracted": 42
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Reconcile, Stats, Index Status
|
||||
|
||||
**Reconcile** (sync filesystem state with DB metadata):
|
||||
|
||||
@@ -77,13 +197,37 @@ Content-Type: multipart/form-data
|
||||
POST /workspaces/:id/reconcile
|
||||
```
|
||||
|
||||
**Auth:** Authenticated user with workspace access.
|
||||
|
||||
Returns:
|
||||
|
||||
```json
|
||||
{
|
||||
"added": 5,
|
||||
"removed": 2,
|
||||
"updated": 3
|
||||
}
|
||||
```
|
||||
|
||||
**Stats:**
|
||||
|
||||
```
|
||||
GET /workspaces/:id/stats
|
||||
```
|
||||
|
||||
Returns `{ "file_count", "storage_bytes", "indexed_files", ... }`.
|
||||
**Auth:** Authenticated user with workspace access.
|
||||
|
||||
Returns a `WorkspaceStats` object:
|
||||
|
||||
```json
|
||||
{
|
||||
"workspace_id": "uuid",
|
||||
"file_count": 42,
|
||||
"dir_count": 8,
|
||||
"total_bytes": 1048576,
|
||||
"max_bytes": 52428800
|
||||
}
|
||||
```
|
||||
|
||||
**Index status:**
|
||||
|
||||
@@ -91,47 +235,166 @@ Returns `{ "file_count", "storage_bytes", "indexed_files", ... }`.
|
||||
GET /workspaces/:id/index-status
|
||||
```
|
||||
|
||||
Returns indexing progress for full-text search.
|
||||
**Auth:** Authenticated user with workspace access.
|
||||
|
||||
Returns indexing progress for full-text search:
|
||||
|
||||
```json
|
||||
{
|
||||
"workspace_id": "uuid",
|
||||
"indexing_enabled": true,
|
||||
"status_counts": {
|
||||
"pending": 3,
|
||||
"indexing": 1,
|
||||
"ready": 38,
|
||||
"error": 0,
|
||||
"skipped": 2
|
||||
},
|
||||
"total_chunks": 156
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Git Integration
|
||||
|
||||
All Git operations are workspace-scoped.
|
||||
All Git operations are workspace-scoped. Require the workspace to
|
||||
have a Git remote configured.
|
||||
|
||||
```
|
||||
POST /workspaces/:id/git/clone ← { "url", "branch", "credential_id" }
|
||||
POST /workspaces/:id/git/pull
|
||||
POST /workspaces/:id/git/push
|
||||
GET /workspaces/:id/git/status → { "files": [{ "path", "status" }] }
|
||||
GET /workspaces/:id/git/status → GitStatus object
|
||||
GET /workspaces/:id/git/diff → { "diff": "..." }
|
||||
POST /workspaces/:id/git/commit ← { "message", "files": [...] }
|
||||
GET /workspaces/:id/git/log → { "commits": [...] }
|
||||
POST /workspaces/:id/git/commit ← { "message", "paths": [...] }
|
||||
GET /workspaces/:id/git/log → { "data": [GitLogEntry] }
|
||||
GET /workspaces/:id/git/branches → { "branches": [...], "current": "main" }
|
||||
POST /workspaces/:id/git/checkout ← { "branch": "feature-x" }
|
||||
```
|
||||
|
||||
**Auth:** Authenticated user with workspace access.
|
||||
|
||||
**`POST .../git/clone`** request:
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://github.com/user/repo.git",
|
||||
"branch": "main",
|
||||
"credential_id": "uuid"
|
||||
}
|
||||
```
|
||||
|
||||
`branch` and `credential_id` are optional. Returns `{"status": "cloned"}`.
|
||||
|
||||
**`POST .../git/pull`** returns `{"status": "pulled"}`.
|
||||
|
||||
**`POST .../git/push`** returns `{"status": "pushed"}`.
|
||||
|
||||
**`GET .../git/status`** returns the full `GitStatus` object:
|
||||
|
||||
```json
|
||||
{
|
||||
"branch": "main",
|
||||
"clean": false,
|
||||
"ahead": 2,
|
||||
"behind": 0,
|
||||
"staged": [{ "path": "file.go", "status": "M" }],
|
||||
"modified": [{ "path": "other.go", "status": "M" }],
|
||||
"untracked": ["new-file.txt"]
|
||||
}
|
||||
```
|
||||
|
||||
**`GET .../git/diff`** accepts optional `?path=file.go` for single-file diff.
|
||||
Returns `{"diff": "..."}` (unified diff string).
|
||||
|
||||
**`POST .../git/commit`** request:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "fix: resolve null pointer",
|
||||
"paths": ["/src/main.go", "/src/util.go"]
|
||||
}
|
||||
```
|
||||
|
||||
`paths` is optional (commits all staged if omitted). Returns
|
||||
`{"status": "committed"}`.
|
||||
|
||||
**`GET .../git/log`** accepts optional `?n=20` (default 20, max 100).
|
||||
Returns `{"data": [GitLogEntry]}`:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"hash": "abc123...",
|
||||
"short_hash": "abc123",
|
||||
"author": "Jane Doe",
|
||||
"date": "2025-06-15T14:30:00Z",
|
||||
"message": "Initial commit"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**`GET .../git/branches`** returns:
|
||||
|
||||
```json
|
||||
{
|
||||
"current": "main",
|
||||
"branches": ["main", "develop", "feature-x"]
|
||||
}
|
||||
```
|
||||
|
||||
**`POST .../git/checkout`** returns
|
||||
`{"status": "checked out", "branch": "feature-x"}`.
|
||||
|
||||
---
|
||||
|
||||
### Git Credentials
|
||||
|
||||
User-owned, encrypted credential storage for Git operations.
|
||||
Encrypted with the user's UEK (same as BYOK keys — admins cannot
|
||||
recover).
|
||||
Encrypted with the user's UEK via the platform vault (same pattern
|
||||
as BYOK keys — admins cannot recover).
|
||||
|
||||
Independent of workspace scope — credentials are user-level.
|
||||
|
||||
```
|
||||
POST /git-credentials ← { "name", "auth_type", "token"|"username"+"password"|"ssh_key" }
|
||||
POST /git-credentials ← { "name", "auth_type", ... }
|
||||
GET /git-credentials → { "data": [GitCredentialSummary] }
|
||||
DELETE /git-credentials/:id
|
||||
```
|
||||
|
||||
`auth_type`: `https_pat`, `https_basic`, or `ssh_key`.
|
||||
**Auth:** Authenticated user. Credentials are scoped to the requesting user.
|
||||
|
||||
Summary response (never exposes encrypted data):
|
||||
**Create request:**
|
||||
|
||||
`auth_type` determines which fields are required:
|
||||
|
||||
| `auth_type` | Required fields |
|
||||
|-------------|----------------|
|
||||
| `https_pat` | `token` |
|
||||
| `https_basic` | `username`, `password` |
|
||||
| `ssh_key` | `private_key` (+ optional `passphrase`) |
|
||||
|
||||
Returns `201` with the credential summary (never exposes encrypted data).
|
||||
|
||||
**List response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"name": "GitHub PAT",
|
||||
"auth_type": "https_pat",
|
||||
"created_at": "..."
|
||||
"data": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"name": "GitHub PAT",
|
||||
"auth_type": "https_pat",
|
||||
"created_at": "2025-06-15T14:30:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**DELETE** returns `{"deleted": true}`. Returns `404` if credential
|
||||
not found or not owned by the requesting user.
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user