Add user feature permissions and unified account management
Magent CI/CD / verify (push) Canceled after 1m19s
Magent CI/CD / deploy-prod (push) Canceled after 0s
Magent CI/CD / deploy-beta (push) Canceled after 0s

This commit is contained in:
2026-09-11 12:31:25 +12:00
parent e2be8b3872
commit ec0a866ef3
32 changed files with 650 additions and 214 deletions
+69
View File
@@ -0,0 +1,69 @@
from fastapi import Depends, HTTPException, Request
from .auth import get_current_user, get_current_user_event_stream
from .db import get_portal_item
def check(user: dict, *features: str) -> None:
access = user.get("features") or {}
if user.get("role") == "admin":
return
if not any(access.get(feature, False) for feature in features):
raise HTTPException(status_code=403, detail="This feature is disabled for your account")
def require_stats(user: dict = Depends(get_current_user)) -> dict:
check(user, "stats")
return user
def require_invites(user: dict = Depends(get_current_user)) -> dict:
check(user, "invites")
return user
def require_request_access(request: Request, user: dict = Depends(get_current_user)) -> None:
path = request.url.path.rstrip("/")
if path.endswith("/search") and "/actions/" not in path:
# The issue picker uses the same media search; creation is checked separately.
check(user, "new_requests", "issues")
elif path.endswith(("/create", "/request-options")):
check(user, "new_requests")
elif path.endswith(("/issue-options", "/replacement-options", "/actions/replace", "/actions/search-missing", "/actions/repair-subtitles")):
check(user, "issues")
else:
check(user, "requests")
async def require_portal_access(request: Request, user: dict = Depends(get_current_user)) -> None:
if user.get("role") == "admin":
return
path = request.url.path.rstrip("/")
access = user.get("features", {})
if access.get("requests") and access.get("issues") and access.get("new_requests"):
return
if "/issues" in path:
check(user, "issues")
elif path.endswith("/requests") or path.endswith("/pipeline"):
check(user, "requests")
elif "item_id" in request.path_params:
try:
item = get_portal_item(int(request.path_params["item_id"]))
except (ValueError, TypeError):
item = None
if not item:
raise HTTPException(status_code=404, detail="Item not found")
check(user, "requests" if item.get("kind") == "request" else "issues")
elif path.endswith("/items") and request.method == "POST":
payload = await request.json()
check(user, "new_requests" if isinstance(payload, dict) and payload.get("kind", "request") == "request" else "issues")
elif path.endswith(("/items", "/overview")) and request.query_params.get("kind"):
check(user, "requests" if request.query_params["kind"] == "request" else "issues")
else:
# Unfiltered lists/overview can include both kinds.
check(user, "requests")
check(user, "issues")
def require_request_stream(user: dict = Depends(get_current_user_event_stream)) -> dict:
check(user, "requests")
return user