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.
This commit is contained in:
Macky
2026-08-09 09:48:55 +07:00
parent 056753e8cb
commit 3d5c81fbd7
36 changed files with 230 additions and 64 deletions

View File

@@ -32,7 +32,17 @@ class UserStore:
# ── org ────────────────────────────────────────────────────────────
def create_org(self, name: str, *, org_id: str | None = None) -> dict[str, Any]:
oid = org_id or new_id("org")
return self.orgs.create({"name": name, "id": oid}, key=oid)
return self.orgs.create(
{
"name": name,
"id": oid,
"plan": "trial",
"seats": 5,
"active": True,
"created_at": __import__("time").strftime("%Y-%m-%dT%H:%M:%SZ"),
},
key=oid,
)
def get_org(self, org_id: str) -> dict[str, Any]:
return self.orgs.get(org_id)
@@ -55,7 +65,16 @@ class UserStore:
) -> dict[str, Any]:
if role not in Config.ROLES:
raise AuthError(f"invalid role: {role}")
self.orgs.get(org_id)
org = self.orgs.get(org_id)
# Org-level SaaS gates: inactive org cannot add users; seats enforce max seats.
if not org.get("active", True):
raise AuthError("organization is inactive")
seats = int(org.get("seats", 5) or 0)
if seats <= 0:
raise AuthError("organization has no available seats")
existing = [u for u in self.users.all() if u.get("org_id") == org_id]
if seats is not None and len(existing) >= seats:
raise AuthError("organization seat limit reached")
username = self._norm(username)
if not username or not password:
raise AuthError("username and password are required")
@@ -156,6 +175,12 @@ class UserStore:
user = self.get_user_or_none(ident) or self.by_email(ident)
if not user or not user.get("active", True):
raise AuthError("invalid credentials")
# SaaS gate: inactive organization cannot sign in (platform can disable a tenant).
oid = user.get("org_id")
if oid:
org = self.orgs.get_or_none(oid)
if org is not None and not org.get("active", True):
raise AuthError("organization is inactive")
if not check_password_hash(user["password_hash"], password):
raise AuthError("invalid credentials")
return user