cargagep-v2/src/core/controller/mod.rs
2026-08-24 11:09:59 +02:00

165 lines
4.7 KiB
Rust

//! The controller holds the business logic: it is the only place that knows the
//! rules of the app. It talks to the outside world through the repository
//! traits, so it depends neither on axum nor on sqlx.
//!
//! Authorization is carried by the **type**, not by a check inside the
//! handlers. Each level derefs into the one below, so a manager can do
//! everything a logged in user can:
//!
//! ```text
//! AnonAppController anybody, logged in or not
//! └─ AppController a logged in user (the api extractor answers 401 without a session)
//! ├─ ManagerAppController a member of one unit, for that unit
//! └─ AdminAppController an admin
//! ```
//!
//! A handler that takes an `AppController` cannot be reached anonymously: there
//! is no way to forget the check.
use std::{ops::Deref, sync::Arc};
use thiserror::Error;
use crate::core::{
controller::{
authn::AuthnControllerError, bikes::BikesControllerError,
reservations::ReservationsControllerError,
},
models::{unit::UnitId, user::User},
repositories::{DatabaseRepository, RepositoryError},
};
pub mod authn;
pub mod bikes;
pub mod reservations;
pub mod users;
/// Entry point of the business logic, for anybody. Cheap to clone: handlers get
/// one per request.
#[derive(Clone)]
pub struct AnonAppController {
pub(crate) db: Arc<Box<dyn DatabaseRepository + Send + Sync>>,
}
impl AnonAppController {
pub fn new(db: Arc<Box<dyn DatabaseRepository + Send + Sync>>) -> Self {
Self { db }
}
/// Called by the api extractor once the session has been resolved
pub fn auth(self, user: User) -> AppController {
AppController { inner: self, user }
}
}
/// A logged in user.
#[derive(Clone)]
pub struct AppController {
inner: AnonAppController,
user: User,
}
impl Deref for AppController {
type Target = AnonAppController;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl AppController {
pub fn user(&self) -> &User {
&self.user
}
pub fn try_into_manager(
self,
unit: Option<UnitId>,
) -> Result<ManagerAppController, ControllerError> {
let allowed = match unit {
Some(unit) => self.user.admin || self.user.units.iter().any(|u| u.id == unit),
None => self.user.admin,
};
if allowed {
Ok(ManagerAppController { inner: self, unit })
} else {
match unit {
Some(unit) => Err(ControllerError::ManagerAuthorizationError(unit)),
None => Err(ControllerError::AdminAuthorizationError),
}
}
}
pub fn try_into_admin(self) -> Result<AdminAppController, ControllerError> {
if self.user.admin {
Ok(AdminAppController { inner: self })
} else {
Err(ControllerError::AdminAuthorizationError)
}
}
}
#[derive(Clone)]
pub struct ManagerAppController {
inner: AppController,
pub(crate) unit: Option<UnitId>,
}
impl Deref for ManagerAppController {
type Target = AppController;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
pub struct AdminAppController {
inner: AppController,
}
impl Deref for AdminAppController {
type Target = AppController;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl AdminAppController {
pub fn into_manager(self, unit: Option<UnitId>) -> ManagerAppController {
ManagerAppController {
inner: self.inner,
unit,
}
}
}
/// Every error the api may have to translate into a status code.
/// Domain specific errors are nested, so each area keeps its own enum.
#[derive(Error, Debug)]
pub enum ControllerError {
#[error("Internal error: {0}")]
InternalError(String),
#[error("Generic repository error")]
RepositoryError(#[from] RepositoryError),
#[error("Authorization denied: the user is not part of the unit {0:?}")]
ManagerAuthorizationError(UnitId),
#[error("Authorization denied: the user is not an admin")]
AdminAuthorizationError,
#[error("Object's unit modification is not allowed")]
ImmutableUnitModificationError,
#[error("Authentication error: {0}")]
Authn(#[from] AuthnControllerError),
#[error("Bike specific error: {0}")]
Bike(#[from] BikesControllerError),
#[error("Reservation specific error: {0}")]
Reservation(#[from] ReservationsControllerError),
}
impl ControllerError {
/// Handy in the handlers: `Err(err) if err.is_not_found() => 404`
pub fn is_not_found(&self) -> bool {
matches!(
self,
ControllerError::RepositoryError(RepositoryError::NotFound(_))
)
}
}