feat: Implement permissions checks for hackathon and testimonial routes, enhancing security with header validation

This commit is contained in:
MythEclipse
2025-10-07 13:08:23 +07:00
parent 7749f6fdec
commit 018124f7b3
6 changed files with 126 additions and 40 deletions
Generated
+1
View File
@@ -2202,6 +2202,7 @@ dependencies = [
"dotenvy",
"http-body-util",
"imphnen-entities",
"imphnen-iam",
"imphnen-libs",
"imphnen-utils",
"lazy_static",
@@ -7,11 +7,12 @@ use super::{
};
use axum::extract::{Path, Query};
use axum::response::IntoResponse;
use axum::{Extension, Json};
use axum::{Extension, Json, http::HeaderMap};
use imphnen_libs::{
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
ResponseSuccessDto,
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
ResponseSuccessDto,
};
use imphnen_iam::{PermissionsEnum, permissions_guard};
#[utoipa::path(
get,
@@ -68,10 +69,14 @@ pub async fn get_event_by_id(
tag = "Events"
)]
pub async fn post_create_event(
Extension(state): Extension<AppState>,
Json(payload): Json<EventsCreateRequestDto>,
headers: HeaderMap,
Extension(state): Extension<AppState>,
Json(payload): Json<EventsCreateRequestDto>,
) -> impl IntoResponse {
EventsService::create_event(&state, payload).await
match permissions_guard(headers, Extension(state), vec![PermissionsEnum::Administrator]).await {
Ok((_claims, state)) => EventsService::create_event(&state, payload).await,
Err(response) => response,
}
}
#[utoipa::path(
@@ -90,11 +95,15 @@ pub async fn post_create_event(
tag = "Events"
)]
pub async fn patch_update_event(
Extension(state): Extension<AppState>,
Path(id): Path<String>,
Json(payload): Json<EventsUpdateRequestDto>,
headers: HeaderMap,
Extension(state): Extension<AppState>,
Path(id): Path<String>,
Json(payload): Json<EventsUpdateRequestDto>,
) -> impl IntoResponse {
EventsService::update_event(&state, id, payload).await
match permissions_guard(headers, Extension(state), vec![PermissionsEnum::Administrator]).await {
Ok((_claims, state)) => EventsService::update_event(&state, id, payload).await,
Err(response) => response,
}
}
#[utoipa::path(
@@ -112,8 +121,12 @@ pub async fn patch_update_event(
tag = "Events"
)]
pub async fn delete_event(
Extension(state): Extension<AppState>,
Path(id): Path<String>,
headers: HeaderMap,
Extension(state): Extension<AppState>,
Path(id): Path<String>,
) -> impl IntoResponse {
EventsService::delete_event(&state, id).await
match permissions_guard(headers, Extension(state), vec![PermissionsEnum::Administrator]).await {
Ok((_claims, state)) => EventsService::delete_event(&state, id).await,
Err(response) => response,
}
}
@@ -7,12 +7,13 @@ use super::{
};
use axum::extract::{Path, Query};
use axum::response::IntoResponse;
use axum::{Extension, Json};
use axum::{Extension, Json, http::HeaderMap};
use imphnen_iam::UsersDetailQueryDto;
use imphnen_libs::{
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
ResponseSuccessDto,
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
ResponseSuccessDto,
};
use imphnen_iam::permissions_guard;
#[utoipa::path(
get,
@@ -69,11 +70,15 @@ pub async fn get_testimonial_by_id(
tag = "Testimonials"
)]
pub async fn post_create_testimonial(
Extension(state): Extension<AppState>,
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
Json(payload): Json<TestimonialsCreateRequestDto>,
headers: HeaderMap,
Extension(state): Extension<AppState>,
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
Json(payload): Json<TestimonialsCreateRequestDto>,
) -> impl IntoResponse {
TestimonialsService::create_testimonial(&state, payload, &authenticated_user).await
match permissions_guard(headers, Extension(state), vec![]).await {
Ok((_claims, state)) => TestimonialsService::create_testimonial(&state, payload, &authenticated_user).await,
Err(response) => response,
}
}
#[utoipa::path(
@@ -92,13 +97,16 @@ pub async fn post_create_testimonial(
tag = "Testimonials"
)]
pub async fn patch_update_testimonial(
Path(id): Path<String>,
Extension(state): Extension<AppState>,
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
Json(payload): Json<TestimonialsUpdateRequestDto>,
headers: HeaderMap,
Path(id): Path<String>,
Extension(state): Extension<AppState>,
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
Json(payload): Json<TestimonialsUpdateRequestDto>,
) -> impl IntoResponse {
TestimonialsService::update_testimonial(&state, id, payload, &authenticated_user)
.await
match permissions_guard(headers, Extension(state), vec![]).await {
Ok((_claims, state)) => TestimonialsService::update_testimonial(&state, id, payload, &authenticated_user).await,
Err(response) => response,
}
}
#[utoipa::path(
@@ -116,9 +124,13 @@ pub async fn patch_update_testimonial(
tag = "Testimonials"
)]
pub async fn delete_testimonial(
Extension(state): Extension<AppState>,
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
Path(id): Path<String>,
headers: HeaderMap,
Extension(state): Extension<AppState>,
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
Path(id): Path<String>,
) -> impl IntoResponse {
TestimonialsService::delete_testimonial(&state, id, &authenticated_user).await
match permissions_guard(headers, Extension(state), vec![]).await {
Ok((_claims, state)) => TestimonialsService::delete_testimonial(&state, id, &authenticated_user).await,
Err(response) => response,
}
}
+39 -3
View File
@@ -40,8 +40,8 @@ use imphnen_iam::v1::teams::teams_dto::{TeamsCreateRequestDto, TeamsUpdateReques
use imphnen_iam::v1::{auth, permissions, roles, users, teams};
use imphnen_iam::v1::users::users_controller::FileUploadSchema;
use utoipa::{
Modify, OpenApi,
openapi::security::{Http, HttpAuthScheme, SecurityScheme},
Modify, OpenApi,
openapi::security::{Http, HttpAuthScheme, SecurityScheme, SecurityRequirement},
};
#[derive(OpenApi)]
@@ -267,7 +267,43 @@ impl Modify for SecurityAddon {
SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)),
);
}
}
// Walk all paths and add a Bearer security requirement to any operation
// that declares 401 or 403 responses. This helps ensure protected
// endpoints are shown with the Bearer lock in the generated docs
// without having to annotate every controller manually.
let paths = &mut openapi.paths;
for (_path, path_item) in paths.paths.iter_mut() {
// helper to process each possible operation on the path
let mut process_op = |op: &mut Option<utoipa::openapi::path::Operation>| {
if let Some(operation) = op.as_mut() {
let mut has_auth_response = false;
let responses = &operation.responses.responses;
for status in responses.keys() {
if status == "401" || status == "403" {
has_auth_response = true;
break;
}
}
if has_auth_response {
// assign security requirement for Bearer if not already present
if operation.security.is_none() {
operation.security = Some(vec![SecurityRequirement::new::<&str, Vec<&str>, &str>("Bearer", vec![])]);
}
}
}
};
process_op(&mut path_item.get);
process_op(&mut path_item.post);
process_op(&mut path_item.put);
process_op(&mut path_item.patch);
process_op(&mut path_item.delete);
process_op(&mut path_item.options);
process_op(&mut path_item.head);
process_op(&mut path_item.trace);
}
}
}
pub fn docs_router() -> utoipa::openapi::OpenApi {
+1
View File
@@ -8,6 +8,7 @@ imphnen-libs.workspace = true
imphnen-utils.workspace = true
imphnen-entities.workspace = true
imphnen-iam.workspace = true
async-trait.workspace = true
axum.workspace = true
serde.workspace = true
@@ -14,10 +14,15 @@ use axum::{
response::IntoResponse,
routing::{delete, get, post, put},
};
use axum::http::HeaderMap;
use imphnen_iam::{PermissionsEnum, permissions_guard};
// Hackathon routes
#[utoipa::path(
post,
security(
("Bearer" = [])
),
path = "/v1/hackathons",
request_body = HackathonCreateRequestDto,
responses(
@@ -28,12 +33,16 @@ use axum::{
tag = "Hackathons"
)]
pub async fn create_hackathon(
headers: HeaderMap,
Extension(state): Extension<AppState>,
Json(payload): Json<HackathonCreateRequestDto>,
) -> impl IntoResponse {
match HackathonService::create_hackathon(payload, &state).await {
match permissions_guard(headers, Extension(state), vec![PermissionsEnum::Administrator]).await {
Ok((_claims, state)) => match HackathonService::create_hackathon(payload, &state).await {
Ok(response) => (axum::http::StatusCode::CREATED, Json(response)).into_response(),
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
},
Err(response) => response,
}
}
@@ -90,6 +99,9 @@ pub async fn list_hackathons(
#[utoipa::path(
put,
security(
("Bearer" = [])
),
path = "/v1/hackathons/{id}",
params(
("id" = String, Path, description = "Hackathon ID")
@@ -104,18 +116,25 @@ pub async fn list_hackathons(
tag = "Hackathons"
)]
pub async fn update_hackathon(
headers: HeaderMap,
Extension(state): Extension<AppState>,
Path(id): Path<String>,
Json(payload): Json<HackathonUpdateRequestDto>,
) -> impl IntoResponse {
match HackathonService::update_hackathon(id, payload, &state).await {
Ok(response) => (axum::http::StatusCode::OK, Json(response)).into_response(),
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
match permissions_guard(headers, Extension(state), vec![PermissionsEnum::Administrator]).await {
Ok((_claims, state)) => match HackathonService::update_hackathon(id, payload, &state).await {
Ok(response) => (axum::http::StatusCode::OK, Json(response)).into_response(),
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
},
Err(response) => response,
}
}
#[utoipa::path(
delete,
security(
("Bearer" = [])
),
path = "/v1/hackathons/{id}",
params(
("id" = String, Path, description = "Hackathon ID")
@@ -128,12 +147,16 @@ pub async fn update_hackathon(
tag = "Hackathons"
)]
pub async fn delete_hackathon(
headers: HeaderMap,
Extension(state): Extension<AppState>,
Path(id): Path<String>,
) -> impl IntoResponse {
match HackathonService::delete_hackathon(id, &state).await {
Ok(response) => (axum::http::StatusCode::OK, Json(response)).into_response(),
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
match permissions_guard(headers, Extension(state), vec![PermissionsEnum::Administrator]).await {
Ok((_claims, state)) => match HackathonService::delete_hackathon(id, &state).await {
Ok(response) => (axum::http::StatusCode::OK, Json(response)).into_response(),
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
},
Err(response) => response,
}
}