1use axum::response::Html;
7use serde_json::{Value, json};
8
9pub 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-rc19",
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
297pub async fn serve_openapi_json() -> axum::Json<Value> {
299 axum::Json(generate_openapi_spec())
300}
301
302pub 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
342pub 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}