Compare commits
	
		
			3 Commits
		
	
	
		
			1d28ec616b
			...
			68100e9b54
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 68100e9b54 | |||
| 1cf634bd19 | |||
| 562d658ea1 | 
							
								
								
									
										2
									
								
								backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -429,6 +429,7 @@ dependencies = [ | ||||
|  "openidconnect", | ||||
|  "serde", | ||||
|  "sqlx", | ||||
|  "utoipa", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| @ -1669,6 +1670,7 @@ dependencies = [ | ||||
|  "log", | ||||
|  "serde", | ||||
|  "sqlx", | ||||
|  "utoipa", | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
|  | ||||
| @ -11,3 +11,4 @@ log.workspace = true | ||||
| openidconnect.workspace = true | ||||
| serde.workspace = true | ||||
| sqlx.workspace = true | ||||
| utoipa.workspace = true | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| use serde::Deserialize; | ||||
| use utoipa::IntoParams; | ||||
| 
 | ||||
| #[derive(Deserialize)] | ||||
| #[derive(Deserialize, IntoParams)] | ||||
| pub struct OidcCallbackQueryDto { | ||||
|     pub code: String, | ||||
|     pub state: String, | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
							
								
								
									
										17
									
								
								backend/feature/auth/src/framework/web/auth_api_doc.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								backend/feature/auth/src/framework/web/auth_api_doc.rs
									
									
									
									
									
										Normal file
									
								
							| @ -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() | ||||
| } | ||||
| @ -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<dyn AuthController>, | ||||
|     session: Session, | ||||
| ) -> impl Responder { | ||||
|     let result = auth_controller.oidc_login(); | ||||
| 
 | ||||
|     match result { | ||||
|         Ok(auth_url) => { | ||||
|             if let Err(e) = session.insert::<String>(SESSION_KEY_AUTH_STATE, auth_url.state) { | ||||
|                 log::error!("{e:?}"); | ||||
|                 return HttpResponse::InternalServerError().finish(); | ||||
|             } | ||||
|             if let Err(e) = session.insert::<String>(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<dyn AuthController>, | ||||
|     query: web::Query<OidcCallbackQueryDto>, | ||||
|     session: Session, | ||||
| ) -> impl Responder { | ||||
|     let expected_state = match session.get::<String>(SESSION_KEY_AUTH_STATE) { | ||||
|         Ok(Some(state)) => state, | ||||
|         _ => return HttpResponse::BadRequest().finish(), | ||||
|     }; | ||||
| 
 | ||||
|     let expected_nonce = match session.get::<String>(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::<i32>(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<dyn AuthController>, | ||||
|     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() | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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<dyn AuthController>, | ||||
|     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() | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -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<dyn AuthController>, | ||||
|     query: web::Query<OidcCallbackQueryDto>, | ||||
|     session: Session, | ||||
| ) -> impl Responder { | ||||
|     let expected_state = match session.get::<String>(SESSION_KEY_AUTH_STATE) { | ||||
|         Ok(Some(state)) => state, | ||||
|         _ => return HttpResponse::BadRequest().finish(), | ||||
|     }; | ||||
| 
 | ||||
|     let expected_nonce = match session.get::<String>(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::<i32>(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() | ||||
|             } | ||||
|         }, | ||||
|     } | ||||
| } | ||||
							
								
								
									
										43
									
								
								backend/feature/auth/src/framework/web/oidc_login_handler.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								backend/feature/auth/src/framework/web/oidc_login_handler.rs
									
									
									
									
									
										Normal file
									
								
							| @ -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<dyn AuthController>, | ||||
|     session: Session, | ||||
| ) -> impl Responder { | ||||
|     let result = auth_controller.oidc_login(); | ||||
| 
 | ||||
|     match result { | ||||
|         Ok(auth_url) => { | ||||
|             if let Err(e) = session.insert::<String>(SESSION_KEY_AUTH_STATE, auth_url.state) { | ||||
|                 log::error!("{e:?}"); | ||||
|                 return HttpResponse::InternalServerError().finish(); | ||||
|             } | ||||
|             if let Err(e) = session.insert::<String>(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() | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -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() | ||||
| } | ||||
| @ -11,5 +11,6 @@ futures.workspace = true | ||||
| log.workspace = true | ||||
| serde.workspace = true | ||||
| sqlx.workspace = true | ||||
| utoipa.workspace = true | ||||
| 
 | ||||
| auth.workspace = true | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -1 +1,5 @@ | ||||
| pub mod image_api_doc; | ||||
| pub mod image_web_routes; | ||||
| 
 | ||||
| mod get_image_by_id_handler; | ||||
| mod upload_image_handler; | ||||
|  | ||||
| @ -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<dyn ImageController>, | ||||
|     path: web::Path<i32>, | ||||
| ) -> 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<u8>); | ||||
							
								
								
									
										13
									
								
								backend/feature/image/src/framework/web/image_api_doc.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								backend/feature/image/src/framework/web/image_api_doc.rs
									
									
									
									
									
										Normal file
									
								
							| @ -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() | ||||
| } | ||||
| @ -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<dyn ImageController>, | ||||
|     mut payload: Multipart, | ||||
|     _: UserId, | ||||
| ) -> impl Responder { | ||||
|     let mut image_request_dto: Option<ImageRequestDto> = 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<dyn ImageController>, | ||||
|     path: web::Path<i32>, | ||||
| ) -> 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() | ||||
|             } | ||||
|         }, | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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<dyn ImageController>, | ||||
|     mut payload: Multipart, | ||||
|     _: UserId, | ||||
| ) -> impl Responder { | ||||
|     let mut image_request_dto: Option<ImageRequestDto> = 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<u8>, | ||||
| } | ||||
| @ -10,3 +10,4 @@ chrono.workspace = true | ||||
| log.workspace = true | ||||
| serde.workspace = true | ||||
| sqlx.workspace = true | ||||
| utoipa.workspace = true | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -2,12 +2,15 @@ use std::sync::Arc; | ||||
| 
 | ||||
