feat: Implement user and role management with repositories

- Added RoleRepository and UserRepository for managing roles and users.
- Implemented methods for creating, retrieving, and assigning roles to users.
- Introduced functions to ensure default roles and an admin user exist in the system.
- Updated UnitOfWork to include user and role repositories.
- Created new security module for password hashing and JWT token management.
- Added tests for authentication flows, including registration, login, and password reset.
- Enhanced HTML templates for user registration, login, and password management with error handling.
- Added a logo image to the static assets.
This commit is contained in:
2025-11-09 21:48:35 +01:00
parent 53879a411f
commit 3601c2e422
22 changed files with 1955 additions and 132 deletions

View File

@@ -6,12 +6,16 @@ from typing import Callable, Sequence
from sqlalchemy.orm import Session
from config.database import SessionLocal
from models import Scenario
from models import Role, Scenario
from services.repositories import (
FinancialInputRepository,
ProjectRepository,
RoleRepository,
ScenarioRepository,
SimulationParameterRepository,
UserRepository,
ensure_admin_user as ensure_admin_user_record,
ensure_default_roles,
)
from services.scenario_validation import ScenarioComparisonValidator
@@ -23,6 +27,12 @@ class UnitOfWork(AbstractContextManager["UnitOfWork"]):
self._session_factory = session_factory
self.session: Session | None = None
self._scenario_validator: ScenarioComparisonValidator | None = None
self.projects: ProjectRepository | None = None
self.scenarios: ScenarioRepository | None = None
self.financial_inputs: FinancialInputRepository | None = None
self.simulation_parameters: SimulationParameterRepository | None = None
self.users: UserRepository | None = None
self.roles: RoleRepository | None = None
def __enter__(self) -> "UnitOfWork":
self.session = self._session_factory()
@@ -31,6 +41,8 @@ class UnitOfWork(AbstractContextManager["UnitOfWork"]):
self.financial_inputs = FinancialInputRepository(self.session)
self.simulation_parameters = SimulationParameterRepository(
self.session)
self.users = UserRepository(self.session)
self.roles = RoleRepository(self.session)
self._scenario_validator = ScenarioComparisonValidator()
return self
@@ -42,6 +54,12 @@ class UnitOfWork(AbstractContextManager["UnitOfWork"]):
self.session.rollback()
self.session.close()
self._scenario_validator = None
self.projects = None
self.scenarios = None
self.financial_inputs = None
self.simulation_parameters = None
self.users = None
self.roles = None
def flush(self) -> None:
if not self.session:
@@ -61,7 +79,7 @@ class UnitOfWork(AbstractContextManager["UnitOfWork"]):
def validate_scenarios_for_comparison(
self, scenario_ids: Sequence[int]
) -> list[Scenario]:
if not self.session or not self._scenario_validator:
if not self.session or not self._scenario_validator or not self.scenarios:
raise RuntimeError("UnitOfWork session is not initialised")
scenarios = [self.scenarios.get(scenario_id)
@@ -75,3 +93,26 @@ class UnitOfWork(AbstractContextManager["UnitOfWork"]):
if not self._scenario_validator:
raise RuntimeError("UnitOfWork session is not initialised")
self._scenario_validator.validate(scenarios)
def ensure_default_roles(self) -> list[Role]:
if not self.roles:
raise RuntimeError("UnitOfWork session is not initialised")
return ensure_default_roles(self.roles)
def ensure_admin_user(
self,
*,
email: str,
username: str,
password: str,
) -> None:
if not self.users or not self.roles:
raise RuntimeError("UnitOfWork session is not initialised")
ensure_default_roles(self.roles)
ensure_admin_user_record(
self.users,
self.roles,
email=email,
username=username,
password=password,
)