Blame
|
1 | --- |
||||||
| 2 | category: reference |
|||||||
| 3 | tags: |
|||||||
| 4 | - phase-2 |
|||||||
| 5 | - management-api |
|||||||
| 6 | last_updated: 2026-03-13 |
|||||||
| 7 | --- |
|||||||
| 8 | ||||||||
| 9 | # P2-4: Management API |
|||||||
| 10 | ||||||||
| 11 | **Branch:** `feat/P2-4-management-api` (from `phase-2`) |
|||||||
| 12 | **Commit:** `1e80a07` |
|||||||
| 13 | **Tests:** 30 passing |
|||||||
| 14 | ||||||||
| 15 | ## What was built |
|||||||
| 16 | ||||||||
| 17 | WSGI middleware (`ManagementMiddleware`) that intercepts `/admin/*` requests before they reach the otterwiki Flask app. All routes require platform JWT authentication. |
|||||||
| 18 | ||||||||
| 19 | ## Files |
|||||||
| 20 | ||||||||
| 21 | | File | Purpose | |
|||||||
| 22 | |------|---------| |
|||||||
| 23 | | `app/management/routes.py` | WSGI middleware with 8 API endpoints | |
|||||||
| 24 | | `app/management/wiki_init.py` | Git repo init + bootstrap template writer | |
|||||||
| 25 | | `app/management/token.py` | MCP bearer token generation (secrets + bcrypt) | |
|||||||
| 26 | | `app/models/user.py` | Added `get_by_email()` scan method | |
|||||||
| 27 | | `tests/test_management_api.py` | 30 unit tests (moto + tmpdir) | |
|||||||
| 28 | ||||||||
| 29 | ## API Endpoints |
|||||||
| 30 | ||||||||
| 31 | | Method | Path | Description | |
|||||||
| 32 | |--------|------|-------------| |
|||||||
| 33 | | POST | `/admin/wikis` | Create wiki (repo init, bootstrap, ACL, token) | |
|||||||
| 34 | | GET | `/admin/wikis` | List user's wikis | |
|||||||
| 35 | | GET | `/admin/wikis/{slug}` | Wiki details + MCP endpoint | |
|||||||
| 36 | | DELETE | `/admin/wikis/{slug}` | Delete wiki (repo, DB records, ACLs) | |
|||||||
| 37 | | POST | `/admin/wikis/{slug}/token` | Regenerate MCP bearer token | |
|||||||
| 38 | | POST | `/admin/wikis/{slug}/acl` | Grant access (body: `{email, role}`) | |
|||||||
| 39 | | DELETE | `/admin/wikis/{slug}/acl/{grantee_id}` | Revoke access | |
|||||||
| 40 | | GET | `/admin/wikis/{slug}/acl` | List ACL entries | |
|||||||
| 41 | ||||||||
| 42 | ## Design decisions |
|||||||
| 43 | ||||||||
| 44 | - **WSGI middleware pattern** — wraps the otterwiki app; if `PATH_INFO` starts with `/admin`, handle it; otherwise pass through. |
|||||||
| 45 | - **Plain JSON responses** — `json.dumps()` with `Content-Type: application/json`, not Flask. |
|||||||
| 46 | - **Ownership enforcement** — delete, token regen, and ACL grant/revoke require `role=owner` in ACL table. |
|||||||
| 47 | - **Tier limits** — `wiki_count >= wiki_limit` check on create; count incremented/decremented atomically. |
|||||||
| 48 | - **Token shown once** — plaintext returned only on create and regenerate; only bcrypt hash stored. |
|||||||
| 49 | - **Repo cleanup on create failure** — if `init_wiki_repo` fails, DB records are rolled back. |
|||||||
| 50 | - **wiki_id format** — `{owner_id}:{wiki_slug}` (consistent with ACL model). |
|||||||
| 51 | - **MCP endpoint** — returns `/{slug}/mcp` for now (no username mapping yet). |
|||||||
| 52 | ||||||||
| 53 | ## Integration notes |
|||||||
| 54 | ||||||||
| 55 | To wire into the Lambda handler, wrap the otterwiki WSGI app: |
|||||||
| 56 | ||||||||
| 57 | ```python |
|||||||
| 58 | from app.management.routes import ManagementMiddleware |
|||||||
| 59 | wrapped = ManagementMiddleware( |
|||||||
| 60 | otterwiki_wsgi_app, |
|||||||
| 61 | auth_middleware=auth_middleware, |
|||||||
| 62 | user_model=users, |
|||||||
| 63 | wiki_model=wikis, |
|||||||
| 64 | acl_model=acls, |
|||||||
| 65 | ) |
|||||||
| 66 | handler = make_lambda_handler(wrapped) |
|||||||
| 67 | ``` |
|||||||
| 68 | ||||||||
| 69 | ## Not included (future work) |
|||||||
| 70 | ||||||||
| 71 | - Lambda handler wiring (will be done when all Phase 2 components integrate) |
|||||||
| 72 | - Username-to-subdomain mapping for MCP endpoint URLs |
|||||||
| 73 | - Rate limiting on wiki creation |
|||||||