use ::aide::{
OperationInput, OperationOutput,
generate::GenContext,
openapi::{ApiKeyLocation, MediaType, OpenApi, Operation, ReferenceOr, Response, SchemaObject, SecurityScheme},
transform::TransformOperation,
};
use serde_json::json;
use crate::{
AuthSession, Guard, GuardError, Rejection, SessionStore, Subject,
axum::{Sessions, StepUpError},
};
pub const SECURITY_SCHEME: &str = "session_cookie";
#[expect(clippy::default_trait_access, reason = "aide does not re-export IndexMap")]
pub fn security_scheme<St: SessionStore, S: Subject>(guard: &Guard<St, S>) -> SecurityScheme {
SecurityScheme::ApiKey {
location: ApiKeyLocation::Cookie,
name: guard.cookie_name().to_owned(),
description: Some("Session cookie, rotated by the server.".to_owned()),
extensions: Default::default(),
}
}
pub fn register<St: SessionStore, S: Subject>(api: &mut OpenApi, guard: &Guard<St, S>) {
api.components
.get_or_insert_default()
.security_schemes
.insert(SECURITY_SCHEME.to_owned(), ReferenceOr::Item(security_scheme(guard)));
}
impl<S> OperationInput for AuthSession<S> {
fn operation_input(_: &mut GenContext, operation: &mut Operation) {
let _ = TransformOperation::new(operation).security_requirement(SECURITY_SCHEME);
}
fn inferred_early_responses(_: &mut GenContext, _: &mut Operation) -> Vec<(Option<u16>, Response)> {
let codes = [
Rejection::NotAuthenticated,
Rejection::Expired,
Rejection::Revoked,
Rejection::StepUpRequired,
]
.map(Rejection::as_str);
vec![(Some(401), error_response("No valid session.", &codes))]
}
}
impl<St, S> OperationInput for Sessions<St, S> {}
impl OperationOutput for GuardError {
type Inner = Self;
fn operation_response(_: &mut GenContext, _: &mut Operation) -> Option<Response> {
Some(internal())
}
fn inferred_responses(_: &mut GenContext, _: &mut Operation) -> Vec<(Option<u16>, Response)> {
vec![(Some(500), internal())]
}
}
impl OperationOutput for StepUpError {
type Inner = Self;
fn inferred_responses(_: &mut GenContext, _: &mut Operation) -> Vec<(Option<u16>, Response)> {
let rejected = [Rejection::NotAuthenticated, Rejection::Expired, Rejection::Revoked].map(Rejection::as_str);
vec![
(Some(401), error_response("No valid session.", &rejected)),
(
Some(403),
error_response("The verified subject does not own the session.", &["subject_mismatch"]),
),
(Some(500), internal()),
]
}
}
fn internal() -> Response {
error_response("Internal error.", &["internal"])
}
fn error_response(description: &str, codes: &[&str]) -> Response {
let schema = json!({
"type": "object",
"required": ["error"],
"properties": { "error": { "type": "string", "enum": codes } },
});
Response {
description: description.to_owned(),
content: [(
"application/json".to_owned(),
MediaType {
schema: Some(SchemaObject {
json_schema: serde_json::from_value(schema).unwrap_or_default(),
external_docs: None,
example: None,
}),
..MediaType::default()
},
)]
.into(),
..Response::default()
}
}