147 lines
4.7 KiB
Rust
147 lines
4.7 KiB
Rust
//! HTTP layer: routing, session handling and OpenAPI generation (aide).
|
|
//!
|
|
//! Authorization is carried by the extractor a handler asks for:
|
|
//! - `AnonAppController` always succeeds;
|
|
//! - `AppController` resolves the session and answers **401** without one, so
|
|
//! a route cannot accidentally be left open.
|
|
//!
|
|
//! Handlers stay thin: extract the controller, call it, map `ControllerError`
|
|
//! to a status code. No business logic here.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use aide::{
|
|
OperationInput, OperationOutput,
|
|
axum::{ApiRouter, routing::get_with},
|
|
generate::GenContext,
|
|
openapi::{OpenApi, Operation, Response},
|
|
};
|
|
use axum::{
|
|
Extension, Json, Router,
|
|
extract::FromRequestParts,
|
|
http::{StatusCode, request::Parts},
|
|
};
|
|
use axum_login::{AuthManagerLayerBuilder, AuthSession, tower_sessions::SessionManagerLayer};
|
|
use indexmap::IndexMap;
|
|
use tower_sessions::{Expiry, MemoryStore, cookie::SameSite, cookie::time::Duration};
|
|
use tracing::error;
|
|
|
|
use crate::{
|
|
core::controller::{AnonAppController, AppController},
|
|
utils,
|
|
};
|
|
|
|
mod auth;
|
|
mod bikes;
|
|
mod docs;
|
|
mod helpers;
|
|
|
|
pub fn get_router(aac: AnonAppController) -> Router {
|
|
aide::generate::on_error(|err| error!("aide generated error: {err}"));
|
|
aide::generate::extract_schemas(true);
|
|
|
|
let config = utils::config::get();
|
|
|
|
// Sessions live in memory: everybody is logged out when the backend
|
|
// restarts. Swap the store for a persistent one if that becomes a problem.
|
|
let session_layer = SessionManagerLayer::new(MemoryStore::default())
|
|
// Over plain http in development the cookie cannot be `Secure`
|
|
.with_secure(config.get_base_url().starts_with("https://"))
|
|
// The provider sends the browser back with a top level navigation
|
|
.with_same_site(SameSite::Lax)
|
|
.with_expiry(Expiry::OnInactivity(Duration::minutes(
|
|
config.get_session_lifetime(),
|
|
)));
|
|
let auth_layer = AuthManagerLayerBuilder::new(aac.clone(), session_layer).build();
|
|
|
|
let mut api = OpenApi::default();
|
|
ApiRouter::new()
|
|
.api_route(
|
|
"/api/version",
|
|
get_with(
|
|
async || Json(env!("CARGO_PKG_VERSION").to_string()),
|
|
|op| op.tag("misc").summary("Get app version"),
|
|
),
|
|
)
|
|
// `auth` carries its own `/api/...` paths, so it is merged, not nested
|
|
.merge(auth::routes())
|
|
.nest_api_service("/api/bikes", bikes::routes())
|
|
.nest_api_service("/api/docs", docs::routes())
|
|
.finish_api_with(&mut api, docs::api_docs_metadata)
|
|
.layer(Extension(aac))
|
|
.layer(Extension(Arc::new(api)))
|
|
.layer(auth_layer)
|
|
}
|
|
|
|
/// Lets a handler take an `AnonAppController`: always available.
|
|
impl<S> FromRequestParts<S> for AnonAppController
|
|
where
|
|
S: Send + Sync,
|
|
{
|
|
type Rejection = StatusCode;
|
|
|
|
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
|
parts
|
|
.extensions
|
|
.get::<AnonAppController>()
|
|
.cloned()
|
|
.ok_or(StatusCode::INTERNAL_SERVER_ERROR)
|
|
}
|
|
}
|
|
|
|
/// Lets a handler take an `AppController`, which requires a session: asking for
|
|
/// it *is* the authentication check.
|
|
impl<S> FromRequestParts<S> for AppController
|
|
where
|
|
S: Send + Sync,
|
|
{
|
|
type Rejection = StatusCode;
|
|
|
|
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
|
use axum::RequestPartsExt;
|
|
|
|
let aac = parts
|
|
.extensions
|
|
.get::<AnonAppController>()
|
|
.cloned()
|
|
.ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;
|
|
|
|
let session = parts
|
|
.extract::<AuthSession<AnonAppController>>()
|
|
.await
|
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
|
|
|
let user = session.user.ok_or(StatusCode::UNAUTHORIZED)?;
|
|
Ok(aac.auth(user))
|
|
}
|
|
}
|
|
|
|
// The controllers are not part of the request/response bodies, but asking for
|
|
// an `AppController` documents the 401 and the cookie requirement.
|
|
impl OperationOutput for AnonAppController {
|
|
type Inner = Self;
|
|
}
|
|
impl OperationInput for AnonAppController {}
|
|
|
|
impl OperationOutput for AppController {
|
|
type Inner = Self;
|
|
}
|
|
|
|
impl OperationInput for AppController {
|
|
fn inferred_early_responses(
|
|
_: &mut GenContext,
|
|
op: &mut Operation,
|
|
) -> Vec<(Option<u16>, Response)> {
|
|
let mut session_cookie = IndexMap::new();
|
|
session_cookie.insert("session_cookie".to_owned(), Vec::new());
|
|
op.security = vec![session_cookie];
|
|
vec![(
|
|
Some(401),
|
|
Response {
|
|
description: "Unauthenticated - a session is required".to_owned(),
|
|
content: IndexMap::new(),
|
|
..Default::default()
|
|
},
|
|
)]
|
|
}
|
|
}
|