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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user