use std::{ error::Error, future::{self, Ready}, net::Ipv4Addr, }; use actix_web::{ dev::{Service, ServiceRequest, ServiceResponse, Transform}, middleware::Logger, web::Data, App, HttpResponse, HttpServer, }; use futures::future::LocalBoxFuture; use utoipa::{ openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}, Modify, OpenApi, }; use utoipa_swagger_ui::SwaggerUi; use crate::todo::{ErrorResponse, TodoStore}; mod todo; const API_KEY_NAME: &str = "todo_apikey"; const API_KEY: &str = "utoipa-rocks"; #[actix_web::main] async fn main() -> Result<(), impl Error> { env_logger::init(); #[derive(OpenApi)] #[openapi( paths( todo::get_todos, todo::create_todo, todo::delete_todo, todo::get_todo_by_id, todo::update_todo, todo::search_todos ), components( schemas(todo::Todo, todo::TodoUpdateRequest, todo::ErrorResponse) ), tags( (name = "todo", description = "Todo management endpoints.") ), modifiers(&SecurityAddon) )] struct ApiDoc; struct SecurityAddon; impl Modify for SecurityAddon { fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { let components = openapi.components.as_mut().unwrap(); // we can unwrap safely since there already is components registered. components.add_security_scheme( "api_key", SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("todo_apikey"))), ) } } let store = Data::new(TodoStore::default()); // Make instance variable of ApiDoc so all worker threads gets the same instance. let openapi = ApiDoc::openapi(); HttpServer::new(move || { // This factory closure is called on each worker thread independently. App::new() .wrap(Logger::default()) .configure(todo::configure(store.clone())) .service( SwaggerUi::new("/swagger-ui/{_:.*}").url("/api-docs/openapi.json", openapi.clone()), ) }) .bind((Ipv4Addr::UNSPECIFIED, 8080))? .run() .await } /// Require api key middleware will actually require valid api key struct RequireApiKey; impl Transform for RequireApiKey where S: Service< ServiceRequest, Response = ServiceResponse, Error = actix_web::Error, >, S::Future: 'static, { type Response = ServiceResponse; type Error = actix_web::Error; type Transform = ApiKeyMiddleware; type InitError = (); type Future = Ready>; fn new_transform(&self, service: S) -> Self::Future { future::ready(Ok(ApiKeyMiddleware { service, log_only: false, })) } } /// Log api key middleware only logs about missing or invalid api keys struct LogApiKey; impl Transform for LogApiKey where S: Service< ServiceRequest, Response = ServiceResponse, Error = actix_web::Error, >, S::Future: 'static, { type Response = ServiceResponse; type Error = actix_web::Error; type Transform = ApiKeyMiddleware; type InitError = (); type Future = Ready>; fn new_transform(&self, service: S) -> Self::Future { future::ready(Ok(ApiKeyMiddleware { service, log_only: true, })) } } struct ApiKeyMiddleware { service: S, log_only: bool, } impl Service for ApiKeyMiddleware where S: Service< ServiceRequest, Response = ServiceResponse, Error = actix_web::Error, >, S::Future: 'static, { type Response = ServiceResponse; type Error = actix_web::Error; type Future = LocalBoxFuture<'static, Result>; fn poll_ready( &self, ctx: &mut core::task::Context<'_>, ) -> std::task::Poll> { self.service.poll_ready(ctx) } fn call(&self, req: ServiceRequest) -> Self::Future { let response = |req: ServiceRequest, response: HttpResponse| -> Self::Future { Box::pin(async { Ok(req.into_response(response)) }) }; match req.headers().get(API_KEY_NAME) { Some(key) if key != API_KEY => { if self.log_only { log::debug!("Incorrect api api provided!!!") } else { return response( req, HttpResponse::Unauthorized().json(ErrorResponse::Unauthorized( String::from("incorrect api key"), )), ); } } None => { if self.log_only { log::debug!("Missing api key!!!") } else { return response( req, HttpResponse::Unauthorized() .json(ErrorResponse::Unauthorized(String::from("missing api key"))), ); } } _ => (), // just passthrough } if self.log_only { log::debug!("Performing operation") } let future = self.service.call(req); Box::pin(async move { let response = future.await?; Ok(response) }) } }