feat: enhance project and scenario management with role-based access control

- Implemented role-based access control for project and scenario routes.
- Added authorization checks to ensure users have appropriate roles for viewing and managing projects and scenarios.
- Introduced utility functions for ensuring project and scenario access based on user roles.
- Refactored project and scenario routes to utilize new authorization helpers.
- Created initial data seeding script to set up default roles and an admin user.
- Added tests for authorization helpers and initial data seeding functionality.
- Updated exception handling to include authorization errors.
This commit is contained in:
2025-11-09 23:14:54 +01:00
parent 27262bdfa3
commit 0f79864188
16 changed files with 997 additions and 132 deletions

View File

@@ -6,7 +6,8 @@ from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from dependencies import get_unit_of_work
from dependencies import get_unit_of_work, require_authenticated_user
from models import User
from models import ScenarioStatus
from services.unit_of_work import UnitOfWork
@@ -27,6 +28,8 @@ def _format_timestamp_with_time(moment: datetime | None) -> str | None:
def _load_metrics(uow: UnitOfWork) -> dict[str, object]:
if not uow.projects or not uow.scenarios or not uow.financial_inputs:
raise RuntimeError("UnitOfWork repositories not initialised")
total_projects = uow.projects.count()
active_scenarios = uow.scenarios.count_by_status(ScenarioStatus.ACTIVE)
pending_simulations = uow.scenarios.count_by_status(ScenarioStatus.DRAFT)
@@ -40,11 +43,15 @@ def _load_metrics(uow: UnitOfWork) -> dict[str, object]:
def _load_recent_projects(uow: UnitOfWork) -> list:
if not uow.projects:
raise RuntimeError("Project repository not initialised")
return list(uow.projects.recent(limit=5))
def _load_simulation_updates(uow: UnitOfWork) -> list[dict[str, object]]:
updates: list[dict[str, object]] = []
if not uow.scenarios:
raise RuntimeError("Scenario repository not initialised")
scenarios = uow.scenarios.recent(limit=5, with_project=True)
for scenario in scenarios:
project_name = scenario.project.name if scenario.project else f"Project #{scenario.project_id}"
@@ -65,6 +72,9 @@ def _load_scenario_alerts(
) -> list[dict[str, object]]:
alerts: list[dict[str, object]] = []
if not uow.scenarios:
raise RuntimeError("Scenario repository not initialised")
drafts = uow.scenarios.list_by_status(
ScenarioStatus.DRAFT, limit=3, with_project=True
)
@@ -102,6 +112,7 @@ def _load_scenario_alerts(
@router.get("/", response_class=HTMLResponse, include_in_schema=False, name="dashboard.home")
def dashboard_home(
request: Request,
_: User = Depends(require_authenticated_user),
uow: UnitOfWork = Depends(get_unit_of_work),
) -> HTMLResponse:
context = {