Some checks failed
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Failing after 0s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Failing after 4m0s
Test Suite / playwright-tests (push) Failing after 3m41s
- Introduced `email_verified` and `account_state` attributes in the `TenantContext` to improve user state management. - Updated the `resolve_tenant_context` function to dynamically fetch `email_verified` status from the database and determine `account_state` based on user roles and memberships. - Implemented `assert_min_account_state` checks across various endpoints to enforce access control based on user account status. - Incremented version to 1.1.0 in version.py to reflect these enhancements in tenant context management and access control.
90 lines
2.7 KiB
Python
90 lines
2.7 KiB
Python
"""
|
|
Zusammenstellung effektiver Rechte für GET /api/me/entitlements (M4).
|
|
|
|
Spez: CAPABILITY_CATALOG.v1.md §7.1, CLUB_MEMBERSHIP_AND_FEATURES.v1.md §8.1
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from typing import Any, Dict, Optional, TYPE_CHECKING
|
|
|
|
from fastapi import HTTPException
|
|
|
|
from capabilities import club_roles_in_club, resolve_capabilities_map
|
|
from club_features import club_features_map
|
|
from club_tenancy import is_platform_admin
|
|
from tenant_context import _club_exists
|
|
|
|
if TYPE_CHECKING:
|
|
from tenant_context import TenantContext
|
|
|
|
|
|
def _serialize_reset_at(value: Any) -> Optional[str]:
|
|
if value is None:
|
|
return None
|
|
if isinstance(value, datetime):
|
|
if value.tzinfo is None:
|
|
return value.replace(tzinfo=None).isoformat() + "Z"
|
|
return value.isoformat()
|
|
return str(value)
|
|
|
|
|
|
def _resolve_target_club_id(
|
|
cur,
|
|
tenant: "TenantContext",
|
|
club_id: Optional[int],
|
|
) -> Optional[int]:
|
|
"""Effektiver Verein für Entitlements (Query > Tenant)."""
|
|
target = int(club_id) if club_id is not None else tenant.effective_club_id
|
|
if target is None:
|
|
return None
|
|
|
|
if is_platform_admin(tenant.global_role):
|
|
if not _club_exists(cur, target):
|
|
raise HTTPException(status_code=400, detail="Verein nicht gefunden")
|
|
return target
|
|
|
|
if target not in tenant.club_ids:
|
|
raise HTTPException(status_code=403, detail="Keine Mitgliedschaft in diesem Verein")
|
|
return target
|
|
|
|
|
|
def build_me_entitlements(
|
|
cur,
|
|
tenant: "TenantContext",
|
|
*,
|
|
club_id: Optional[int] = None,
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Kombiniert Account-Status, Capabilities und Feature-Kontingente.
|
|
"""
|
|
target_club = _resolve_target_club_id(cur, tenant, club_id)
|
|
club_roles = club_roles_in_club(tenant, target_club) if target_club is not None else []
|
|
|
|
capabilities = resolve_capabilities_map(cur, tenant, club_id=target_club)
|
|
|
|
features: Dict[str, Any] = {}
|
|
plan_id = None
|
|
if target_club is not None:
|
|
raw = club_features_map(cur, target_club)
|
|
plan_id = raw.get("plan_id")
|
|
for fid, row in (raw.get("features") or {}).items():
|
|
features[fid] = {
|
|
"allowed": row.get("allowed"),
|
|
"used": row.get("used"),
|
|
"limit": row.get("limit"),
|
|
"remaining": row.get("remaining"),
|
|
"reset_at": _serialize_reset_at(row.get("reset_at")),
|
|
"reason": row.get("reason"),
|
|
}
|
|
|
|
return {
|
|
"account_state": tenant.account_state,
|
|
"portal_role": tenant.global_role,
|
|
"club_id": target_club,
|
|
"plan_id": plan_id,
|
|
"club_roles": club_roles,
|
|
"capabilities": capabilities,
|
|
"features": features,
|
|
}
|