Mitgliederladen/backend/main.rs

202 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)
})
}
}