From 32f0ed27a7947e9631d2e0d9f853e1b280283455 Mon Sep 17 00:00:00 2001 From: Frank Denzer Date: Sun, 16 Jul 2023 21:48:45 +0200 Subject: [PATCH] rm complex sample use dumb one. not success --- backend/Cargo.toml | 14 ++- backend/main.rs | 212 ++--------------------------------- backend/todo.rs | 269 --------------------------------------------- backend/web.rs | 9 -- 4 files changed, 22 insertions(+), 482 deletions(-) delete mode 100644 backend/todo.rs delete mode 100644 backend/web.rs diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 476ddb3..993e5bf 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -6,10 +6,20 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +# Compiling actix-server v2.2.0 +# Compiling actix-http v3.3.1 +# Compiling actix-web v4.3.1 actix-web ="4" + +# Compiling actix-rt v2.8.0 +actix-rt = "*" + +#actix-swagger = "*" +serde = "*" +#utoipa = { version = "3", features = ["actix_extras"] } +# Compiling utoipa-swagger-ui v3.1.4 utoipa-swagger-ui = { version = "3", features = ["actix-web"] } -# utoipa = { version = "3", features = ["actix_extras"] } [[bin]] name = "webserver" -path = "web.rs" +path = "main.rs" diff --git a/backend/main.rs b/backend/main.rs index 52cde8c..2c06644 100644 --- a/backend/main.rs +++ b/backend/main.rs @@ -1,202 +1,10 @@ -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) - }) - } -} +HttpServer::new(move || { + App::new() + .service( + SwaggerUi::new("/swagger-ui/{_:.*}") + .url("/api-docs/openapi.json", ApiDoc::openapi()), + ) + }) +.bind((Ipv4Addr::UNSPECIFIED, 8000)).unwrap() + .run(); + \ No newline at end of file diff --git a/backend/todo.rs b/backend/todo.rs deleted file mode 100644 index a08aab1..0000000 --- a/backend/todo.rs +++ /dev/null @@ -1,269 +0,0 @@ -use std::sync::Mutex; - -use actix_web::{ - delete, get, post, put, - web::{Data, Json, Path, Query, ServiceConfig}, - HttpResponse, Responder, -}; -use serde::{Deserialize, Serialize}; -use utoipa::{ToSchema, IntoParams}; - -use crate::{LogApiKey, RequireApiKey}; - -#[derive(Default)] -pub(super) struct TodoStore { - todos: Mutex>, -} - -pub(super) fn configure(store: Data) -> impl FnOnce(&mut ServiceConfig) { - |config: &mut ServiceConfig| { - config - .app_data(store) - .service(search_todos) - .service(get_todos) - .service(create_todo) - .service(delete_todo) - .service(get_todo_by_id) - .service(update_todo); - } -} - -/// Task to do. -#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)] -pub(super) struct Todo { - /// Unique id for the todo item. - #[schema(example = 1)] - id: i32, - /// Description of the tasks to do. - #[schema(example = "Remember to buy groceries")] - value: String, - /// Mark is the task done or not - checked: bool, -} - -/// Request to update existing `Todo` item. -#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)] -pub(super) struct TodoUpdateRequest { - /// Optional new value for the `Todo` task. - #[schema(example = "Dentist at 14.00")] - value: Option, - /// Optional check status to mark is the task done or not. - checked: Option, -} - -/// Todo endpoint error responses -#[derive(Serialize, Deserialize, Clone, ToSchema)] -pub(super) enum ErrorResponse { - /// When Todo is not found by search term. - NotFound(String), - /// When there is a conflict storing a new todo. - Conflict(String), - /// When todo endpoint was called without correct credentials - Unauthorized(String), -} - -/// Get list of todos. -/// -/// List todos from in-memory todo store. -/// -/// One could call the api endpoint with following curl. -/// ```text -/// curl localhost:8080/todo -/// ``` -#[utoipa::path( - responses( - (status = 200, description = "List current todo items", body = [Todo]) - ) -)] -#[get("/todo")] -pub(super) async fn get_todos(todo_store: Data) -> impl Responder { - let todos = todo_store.todos.lock().unwrap(); - - HttpResponse::Ok().json(todos.clone()) -} - -/// Create new Todo to shared in-memory storage. -/// -/// Post a new `Todo` in request body as json to store it. Api will return -/// created `Todo` on success or `ErrorResponse::Conflict` if todo with same id already exists. -/// -/// One could call the api with. -/// ```text -/// curl localhost:8080/todo -d '{"id": 1, "value": "Buy movie ticket", "checked": false}' -/// ``` -#[utoipa::path( - request_body = Todo, - responses( - (status = 201, description = "Todo created successfully", body = Todo), - (status = 409, description = "Todo with id already exists", body = ErrorResponse, example = json!(ErrorResponse::Conflict(String::from("id = 1")))) - ) -)] -#[post("/todo")] -pub(super) async fn create_todo(todo: Json, todo_store: Data) -> impl Responder { - let mut todos = todo_store.todos.lock().unwrap(); - let todo = &todo.into_inner(); - - todos - .iter() - .find(|existing| existing.id == todo.id) - .map(|existing| { - HttpResponse::Conflict().json(ErrorResponse::Conflict(format!("id = {}", existing.id))) - }) - .unwrap_or_else(|| { - todos.push(todo.clone()); - - HttpResponse::Ok().json(todo) - }) -} - -/// Delete Todo by given path variable id. -/// -/// This endpoint needs `api_key` authentication in order to call. Api key can be found from README.md. -/// -/// Api will delete todo from shared in-memory storage by the provided id and return success 200. -/// If storage does not contain `Todo` with given id 404 not found will be returned. -#[utoipa::path( - responses( - (status = 200, description = "Todo deleted successfully"), - (status = 401, description = "Unauthorized to delete Todo", body = ErrorResponse, example = json!(ErrorResponse::Unauthorized(String::from("missing api key")))), - (status = 404, description = "Todo not found by id", body = ErrorResponse, example = json!(ErrorResponse::NotFound(String::from("id = 1")))) - ), - params( - ("id", description = "Unique storage id of Todo") - ), - security( - ("api_key" = []) - ) -)] -#[delete("/todo/{id}", wrap = "RequireApiKey")] -pub(super) async fn delete_todo(id: Path, todo_store: Data) -> impl Responder { - let mut todos = todo_store.todos.lock().unwrap(); - let id = id.into_inner(); - - let new_todos = todos - .iter() - .filter(|todo| todo.id != id) - .cloned() - .collect::>(); - - if new_todos.len() == todos.len() { - HttpResponse::NotFound().json(ErrorResponse::NotFound(format!("id = {id}"))) - } else { - *todos = new_todos; - HttpResponse::Ok().finish() - } -} - -/// Get Todo by given todo id. -/// -/// Return found `Todo` with status 200 or 404 not found if `Todo` is not found from shared in-memory storage. -#[utoipa::path( - responses( - (status = 200, description = "Todo found from storage", body = Todo), - (status = 404, description = "Todo not found by id", body = ErrorResponse, example = json!(ErrorResponse::NotFound(String::from("id = 1")))) - ), - params( - ("id", description = "Unique storage id of Todo") - ) -)] -#[get("/todo/{id}")] -pub(super) async fn get_todo_by_id(id: Path, todo_store: Data) -> impl Responder { - let todos = todo_store.todos.lock().unwrap(); - let id = id.into_inner(); - - todos - .iter() - .find(|todo| todo.id == id) - .map(|todo| HttpResponse::Ok().json(todo)) - .unwrap_or_else(|| { - HttpResponse::NotFound().json(ErrorResponse::NotFound(format!("id = {id}"))) - }) -} - -/// Update Todo with given id. -/// -/// This endpoint supports optional authentication. -/// -/// Tries to update `Todo` by given id as path variable. If todo is found by id values are -/// updated according `TodoUpdateRequest` and updated `Todo` is returned with status 200. -/// If todo is not found then 404 not found is returned. -#[utoipa::path( - request_body = TodoUpdateRequest, - responses( - (status = 200, description = "Todo updated successfully", body = Todo), - (status = 404, description = "Todo not found by id", body = ErrorResponse, example = json!(ErrorResponse::NotFound(String::from("id = 1")))) - ), - params( - ("id", description = "Unique storage id of Todo") - ), - security( - (), - ("api_key" = []) - ) -)] -#[put("/todo/{id}", wrap = "LogApiKey")] -pub(super) async fn update_todo( - id: Path, - todo: Json, - todo_store: Data, -) -> impl Responder { - let mut todos = todo_store.todos.lock().unwrap(); - let id = id.into_inner(); - let todo = todo.into_inner(); - - todos - .iter_mut() - .find_map(|todo| if todo.id == id { Some(todo) } else { None }) - .map(|existing_todo| { - if let Some(checked) = todo.checked { - existing_todo.checked = checked; - } - if let Some(value) = todo.value { - existing_todo.value = value; - } - - HttpResponse::Ok().json(existing_todo) - }) - .unwrap_or_else(|| { - HttpResponse::NotFound().json(ErrorResponse::NotFound(format!("id = {id}"))) - }) -} - -/// Search todos Query -#[derive(Deserialize, Debug, IntoParams)] -pub(super) struct SearchTodos { - /// Content that should be found from Todo's value field - value: String, -} - -/// Search Todos with by value -/// -/// Perform search from `Todo`s present in in-memory storage by matching Todo's value to -/// value provided as query parameter. Returns 200 and matching `Todo` items. -#[utoipa::path( - params( - SearchTodos - ), - responses( - (status = 200, description = "Search Todos did not result error", body = [Todo]), - ) -)] -#[get("/todo/search")] -pub(super) async fn search_todos( - query: Query, - todo_store: Data, -) -> impl Responder { - let todos = todo_store.todos.lock().unwrap(); - - HttpResponse::Ok().json( - todos - .iter() - .filter(|todo| { - todo.value - .to_lowercase() - .contains(&query.value.to_lowercase()) - }) - .cloned() - .collect::>(), - ) -} diff --git a/backend/web.rs b/backend/web.rs deleted file mode 100644 index 4ede752..0000000 --- a/backend/web.rs +++ /dev/null @@ -1,9 +0,0 @@ -HttpServer::new(move || { - App::new() - .service( - SwaggerUi::new("/swagger-ui/{_:.*}") - .url("/api-docs/openapi.json", ApiDoc::openapi()), - ) - }) - .bind((Ipv4Addr::UNSPECIFIED, 8000)).unwrap() - .run(); \ No newline at end of file