| use async_trait::async_trait; | ||||
| 
 | ||||
| use crate::application::{ | ||||
| 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, | ||||
|         }, | ||||
|     }, | ||||
| }; | ||||
| 
 | ||||
| use super::{post_info_response_dto::PostInfoResponseDto, post_response_dto::PostResponseDto}; | ||||
| @ -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<Vec<PostInfoResponseDto>, PostError>; | ||||
| 
 | ||||
|     async fn get_full_post(&self, id: i32) -> Result<PostResponseDto, PostError>; | ||||
|     async fn get_post_by_id(&self, id: i32) -> Result<PostResponseDto, PostError>; | ||||
| } | ||||
| 
 | ||||
| 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<Vec<PostInfoResponseDto>, 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<PostInfoResponseDto> = post_info_list | ||||
| @ -57,7 +63,7 @@ impl PostController for PostControllerImpl { | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     async fn get_full_post(&self, id: i32) -> Result<PostResponseDto, PostError> { | ||||
|     async fn get_post_by_id(&self, id: i32) -> Result<PostResponseDto, PostError> { | ||||
|         let result = self.get_full_post_use_case.execute(id).await; | ||||
| 
 | ||||
|         result.map(PostResponseDto::from) | ||||
|  | ||||
| @ -1,6 +1,8 @@ | ||||
| use serde::Deserialize; | ||||
| use utoipa::IntoParams; | ||||
| 
 | ||||
| #[derive(Deserialize)] | ||||
| #[derive(Deserialize, IntoParams)] | ||||
| pub struct PostQueryDto { | ||||
|     #[param(default = true)] | ||||
|     pub is_published_only: Option<bool>, | ||||
| } | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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<dyn PostController>, | ||||
|     query: web::Query<PostQueryDto>, | ||||
| ) -> 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() | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -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<dyn PostController>, | ||||
|     path: web::Path<i32>, | ||||
| ) -> 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() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										13
									
								
								backend/feature/post/src/framework/web/post_api_doc.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								backend/feature/post/src/framework/web/post_api_doc.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| use crate::framework::web::{get_all_post_info_handler, get_post_by_id_handler}; | ||||
| use utoipa::{OpenApi, openapi}; | ||||
| 
 | ||||
| #[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 ApiDoc; | ||||
| 
 | ||||
| pub fn openapi() -> openapi::OpenApi { | ||||
|     ApiDoc::openapi() | ||||
| } | ||||
| @ -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<dyn PostController>, | ||||
|     query: web::Query<PostQueryDto>, | ||||
| ) -> 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<dyn PostController>, | ||||
|     path: web::Path<i32>, | ||||
| ) -> 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() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										46
									
								
								backend/server/src/api_doc.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								backend/server/src/api_doc.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | ||||
| 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::{ | ||||
|     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(auth_api_doc::openapi()) | ||||
|         .merge_from(image_api_doc::openapi()) | ||||
|         .merge_from(post_api_doc::openapi()); | ||||
| 
 | ||||
|     cfg.service(Redoc::with_url("/redoc", openapi)); | ||||
| } | ||||
| @ -1,14 +0,0 @@ | ||||
| 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())); | ||||
| } | ||||
| @ -1,3 +1,3 @@ | ||||
| pub mod apidoc; | ||||
| pub mod api_doc; | ||||
| pub mod configuration; | ||||
| pub mod container; | ||||
|  | ||||
| @ -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] | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user