diff --git a/backend/Cargo.lock b/backend/Cargo.lock index ad782b8..6893dc9 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1669,6 +1669,7 @@ dependencies = [ "log", "serde", "sqlx", + "utoipa", ] [[package]] diff --git a/backend/feature/image/Cargo.toml b/backend/feature/image/Cargo.toml index d7777cf..36aee66 100644 --- a/backend/feature/image/Cargo.toml +++ b/backend/feature/image/Cargo.toml @@ -11,5 +11,6 @@ futures.workspace = true log.workspace = true serde.workspace = true sqlx.workspace = true +utoipa.workspace = true auth.workspace = true diff --git a/backend/feature/image/src/adapter/delivery/image_info_response_dto.rs b/backend/feature/image/src/adapter/delivery/image_info_response_dto.rs index cb5b9f7..02611c8 100644 --- a/backend/feature/image/src/adapter/delivery/image_info_response_dto.rs +++ b/backend/feature/image/src/adapter/delivery/image_info_response_dto.rs @@ -1,6 +1,7 @@ use serde::Serialize; +use utoipa::ToSchema; -#[derive(Serialize)] +#[derive(Serialize, ToSchema)] pub struct ImageInfoResponseDto { pub id: i32, pub mime_type: String, diff --git a/backend/feature/image/src/framework/web.rs b/backend/feature/image/src/framework/web.rs index 0de5a4e..34ef9b2 100644 --- a/backend/feature/image/src/framework/web.rs +++ b/backend/feature/image/src/framework/web.rs @@ -1 +1,5 @@ -pub mod image_web_routes; \ No newline at end of file +pub mod image_api_doc; +pub mod image_web_routes; + +mod get_image_by_id_handler; +mod upload_image_handler; diff --git a/backend/feature/image/src/framework/web/get_image_by_id_handler.rs b/backend/feature/image/src/framework/web/get_image_by_id_handler.rs new file mode 100644 index 0000000..0fd65c9 --- /dev/null +++ b/backend/feature/image/src/framework/web/get_image_by_id_handler.rs @@ -0,0 +1,43 @@ +use actix_web::{HttpResponse, Responder, web}; +use utoipa::ToSchema; + +use crate::{ + adapter::delivery::image_controller::ImageController, + application::error::image_error::ImageError, +}; + +#[utoipa::path( + get, + path = "/image/{id}", + tag = "image", + summary = "Get image by ID", + responses ( + (status = 200, body = inline(ResponseBodySchema), content_type = "image/*"), + (status = 404, description = "Image not found") + ) +)] +pub async fn get_image_by_id_handler( + image_controller: web::Data, + path: web::Path, +) -> impl Responder { + let id = path.into_inner(); + let result = image_controller.get_image_by_id(id).await; + + match result { + Ok(image_response) => HttpResponse::Ok() + .content_type(image_response.mime_type) + .body(image_response.data), + Err(e) => match e { + ImageError::NotFound => HttpResponse::NotFound().finish(), + _ => { + log::error!("{e:?}"); + HttpResponse::InternalServerError().finish() + } + }, + } +} + +#[derive(ToSchema)] +#[schema(value_type = String, format = Binary)] +#[allow(dead_code)] +struct ResponseBodySchema(Vec); diff --git a/backend/feature/image/src/framework/web/image_api_doc.rs b/backend/feature/image/src/framework/web/image_api_doc.rs new file mode 100644 index 0000000..b84f621 --- /dev/null +++ b/backend/feature/image/src/framework/web/image_api_doc.rs @@ -0,0 +1,13 @@ +use crate::framework::web::{get_image_by_id_handler, upload_image_handler}; +use utoipa::{OpenApi, openapi}; + +#[derive(OpenApi)] +#[openapi(paths( + get_image_by_id_handler::get_image_by_id_handler, + upload_image_handler::upload_image_handler +))] +struct ApiDoc; + +pub fn openapi() -> openapi::OpenApi { + ApiDoc::openapi() +} diff --git a/backend/feature/image/src/framework/web/image_web_routes.rs b/backend/feature/image/src/framework/web/image_web_routes.rs index e8ae4bb..2f03d3f 100644 --- a/backend/feature/image/src/framework/web/image_web_routes.rs +++ b/backend/feature/image/src/framework/web/image_web_routes.rs @@ -1,11 +1,7 @@ -use actix_multipart::Multipart; -use actix_web::{HttpResponse, Responder, web}; -use auth::framework::web::auth_middleware::UserId; -use futures::StreamExt; +use actix_web::web; -use crate::{ - adapter::delivery::{image_controller::ImageController, image_request_dto::ImageRequestDto}, - application::error::image_error::ImageError, +use crate::framework::web::{ + get_image_by_id_handler::get_image_by_id_handler, upload_image_handler::upload_image_handler, }; pub fn configure_image_routes(cfg: &mut web::ServiceConfig) { @@ -15,77 +11,3 @@ pub fn configure_image_routes(cfg: &mut web::ServiceConfig) { .route("/{id}", web::get().to(get_image_by_id_handler)), ); } - -async fn upload_image_handler( - image_controller: web::Data, - mut payload: Multipart, - _: UserId, -) -> impl Responder { - let mut image_request_dto: Option = None; - - while let Some(item) = payload.next().await { - let mut field = match item { - Ok(field) => field, - Err(_) => return HttpResponse::BadRequest().finish(), - }; - - if field.name() != Some("file") { - continue; - } - - let mime_type = field - .content_type() - .cloned() - .map(|mt| mt.to_string()) - .unwrap_or_else(|| "application/octet-stream".to_string()); - - let mut data = Vec::new(); - while let Some(chunk) = field.next().await { - match chunk { - Ok(bytes) => data.extend_from_slice(&bytes), - Err(_) => return HttpResponse::InternalServerError().finish(), - } - } - - image_request_dto = Some(ImageRequestDto { mime_type, data }); - break; - } - - let image_request_dto = match image_request_dto { - Some(dto) => dto, - None => return HttpResponse::BadRequest().finish(), - }; - let result = image_controller.upload_image(image_request_dto).await; - - match result { - Ok(image_info) => HttpResponse::Created().json(image_info), - Err(e) => match e { - ImageError::UnsupportedMimeType => HttpResponse::BadRequest().body(format!("{e:?}")), - _ => { - log::error!("{e:?}"); - HttpResponse::InternalServerError().finish() - } - }, - } -} - -async fn get_image_by_id_handler( - image_controller: web::Data, - path: web::Path, -) -> impl Responder { - let id = path.into_inner(); - let result = image_controller.get_image_by_id(id).await; - - match result { - Ok(image_response) => HttpResponse::Ok() - .content_type(image_response.mime_type) - .body(image_response.data), - Err(e) => match e { - ImageError::NotFound => HttpResponse::NotFound().finish(), - _ => { - log::error!("{e:?}"); - HttpResponse::InternalServerError().finish() - } - }, - } -} diff --git a/backend/feature/image/src/framework/web/upload_image_handler.rs b/backend/feature/image/src/framework/web/upload_image_handler.rs new file mode 100644 index 0000000..67fc86e --- /dev/null +++ b/backend/feature/image/src/framework/web/upload_image_handler.rs @@ -0,0 +1,90 @@ +use actix_multipart::Multipart; +use actix_web::{HttpResponse, Responder, web}; +use auth::framework::web::auth_middleware::UserId; +use futures::StreamExt; +use utoipa::ToSchema; + +use crate::{ + adapter::delivery::{ + image_controller::ImageController, image_info_response_dto::ImageInfoResponseDto, + image_request_dto::ImageRequestDto, + }, + application::error::image_error::ImageError, +}; + +#[utoipa::path( + post, + path = "/image/upload", + tag = "image", + summary = "Upload an image", + request_body ( + content = RequestBodySchema, + content_type = "multipart/form-data", + ), + responses ( + (status = 201, body = ImageInfoResponseDto), + (status = 400, description = "Unsupported MIME type or file field not found"), + ), + security( + ("oauth2" = []) + ) +)] +pub async fn upload_image_handler( + image_controller: web::Data, + mut payload: Multipart, + _: UserId, +) -> impl Responder { + let mut image_request_dto: Option = None; + + while let Some(item) = payload.next().await { + let mut field = match item { + Ok(field) => field, + Err(_) => return HttpResponse::BadRequest().finish(), + }; + + if field.name() != Some("file") { + continue; + } + + let mime_type = field + .content_type() + .cloned() + .map(|mt| mt.to_string()) + .unwrap_or_else(|| "application/octet-stream".to_string()); + + let mut data = Vec::new(); + while let Some(chunk) = field.next().await { + match chunk { + Ok(bytes) => data.extend_from_slice(&bytes), + Err(_) => return HttpResponse::InternalServerError().finish(), + } + } + + image_request_dto = Some(ImageRequestDto { mime_type, data }); + break; + } + + let image_request_dto = match image_request_dto { + Some(dto) => dto, + None => return HttpResponse::BadRequest().finish(), + }; + let result = image_controller.upload_image(image_request_dto).await; + + match result { + Ok(image_info) => HttpResponse::Created().json(image_info), + Err(e) => match e { + ImageError::UnsupportedMimeType => HttpResponse::BadRequest().body(format!("{e:?}")), + _ => { + log::error!("{e:?}"); + HttpResponse::InternalServerError().finish() + } + }, + } +} + +#[derive(ToSchema)] +#[allow(dead_code)] +struct RequestBodySchema { + #[schema(value_type = String, format = Binary)] + file: Vec, +} diff --git a/backend/feature/post/src/adapter/delivery/post_info_query_dto.rs b/backend/feature/post/src/adapter/delivery/post_info_query_dto.rs index 5132186..8b24d7c 100644 --- a/backend/feature/post/src/adapter/delivery/post_info_query_dto.rs +++ b/backend/feature/post/src/adapter/delivery/post_info_query_dto.rs @@ -3,5 +3,6 @@ use utoipa::IntoParams; #[derive(Deserialize, IntoParams)] pub struct PostQueryDto { + #[param(default = true)] pub is_published_only: Option, } diff --git a/backend/feature/post/src/framework/web/post_api_doc.rs b/backend/feature/post/src/framework/web/post_api_doc.rs index 13ee1ce..70cef6f 100644 --- a/backend/feature/post/src/framework/web/post_api_doc.rs +++ b/backend/feature/post/src/framework/web/post_api_doc.rs @@ -1,16 +1,13 @@ +use crate::framework::web::{get_all_post_info_handler, get_post_by_id_handler}; use utoipa::{OpenApi, openapi}; -use crate::framework::web::{ - get_all_post_info_handler, - get_post_by_id_handler -}; #[derive(OpenApi)] #[openapi(paths( get_all_post_info_handler::get_all_post_info_handler, get_post_by_id_handler::get_post_by_id_handler ))] -struct PostApiDoc; +struct ApiDoc; pub fn openapi() -> openapi::OpenApi { - PostApiDoc::openapi() + ApiDoc::openapi() } diff --git a/backend/server/src/api_doc.rs b/backend/server/src/api_doc.rs new file mode 100644 index 0000000..a4ae5d9 --- /dev/null +++ b/backend/server/src/api_doc.rs @@ -0,0 +1,44 @@ +use actix_web::web; +use image::framework::web::image_api_doc; +use post::framework::web::post_api_doc; +use utoipa::{ + OpenApi, + openapi::{ + Components, InfoBuilder, OpenApiBuilder, + security::{AuthorizationCode, Flow, OAuth2, Scopes, SecurityScheme}, + }, +}; +use utoipa_redoc::{Redoc, Servable}; + +pub struct ApiDoc; + +impl utoipa::OpenApi for ApiDoc { + fn openapi() -> utoipa::openapi::OpenApi { + let mut components = Components::new(); + + components.add_security_scheme( + "oauth2", + SecurityScheme::OAuth2(OAuth2::new(vec![Flow::AuthorizationCode( + AuthorizationCode::new("/auth/login", "/auth/callback", Scopes::new()), + )])), + ); + + OpenApiBuilder::new() + .info( + InfoBuilder::new() + .title("SquidSpirit API") + .version(env!("CARGO_PKG_VERSION")) + .build(), + ) + .components(Some(components)) + .build() + } +} + +pub fn configure_api_doc_routes(cfg: &mut web::ServiceConfig) { + let openapi = ApiDoc::openapi() + .merge_from(post_api_doc::openapi()) + .merge_from(image_api_doc::openapi()); + + cfg.service(Redoc::with_url("/redoc", openapi)); +} diff --git a/backend/server/src/apidoc.rs b/backend/server/src/apidoc.rs deleted file mode 100644 index 66ff21c..0000000 --- a/backend/server/src/apidoc.rs +++ /dev/null @@ -1,19 +0,0 @@ -use actix_web::web; -use post::framework::web::post_api_doc; -use utoipa::OpenApi; -use utoipa_redoc::{Redoc, Servable}; - -#[derive(OpenApi)] -#[openapi( - info( - title = "SquidSpirit API", - version = env!("CARGO_PKG_VERSION") - ) -)] -pub struct ApiDoc; - -pub fn configure_api_doc_routes(cfg: &mut web::ServiceConfig) { - let openapi = ApiDoc::openapi().merge_from(post_api_doc::openapi()); - - cfg.service(Redoc::with_url("/redoc", openapi)); -} diff --git a/backend/server/src/lib.rs b/backend/server/src/lib.rs index 9703715..0daadb0 100644 --- a/backend/server/src/lib.rs +++ b/backend/server/src/lib.rs @@ -1,3 +1,3 @@ -pub mod apidoc; +pub mod api_doc; pub mod configuration; pub mod container; diff --git a/backend/server/src/main.rs b/backend/server/src/main.rs index 1e36bf1..cae50d6 100644 --- a/backend/server/src/main.rs +++ b/backend/server/src/main.rs @@ -11,7 +11,7 @@ use auth::framework::web::auth_web_routes::configure_auth_routes; use image::framework::web::image_web_routes::configure_image_routes; use openidconnect::reqwest; use post::framework::web::post_web_routes::configure_post_routes; -use server::{apidoc::configure_api_doc_routes, configuration::Configuration, container::Container}; +use server::{api_doc::configure_api_doc_routes, configuration::Configuration, container::Container}; use sqlx::{Pool, Postgres}; #[actix_web::main]