Skip to main content

auth_framework/api/
openapi.rs

1//! OpenAPI documentation generator.
2//!
3//! Generates a lightweight OpenAPI 3.0 overview for the core REST API routes
4//! and the bundled Swagger UI served by the API server.
5
6use axum::response::Html;
7use serde_json::{Value, json};
8
9/// Generate a lightweight OpenAPI specification for the core auth and health routes.
10pub fn generate_openapi_spec() -> Value {
11    json!({
12        "openapi": "3.0.3",
13        "info": {
14            "title": "AuthFramework API",
15            "description": "Lightweight generated overview of the core authentication and health routes exposed by AuthFramework",
16            "version": "0.5.0-rc18",
17            "contact": {
18                "name": "AuthFramework Team",
19                "url": "https://github.com/ciresnave/auth-framework"
20            },
21            "license": {
22                "name": "MIT OR Apache-2.0",
23                "url": "https://github.com/ciresnave/auth-framework/blob/main/LICENSE"
24            }
25        },
26        "servers": [
27            {
28                "url": "https://api.example.com/api/v1",
29                "description": "Production server"
30            },
31            {
32                "url": "http://localhost:8080/api/v1",
33                "description": "Development server"
34            }
35        ],
36        "paths": generate_paths(),
37        "components": {
38            "schemas": generate_schemas(),
39            "securitySchemes": {
40                "bearerAuth": {
41                    "type": "http",
42                    "scheme": "bearer",
43                    "bearerFormat": "JWT"
44                },
45                "apiKey": {
46                    "type": "apiKey",
47                    "in": "header",
48                    "name": "X-API-Key"
49                }
50            }
51        },
52        "security": [
53            { "bearerAuth": [] }
54        ]
55    })
56}
57
58fn generate_paths() -> Value {
59    json!({
60        "/auth/login": {
61            "post": {
62                "tags": ["Authentication"],
63                "summary": "User login",
64                "description": "Authenticate user with credentials",
65                "requestBody": {
66                    "required": true,
67                    "content": {
68                        "application/json": {
69                            "schema": { "$ref": "#/components/schemas/LoginRequest" }
70                        }
71                    }
72                },
73                "responses": {
74                    "200": {
75                        "description": "Login successful",
76                        "content": {
77                            "application/json": {
78                                "schema": { "$ref": "#/components/schemas/LoginResponse" }
79                            }
80                        }
81                    },
82                    "401": {
83                        "description": "Invalid credentials",
84                        "content": {
85                            "application/json": {
86                                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
87                            }
88                        }
89                    }
90                }
91            }
92        },
93        "/auth/refresh": {
94            "post": {
95                "tags": ["Authentication"],
96                "summary": "Refresh access token",
97                "description": "Get new access token using refresh token",
98                "requestBody": {
99                    "required": true,
100                    "content": {
101                        "application/json": {
102                            "schema": { "$ref": "#/components/schemas/RefreshRequest" }
103                        }
104                    }
105                },
106                "responses": {
107                    "200": {
108                        "description": "Token refreshed successfully",
109                        "content": {
110                            "application/json": {
111                                "schema": { "$ref": "#/components/schemas/RefreshResponse" }
112                            }
113                        }
114                    }
115                }
116            }
117        },
118        "/auth/logout": {
119            "post": {
120                "tags": ["Authentication"],
121                "summary": "User logout",
122                "description": "Invalidate user session and tokens",
123                "security": [{ "bearerAuth": [] }],
124                "requestBody": {
125                    "required": false,
126                    "content": {
127                        "application/json": {
128                            "schema": { "$ref": "#/components/schemas/LogoutRequest" }
129                        }
130                    }
131                },
132                "responses": {
133                    "200": {
134                        "description": "Logout successful",
135                        "content": {
136                            "application/json": {
137                                "schema": { "$ref": "#/components/schemas/MessageResponse" }
138                            }
139                        }
140                    }
141                }
142            }
143        },
144        "/health": {
145            "get": {
146                "tags": ["System"],
147                "summary": "Health check",
148                "description": "Get system health status",
149                "responses": {
150                    "200": {
151                        "description": "System is healthy",
152                        "content": {
153                            "application/json": {
154                                "schema": { "$ref": "#/components/schemas/HealthResponse" }
155                            }
156                        }
157                    }
158                }
159            }
160        }
161    })
162}
163
164fn generate_schemas() -> Value {
165    json!({
166        "LoginRequest": {
167            "type": "object",
168            "required": ["username", "password"],
169            "properties": {
170                "username": {
171                    "type": "string",
172                    "description": "User's username or email"
173                },
174                "password": {
175                    "type": "string",
176                    "format": "password",
177                    "description": "User's password"
178                },
179                "mfa_code": {
180                    "type": "string",
181                    "description": "Multi-factor authentication code (if required)"
182                },
183                "challenge_id": {
184                    "type": "string",
185                    "description": "Pending MFA challenge identifier returned by a previous login attempt"
186                },
187                "remember_me": {
188                    "type": "boolean",
189                    "default": false,
190                    "description": "Extended session duration"
191                }
192            }
193        },
194        "LoginResponse": {
195            "type": "object",
196            "properties": {
197                "success": { "type": "boolean" },
198                "data": {
199                    "type": "object",
200                    "properties": {
201                        "access_token": { "type": "string" },
202                        "refresh_token": { "type": "string" },
203                        "token_type": { "type": "string", "example": "Bearer" },
204                        "expires_in": { "type": "integer" },
205                        "user": { "$ref": "#/components/schemas/UserInfo" },
206                        "login_risk_level": {
207                            "type": "string",
208                            "enum": ["low", "medium", "high", "critical"]
209                        },
210                        "security_warnings": {
211                            "type": "array",
212                            "items": { "type": "string" }
213                        }
214                    }
215                }
216            }
217        },
218        "RefreshResponse": {
219            "type": "object",
220            "properties": {
221                "success": { "type": "boolean" },
222                "data": {
223                    "type": "object",
224                    "properties": {
225                        "access_token": { "type": "string" },
226                        "token_type": { "type": "string", "example": "Bearer" },
227                        "expires_in": { "type": "integer" }
228                    }
229                }
230            }
231        },
232        "LogoutRequest": {
233            "type": "object",
234            "properties": {
235                "refresh_token": {
236                    "type": "string",
237                    "description": "Optional refresh token to revoke alongside the access token"
238                }
239            }
240        },
241        "UserInfo": {
242            "type": "object",
243            "properties": {
244                "id": { "type": "string" },
245                "username": { "type": "string" },
246                "roles": {
247                    "type": "array",
248                    "items": { "type": "string" }
249                },
250                "permissions": {
251                    "type": "array",
252                    "items": { "type": "string" }
253                }
254            }
255        },
256        "ErrorResponse": {
257            "type": "object",
258            "properties": {
259                "success": { "type": "boolean", "example": false },
260                "error": {
261                    "type": "object",
262                    "properties": {
263                        "code": { "type": "string" },
264                        "message": { "type": "string" },
265                        "details": { "type": "object" }
266                    }
267                }
268            }
269        },
270        "MessageResponse": {
271            "type": "object",
272            "properties": {
273                "success": { "type": "boolean" },
274                "message": { "type": "string" }
275            }
276        },
277        "HealthResponse": {
278            "type": "object",
279            "properties": {
280                "status": { "type": "string", "enum": ["healthy", "degraded", "unhealthy"] },
281                "timestamp": { "type": "string", "format": "date-time" },
282                "checks": {
283                    "type": "object",
284                    "additionalProperties": {
285                        "type": "object",
286                        "properties": {
287                            "status": { "type": "string" },
288                            "details": { "type": "object" }
289                        }
290                    }
291                }
292            }
293        }
294    })
295}
296
297/// Serve the generated OpenAPI specification as JSON.
298pub async fn serve_openapi_json() -> axum::Json<Value> {
299    axum::Json(generate_openapi_spec())
300}
301
302/// Generate Swagger UI HTML.
303pub fn generate_swagger_ui() -> String {
304    r#"<!DOCTYPE html>
305<html lang="en">
306<head>
307    <meta charset="UTF-8">
308    <meta name="viewport" content="width=device-width, initial-scale=1.0">
309    <title>AuthFramework API Documentation</title>
310    <link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui.css" />
311    <style>
312        html { box-sizing: border-box; overflow: -moz-scrollbars-vertical; overflow-y: scroll; }
313        *, *:before, *:after { box-sizing: inherit; }
314        body { margin:0; background: #fafafa; }
315    </style>
316</head>
317<body>
318    <div id="swagger-ui"></div>
319    <script src="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui-bundle.js"></script>
320    <script src="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui-standalone-preset.js"></script>
321    <script>
322        window.onload = function() {
323            const ui = SwaggerUIBundle({
324                url: '/api/openapi.json',
325                dom_id: '#swagger-ui',
326                deepLinking: true,
327                presets: [
328                    SwaggerUIBundle.presets.apis,
329                    SwaggerUIStandalonePreset
330                ],
331                plugins: [
332                    SwaggerUIBundle.plugins.DownloadUrl
333                ],
334                layout: "StandaloneLayout"
335            });
336        };
337    </script>
338</body>
339</html>"#.to_string()
340}
341
342/// Serve the bundled Swagger UI.
343pub async fn serve_swagger_ui() -> Html<String> {
344    Html(generate_swagger_ui())
345}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350
351    #[test]
352    fn test_openapi_spec_generation() {
353        let spec = generate_openapi_spec();
354        assert_eq!(spec["openapi"], "3.0.3");
355        assert_eq!(spec["info"]["title"], "AuthFramework API");
356        assert!(spec["paths"].is_object());
357        assert!(spec["components"]["schemas"].is_object());
358    }
359
360    #[test]
361    fn test_swagger_ui_generation() {
362        let html = generate_swagger_ui();
363        assert!(html.contains("swagger-ui"));
364        assert!(html.contains("/api/openapi.json"));
365    }
366}