Files
sales-trainer/docs/SAAS_PLAN.md
Macky 3d5c81fbd7 feat(saas): Phase 3 — plan/seats/active model, ToS consent, signed expiring export
P3a: org carries plan/seats/active/created_at; create_user enforces seats + rejects
inactive org; verify blocks login for inactive orgs; PATCH /api/admin/orgs (super_admin)
updates plan/seats/active with audit. Fixed verify swallowing its AuthError.
P3b: export/token issues a 5-min HMAC one-time CSV link; export accepts ?token=.
P3c: setup requires accepted_terms (consent stored); Setup.vue consent checkbox.
All 8 backend suites pass. Rebuilt dist.
2026-08-09 09:48:55 +07:00

51 lines
3.1 KiB
Markdown

# SaaS / Multi-tenant Plan — Sales Trainer
Product is being sold as SaaS. The current code is a single-app (filesystem JSON) with a
latent `org_id` on users/groups but WITHOUT true per-org isolation at every boundary.
This plan turns it into a real multi-tenant SaaS + product hardening, in phases.
## Current state (verified)
- `org_id` exists on: users (`users.py:30,78`), groups (`groups.py:33`), created at bootstrap
as `org-default` (`factory.py:19`).
- Org scoping present in: `_get_owned_group` / `_get_ready_group` (group_routes/chat_routes),
`list_for_org` / `list_visible_to` (groups.py), `list_users(org_id=...)` (users.py),
admin/me/analytics routes filter by `actor.org_id`.
- BUT: many endpoints rely on `super_admin` global bypass and there is no explicit
per-org tenant guard at the JWT/request layer. Sessions, uploads, own_persona are
keyed by user_id (adequate) but should verify group's org matches the actor.
## Phase 1 — Enforce tenant isolation (multi-tenant correctness) ✅ DONE
1. **Tenant context on auth**: `g.org_id` set in `require_auth`. ✅
2. **Single choke-point guard**: `assert_tenant()` + `current_org_id()` in helpers; existing
`_authorize_group` org check kept. super_admin is global; admins org-scoped. ✅
3. **Multi-org create**: `POST /api/admin/users {new_org:true}` (super_admin) creates a new
org + its first admin; `GET /api/admin/orgs` platform view (super_admin sees all,
admin sees own). Fixed `create_org` double-id bug. ✅
4. **Regression test** `test_saas_tenant.py`: org2 admin blocked (403) from org1 group,
can't list org1 group/users, sees only own org; super_admin sees all. ✅
## Phase 2 — Product hardening (provenance, abuse, secrets) ✅ DONE
1. **Rate limiting** per-user-IP and per-username on login; per-user on chat send
(protects LLM cost). New `services/rate_limit.py` (in-memory + disk, no deps). ✅
2. **Audit log** `data/audit/audit.jsonl` on org.create, user.promote_super_admin,
analytics.export. ✅
3. **CSV export org-scoped** — admin exports only own org's sessions. ✅
## Phase 3 — SaaS-launch readiness ✅ DONE
1. **Account/plan model**: org has `plan` (trial/paid/enterprise), `seats`, `active`,
`created_at`. `create_user` enforces seats + active; `verify` blocks login for
inactive orgs. `PATCH /api/admin/orgs/<id>` (super_admin) sets plan/seats/active + audit.
2. **ToS/consent**: setup now requires `accepted_terms` (stored as accepted_terms_at);
Setup.vue shows a ToS/Privacy consent checkbox (links /legal stub). ✅
3. **Signed expiring export link**: `/api/analytics/export/token` issues a 5-min HMAC
signed one-time link; `/api/analytics/export?token=` accepts it (Bearer JWT still works).
Seats/active enforced on user creation. ✅
4. Persona "recipe" server-side obfuscation: interim since strip (Phase 1); full move of
internal logic server-side remains a longer-term item (out of v1 launch).
## Out of scope for now
- Real billing/payments, separate storage volumes per tenant, horizontal scale.
---
Status flags next to each phase item updated as work completes.