docs(blueprint): document app members CRUD endpoints in §11.6
Adds a new "App Member Management Endpoints" subsection covering the shipped CRUD surface, the `my_role` field on the app lookup response, and the no-last-app-admin-guard decision (with the corrected rationale that owners — not admins — are what makes orphaning impossible). Also updates the deferred-surfaces line so it stops claiming dashboard member management is still curl-only, and bumps the Last Updated header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
# Project Blueprint: Lightweight Event-Based Serverless Cloud
|
# Project Blueprint: Lightweight Event-Based Serverless Cloud
|
||||||
|
|
||||||
**Status**: Phase 4 — Blueprint Complete
|
**Status**: Phase 4 — Blueprint Complete
|
||||||
**Last Updated**: 2026-04-10
|
**Last Updated**: 2026-05-27
|
||||||
**Audience**: Solo developer (DIY self-hosted)
|
**Audience**: Solo developer (DIY self-hosted)
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -1156,6 +1156,35 @@ DELETE /api/v1/admin/api-keys/{id} — caller's own only
|
|||||||
|
|
||||||
Every existing `/api/v1/admin/*` endpoint is re-gated from "any authed admin" to a specific `Capability`. Request/response shapes are unchanged; what changes is the set of callers each endpoint accepts (a `member` now gets 403 on app surfaces they're not part of, where before they would have been 401-or-200 depending only on session validity).
|
Every existing `/api/v1/admin/*` endpoint is re-gated from "any authed admin" to a specific `Capability`. Request/response shapes are unchanged; what changes is the set of callers each endpoint accepts (a `member` now gets 403 on app surfaces they're not part of, where before they would have been 401-or-200 depending only on session validity).
|
||||||
|
|
||||||
|
### App Member Management Endpoints
|
||||||
|
|
||||||
|
Exposes the `app_members` table as a first-class CRUD surface so app admins can manage who they share an app with from the dashboard, not just from SQL.
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v1/admin/apps/{id_or_slug}/members — list members (ordered by username),
|
||||||
|
joined with admin_users for
|
||||||
|
username / email / instance_role / is_active
|
||||||
|
POST /api/v1/admin/apps/{id_or_slug}/members — { user_id, role } → 201 enriched DTO
|
||||||
|
409 on duplicate (promotions go through PATCH)
|
||||||
|
422 if target user is_active = false
|
||||||
|
422 if target user instance_role != 'member'
|
||||||
|
(owners/admins have implicit authority;
|
||||||
|
an explicit row would be dead weight)
|
||||||
|
PATCH /api/v1/admin/apps/{id_or_slug}/members/{user_id} — { role } → 200 enriched DTO
|
||||||
|
404 if no existing membership
|
||||||
|
DELETE /api/v1/admin/apps/{id_or_slug}/members/{user_id} — 204 (idempotent — 204 also when missing)
|
||||||
|
```
|
||||||
|
|
||||||
|
All four are gated on `Capability::AppAdmin(app_id)`. Editors and viewers get 403 on list and never see the dashboard's Members tab.
|
||||||
|
|
||||||
|
**`my_role` on the app lookup endpoint.** `GET /api/v1/admin/apps/{id_or_slug}` now returns an additional `my_role: Option<AppRole>`, computed server-side from the principal: `Owner → app_admin`, `Admin → editor`, `Member → app_members.role`. The dashboard uses this single field to decide whether to render the Members tab (visible iff `my_role == app_admin`), keeping API and UI gate logic identical.
|
||||||
|
|
||||||
|
**No last-app-admin guard.** Unlike the last-owner protection on `admin_users`, removing the final `app_admin` row from `app_members` is allowed. Every `owner` instance-role user implicitly satisfies `Capability::AppAdmin(_)` via the top-level `role_grants` branch, so no app can become permanently orphaned — an owner can always re-issue grants. The `admin` instance role is only implicit *editor*, so it does **not** provide a fallback path; the owner guarantee alone is what makes the no-guard position safe.
|
||||||
|
|
||||||
|
**Dead-row sweep on promotion (deferred).** Promoting a user from `member` → `admin`/`owner` leaves their `app_members` rows in place. They become inert (implicit grants supersede), but are not auto-deleted. A future hook can sweep them; harmless for now.
|
||||||
|
|
||||||
|
Additive within `/api/v1/admin/...` — no API major bump per [docs/versioning.md](docs/versioning.md).
|
||||||
|
|
||||||
### Out of Scope (Phase 3.5)
|
### Out of Scope (Phase 3.5)
|
||||||
|
|
||||||
Schema room only, not built:
|
Schema room only, not built:
|
||||||
@@ -1164,7 +1193,7 @@ Schema room only, not built:
|
|||||||
- **MFA / TOTP** — `mfa_secret` column reserved on `admin_users`.
|
- **MFA / TOTP** — `mfa_secret` column reserved on `admin_users`.
|
||||||
- **Service accounts** — reserved as a future table; for now, every API key belongs to a human `admin_users` row.
|
- **Service accounts** — reserved as a future table; for now, every API key belongs to a human `admin_users` row.
|
||||||
|
|
||||||
Defer to follow-up sessions: dashboard surfaces for invites / member management / key minting (curl is the supported interface this phase), OIDC / SAML / SCIM, the `picloud` CLI binary itself, email/SMTP delivery of invites, audit log shipping.
|
Defer to follow-up sessions: dashboard surfaces for invites / key minting (curl is the supported interface this phase — member management has a dashboard tab; see above), OIDC / SAML / SCIM, the `picloud` CLI binary itself, email/SMTP delivery of invites, audit log shipping.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user