# 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.

---

### Bootstrap (v0.37.15)

```
GET /profile/bootstrap
```

**Auth:** Authenticated user

Single-call boot payload for the SDK. Collapses what previously required
3–4 sequential requests (profile, permissions, teams/mine, settings) into
one call. The SDK calls this at startup and on token refresh.

**Response:**

```json
{
  "user": {
    "id": "uuid",
    "username": "jdoe",
    "display_name": "Jane Doe",
    "email": "jdoe@example.com",
    "role": "user",
    "avatar": "data:image/png;base64,..."
  },
  "permissions": ["channel.create", "kb.read", "model.use"],
  "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
  },
  "settings": {
    "theme": "dark",
    "editor_keybindings": "vim"
  }
}
```

| Field | Type | Description |
|-------|------|-------------|
| `user` | object | Curated profile (same fields as `GET /profile` minus timestamps) |
| `permissions` | string[] | Resolved permission set (sorted). Admins get all. |
| `groups` | string[] | Contributing group IDs (always includes Everyone) |
| `teams` | object[] | Active team memberships with `my_role` |
| `policies` | object | Boolean policy flags for UI feature gating |
| `settings` | object | User preferences (`{}` if never saved) |

`user.avatar` is omitted when unset (not `null`).

This is a **superset** of `GET /profile/permissions` — it includes
everything that endpoint returns plus `user` and `settings`. Clients
that only need permissions can continue using the narrower endpoint.

---
