From 964f900d1d5cbef9b3d5b943634baea420dc0cf5 Mon Sep 17 00:00:00 2001 From: "ilblackdragon@gmail.com" Date: Wed, 25 Mar 2026 22:47:06 -0700 Subject: [PATCH] Adding user management api --- docs/USER_MANAGEMENT_API.md | 430 ++++++++++++++++++++++++++++++++++++ 1 file changed, 430 insertions(+) create mode 100644 docs/USER_MANAGEMENT_API.md diff --git a/docs/USER_MANAGEMENT_API.md b/docs/USER_MANAGEMENT_API.md new file mode 100644 index 00000000..189697e6 --- /dev/null +++ b/docs/USER_MANAGEMENT_API.md @@ -0,0 +1,430 @@ +# User Management API + +DB-backed user management for multi-tenant IronClaw deployments. Covers admin user CRUD, self-service profile, API token management, and usage reporting. + +## Authentication + +All endpoints require `Authorization: Bearer `. Tokens are either: +- **Env-var tokens** — configured via `GATEWAY_AUTH_TOKEN` (single-user) at startup +- **DB-backed tokens** — created via `POST /api/tokens` or `POST /api/admin/users` + +DB tokens are SHA-256 hashed at rest; plaintext is returned exactly once at creation time. + +Auth is cached in a bounded LRU (1024 entries, 60s TTL). Suspending a user or revoking a token may take up to 60s to take effect. + +## Roles + +| Role | Scope | +|------|-------| +| `admin` | Full access to all endpoints | +| `member` | Self-service profile + own token management only | + +Endpoints marked **Admin** return `403 Forbidden` for `member` role. + +--- + +## Admin: Users + +### POST /api/admin/users + +Create a new user. Returns the user record and a one-time plaintext API token. + +**Auth:** Admin + +**Request body:** + +```json +{ + "display_name": "Alice Smith", + "email": "alice@example.com", + "role": "member" +} +``` + +| Field | Type | Required | Default | Notes | +|-------|------|----------|---------|-------| +| `display_name` | string | yes | | | +| `email` | string | no | `null` | Must be unique if provided | +| `role` | string | no | `"member"` | `"admin"` or `"member"` | + +**Response:** `200 OK` + +```json +{ + "id": "550e8400-e29b-41d4-a716-446655440000", + "email": "alice@example.com", + "display_name": "Alice Smith", + "status": "active", + "role": "member", + "token": "a1b2c3d4e5f6...64-char hex...", + "created_at": "2026-03-25T12:00:00+00:00", + "created_by": "admin-user-id" +} +``` + +The `token` field is the plaintext API token. It is shown **only once** — store it securely. + +**Errors:** `400` (missing display_name, invalid role), `403` (not admin), `503` (no database) + +--- + +### GET /api/admin/users + +List all users. + +**Auth:** Admin + +**Response:** `200 OK` + +```json +{ + "users": [ + { + "id": "550e8400-...", + "email": "alice@example.com", + "display_name": "Alice Smith", + "status": "active", + "role": "member", + "created_at": "2026-03-25T12:00:00+00:00", + "updated_at": "2026-03-25T12:00:00+00:00", + "last_login_at": "2026-03-25T14:30:00+00:00", + "created_by": "admin-user-id" + } + ] +} +``` + +--- + +### GET /api/admin/users/{id} + +Get a single user by ID. + +**Auth:** Admin + +**Response:** `200 OK` + +```json +{ + "id": "550e8400-...", + "email": "alice@example.com", + "display_name": "Alice Smith", + "status": "active", + "role": "member", + "created_at": "2026-03-25T12:00:00+00:00", + "updated_at": "2026-03-25T12:00:00+00:00", + "last_login_at": "2026-03-25T14:30:00+00:00", + "created_by": "admin-user-id", + "metadata": {} +} +``` + +**Errors:** `404` (user not found), `403` (not admin) + +--- + +### PATCH /api/admin/users/{id} + +Update a user's display name and/or metadata. Omitted fields are left unchanged. + +**Auth:** Admin + +**Request body:** + +```json +{ + "display_name": "Alice Johnson", + "metadata": {"department": "engineering"} +} +``` + +| Field | Type | Required | Notes | +|-------|------|----------|-------| +| `display_name` | string | no | | +| `metadata` | object | no | Replaces entire metadata object (merge patch) | + +**Response:** `200 OK` — returns the full updated user record (same shape as GET detail, without `last_login_at`/`created_by`). + +**Errors:** `404` (user not found), `403` (not admin) + +--- + +### POST /api/admin/users/{id}/suspend + +Suspend a user. Suspended users cannot authenticate (DB auth checks user status). + +**Auth:** Admin + +**Response:** `200 OK` + +```json +{ + "id": "550e8400-...", + "status": "suspended" +} +``` + +**Errors:** `404` (user not found), `403` (not admin) + +--- + +### POST /api/admin/users/{id}/activate + +Re-activate a suspended user. + +**Auth:** Admin + +**Response:** `200 OK` + +```json +{ + "id": "550e8400-...", + "status": "active" +} +``` + +**Errors:** `404` (user not found), `403` (not admin) + +--- + +### DELETE /api/admin/users/{id} + +Permanently delete a user and all associated data (tokens, jobs, conversations, memory, routines, settings, secrets). + +**Auth:** Admin + +**Response:** `200 OK` + +```json +{ + "id": "550e8400-...", + "deleted": true +} +``` + +**Errors:** `404` (user not found), `403` (not admin) + +**Cascade:** Deletes from `api_tokens`, `agent_jobs`, `conversations`, `memory_documents`, `routines`, `secrets`, `settings`, `wasm_tools`, and related tables. On PostgreSQL this uses FK cascades; on libSQL it uses explicit deletes. + +--- + +## Admin: Usage + +### GET /api/admin/usage + +Per-user LLM usage statistics aggregated from `llm_calls` via `agent_jobs.user_id`. + +**Auth:** Admin + +**Query parameters:** + +| Param | Type | Default | Notes | +|-------|------|---------|-------| +| `user_id` | string | all users | Filter to a single user | +| `period` | string | `"day"` | `"day"` (24h), `"week"` (7d), or `"month"` (30d) | + +**Response:** `200 OK` + +```json +{ + "period": "week", + "since": "2026-03-18T12:00:00+00:00", + "usage": [ + { + "user_id": "alice-id", + "model": "claude-sonnet-4-5-20250514", + "call_count": 42, + "input_tokens": 150000, + "output_tokens": 30000, + "total_cost": "1.23" + } + ] +} +``` + +--- + +## Self-Service: Profile + +### GET /api/profile + +Get the authenticated user's own profile. + +**Auth:** Any authenticated user + +**Response:** `200 OK` + +```json +{ + "id": "550e8400-...", + "email": "alice@example.com", + "display_name": "Alice Smith", + "status": "active", + "role": "member", + "created_at": "2026-03-25T12:00:00+00:00", + "last_login_at": "2026-03-25T14:30:00+00:00" +} +``` + +--- + +### PATCH /api/profile + +Update the authenticated user's own display name and/or metadata. + +**Auth:** Any authenticated user + +**Request body:** + +```json +{ + "display_name": "Alice Johnson", + "metadata": {"theme": "dark"} +} +``` + +**Response:** `200 OK` + +```json +{ + "id": "550e8400-...", + "display_name": "Alice Johnson", + "updated": true +} +``` + +--- + +## Self-Service: Tokens + +### POST /api/tokens + +Create a new API token for the authenticated user. Admins can optionally create tokens for other users by including `user_id`. + +**Auth:** Any authenticated user + +**Request body:** + +```json +{ + "name": "CI pipeline", + "expires_in_days": 90, + "user_id": "other-user-id" +} +``` + +| Field | Type | Required | Notes | +|-------|------|----------|-------| +| `name` | string | yes | Human-readable label | +| `expires_in_days` | integer | no | `null` = never expires | +| `user_id` | string | no | Admin-only; create token for another user | + +**Response:** `200 OK` + +```json +{ + "token": "a1b2c3d4...64-char hex...", + "id": "token-uuid", + "name": "CI pipeline", + "token_prefix": "a1b2c3d4", + "expires_at": "2026-06-23T12:00:00+00:00", + "created_at": "2026-03-25T12:00:00+00:00" +} +``` + +The `token` field is shown **only once**. + +--- + +### GET /api/tokens + +List the authenticated user's tokens. Token hashes are never returned. + +**Auth:** Any authenticated user + +**Response:** `200 OK` + +```json +{ + "tokens": [ + { + "id": "token-uuid", + "name": "CI pipeline", + "token_prefix": "a1b2c3d4", + "expires_at": "2026-06-23T12:00:00+00:00", + "last_used_at": "2026-03-25T14:00:00+00:00", + "created_at": "2026-03-25T12:00:00+00:00", + "revoked_at": null + } + ] +} +``` + +--- + +### DELETE /api/tokens/{id} + +Revoke one of the authenticated user's tokens. Users can only revoke their own tokens. + +**Auth:** Any authenticated user + +**Path:** `id` — UUID of the token to revoke + +**Response:** `200 OK` + +```json +{ + "status": "revoked", + "id": "token-uuid" +} +``` + +**Errors:** `400` (invalid UUID), `404` (token not found or belongs to another user) + +--- + +## Error Format + +All error responses return a plain text body with the error message and the corresponding HTTP status code: + +| Code | Meaning | +|------|---------| +| `400` | Bad request (missing fields, invalid input) | +| `401` | Missing or invalid bearer token | +| `403` | Authenticated but insufficient role (member accessing admin endpoint) | +| `404` | Resource not found | +| `503` | Database not available | +| `500` | Internal server error | + +--- + +## Database Schema + +### users + +| Column | Type (PG / libSQL) | Notes | +|--------|--------------------|-------| +| `id` | `UUID` / `TEXT` | Primary key, UUID v4 | +| `email` | `TEXT UNIQUE` | Nullable | +| `display_name` | `TEXT NOT NULL` | | +| `status` | `TEXT NOT NULL` | `"active"` or `"suspended"` | +| `role` | `TEXT NOT NULL` | `"admin"` or `"member"` | +| `created_at` | `TIMESTAMPTZ` / `TEXT` | | +| `updated_at` | `TIMESTAMPTZ` / `TEXT` | | +| `last_login_at` | `TIMESTAMPTZ` / `TEXT` | Nullable | +| `created_by` | `TEXT` | Nullable, references `users.id` | +| `metadata` | `JSONB` / `TEXT` | Default `{}` | + +### api_tokens + +| Column | Type (PG / libSQL) | Notes | +|--------|--------------------|-------| +| `id` | `UUID` / `TEXT` | Primary key | +| `user_id` | `TEXT NOT NULL` | FK to `users.id` (PG cascades; libSQL uses explicit cleanup) | +| `token_hash` | `BYTEA` / `BLOB` | SHA-256 of hex-encoded plaintext | +| `token_prefix` | `TEXT NOT NULL` | First 8 chars for identification | +| `name` | `TEXT NOT NULL` | Human-readable label | +| `expires_at` | `TIMESTAMPTZ` / `TEXT` | Nullable | +| `last_used_at` | `TIMESTAMPTZ` / `TEXT` | Nullable | +| `created_at` | `TIMESTAMPTZ` / `TEXT` | | +| `revoked_at` | `TIMESTAMPTZ` / `TEXT` | Nullable; set on revocation |