1use ::aide::{
33 OperationInput, OperationOutput,
34 generate::GenContext,
35 openapi::{ApiKeyLocation, MediaType, OpenApi, Operation, ReferenceOr, Response, SchemaObject, SecurityScheme},
36 transform::TransformOperation,
37};
38use serde_json::json;
39
40use crate::{
41 AuthSession, Guard, GuardError, Rejection, SessionStore, Subject,
42 axum::{Sessions, StepUpError},
43};
44
45pub const SECURITY_SCHEME: &str = "session_cookie";
47
48#[expect(clippy::default_trait_access, reason = "aide does not re-export IndexMap")]
50pub fn security_scheme<St: SessionStore, S: Subject>(guard: &Guard<St, S>) -> SecurityScheme {
51 SecurityScheme::ApiKey {
52 location: ApiKeyLocation::Cookie,
53 name: guard.cookie_name().to_owned(),
54 description: Some("Session cookie, rotated by the server.".to_owned()),
55 extensions: Default::default(),
56 }
57}
58
59pub fn register<St: SessionStore, S: Subject>(api: &mut OpenApi, guard: &Guard<St, S>) {
61 api.components
62 .get_or_insert_default()
63 .security_schemes
64 .insert(SECURITY_SCHEME.to_owned(), ReferenceOr::Item(security_scheme(guard)));
65}
66
67impl<S> OperationInput for AuthSession<S> {
68 fn operation_input(_: &mut GenContext, operation: &mut Operation) {
69 let _ = TransformOperation::new(operation).security_requirement(SECURITY_SCHEME);
70 }
71
72 fn inferred_early_responses(_: &mut GenContext, _: &mut Operation) -> Vec<(Option<u16>, Response)> {
73 let codes = [
74 Rejection::NotAuthenticated,
75 Rejection::Expired,
76 Rejection::Revoked,
77 Rejection::StepUpRequired,
78 ]
79 .map(Rejection::as_str);
80 vec![(Some(401), error_response("No valid session.", &codes))]
81 }
82}
83
84impl<St, S> OperationInput for Sessions<St, S> {}
85
86impl OperationOutput for GuardError {
87 type Inner = Self;
88
89 fn operation_response(_: &mut GenContext, _: &mut Operation) -> Option<Response> {
90 Some(internal())
91 }
92
93 fn inferred_responses(_: &mut GenContext, _: &mut Operation) -> Vec<(Option<u16>, Response)> {
94 vec![(Some(500), internal())]
95 }
96}
97
98impl OperationOutput for StepUpError {
99 type Inner = Self;
100
101 fn inferred_responses(_: &mut GenContext, _: &mut Operation) -> Vec<(Option<u16>, Response)> {
102 let rejected = [Rejection::NotAuthenticated, Rejection::Expired, Rejection::Revoked].map(Rejection::as_str);
103 vec![
104 (Some(401), error_response("No valid session.", &rejected)),
105 (
106 Some(403),
107 error_response("The verified subject does not own the session.", &["subject_mismatch"]),
108 ),
109 (Some(500), internal()),
110 ]
111 }
112}
113
114fn internal() -> Response {
115 error_response("Internal error.", &["internal"])
116}
117
118fn error_response(description: &str, codes: &[&str]) -> Response {
120 let schema = json!({
121 "type": "object",
122 "required": ["error"],
123 "properties": { "error": { "type": "string", "enum": codes } },
124 });
125 Response {
126 description: description.to_owned(),
127 content: [(
128 "application/json".to_owned(),
129 MediaType {
130 schema: Some(SchemaObject {
131 json_schema: serde_json::from_value(schema).unwrap_or_default(),
132 external_docs: None,
133 example: None,
134 }),
135 ..MediaType::default()
136 },
137 )]
138 .into(),
139 ..Response::default()
140 }
141}