BLOG-103 Add API documentation with Utoipa #106
1
backend/Cargo.lock
generated
1
backend/Cargo.lock
generated
@ -429,6 +429,7 @@ dependencies = [
|
|||||||
"openidconnect",
|
"openidconnect",
|
||||||
"serde",
|
"serde",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
"utoipa",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -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()
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
use actix_web::web;
|
use actix_web::web;
|
||||||
|
use auth::framework::web::auth_api_doc;
|
||||||
use image::framework::web::image_api_doc;
|
use image::framework::web::image_api_doc;
|
||||||
use post::framework::web::post_api_doc;
|
use post::framework::web::post_api_doc;
|
||||||
use utoipa::{
|
use utoipa::{
|
||||||
@ -37,8 +38,9 @@ impl utoipa::OpenApi for ApiDoc {
|
|||||||
|
|
||||||
pub fn configure_api_doc_routes(cfg: &mut web::ServiceConfig) {
|
pub fn configure_api_doc_routes(cfg: &mut web::ServiceConfig) {
|
||||||
let openapi = ApiDoc::openapi()
|
let openapi = ApiDoc::openapi()
|
||||||
.merge_from(post_api_doc::openapi())
|
.merge_from(auth_api_doc::openapi())
|
||||||
.merge_from(image_api_doc::openapi());
|
.merge_from(image_api_doc::openapi())
|
||||||
|
.merge_from(post_api_doc::openapi());
|
||||||
|
|
||||||
cfg.service(Redoc::with_url("/redoc", openapi));
|
cfg.service(Redoc::with_url("/redoc", openapi));
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user