From 1d28ec616b71c7dfb3917a3ca61c0ef0faf04300 Mon Sep 17 00:00:00 2001 From: SquidSpirit Date: Sat, 2 Aug 2025 03:35:03 +0800 Subject: [PATCH 1/4] BLOG-103 feat: add API documentation with Utoipa and configure routes --- ...ee7a7d8ede2410b423985caffd86361ad9263.json | 46 +++++++++++++++++++ backend/Cargo.lock | 39 ++++++++++++++++ backend/Cargo.toml | 2 + backend/server/Cargo.toml | 2 + backend/server/src/apidoc.rs | 14 ++++++ backend/server/src/lib.rs | 1 + backend/server/src/main.rs | 3 +- 7 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 backend/.sqlx/query-9d1ffa7a71c8830d75eeeb26800ee7a7d8ede2410b423985caffd86361ad9263.json create mode 100644 backend/server/src/apidoc.rs diff --git a/backend/.sqlx/query-9d1ffa7a71c8830d75eeeb26800ee7a7d8ede2410b423985caffd86361ad9263.json b/backend/.sqlx/query-9d1ffa7a71c8830d75eeeb26800ee7a7d8ede2410b423985caffd86361ad9263.json new file mode 100644 index 0000000..e8eb2c6 --- /dev/null +++ b/backend/.sqlx/query-9d1ffa7a71c8830d75eeeb26800ee7a7d8ede2410b423985caffd86361ad9263.json @@ -0,0 +1,46 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, issuer, source_id, displayed_name, email\n FROM \"user\"\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "issuer", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "source_id", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "displayed_name", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "email", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false, + false, + false, + false, + false + ] + }, + "hash": "9d1ffa7a71c8830d75eeeb26800ee7a7d8ede2410b423985caffd86361ad9263" +} diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 7ffa59d..ad782b8 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -2234,6 +2234,7 @@ dependencies = [ "log", "serde", "sqlx", + "utoipa", ] [[package]] @@ -2819,6 +2820,8 @@ dependencies = [ "percent-encoding", "post", "sqlx", + "utoipa", + "utoipa-redoc", ] [[package]] @@ -3509,6 +3512,42 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "utoipa" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" +dependencies = [ + "indexmap 2.9.0", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn", +] + +[[package]] +name = "utoipa-redoc" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6427547f6db7ec006cbbef95f7565952a16f362e298b416d2d497d9706fef72d" +dependencies = [ + "actix-web", + "serde", + "serde_json", + "utoipa", +] + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 272ab65..91ce1e9 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -30,6 +30,8 @@ sqlx = { version = "0.8.5", features = [ "runtime-tokio-rustls", ] } tokio = { version = "1.45.0", features = ["full"] } +utoipa = { version = "5.4.0", features = ["actix_extras"] } +utoipa-redoc = { version = "6.0.0", features = ["actix-web"] } server.path = "server" auth.path = "feature/auth" diff --git a/backend/server/Cargo.toml b/backend/server/Cargo.toml index 938f82e..8e5a7b1 100644 --- a/backend/server/Cargo.toml +++ b/backend/server/Cargo.toml @@ -12,6 +12,8 @@ hex.workspace = true openidconnect.workspace = true percent-encoding.workspace = true sqlx.workspace = true +utoipa.workspace = true +utoipa-redoc.workspace = true auth.workspace = true image.workspace = true diff --git a/backend/server/src/apidoc.rs b/backend/server/src/apidoc.rs new file mode 100644 index 0000000..e844567 --- /dev/null +++ b/backend/server/src/apidoc.rs @@ -0,0 +1,14 @@ +use actix_web::web; +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) { + cfg.service(Redoc::with_url("/redoc", ApiDoc::openapi())); +} diff --git a/backend/server/src/lib.rs b/backend/server/src/lib.rs index 18fb4fc..9703715 100644 --- a/backend/server/src/lib.rs +++ b/backend/server/src/lib.rs @@ -1,2 +1,3 @@ +pub mod apidoc; pub mod configuration; pub mod container; diff --git a/backend/server/src/main.rs b/backend/server/src/main.rs index adc4724..1e36bf1 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::{configuration::Configuration, container::Container}; +use server::{apidoc::configure_api_doc_routes, configuration::Configuration, container::Container}; use sqlx::{Pool, Postgres}; #[actix_web::main] @@ -68,6 +68,7 @@ fn create_app( .app_data(web::Data::from(container.auth_controller)) .app_data(web::Data::from(container.image_controller)) .app_data(web::Data::from(container.post_controller)) + .configure(configure_api_doc_routes) .configure(configure_auth_routes) .configure(configure_image_routes) .configure(configure_post_routes) -- 2.47.2 From 562d658ea109823afb570df92aab7c9f25801380 Mon Sep 17 00:00:00 2001 From: SquidSpirit Date: Sat, 2 Aug 2025 04:29:49 +0800 Subject: [PATCH 2/4] BLOG-103 feat: api doc for post routes --- backend/feature/post/Cargo.toml | 1 + .../adapter/delivery/color_response_dto.rs | 3 +- .../adapter/delivery/label_response_dto.rs | 3 +- .../src/adapter/delivery/post_controller.rs | 26 +++++++---- .../adapter/delivery/post_info_query_dto.rs | 3 +- .../delivery/post_info_response_dto.rs | 3 +- .../src/adapter/delivery/post_response_dto.rs | 3 +- backend/feature/post/src/framework/web.rs | 4 ++ .../web/get_all_post_info_handler.rs | 33 +++++++++++++ .../framework/web/get_post_by_id_handler.rs | 36 +++++++++++++++ .../post/src/framework/web/post_api_doc.rs | 16 +++++++ .../post/src/framework/web/post_web_routes.rs | 46 ++----------------- backend/server/src/apidoc.rs | 15 ++++-- 13 files changed, 131 insertions(+), 61 deletions(-) create mode 100644 backend/feature/post/src/framework/web/get_all_post_info_handler.rs create mode 100644 backend/feature/post/src/framework/web/get_post_by_id_handler.rs create mode 100644 backend/feature/post/src/framework/web/post_api_doc.rs diff --git a/backend/feature/post/Cargo.toml b/backend/feature/post/Cargo.toml index 052b52c..dc63b3e 100644 --- a/backend/feature/post/Cargo.toml +++ b/backend/feature/post/Cargo.toml @@ -10,3 +10,4 @@ chrono.workspace = true log.workspace = true serde.workspace = true sqlx.workspace = true +utoipa.workspace = true diff --git a/backend/feature/post/src/adapter/delivery/color_response_dto.rs b/backend/feature/post/src/adapter/delivery/color_response_dto.rs index 0d8e1cf..274f95a 100644 --- a/backend/feature/post/src/adapter/delivery/color_response_dto.rs +++ b/backend/feature/post/src/adapter/delivery/color_response_dto.rs @@ -1,8 +1,9 @@ use serde::Serialize; +use utoipa::ToSchema; use crate::domain::entity::color::Color; -#[derive(Serialize)] +#[derive(Serialize, ToSchema)] pub struct ColorResponseDto { pub red: u8, pub green: u8, diff --git a/backend/feature/post/src/adapter/delivery/label_response_dto.rs b/backend/feature/post/src/adapter/delivery/label_response_dto.rs index 9da46d3..bd9a620 100644 --- a/backend/feature/post/src/adapter/delivery/label_response_dto.rs +++ b/backend/feature/post/src/adapter/delivery/label_response_dto.rs @@ -1,10 +1,11 @@ use serde::Serialize; +use utoipa::ToSchema; use crate::{ adapter::delivery::color_response_dto::ColorResponseDto, domain::entity::label::Label, }; -#[derive(Serialize)] +#[derive(Serialize, ToSchema)] pub struct LabelResponseDto { pub id: i32, pub name: String, diff --git a/backend/feature/post/src/adapter/delivery/post_controller.rs b/backend/feature/post/src/adapter/delivery/post_controller.rs index 861bfb8..454be2a 100644 --- a/backend/feature/post/src/adapter/delivery/post_controller.rs +++ b/backend/feature/post/src/adapter/delivery/post_controller.rs @@ -2,11 +2,14 @@ use std::sync::Arc; use async_trait::async_trait; -use crate::application::{ - error::post_error::PostError, - use_case::{ - get_all_post_info_use_case::GetAllPostInfoUseCase, - get_full_post_use_case::GetFullPostUseCase, +use crate::{ + adapter::delivery::post_info_query_dto::PostQueryDto, + application::{ + error::post_error::PostError, + use_case::{ + get_all_post_info_use_case::GetAllPostInfoUseCase, + get_full_post_use_case::GetFullPostUseCase, + }, }, }; @@ -16,10 +19,10 @@ use super::{post_info_response_dto::PostInfoResponseDto, post_response_dto::Post pub trait PostController: Send + Sync { async fn get_all_post_info( &self, - is_published_only: bool, + query: PostQueryDto, ) -> Result, PostError>; - async fn get_full_post(&self, id: i32) -> Result; + async fn get_post_by_id(&self, id: i32) -> Result; } pub struct PostControllerImpl { @@ -43,9 +46,12 @@ impl PostControllerImpl { impl PostController for PostControllerImpl { async fn get_all_post_info( &self, - is_published_only: bool, + query: PostQueryDto, ) -> Result, PostError> { - let result = self.get_all_post_info_use_case.execute(is_published_only).await; + let result = self + .get_all_post_info_use_case + .execute(query.is_published_only.unwrap_or(true)) + .await; result.map(|post_info_list| { let post_info_response_dto_list: Vec = post_info_list @@ -57,7 +63,7 @@ impl PostController for PostControllerImpl { }) } - async fn get_full_post(&self, id: i32) -> Result { + async fn get_post_by_id(&self, id: i32) -> Result { let result = self.get_full_post_use_case.execute(id).await; result.map(PostResponseDto::from) 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 22e3676..5132186 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 @@ -1,6 +1,7 @@ use serde::Deserialize; +use utoipa::IntoParams; -#[derive(Deserialize)] +#[derive(Deserialize, IntoParams)] pub struct PostQueryDto { pub is_published_only: Option, } diff --git a/backend/feature/post/src/adapter/delivery/post_info_response_dto.rs b/backend/feature/post/src/adapter/delivery/post_info_response_dto.rs index da41320..dd33091 100644 --- a/backend/feature/post/src/adapter/delivery/post_info_response_dto.rs +++ b/backend/feature/post/src/adapter/delivery/post_info_response_dto.rs @@ -1,10 +1,11 @@ use serde::Serialize; +use utoipa::ToSchema; use crate::domain::entity::post_info::PostInfo; use super::label_response_dto::LabelResponseDto; -#[derive(Serialize)] +#[derive(Serialize, ToSchema)] pub struct PostInfoResponseDto { pub id: i32, pub title: String, diff --git a/backend/feature/post/src/adapter/delivery/post_response_dto.rs b/backend/feature/post/src/adapter/delivery/post_response_dto.rs index a4521c3..85c0c02 100644 --- a/backend/feature/post/src/adapter/delivery/post_response_dto.rs +++ b/backend/feature/post/src/adapter/delivery/post_response_dto.rs @@ -1,10 +1,11 @@ use serde::Serialize; +use utoipa::ToSchema; use crate::domain::entity::post::Post; use super::post_info_response_dto::PostInfoResponseDto; -#[derive(Serialize)] +#[derive(Serialize, ToSchema)] pub struct PostResponseDto { pub id: i32, pub info: PostInfoResponseDto, diff --git a/backend/feature/post/src/framework/web.rs b/backend/feature/post/src/framework/web.rs index f41f9a6..bacfac4 100644 --- a/backend/feature/post/src/framework/web.rs +++ b/backend/feature/post/src/framework/web.rs @@ -1 +1,5 @@ +pub mod post_api_doc; pub mod post_web_routes; + +mod get_all_post_info_handler; +mod get_post_by_id_handler; diff --git a/backend/feature/post/src/framework/web/get_all_post_info_handler.rs b/backend/feature/post/src/framework/web/get_all_post_info_handler.rs new file mode 100644 index 0000000..f0e34ca --- /dev/null +++ b/backend/feature/post/src/framework/web/get_all_post_info_handler.rs @@ -0,0 +1,33 @@ +use actix_web::{HttpResponse, Responder, web}; + +use crate::adapter::delivery::{ + post_controller::PostController, post_info_query_dto::PostQueryDto, + post_info_response_dto::PostInfoResponseDto, +}; + +#[utoipa::path( + get, + path = "/post/all", + tag = "post", + summary = "Get all post information", + params( + PostQueryDto + ), + responses ( + (status = 200, body = [PostInfoResponseDto]) + ) +)] +pub async fn get_all_post_info_handler( + post_controller: web::Data, + query: web::Query, +) -> impl Responder { + let result = post_controller.get_all_post_info(query.into_inner()).await; + + match result { + Ok(post_info_list) => HttpResponse::Ok().json(post_info_list), + Err(e) => { + log::error!("{e:?}"); + HttpResponse::InternalServerError().finish() + } + } +} diff --git a/backend/feature/post/src/framework/web/get_post_by_id_handler.rs b/backend/feature/post/src/framework/web/get_post_by_id_handler.rs new file mode 100644 index 0000000..03576c8 --- /dev/null +++ b/backend/feature/post/src/framework/web/get_post_by_id_handler.rs @@ -0,0 +1,36 @@ +use actix_web::{HttpResponse, Responder, web}; + +use crate::{ + adapter::delivery::{post_controller::PostController, post_response_dto::PostResponseDto}, + application::error::post_error::PostError, +}; + +#[utoipa::path( + get, + path = "/post/{id}", + tag = "post", + summary = "Get post by ID", + responses ( + (status = 200, body = PostResponseDto), + (status = 404, description = "Post not found") + ) +)] +pub async fn get_post_by_id_handler( + post_controller: web::Data, + path: web::Path, +) -> impl Responder { + let id = path.into_inner(); + let result = post_controller.get_post_by_id(id).await; + + match result { + Ok(post) => HttpResponse::Ok().json(post), + Err(e) => { + if e == PostError::NotFound { + HttpResponse::NotFound().finish() + } else { + log::error!("{e:?}"); + HttpResponse::InternalServerError().finish() + } + } + } +} diff --git a/backend/feature/post/src/framework/web/post_api_doc.rs b/backend/feature/post/src/framework/web/post_api_doc.rs new file mode 100644 index 0000000..13ee1ce --- /dev/null +++ b/backend/feature/post/src/framework/web/post_api_doc.rs @@ -0,0 +1,16 @@ +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; + +pub fn openapi() -> openapi::OpenApi { + PostApiDoc::openapi() +} diff --git a/backend/feature/post/src/framework/web/post_web_routes.rs b/backend/feature/post/src/framework/web/post_web_routes.rs index bb68943..74ea8d3 100644 --- a/backend/feature/post/src/framework/web/post_web_routes.rs +++ b/backend/feature/post/src/framework/web/post_web_routes.rs @@ -1,50 +1,14 @@ -use actix_web::{HttpResponse, Responder, web}; +use actix_web::web; -use crate::{ - adapter::delivery::{post_controller::PostController, post_info_query_dto::PostQueryDto}, - application::error::post_error::PostError, +use crate::framework::web::{ + get_all_post_info_handler::get_all_post_info_handler, + get_post_by_id_handler::get_post_by_id_handler, }; pub fn configure_post_routes(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("/post") .route("/all", web::get().to(get_all_post_info_handler)) - .route("/{id}", web::get().to(get_full_post_handler)), + .route("/{id}", web::get().to(get_post_by_id_handler)), ); } - -async fn get_all_post_info_handler( - post_controller: web::Data, - query: web::Query, -) -> impl Responder { - let is_published_only = query.is_published_only.unwrap_or_else(|| true); - let result = post_controller.get_all_post_info(is_published_only).await; - - match result { - Ok(post_info_list) => HttpResponse::Ok().json(post_info_list), - Err(e) => { - log::error!("{e:?}"); - HttpResponse::InternalServerError().finish() - } - } -} - -async fn get_full_post_handler( - post_controller: web::Data, - path: web::Path, -) -> impl Responder { - let id = path.into_inner(); - let result = post_controller.get_full_post(id).await; - - match result { - Ok(post) => HttpResponse::Ok().json(post), - Err(e) => { - if e == PostError::NotFound { - HttpResponse::NotFound().finish() - } else { - log::error!("{e:?}"); - HttpResponse::InternalServerError().finish() - } - } - } -} diff --git a/backend/server/src/apidoc.rs b/backend/server/src/apidoc.rs index e844567..66ff21c 100644 --- a/backend/server/src/apidoc.rs +++ b/backend/server/src/apidoc.rs @@ -1,14 +1,19 @@ 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") -))] +#[openapi( + info( + title = "SquidSpirit API", + version = env!("CARGO_PKG_VERSION") + ) +)] pub struct ApiDoc; pub fn configure_api_doc_routes(cfg: &mut web::ServiceConfig) { - cfg.service(Redoc::with_url("/redoc", ApiDoc::openapi())); + let openapi = ApiDoc::openapi().merge_from(post_api_doc::openapi()); + + cfg.service(Redoc::with_url("/redoc", openapi)); } -- 2.47.2 From 1cf634bd19fe39ef7220bfff5908f173e54a041f Mon Sep 17 00:00:00 2001 From: SquidSpirit Date: Sat, 2 Aug 2025 06:10:28 +0800 Subject: [PATCH 3/4] BLOG-103 feat: integrate Utoipa for API documentation and add image upload functionality --- backend/Cargo.lock | 1 + backend/feature/image/Cargo.toml | 1 + .../delivery/image_info_response_dto.rs | 3 +- backend/feature/image/src/framework/web.rs | 6 +- .../framework/web/get_image_by_id_handler.rs | 43 +++++++++ .../image/src/framework/web/image_api_doc.rs | 13 +++ .../src/framework/web/image_web_routes.rs | 84 +---------------- .../src/framework/web/upload_image_handler.rs | 90 +++++++++++++++++++ .../adapter/delivery/post_info_query_dto.rs | 1 + .../post/src/framework/web/post_api_doc.rs | 9 +- backend/server/src/api_doc.rs | 44 +++++++++ backend/server/src/apidoc.rs | 19 ---- backend/server/src/lib.rs | 2 +- backend/server/src/main.rs | 2 +- 14 files changed, 208 insertions(+), 110 deletions(-) create mode 100644 backend/feature/image/src/framework/web/get_image_by_id_handler.rs create mode 100644 backend/feature/image/src/framework/web/image_api_doc.rs create mode 100644 backend/feature/image/src/framework/web/upload_image_handler.rs create mode 100644 backend/server/src/api_doc.rs delete mode 100644 backend/server/src/apidoc.rs 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] -- 2.47.2 From 68100e9b5461c7a454af14df428579deee3b0782 Mon Sep 17 00:00:00 2001 From: SquidSpirit Date: Sat, 2 Aug 2025 06:44:44 +0800 Subject: [PATCH 4/4] BLOG-103 feat: add OIDC authentication handlers and integrate Utoipa for API documentation --- backend/Cargo.lock | 1 + backend/feature/auth/Cargo.toml | 1 + .../delivery/oidc_callback_query_dto.rs | 3 +- .../src/adapter/delivery/user_response_dto.rs | 3 +- backend/feature/auth/src/framework/web.rs | 5 + .../auth/src/framework/web/auth_api_doc.rs | 17 +++ .../auth/src/framework/web/auth_web_routes.rs | 111 +----------------- .../web/get_logged_in_user_handler.rs | 33 ++++++ .../framework/web/oidc_callback_handler.rs | 69 +++++++++++ .../src/framework/web/oidc_login_handler.rs | 43 +++++++ .../src/framework/web/oidc_logout_handler.rs | 18 +++ backend/server/src/api_doc.rs | 6 +- 12 files changed, 201 insertions(+), 109 deletions(-) create mode 100644 backend/feature/auth/src/framework/web/auth_api_doc.rs create mode 100644 backend/feature/auth/src/framework/web/get_logged_in_user_handler.rs create mode 100644 backend/feature/auth/src/framework/web/oidc_callback_handler.rs create mode 100644 backend/feature/auth/src/framework/web/oidc_login_handler.rs create mode 100644 backend/feature/auth/src/framework/web/oidc_logout_handler.rs diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 6893dc9..678fc45 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -429,6 +429,7 @@ dependencies = [ "openidconnect", "serde", "sqlx", + "utoipa", ] [[package]] diff --git a/backend/feature/auth/Cargo.toml b/backend/feature/auth/Cargo.toml index 27362a0..f820b30 100644 --- a/backend/feature/auth/Cargo.toml +++ b/backend/feature/auth/Cargo.toml @@ -11,3 +11,4 @@ log.workspace = true openidconnect.workspace = true serde.workspace = true sqlx.workspace = true +utoipa.workspace = true diff --git a/backend/feature/auth/src/adapter/delivery/oidc_callback_query_dto.rs b/backend/feature/auth/src/adapter/delivery/oidc_callback_query_dto.rs index 5cf4eb9..4bda4eb 100644 --- a/backend/feature/auth/src/adapter/delivery/oidc_callback_query_dto.rs +++ b/backend/feature/auth/src/adapter/delivery/oidc_callback_query_dto.rs @@ -1,6 +1,7 @@ use serde::Deserialize; +use utoipa::IntoParams; -#[derive(Deserialize)] +#[derive(Deserialize, IntoParams)] pub struct OidcCallbackQueryDto { pub code: String, pub state: String, diff --git a/backend/feature/auth/src/adapter/delivery/user_response_dto.rs b/backend/feature/auth/src/adapter/delivery/user_response_dto.rs index 2726c57..f040f2d 100644 --- a/backend/feature/auth/src/adapter/delivery/user_response_dto.rs +++ b/backend/feature/auth/src/adapter/delivery/user_response_dto.rs @@ -1,8 +1,9 @@ use serde::Serialize; +use utoipa::ToSchema; use crate::domain::entity::user::User; -#[derive(Serialize)] +#[derive(Serialize, ToSchema)] pub struct UserResponseDto { pub id: i32, pub displayed_name: String, diff --git a/backend/feature/auth/src/framework/web.rs b/backend/feature/auth/src/framework/web.rs index 54b1487..a8945cb 100644 --- a/backend/feature/auth/src/framework/web.rs +++ b/backend/feature/auth/src/framework/web.rs @@ -1,4 +1,9 @@ +pub mod auth_api_doc; pub mod auth_middleware; pub mod auth_web_routes; +pub mod get_logged_in_user_handler; +pub mod oidc_callback_handler; +pub mod oidc_login_handler; +pub mod oidc_logout_handler; mod constants; diff --git a/backend/feature/auth/src/framework/web/auth_api_doc.rs b/backend/feature/auth/src/framework/web/auth_api_doc.rs new file mode 100644 index 0000000..4ab1dbe --- /dev/null +++ b/backend/feature/auth/src/framework/web/auth_api_doc.rs @@ -0,0 +1,17 @@ +use crate::framework::web::{ + get_logged_in_user_handler, oidc_callback_handler, oidc_login_handler, oidc_logout_handler, +}; +use utoipa::{OpenApi, openapi}; + +#[derive(OpenApi)] +#[openapi(paths( + get_logged_in_user_handler::get_logged_in_user_handler, + oidc_callback_handler::oidc_callback_handler, + oidc_login_handler::oidc_login_handler, + oidc_logout_handler::oidc_logout_handler +))] +struct ApiDoc; + +pub fn openapi() -> openapi::OpenApi { + ApiDoc::openapi() +} diff --git a/backend/feature/auth/src/framework/web/auth_web_routes.rs b/backend/feature/auth/src/framework/web/auth_web_routes.rs index d5dde49..5e2f9c8 100644 --- a/backend/feature/auth/src/framework/web/auth_web_routes.rs +++ b/backend/feature/auth/src/framework/web/auth_web_routes.rs @@ -1,15 +1,9 @@ -use actix_session::Session; -use actix_web::{HttpResponse, Responder, http::header, web}; +use actix_web::web; -use crate::{ - adapter::delivery::{ - auth_controller::AuthController, oidc_callback_query_dto::OidcCallbackQueryDto, - }, - application::error::auth_error::AuthError, - framework::web::{ - auth_middleware::UserId, - constants::{SESSION_KEY_AUTH_NONCE, SESSION_KEY_AUTH_STATE, SESSION_KEY_USER_ID}, - }, +use crate::framework::web::{ + get_logged_in_user_handler::get_logged_in_user_handler, + oidc_callback_handler::oidc_callback_handler, oidc_login_handler::oidc_login_handler, + oidc_logout_handler::oidc_logout_handler, }; pub fn configure_auth_routes(cfg: &mut web::ServiceConfig) { @@ -17,101 +11,8 @@ pub fn configure_auth_routes(cfg: &mut web::ServiceConfig) { web::scope("/auth") .route("/login", web::get().to(oidc_login_handler)) .route("/callback", web::get().to(oidc_callback_handler)) - .route("/logout", web::get().to(logout_handler)), + .route("/logout", web::get().to(oidc_logout_handler)), ); cfg.service(web::resource("/me").route(web::get().to(get_logged_in_user_handler))); } - -async fn oidc_login_handler( - auth_controller: web::Data, - session: Session, -) -> impl Responder { - let result = auth_controller.oidc_login(); - - match result { - Ok(auth_url) => { - if let Err(e) = session.insert::(SESSION_KEY_AUTH_STATE, auth_url.state) { - log::error!("{e:?}"); - return HttpResponse::InternalServerError().finish(); - } - if let Err(e) = session.insert::(SESSION_KEY_AUTH_NONCE, auth_url.nonce) { - log::error!("{e:?}"); - return HttpResponse::InternalServerError().finish(); - } - HttpResponse::Found() - .append_header((header::LOCATION, auth_url.url)) - .finish() - } - Err(e) => { - log::error!("{e:?}"); - HttpResponse::InternalServerError().finish() - } - } -} - -async fn oidc_callback_handler( - auth_controller: web::Data, - query: web::Query, - session: Session, -) -> impl Responder { - let expected_state = match session.get::(SESSION_KEY_AUTH_STATE) { - Ok(Some(state)) => state, - _ => return HttpResponse::BadRequest().finish(), - }; - - let expected_nonce = match session.get::(SESSION_KEY_AUTH_NONCE) { - Ok(Some(nonce)) => nonce, - _ => return HttpResponse::BadRequest().finish(), - }; - - let result = auth_controller - .oidc_callback(query.into_inner(), &expected_state, &expected_nonce) - .await; - - session.remove(SESSION_KEY_AUTH_STATE); - session.remove(SESSION_KEY_AUTH_NONCE); - match result { - Ok(user) => { - if let Err(e) = session.insert::(SESSION_KEY_USER_ID, user.id) { - log::error!("{e:?}"); - return HttpResponse::InternalServerError().finish(); - } - HttpResponse::Found() - .append_header((header::LOCATION, "/")) - .finish() - } - Err(e) => match e { - AuthError::InvalidAuthCode - | AuthError::InvalidIdToken - | AuthError::InvalidNonce - | AuthError::InvalidState => HttpResponse::BadRequest().finish(), - _ => { - log::error!("{e:?}"); - HttpResponse::InternalServerError().finish() - } - }, - } -} - -async fn logout_handler(session: Session) -> impl Responder { - session.clear(); - HttpResponse::Found() - .append_header((header::LOCATION, "/")) - .finish() -} - -async fn get_logged_in_user_handler( - auth_controller: web::Data, - user_id: UserId, -) -> impl Responder { - let result = auth_controller.get_user(user_id.get()).await; - - match result { - Ok(user) => HttpResponse::Ok().json(user), - Err(e) => { - log::error!("{e:?}"); - HttpResponse::InternalServerError().finish() - } - } -} diff --git a/backend/feature/auth/src/framework/web/get_logged_in_user_handler.rs b/backend/feature/auth/src/framework/web/get_logged_in_user_handler.rs new file mode 100644 index 0000000..a1415f8 --- /dev/null +++ b/backend/feature/auth/src/framework/web/get_logged_in_user_handler.rs @@ -0,0 +1,33 @@ +use actix_web::{HttpResponse, Responder, web}; + +use crate::{ + adapter::delivery::{auth_controller::AuthController, user_response_dto::UserResponseDto}, + framework::web::auth_middleware::UserId, +}; + +#[utoipa::path( + get, + path = "/me", + tag = "auth", + summary = "Get logged-in user information", + responses( + (status = 200, body = UserResponseDto), + ), + security( + ("oauth2" = []) + ) +)] +pub async fn get_logged_in_user_handler( + auth_controller: web::Data, + user_id: UserId, +) -> impl Responder { + let result = auth_controller.get_user(user_id.get()).await; + + match result { + Ok(user) => HttpResponse::Ok().json(user), + Err(e) => { + log::error!("{e:?}"); + HttpResponse::InternalServerError().finish() + } + } +} diff --git a/backend/feature/auth/src/framework/web/oidc_callback_handler.rs b/backend/feature/auth/src/framework/web/oidc_callback_handler.rs new file mode 100644 index 0000000..a6494c7 --- /dev/null +++ b/backend/feature/auth/src/framework/web/oidc_callback_handler.rs @@ -0,0 +1,69 @@ +use actix_session::Session; +use actix_web::{HttpResponse, Responder, http::header, web}; + +use crate::{ + adapter::delivery::{ + auth_controller::AuthController, oidc_callback_query_dto::OidcCallbackQueryDto, + }, + application::error::auth_error::AuthError, + framework::web::constants::{ + SESSION_KEY_AUTH_NONCE, SESSION_KEY_AUTH_STATE, SESSION_KEY_USER_ID, + }, +}; + +#[utoipa::path( + get, + path = "/auth/callback", + tag = "auth", + summary = "Handle OIDC callback", + params( + OidcCallbackQueryDto + ), + responses( + (status = 302, description = "Redirect to home page"), + (status = 400, description = "Invalid state or nonce"), + ) +)] +pub async fn oidc_callback_handler( + auth_controller: web::Data, + query: web::Query, + session: Session, +) -> impl Responder { + let expected_state = match session.get::(SESSION_KEY_AUTH_STATE) { + Ok(Some(state)) => state, + _ => return HttpResponse::BadRequest().finish(), + }; + + let expected_nonce = match session.get::(SESSION_KEY_AUTH_NONCE) { + Ok(Some(nonce)) => nonce, + _ => return HttpResponse::BadRequest().finish(), + }; + + let result = auth_controller + .oidc_callback(query.into_inner(), &expected_state, &expected_nonce) + .await; + + session.remove(SESSION_KEY_AUTH_STATE); + session.remove(SESSION_KEY_AUTH_NONCE); + match result { + Ok(user) => { + if let Err(e) = session.insert::(SESSION_KEY_USER_ID, user.id) { + log::error!("{e:?}"); + return HttpResponse::InternalServerError().finish(); + } + HttpResponse::Found() + .append_header((header::LOCATION, "/")) + .finish() + } + Err(e) => match e { + AuthError::InvalidAuthCode + | AuthError::InvalidIdToken + | AuthError::InvalidNonce + | AuthError::InvalidState => HttpResponse::BadRequest().finish(), + _ => { + log::error!("{e:?}"); + HttpResponse::InternalServerError().finish() + } + }, + } +} diff --git a/backend/feature/auth/src/framework/web/oidc_login_handler.rs b/backend/feature/auth/src/framework/web/oidc_login_handler.rs new file mode 100644 index 0000000..b804676 --- /dev/null +++ b/backend/feature/auth/src/framework/web/oidc_login_handler.rs @@ -0,0 +1,43 @@ +use actix_session::Session; +use actix_web::{HttpResponse, Responder, http::header, web}; + +use crate::{ + adapter::delivery::auth_controller::AuthController, + framework::web::constants::{SESSION_KEY_AUTH_NONCE, SESSION_KEY_AUTH_STATE}, +}; + +#[utoipa::path( + get, + path = "/auth/login", + tag = "auth", + summary = "Initiate OIDC login", + responses( + (status = 302, description = "Redirect to OIDC provider") + ) +)] +pub async fn oidc_login_handler( + auth_controller: web::Data, + session: Session, +) -> impl Responder { + let result = auth_controller.oidc_login(); + + match result { + Ok(auth_url) => { + if let Err(e) = session.insert::(SESSION_KEY_AUTH_STATE, auth_url.state) { + log::error!("{e:?}"); + return HttpResponse::InternalServerError().finish(); + } + if let Err(e) = session.insert::(SESSION_KEY_AUTH_NONCE, auth_url.nonce) { + log::error!("{e:?}"); + return HttpResponse::InternalServerError().finish(); + } + HttpResponse::Found() + .append_header((header::LOCATION, auth_url.url)) + .finish() + } + Err(e) => { + log::error!("{e:?}"); + HttpResponse::InternalServerError().finish() + } + } +} diff --git a/backend/feature/auth/src/framework/web/oidc_logout_handler.rs b/backend/feature/auth/src/framework/web/oidc_logout_handler.rs new file mode 100644 index 0000000..ea02cac --- /dev/null +++ b/backend/feature/auth/src/framework/web/oidc_logout_handler.rs @@ -0,0 +1,18 @@ +use actix_session::Session; +use actix_web::{HttpResponse, Responder, http::header}; + +#[utoipa::path( + get, + path = "/auth/logout", + tag = "auth", + summary = "Logout user", + responses( + (status = 302, description = "Redirect to home page") + ) +)] +pub async fn oidc_logout_handler(session: Session) -> impl Responder { + session.clear(); + HttpResponse::Found() + .append_header((header::LOCATION, "/")) + .finish() +} diff --git a/backend/server/src/api_doc.rs b/backend/server/src/api_doc.rs index a4ae5d9..e4d2e2a 100644 --- a/backend/server/src/api_doc.rs +++ b/backend/server/src/api_doc.rs @@ -1,4 +1,5 @@ use actix_web::web; +use auth::framework::web::auth_api_doc; use image::framework::web::image_api_doc; use post::framework::web::post_api_doc; use utoipa::{ @@ -37,8 +38,9 @@ impl utoipa::OpenApi for ApiDoc { 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()); + .merge_from(auth_api_doc::openapi()) + .merge_from(image_api_doc::openapi()) + .merge_from(post_api_doc::openapi()); cfg.service(Redoc::with_url("/redoc", openapi)); } -- 2.47.2