BLOG-103 Add API documentation with Utoipa #106
46
backend/.sqlx/query-9d1ffa7a71c8830d75eeeb26800ee7a7d8ede2410b423985caffd86361ad9263.json
generated
Normal file
46
backend/.sqlx/query-9d1ffa7a71c8830d75eeeb26800ee7a7d8ede2410b423985caffd86361ad9263.json
generated
Normal file
@ -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"
|
||||||
|
}
|
41
backend/Cargo.lock
generated
41
backend/Cargo.lock
generated
@ -429,6 +429,7 @@ dependencies = [
|
|||||||
"openidconnect",
|
"openidconnect",
|
||||||
"serde",
|
"serde",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
"utoipa",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1669,6 +1670,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
"utoipa",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2234,6 +2236,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
"utoipa",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2819,6 +2822,8 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"post",
|
"post",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
"utoipa",
|
||||||
|
"utoipa-redoc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3509,6 +3514,42 @@ version = "0.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
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]]
|
[[package]]
|
||||||
name = "vcpkg"
|
name = "vcpkg"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
|
@ -30,6 +30,8 @@ sqlx = { version = "0.8.5", features = [
|
|||||||
"runtime-tokio-rustls",
|
"runtime-tokio-rustls",
|
||||||
] }
|
] }
|
||||||
tokio = { version = "1.45.0", features = ["full"] }
|
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"
|
server.path = "server"
|
||||||
auth.path = "feature/auth"
|
auth.path = "feature/auth"
|
||||||
|
@ -11,3 +11,4 @@ log.workspace = true
|
|||||||
openidconnect.workspace = true
|
openidconnect.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
sqlx.workspace = true
|
sqlx.workspace = true
|
||||||
|
utoipa.workspace = true
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, IntoParams)]
|
||||||
pub struct OidcCallbackQueryDto {
|
pub struct OidcCallbackQueryDto {
|
||||||
pub code: String,
|
pub code: String,
|
||||||
pub state: String,
|
pub state: String,
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
use crate::domain::entity::user::User;
|
use crate::domain::entity::user::User;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize, ToSchema)]
|
||||||
pub struct UserResponseDto {
|
pub struct UserResponseDto {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub displayed_name: String,
|
pub displayed_name: String,
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
|
pub mod auth_api_doc;
|
||||||
pub mod auth_middleware;
|
pub mod auth_middleware;
|
||||||
pub mod auth_web_routes;
|
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;
|
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::web;
|
||||||
use actix_web::{HttpResponse, Responder, http::header, web};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::framework::web::{
|
||||||
adapter::delivery::{
|
get_logged_in_user_handler::get_logged_in_user_handler,
|
||||||
auth_controller::AuthController, oidc_callback_query_dto::OidcCallbackQueryDto,
|
oidc_callback_handler::oidc_callback_handler, oidc_login_handler::oidc_login_handler,
|
||||||
},
|
oidc_logout_handler::oidc_logout_handler,
|
||||||
application::error::auth_error::AuthError,
|
|
||||||
framework::web::{
|
|
||||||
auth_middleware::UserId,
|
|
||||||
constants::{SESSION_KEY_AUTH_NONCE, SESSION_KEY_AUTH_STATE, SESSION_KEY_USER_ID},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn configure_auth_routes(cfg: &mut web::ServiceConfig) {
|
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")
|
web::scope("/auth")
|
||||||
.route("/login", web::get().to(oidc_login_handler))
|
.route("/login", web::get().to(oidc_login_handler))
|
||||||
.route("/callback", web::get().to(oidc_callback_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)));
|
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
|
log.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
sqlx.workspace = true
|
sqlx.workspace = true
|
||||||
|
utoipa.workspace = true
|
||||||
|
|
||||||
auth.workspace = true
|
auth.workspace = true
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize, ToSchema)]
|
||||||
pub struct ImageInfoResponseDto {
|
pub struct ImageInfoResponseDto {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub mime_type: String,
|
pub mime_type: String,
|
||||||
|
@ -1 +1,5 @@
|
|||||||
|
pub mod image_api_doc;
|
||||||
pub mod image_web_routes;
|
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::web;
|
||||||
use actix_web::{HttpResponse, Responder, web};
|
|
||||||
use auth::framework::web::auth_middleware::UserId;
|
|
||||||
use futures::StreamExt;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::framework::web::{
|
||||||
adapter::delivery::{image_controller::ImageController, image_request_dto::ImageRequestDto},
|
get_image_by_id_handler::get_image_by_id_handler, upload_image_handler::upload_image_handler,
|
||||||
application::error::image_error::ImageError,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn configure_image_routes(cfg: &mut web::ServiceConfig) {
|
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)),
|
.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
|
log.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
sqlx.workspace = true
|
sqlx.workspace = true
|
||||||
|
utoipa.workspace = true
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
use crate::domain::entity::color::Color;
|
use crate::domain::entity::color::Color;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize, ToSchema)]
|
||||||
pub struct ColorResponseDto {
|
pub struct ColorResponseDto {
|
||||||
pub red: u8,
|
pub red: u8,
|
||||||
pub green: u8,
|
pub green: u8,
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
adapter::delivery::color_response_dto::ColorResponseDto, domain::entity::label::Label,
|
adapter::delivery::color_response_dto::ColorResponseDto, domain::entity::label::Label,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize, ToSchema)]
|
||||||
pub struct LabelResponseDto {
|
pub struct LabelResponseDto {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
@ -2,12 +2,15 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
use crate::application::{
|
use crate::{
|
||||||
|
adapter::delivery::post_info_query_dto::PostQueryDto,
|
||||||
|
application::{
|
||||||
error::post_error::PostError,
|
error::post_error::PostError,
|
||||||
use_case::{
|
use_case::{
|
||||||
get_all_post_info_use_case::GetAllPostInfoUseCase,
|
get_all_post_info_use_case::GetAllPostInfoUseCase,
|
||||||
get_full_post_use_case::GetFullPostUseCase,
|
get_full_post_use_case::GetFullPostUseCase,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{post_info_response_dto::PostInfoResponseDto, post_response_dto::PostResponseDto};
|
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 {
|
pub trait PostController: Send + Sync {
|
||||||
async fn get_all_post_info(
|
async fn get_all_post_info(
|
||||||
&self,
|
&self,
|
||||||
is_published_only: bool,
|
query: PostQueryDto,
|
||||||
) -> Result<Vec<PostInfoResponseDto>, PostError>;
|
) -> 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 {
|
pub struct PostControllerImpl {
|
||||||
@ -43,9 +46,12 @@ impl PostControllerImpl {
|
|||||||
impl PostController for PostControllerImpl {
|
impl PostController for PostControllerImpl {
|
||||||
async fn get_all_post_info(
|
async fn get_all_post_info(
|
||||||
&self,
|
&self,
|
||||||
is_published_only: bool,
|
query: PostQueryDto,
|
||||||
) -> Result<Vec<PostInfoResponseDto>, PostError> {
|
) -> 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| {
|
result.map(|post_info_list| {
|
||||||
let post_info_response_dto_list: Vec<PostInfoResponseDto> = 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;
|
let result = self.get_full_post_use_case.execute(id).await;
|
||||||
|
|
||||||
result.map(PostResponseDto::from)
|
result.map(PostResponseDto::from)
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, IntoParams)]
|
||||||
pub struct PostQueryDto {
|
pub struct PostQueryDto {
|
||||||
|
#[param(default = true)]
|
||||||
pub is_published_only: Option<bool>,
|
pub is_published_only: Option<bool>,
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
use crate::domain::entity::post_info::PostInfo;
|
use crate::domain::entity::post_info::PostInfo;
|
||||||
|
|
||||||
use super::label_response_dto::LabelResponseDto;
|
use super::label_response_dto::LabelResponseDto;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize, ToSchema)]
|
||||||
pub struct PostInfoResponseDto {
|
pub struct PostInfoResponseDto {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
use crate::domain::entity::post::Post;
|
use crate::domain::entity::post::Post;
|
||||||
|
|
||||||
use super::post_info_response_dto::PostInfoResponseDto;
|
use super::post_info_response_dto::PostInfoResponseDto;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize, ToSchema)]
|
||||||
pub struct PostResponseDto {
|
pub struct PostResponseDto {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub info: PostInfoResponseDto,
|
pub info: PostInfoResponseDto,
|
||||||
|
@ -1 +1,5 @@
|
|||||||
|
pub mod post_api_doc;
|
||||||
pub mod post_web_routes;
|
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::{
|
use crate::framework::web::{
|
||||||
adapter::delivery::{post_controller::PostController, post_info_query_dto::PostQueryDto},
|
get_all_post_info_handler::get_all_post_info_handler,
|
||||||
application::error::post_error::PostError,
|
get_post_by_id_handler::get_post_by_id_handler,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn configure_post_routes(cfg: &mut web::ServiceConfig) {
|
pub fn configure_post_routes(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(
|
cfg.service(
|
||||||
web::scope("/post")
|
web::scope("/post")
|
||||||
.route("/all", web::get().to(get_all_post_info_handler))
|
.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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -12,6 +12,8 @@ hex.workspace = true
|
|||||||
openidconnect.workspace = true
|
openidconnect.workspace = true
|
||||||
percent-encoding.workspace = true
|
percent-encoding.workspace = true
|
||||||
sqlx.workspace = true
|
sqlx.workspace = true
|
||||||
|
utoipa.workspace = true
|
||||||
|
utoipa-redoc.workspace = true
|
||||||
|
|
||||||
auth.workspace = true
|
auth.workspace = true
|
||||||
image.workspace = true
|
image.workspace = true
|
||||||
|
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,2 +1,3 @@
|
|||||||
|
pub mod api_doc;
|
||||||
pub mod configuration;
|
pub mod configuration;
|
||||||
pub mod container;
|
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 image::framework::web::image_web_routes::configure_image_routes;
|
||||||
use openidconnect::reqwest;
|
use openidconnect::reqwest;
|
||||||
use post::framework::web::post_web_routes::configure_post_routes;
|
use post::framework::web::post_web_routes::configure_post_routes;
|
||||||
use server::{configuration::Configuration, container::Container};
|
use server::{api_doc::configure_api_doc_routes, configuration::Configuration, container::Container};
|
||||||
use sqlx::{Pool, Postgres};
|
use sqlx::{Pool, Postgres};
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
@ -68,6 +68,7 @@ fn create_app(
|
|||||||
.app_data(web::Data::from(container.auth_controller))
|
.app_data(web::Data::from(container.auth_controller))
|
||||||
.app_data(web::Data::from(container.image_controller))
|
.app_data(web::Data::from(container.image_controller))
|
||||||
.app_data(web::Data::from(container.post_controller))
|
.app_data(web::Data::from(container.post_controller))
|
||||||
|
.configure(configure_api_doc_routes)
|
||||||
.configure(configure_auth_routes)
|
.configure(configure_auth_routes)
|
||||||
.configure(configure_image_routes)
|
.configure(configure_image_routes)
|
||||||
.configure(configure_post_routes)
|
.configure(configure_post_routes)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user