203 lines
5.6 KiB
Rust
203 lines
5.6 KiB
Rust
|
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<S> Transform<S, ServiceRequest> for RequireApiKey
|
||
|
where
|
||
|
S: Service<
|
||
|
ServiceRequest,
|
||
|
Response = ServiceResponse<actix_web::body::BoxBody>,
|
||
|
Error = actix_web::Error,
|
||
|
>,
|
||
|
S::Future: 'static,
|
||
|
{
|
||
|
type Response = ServiceResponse<actix_web::body::BoxBody>;
|
||
|
type Error = actix_web::Error;
|
||
|
type Transform = ApiKeyMiddleware<S>;
|
||
|
type InitError = ();
|
||
|
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
||
|
|
||
|
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<S> Transform<S, ServiceRequest> for LogApiKey
|
||
|
where
|
||
|
S: Service<
|
||
|
ServiceRequest,
|
||
|
Response = ServiceResponse<actix_web::body::BoxBody>,
|
||
|
Error = actix_web::Error,
|
||
|
>,
|
||
|
S::Future: 'static,
|
||
|
{
|
||
|
type Response = ServiceResponse<actix_web::body::BoxBody>;
|
||
|
type Error = actix_web::Error;
|
||
|
type Transform = ApiKeyMiddleware<S>;
|
||
|
type InitError = ();
|
||
|
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
||
|
|
||
|
fn new_transform(&self, service: S) -> Self::Future {
|
||
|
future::ready(Ok(ApiKeyMiddleware {
|
||
|
service,
|
||
|
log_only: true,
|
||
|
}))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
struct ApiKeyMiddleware<S> {
|
||
|
service: S,
|
||
|
log_only: bool,
|
||
|
}
|
||
|
|
||
|
impl<S> Service<ServiceRequest> for ApiKeyMiddleware<S>
|
||
|
where
|
||
|
S: Service<
|
||
|
ServiceRequest,
|
||
|
Response = ServiceResponse<actix_web::body::BoxBody>,
|
||
|
Error = actix_web::Error,
|
||
|
>,
|
||
|
S::Future: 'static,
|
||
|
{
|
||
|
type Response = ServiceResponse<actix_web::body::BoxBody>;
|
||
|
type Error = actix_web::Error;
|
||
|
type Future = LocalBoxFuture<'static, Result<Self::Response, actix_web::Error>>;
|
||
|
|
||
|
fn poll_ready(
|
||
|
&self,
|
||
|
ctx: &mut core::task::Context<'_>,
|
||
|
) -> std::task::Poll<Result<(), Self::Error>> {
|
||
|
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)
|
||
|
})
|
||
|
}
|
||
|
}
|