@@ -370,6 +370,129 @@ impl RustApi {
370370 . route ( path, get ( docs_handler) )
371371 }
372372
373+ /// Enable Swagger UI documentation with Basic Auth protection
374+ ///
375+ /// When username and password are provided, the docs endpoint will require
376+ /// Basic Authentication. This is useful for protecting API documentation
377+ /// in production environments.
378+ ///
379+ /// # Example
380+ ///
381+ /// ```rust,ignore
382+ /// RustApi::new()
383+ /// .route("/users", get(list_users))
384+ /// .docs_with_auth("/docs", "admin", "secret123")
385+ /// .run("127.0.0.1:8080")
386+ /// .await
387+ /// ```
388+ #[ cfg( feature = "swagger-ui" ) ]
389+ pub fn docs_with_auth ( self , path : & str , username : & str , password : & str ) -> Self {
390+ let title = self . openapi_spec . info . title . clone ( ) ;
391+ let version = self . openapi_spec . info . version . clone ( ) ;
392+ let description = self . openapi_spec . info . description . clone ( ) ;
393+
394+ self . docs_with_auth_and_info (
395+ path,
396+ username,
397+ password,
398+ & title,
399+ & version,
400+ description. as_deref ( ) ,
401+ )
402+ }
403+
404+ /// Enable Swagger UI documentation with Basic Auth and custom API info
405+ ///
406+ /// # Example
407+ ///
408+ /// ```rust,ignore
409+ /// RustApi::new()
410+ /// .docs_with_auth_and_info(
411+ /// "/docs",
412+ /// "admin",
413+ /// "secret",
414+ /// "My API",
415+ /// "2.0.0",
416+ /// Some("Protected API documentation")
417+ /// )
418+ /// ```
419+ #[ cfg( feature = "swagger-ui" ) ]
420+ pub fn docs_with_auth_and_info (
421+ mut self ,
422+ path : & str ,
423+ username : & str ,
424+ password : & str ,
425+ title : & str ,
426+ version : & str ,
427+ description : Option < & str > ,
428+ ) -> Self {
429+ use crate :: router:: MethodRouter ;
430+ use base64:: { engine:: general_purpose:: STANDARD , Engine } ;
431+ use std:: collections:: HashMap ;
432+
433+ // Update spec info
434+ self . openapi_spec . info . title = title. to_string ( ) ;
435+ self . openapi_spec . info . version = version. to_string ( ) ;
436+ if let Some ( desc) = description {
437+ self . openapi_spec . info . description = Some ( desc. to_string ( ) ) ;
438+ }
439+
440+ let path = path. trim_end_matches ( '/' ) ;
441+ let openapi_path = format ! ( "{}/openapi.json" , path) ;
442+
443+ // Create expected auth header value
444+ let credentials = format ! ( "{}:{}" , username, password) ;
445+ let encoded = STANDARD . encode ( credentials. as_bytes ( ) ) ;
446+ let expected_auth = format ! ( "Basic {}" , encoded) ;
447+
448+ // Clone values for closures
449+ let spec_json =
450+ serde_json:: to_string_pretty ( & self . openapi_spec . to_json ( ) ) . unwrap_or_default ( ) ;
451+ let openapi_url = openapi_path. clone ( ) ;
452+ let expected_auth_spec = expected_auth. clone ( ) ;
453+ let expected_auth_docs = expected_auth;
454+
455+ // Create spec handler with auth check
456+ let spec_handler: crate :: handler:: BoxedHandler = std:: sync:: Arc :: new ( move |req : crate :: Request | {
457+ let json = spec_json. clone ( ) ;
458+ let expected = expected_auth_spec. clone ( ) ;
459+ Box :: pin ( async move {
460+ if !check_basic_auth ( & req, & expected) {
461+ return unauthorized_response ( ) ;
462+ }
463+ http:: Response :: builder ( )
464+ . status ( http:: StatusCode :: OK )
465+ . header ( http:: header:: CONTENT_TYPE , "application/json" )
466+ . body ( http_body_util:: Full :: new ( bytes:: Bytes :: from ( json) ) )
467+ . unwrap ( )
468+ } ) as std:: pin:: Pin < Box < dyn std:: future:: Future < Output = crate :: Response > + Send > >
469+ } ) ;
470+
471+ // Create docs handler with auth check
472+ let docs_handler: crate :: handler:: BoxedHandler = std:: sync:: Arc :: new ( move |req : crate :: Request | {
473+ let url = openapi_url. clone ( ) ;
474+ let expected = expected_auth_docs. clone ( ) ;
475+ Box :: pin ( async move {
476+ if !check_basic_auth ( & req, & expected) {
477+ return unauthorized_response ( ) ;
478+ }
479+ rustapi_openapi:: swagger_ui_html ( & url)
480+ } ) as std:: pin:: Pin < Box < dyn std:: future:: Future < Output = crate :: Response > + Send > >
481+ } ) ;
482+
483+ // Create method routers with boxed handlers
484+ let mut spec_handlers = HashMap :: new ( ) ;
485+ spec_handlers. insert ( http:: Method :: GET , spec_handler) ;
486+ let spec_router = MethodRouter :: from_boxed ( spec_handlers) ;
487+
488+ let mut docs_handlers = HashMap :: new ( ) ;
489+ docs_handlers. insert ( http:: Method :: GET , docs_handler) ;
490+ let docs_router = MethodRouter :: from_boxed ( docs_handlers) ;
491+
492+ self . route ( & openapi_path, spec_router)
493+ . route ( path, docs_router)
494+ }
495+
373496 /// Run the server
374497 ///
375498 /// # Example
@@ -407,3 +530,24 @@ impl Default for RustApi {
407530 Self :: new ( )
408531 }
409532}
533+
534+ /// Check Basic Auth header against expected credentials
535+ #[ cfg( feature = "swagger-ui" ) ]
536+ fn check_basic_auth ( req : & crate :: Request , expected : & str ) -> bool {
537+ req. headers ( )
538+ . get ( http:: header:: AUTHORIZATION )
539+ . and_then ( |v| v. to_str ( ) . ok ( ) )
540+ . map ( |auth| auth == expected)
541+ . unwrap_or ( false )
542+ }
543+
544+ /// Create 401 Unauthorized response with WWW-Authenticate header
545+ #[ cfg( feature = "swagger-ui" ) ]
546+ fn unauthorized_response ( ) -> crate :: Response {
547+ http:: Response :: builder ( )
548+ . status ( http:: StatusCode :: UNAUTHORIZED )
549+ . header ( http:: header:: WWW_AUTHENTICATE , "Basic realm=\" API Documentation\" " )
550+ . header ( http:: header:: CONTENT_TYPE , "text/plain" )
551+ . body ( http_body_util:: Full :: new ( bytes:: Bytes :: from ( "Unauthorized" ) ) )
552+ . unwrap ( )
553+ }
0 commit comments