cargagep-v2/src/api/mod.rs
Antoine Pelletier 7e958b89c6 feat: add oidc
2026-08-23 22:20:27 +02:00

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()
},
)]
}
}