165 lines
4.7 KiB
Rust
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(_))
|
|
)
|
|
}
|
|
}
|