Co-authored-by: gobha <jasafpro@gmail.com> Co-committed-by: gobha <jasafpro@gmail.com>
251 lines
4.8 KiB
Markdown
251 lines
4.8 KiB
Markdown
# User Profile & Settings
|
||
|
||
User identity, preferences, and credentials.
|
||
|
||
**All endpoints require:** `Auth()` middleware (JWT bearer token).
|
||
|
||
---
|
||
|
||
### Get Profile
|
||
|
||
```
|
||
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",
|
||
"avatar": "data:image/png;base64,...",
|
||
"settings": { "theme": "dark" },
|
||
"created_at": "2025-06-15T14:30:00Z",
|
||
"last_login_at": "2025-06-15T14:30:00Z"
|
||
}
|
||
```
|
||
|
||
| 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
|
||
|
||
```
|
||
DELETE /profile/avatar
|
||
```
|
||
|
||
**Auth:** Authenticated user
|
||
|
||
**Response:**
|
||
|
||
```json
|
||
{
|
||
"message": "avatar removed"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Change Password
|
||
|
||
```
|
||
POST /profile/password
|
||
```
|
||
|
||
**Auth:** Authenticated user (builtin auth only)
|
||
|
||
```json
|
||
{
|
||
"current_password": "old-pass",
|
||
"new_password": "new-pass-min-8"
|
||
}
|
||
```
|
||
|
||
Validates `current_password` against stored bcrypt hash. `new_password`
|
||
must be 8–128 characters.
|
||
|
||
On success, the UEK (User Encryption Key) is re-wrapped with the new
|
||
password so BYOK-encrypted provider keys remain accessible.
|
||
|
||
**Response:**
|
||
|
||
```json
|
||
{
|
||
"message": "password updated"
|
||
}
|
||
```
|
||
|
||
**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`).
|
||
|
||
---
|
||
|
||
### Get My Permissions
|
||
|
||
```
|
||
GET /profile/permissions
|
||
```
|
||
|
||
**Auth:** Authenticated user
|
||
|
||
Returns the current user's resolved permission set, contributing group
|
||
IDs, team memberships, and UI-relevant policies. Admin users receive all
|
||
permissions by definition.
|
||
|
||
**Response:**
|
||
|
||
```json
|
||
{
|
||
"permissions": ["model.use", "kb.read", "channel.create"],
|
||
"groups": ["group-id-1", "00000000-0000-0000-0000-000000000001"],
|
||
"teams": [
|
||
{ "id": "uuid", "name": "Engineering", "my_role": "member" }
|
||
],
|
||
"policies": {
|
||
"allow_user_byok": false,
|
||
"allow_user_personas": false,
|
||
"allow_raw_model_access": true,
|
||
"kb_direct_access": true
|
||
}
|
||
}
|
||
```
|
||
|
||
| Field | Type | Description |
|
||
|-------|------|-------------|
|
||
| `permissions` | string[] | Resolved permission set (union of all group permissions) |
|
||
| `groups` | string[] | Contributing group IDs (always includes Everyone) |
|
||
| `teams` | object[] | User's active team memberships with `my_role` |
|
||
| `policies` | object | Boolean policy flags relevant to UI feature gating |
|
||
|
||
`permissions` is sorted alphabetically. `teams` includes only active teams
|
||
the user is a member of.
|
||
|
||
This endpoint is the SDK's bootstrap source for `sw.can(permission)`.
|
||
Called once at login and on each token refresh.
|
||
|
||
---
|