Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
252 lines
14 KiB
Markdown
252 lines
14 KiB
Markdown
# 02 — Current inventory: admin, roles, plans (A1/legacy), support
|
||
|
||
**Agent:** 2/20 · **Mode:** read-only inventory (docs only)
|
||
**Tools:** codehelper `project_context` (short) → `kickoff` → `investigate`/`query`/`context`; Read/Grep/Glob fallback where workspace file tools were unavailable.
|
||
|
||
**Desired target (from parent brief / sibling agents 3–6):**
|
||
|
||
| Persona / package | Desired access |
|
||
|-------------------|----------------|
|
||
| **Legacy plan (A1 + legacy-marked)** | Limited product nav only (Dashboard, Products, Feeds, Export Feeds, Categories, Attributes, Standard Fields, Billing, Settings). **No** Background Tasks / `processing.monitor`, stores, marketing extras |
|
||
| **Platform admin + developer** | Full `/admin` + full ops capabilities |
|
||
| **support_staff** | Support ticket queue / assign / reply only — **not** billing/plan mutation |
|
||
|
||
---
|
||
|
||
## 1. Executive summary
|
||
|
||
Today the platform has a **binary** staff gate (`users.is_platform_admin`) and **company membership** roles (`admin` | `member`, plus API-key role `api`). There is **no** `staff_role` / `support_staff` / `developer` enum in code or schema (`is_legacy`, `staff_role`, `support_staff`, `developer` → **zero** matches).
|
||
|
||
**A1** is modeled as a **client deal / custom package** (non-public plan name → `IsCustomPackage` → default **all features ON**), not as a limited “legacy nav” profile. Demo tenant **Local Demo Co** (ex–A1 Slovenija) is on public **Enterprise** (`is_custom=true`), which also resolves to all-ON features.
|
||
|
||
Support desk exists (tenant + admin APIs, assignee field, agent replies) but **admin support routes require full platform admin**. No CSAT/rating fields found under `apps/api/internal/support`.
|
||
|
||
---
|
||
|
||
## 2. Role model (what exists)
|
||
|
||
### 2.1 Platform staff — single boolean
|
||
|
||
| Symbol / field | Path | Notes |
|
||
|----------------|------|-------|
|
||
| `User.IsPlatformAdmin` | `apps/api/internal/auth/service.go` (`User`) | JSON `is_platform_admin` |
|
||
| `Service.IsPlatformAdmin` | `apps/api/internal/auth/invites.go:209` | DB: `SELECT is_platform_admin FROM users WHERE id = $1 AND is_active` |
|
||
| `RequirePlatformAdmin` | `apps/api/internal/httpapi/middleware.go:245` | Session + DB check; **not** a client claim |
|
||
| `checkPlatformAdmin` | `apps/api/internal/httpapi/middleware.go:262` | Test hook `testPlatformAdmin` or `Auth.IsPlatformAdmin` |
|
||
| `requirePlatformAdmin` | `apps/web/src/lib/admin-gate.ts:8` | Client gate via `GET /api/auth/me` |
|
||
| `authSession.isPlatformAdmin` | `apps/web/src/lib/auth-session.svelte.ts` | UI session mirror |
|
||
| `applyPlatformAdmins` | `apps/api/cmd/migrator/admins.go:13` | Legacy MySQL `admin_users` → `users.is_platform_admin` |
|
||
| `seed-demo` | `apps/api/cmd/seed-demo/main.go` | Forces demo user `is_platform_admin = true` |
|
||
|
||
**Missing vs desired:** `admin` / `developer` / `support_staff` staff roles; APIs to list/assign staff roles; middleware that allows support-only routes without full platform admin.
|
||
|
||
### 2.2 Company membership roles
|
||
|
||
| Concept | Where | Values |
|
||
|---------|-------|--------|
|
||
| Membership `role` | `memberships.role` (auth/company handlers) | `admin`, `member` (invite normalize rejects `owner`) |
|
||
| `CompanyAdminAllowed` | `middleware.go:43` | `admin` \|\| `api` |
|
||
| `requireCompanyAdmin` | `middleware.go:48` | 403 `"admin required"` |
|
||
| `allowCompanyAdminOrPlatform` | `middleware.go:58` | Company admin **or** platform admin (cutover: all-member tenants) |
|
||
| Team promote/demote | `company_handlers.go`, `company_member_role_test.go` | Last-admin guards |
|
||
|
||
Orthogonal to plan features (also noted in `docs/plan-permissions/02-plans-permissions-current.md` § membership).
|
||
|
||
### 2.3 Support message “agent” role (not staff RBAC)
|
||
|
||
Ticket messages use `author_role` ∈ `user` | `agent` | `system` (`apps/web/src/lib/support/admin-api.ts`). `ReplyAsAgent` (`support/tickets.go:325`) writes `author_role='agent'`. That is **message authorship**, not a user staff role.
|
||
|
||
---
|
||
|
||
## 3. Admin panel — web routes
|
||
|
||
All under `apps/web/src/routes/admin/`. Pages call `requirePlatformAdmin()` (except layout which only shows chrome when `me.user.is_platform_admin`).
|
||
|
||
| Route | File | In `AdminNav`? | Purpose |
|
||
|-------|------|----------------|---------|
|
||
| `/admin` | `+page.svelte` | Overview | Analytics summary + tool cards |
|
||
| `/admin/users` | `users/+page.svelte` | Yes | User list, set-password emails, **dev** set-password / impersonate |
|
||
| `/admin/analytics` | `analytics/+page.svelte` | Yes | Tokens / credits / jobs |
|
||
| `/admin/billing` | `billing/+page.svelte` | Yes | Plans, assign, credits, cycles + **PlanPermissionsPanel** |
|
||
| `/admin/support` | `support/+page.svelte` | Yes | Ticket queue |
|
||
| `/admin/support/[id]` | `support/[id]/+page.svelte` | (via Support) | Ticket detail / reply |
|
||
| `/admin/stuck-products` | `stuck-products/+page.svelte` | Yes | Stuck jobs |
|
||
| `/admin/settings` | `settings/+page.svelte` | Yes | Platform settings |
|
||
| `/admin/tasks-cleanup` | `tasks-cleanup/+page.svelte` | No (overview card only) | Same stuck cleanup |
|
||
| `/admin/logs` | `logs/+page.svelte` | No (deep link; nav comment: no logs API) | Placeholder |
|
||
| `/admin/bootstrap` | `bootstrap/+page.svelte` | No | Check admin flag; cannot create admins via API |
|
||
| `/admin/migrate-organizations` | `migrate-organizations/+page.svelte` | No | Migration helper UI |
|
||
|
||
**Nav chrome:** `AdminNav` — `apps/web/src/lib/components/AdminNav.svelte` (`menuItems`).
|
||
**Layout wiring:** `apps/web/src/routes/+layout.svelte` — `showAdminNav = Boolean(me?.user?.is_platform_admin)`; main app `Nav` gets `showAdmin` for “Platform admin” link (`adminOnly: true`).
|
||
|
||
---
|
||
|
||
## 4. Admin panel — API routes
|
||
|
||
Mounted in `Server.Router` — `apps/api/internal/httpapi/server.go:294–325`:
|
||
|
||
```
|
||
/api/admin/* → RequireSession + RequirePlatformAdmin
|
||
```
|
||
|
||
| Method | Path | Handler (approx) |
|
||
|--------|------|------------------|
|
||
| GET | `/users` | `handleAdminListUsers` |
|
||
| POST | `/users/{id}/dev-password` | `handleAdminDevSetPassword` (non-prod) |
|
||
| POST | `/users/{id}/impersonate` | `handleAdminDevImpersonate` (non-prod) |
|
||
| GET | `/companies` | `handleAdminListCompanies` |
|
||
| GET | `/readiness` | `handleAdminReadiness` |
|
||
| GET | `/analytics` | `handleAdminAnalytics` |
|
||
| GET | `/jobs` | `handleAdminListJobs` |
|
||
| POST | `/jobs/stuck-cleanup` | `handleAdminStuckCleanup` |
|
||
| GET/PUT | `/settings` | platform settings |
|
||
| POST | `/settings/mail/test` | mail probe |
|
||
| GET/POST | `/plans` | list / upsert |
|
||
| GET/PUT | `/plans/{planID}/features` | plan feature overrides |
|
||
| POST | `/plans/{planID}/features/enable-all` | convenience |
|
||
| POST | `/plans/{planID}/features/disable-all` | convenience |
|
||
| GET/PUT | `/feature-gates` | global gates |
|
||
| PUT | `/feature-gates/sections/{section}` | section toggle |
|
||
| POST | `/plans/assign` | assign plan to company |
|
||
| POST | `/credits` | add credits |
|
||
| POST | `/billing/run-cycles` | renewals |
|
||
| POST | `/emails/set-password` | cutover emails |
|
||
| GET | `/support/tickets` | admin queue |
|
||
| GET | `/support/tickets/{id}` | admin get |
|
||
| POST | `/support/tickets/{id}/messages` | `ReplyAsAgent` |
|
||
| PATCH | `/support/tickets/{id}` | `UpdateAdmin` (status/priority/assignee) |
|
||
|
||
**Dev-only staff tooling** (not a “developer” role): `admin_dev_handlers.go` — blocked when `Config.IsProduction()`.
|
||
|
||
---
|
||
|
||
## 5. PlanPermissionsPanel & plan features
|
||
|
||
| Piece | Path / symbol |
|
||
|-------|----------------|
|
||
| UI panel | `apps/web/src/lib/components/admin/PlanPermissionsPanel.svelte` |
|
||
| Host page | `/admin/billing` Permissions tab — `billing/+page.svelte` |
|
||
| Client API | `apps/web/src/lib/admin-plan-permissions.ts` (`listAdminPlansWithFeatures`, `loadFeatureGates`, enable/disable-all, …) |
|
||
| Catalog | `apps/web/src/lib/plan-feature-catalog.ts` (`isDefaultPublicPlanName`, sections) |
|
||
| Prior design doc | `docs/plan-permissions/08-admin-ui.md` |
|
||
|
||
**Package kind badges** (panel `$derived packageKind`):
|
||
|
||
- `default` — public ladder name + `!is_custom`
|
||
- `ladder_custom` — public ladder + `is_custom` (Enterprise)
|
||
- `custom` — `is_custom`
|
||
- `deal` — non-ladder name without treating as custom badge path
|
||
|
||
Stub mode when feature APIs return 404/501 (`isPlanPermissionsApiUnavailable`).
|
||
|
||
Backend resolve path: `DefaultPlanFeatures` → `PlanAllowsFeature` → `ResolveEffectiveFeatures` (`apps/api/internal/billing/plan_features.go`). Custom packages default **all catalog keys ON**.
|
||
|
||
---
|
||
|
||
## 6. Plans: `is_custom`, public ladder, A1 / “legacy”
|
||
|
||
### 6.1 Public vs client deals
|
||
|
||
| Symbol | Path | Behavior |
|
||
|--------|------|----------|
|
||
| `IsPublicProductPlan` | `billing/service.go:485` | `free\|starter\|growth\|business\|enterprise` only |
|
||
| `ListPublicPlans` | `billing/service.go:600` | Marketing / self-serve; **excludes** client deals |
|
||
| Admin `ListPlans` | via `/api/admin/plans` | **All** plan rows (incl. A1, Merkur, …) |
|
||
| `EnsureDefaultPlans` | `service.go:496` | Syncs public ladder only; named deals untouched |
|
||
|
||
Comments explicitly name **A1**, Merkur trial, legacy Basic/Professional as non-public client deals.
|
||
|
||
### 6.2 `IsCustomPackage` / `is_custom`
|
||
|
||
| Symbol | Path | Rule |
|
||
|--------|------|------|
|
||
| `IsCustomPackage(name, isCustom)` | `billing/custom_package_features.go:19` | `isCustom \|\| !IsPublicProductPlan(name)` |
|
||
| `prepareCustomPackageCreateFeatures` | same file `:49` | Non-public names force `IsCustom=true`; empty features → all-ON map |
|
||
| Enterprise seed | `defaultPublicPlans()` | Public name **with** `IsCustom: true` |
|
||
|
||
**Tests:** `custom_package_features_test.go` — `"A1", false, true` (name alone ⇒ custom treatment).
|
||
|
||
### 6.3 Company A1 / Local Demo Co
|
||
|
||
| Fact | Source |
|
||
|------|--------|
|
||
| Legacy MySQL company | `A1 Slovenija` / `97e1a309-3d23-4aa2-b518-8e8d7afdfec7` |
|
||
| Postgres id | `ee246275-dec0-4446-9e83-58d0c16c258a` |
|
||
| Display name after seed | **Local Demo Co** (`docs/demo-user.md`, `docs/migrate-from-descrybe-new.md`) |
|
||
| Demo plan | **Enterprise** (`is_custom=true`, 1M credits) — **not** a plan named `A1` |
|
||
| Migrated teammate emails | `…@legacy.local` (dev password / impersonate) |
|
||
|
||
**Implication:** Even if a DB row named `A1` exists as a client plan, feature defaults are **enable-all**, opposite of desired **legacy limited nav**. There is **no** `is_legacy` column or legacy feature profile in billing today.
|
||
|
||
### 6.4 Product nav gating (tenant UI)
|
||
|
||
`Nav.svelte` filters items via `planCapabilities.can(featureKey)`.
|
||
`processing.monitor` (Background Tasks / Processing) is a normal feature key — currently ON for custom/Enterprise. Desired legacy matrix would turn it **OFF** (and hide stores/marketing extras).
|
||
|
||
---
|
||
|
||
## 7. Support — tenant + admin
|
||
|
||
### 7.1 Tenant (company-scoped)
|
||
|
||
| Surface | Path |
|
||
|---------|------|
|
||
| UI | `/support`, `/support/new`, `/support/[ticketId]` under `apps/web/src/routes/support/` |
|
||
| API | `server.go:472+` — `GET/POST /api/support/tickets`, messages, notifications |
|
||
| Handlers | `handleListSupportTickets`, `Create`, `Get`, `Reply` — session + **RequireCompany** |
|
||
| Service | `ListForUser` / `Create` / `GetForUser` / `ReplyAsUser` |
|
||
|
||
Nav item: `/support` gated by feature `support.center`.
|
||
|
||
### 7.2 Admin / staff queue
|
||
|
||
| Surface | Path |
|
||
|---------|------|
|
||
| UI | `/admin/support`, `/admin/support/[id]` |
|
||
| Client | `apps/web/src/lib/support/admin-api.ts` |
|
||
| API | `/api/admin/support/tickets*` (platform admin only) |
|
||
| Service | `ListAdmin`, `GetAdmin`, `ReplyAsAgent`, `UpdateAdmin` |
|
||
| Schema | `apps/api/sql/schema/025_support_center.sql` (indexed; includes `assignee_admin_user_id`) |
|
||
|
||
**Assignee:** `UpdateAdmin` / PATCH body `assignee_admin_user_id` / `clear_assignee`. No separate “support staff inbox” or claim-only policy — any platform admin sees the full queue.
|
||
|
||
**Ratings / CSAT:** not present in support package types/handlers (gap for agent 11+).
|
||
|
||
---
|
||
|
||
## 8. Gaps vs desired state
|
||
|
||
| Desired | Current | Gap |
|
||
|---------|---------|-----|
|
||
| Legacy limited nav (A1 / legacy plans) | Custom/non-public → **all features ON**; demo A1 tenant on **Enterprise** all-ON | Need legacy profile / `is_legacy` / seed matrix; map A1 (+ similar) to sparse features; turn off `processing.monitor`, stores, marketing extras |
|
||
| Admin + developer full admin | Only `is_platform_admin` boolean | Need staff role enum; map admin+developer → full `/api/admin`; optionally keep prod-safe “dev tools” separate from role |
|
||
| support_staff ticket access only | Support admin APIs behind **same** `RequirePlatformAdmin` as billing | Need least-privilege middleware + AdminNav subset (Support only); block plans/settings/credits |
|
||
| Role chips / assign staff from orgs UI | Users list shows `is_platform_admin` badge only | No assign APIs; bootstrap page cannot create admins |
|
||
| Support assign + staff inbox + CSAT | Assignee field exists; no staff role, no CSAT | Extend UpdateAdmin + UI; add rating schema |
|
||
| Differentiate legacy vs `is_custom` | `is_custom` / deal name ⇒ enable-all | Contract must separate **custom deal packaging** from **legacy feature profile** |
|
||
|
||
---
|
||
|
||
## 9. Extension points (summary)
|
||
|
||
Prefer extending existing gates rather than parallel systems:
|
||
|
||
1. **Staff RBAC** — extend `checkPlatformAdmin` / `RequirePlatformAdmin` (or sibling `RequireStaffRole`) reading a new `users.staff_role` (or replace boolean carefully).
|
||
2. **Legacy features** — extend `DefaultPlanFeatures` / `IsCustomPackage` / seed (`default_plan_features_seed.go`) with a `legacy` profile; detection: plan name patterns (`A1`, …) and/or `is_legacy` flag.
|
||
3. **Admin nav** — `AdminNav.menuItems` filtered by staff capability; support_staff → Support (+ maybe Overview read-only if needed).
|
||
4. **Support** — keep `/api/admin/support/*` but authorize `admin|developer|support_staff`; optionally filter `ListAdmin` by assignee for least privilege.
|
||
5. **PlanPermissionsPanel** — already the UI to inspect/set matrices; add Legacy badge + apply-legacy-profile action once backend exists.
|
||
|
||
Machine-readable companion: [`02-extension-points.json`](./02-extension-points.json).
|
||
|
||
---
|
||
|
||
## 10. Related docs
|
||
|
||
- `docs/plan-permissions/02-plans-permissions-current.md` — plans / features / roles orthogonality
|
||
- `docs/plan-permissions/08-admin-ui.md` — PlanPermissionsPanel contract
|
||
- `docs/demo-user.md` — Local Demo Co / A1 migration
|
||
- `docs/migrate-from-descrybe-new.md` — A1-only migrator scope
|
||
- Sibling agents will add `03-roles-matrix`, `04-contract`, `05-legacy-seed`, `06-staff-roles`, …
|