Files

129 lines
4.9 KiB
Markdown
Raw Permalink Normal View History

# 05 — Plan permissions API (agent 5/10)
Session/dashboard HTTP surface for plan feature permissions. Aligns with
`03-permission-contract.md`. Public `/api/v1` OpenAPI is unchanged (API-key product
API); schemas below are the session contract for admin UI + dashboard gating.
Backend storage + resolve live in `billing` per `04-backend-model.md` /
contract §2 / §6 (`plans.features` JSONB + `platform_feature_gates`). Migration:
`apps/api/sql/schema/026_plan_features.sql`. Apply with `make migrate` /
`.\scripts\migrate.ps1` before relying on writes.
Handlers call: `CapabilitiesForCompany`, `GetPlanFeatures`, `SetPlanFeatures`,
`EnableAllPlanFeatures`, `GetFeatureGates`, `SetFeatureGates`, `SetSectionGate`
(and agent-4 aliases `EnableAllForPlan` / `SetGlobalFeature` / `AssertFeature`).
---
## Endpoint list + auth
| Method | Path | Auth | Purpose |
|--------|------|------|---------|
| `GET` | `/api/billing/capabilities` | Session + company | Effective features = plan ∩ global |
| `GET` | `/api/billing/credits` | Session + company | Existing credits overview **plus** `features`, `disabled_features`, `feature_etag` |
| `GET` | `/api/auth/me` | Session | Embeds credits overview (same feature fields when credits load) |
| `GET` | `/api/admin/plans` | Session + **platform admin** | Plans include `features` (overrides) + `resolved_features` (plan_allows) |
| `POST` | `/api/admin/plans` | Session + **platform admin** | Upsert plan; optional `features` replaces overrides when present |
| `GET` | `/api/admin/plans/{planID}/features` | Session + **platform admin** | Plan feature editor payload |
| `PUT` | `/api/admin/plans/{planID}/features` | Session + **platform admin** | Replace overrides `{ "features": { "key": true } }` |
| `POST` | `/api/admin/plans/{planID}/features/enable-all` | Session + **platform admin** | Set every registry key `true` (custom packages) |
| `GET` | `/api/admin/feature-gates` | Session + **platform admin** | Global section + feature master switches |
| `PUT` | `/api/admin/feature-gates` | Session + **platform admin** | Partial upsert `{ "sections": {...}, "features": {...} }` |
| `PUT` | `/api/admin/feature-gates/sections/{section}` | Session + **platform admin** | Enable/disable one section for **ALL** plans |
Mutations require CSRF (dashboard session stack) + platform admin for `/api/admin/*`.
Unknown feature keys / sections → `400` `{ "error": "…" }` via `billing.ClientError`.
---
## Response shapes
### `GET /api/billing/capabilities`
```json
{
"plan_id": 3,
"plan_name": "Starter",
"is_custom": false,
"has_active_plan": true,
"features": { "catalog.products": true, "settings.api_keys": true },
"sections": { "catalog": true, "marketing": true },
"disabled_features": ["integrations.ai.byok"],
"feature_etag": "sha256:…",
"entitlements": {
"plan_name": "Starter",
"is_free_plan": false,
"is_paid_plan": true,
"can_use_ai": true,
"can_use_eprel": true
}
}
```
### `GET|PUT /api/admin/plans/{planID}/features`
```json
{
"plan_id": 3,
"plan_name": "Starter",
"is_custom": false,
"features": { "settings.api_keys": true },
"resolved_features": { "catalog.products": true, "integrations.ai.byok": false }
}
```
- `features` = stored sparse overrides (`{}` if none)
- `resolved_features` = full registry via `plan_allows` only (globals ignored)
### `GET|PUT /api/admin/feature-gates`
```json
{
"sections": { "shell": true, "marketing": false },
"features": { "capability.byok": false }
}
```
Missing gate rows default **enabled** (migration-safe).
### `PUT /api/admin/feature-gates/sections/{section}`
```json
{ "enabled": false }
```
Returns full `FeatureGatesView`.
---
## OpenAPI (session fragment)
See `docs/plan-permissions/05-openapi-fragment.yaml` for OpenAPI 3.1 path/components
suitable for admin/dashboard clients. Not mounted on `/api/v1/openapi.yaml`.
---
## Files touched (agent 5)
| File | Intent |
|------|--------|
| `apps/api/internal/httpapi/plan_features_handlers.go` | Handlers |
| `apps/api/internal/httpapi/server.go` | Route mount |
| `apps/api/internal/billing/plan_features.go` | Service + DTOs (shared with agent 4) |
| `apps/api/internal/billing/feature_catalog.go` | Registry from `01-feature-keys.json` |
| `apps/api/internal/billing/service.go` | `Plan`/`CreditsOverview`/`ListPlans`/`UpsertPlan` |
| `apps/api/internal/billing/client_errors.go` | Client-facing feature errors |
| `apps/api/sql/schema/026_plan_features.sql` | Additive migration |
---
## Manual verify
1. `.\scripts\migrate.ps1` (or `make migrate`)
2. Platform admin: `GET /api/admin/feature-gates` → all sections true
3. `PUT /api/admin/feature-gates/sections/marketing` `{ "enabled": false }`
4. Tenant session: `GET /api/billing/capabilities` → marketing keys in `disabled_features`
5. `POST /api/admin/plans/{id}/features/enable-all` then `GET` → all `resolved_features` true
6. Non-admin `PUT /api/admin/feature-gates` → 403