feat: Enhance validation and permissions handling across controllers
- Added `ValidatedJson` extractor for automatic JSON validation in `events_controller.rs`, `testimonials_controller.rs`, `mentors_controller.rs`, `gacha_items_controller.rs`, and `hackathon_controller.rs`. - Replaced manual permission checks with `require_permissions!` and `require_auth!` macros in relevant controllers to streamline permission handling. - Introduced `sanitization` utilities in `sanitization.rs` for improved input sanitization. - Added `permission_macros.rs` to encapsulate permission checking logic and reduce boilerplate. - Updated dependencies in `Cargo.toml` to include `serde_json` and `validator`. - Implemented error handling improvements in `notification_service.rs` for better response management.
This commit is contained in:
Generated
+3
@@ -2293,11 +2293,13 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
"surrealdb",
|
"surrealdb",
|
||||||
"tokio",
|
"tokio",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
"validator",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2342,6 +2344,7 @@ dependencies = [
|
|||||||
"imphnen-entities",
|
"imphnen-entities",
|
||||||
"imphnen-libs",
|
"imphnen-libs",
|
||||||
"rand 0.9.2",
|
"rand 0.9.2",
|
||||||
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ use super::{
|
|||||||
};
|
};
|
||||||
use axum::extract::{Path, Query};
|
use axum::extract::{Path, Query};
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::{Extension, Json, http::HeaderMap};
|
use axum::{Extension, http::HeaderMap};
|
||||||
use imphnen_libs::{
|
use imphnen_libs::{
|
||||||
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
|
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
|
||||||
ResponseSuccessDto,
|
ResponseSuccessDto, ValidatedJson,
|
||||||
};
|
};
|
||||||
use imphnen_iam::{PermissionsEnum, permissions_guard};
|
use imphnen_iam::{PermissionsEnum, require_permissions};
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
@@ -71,12 +71,11 @@ pub async fn get_event_by_id(
|
|||||||
pub async fn post_create_event(
|
pub async fn post_create_event(
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Extension(state): Extension<AppState>,
|
Extension(state): Extension<AppState>,
|
||||||
Json(payload): Json<EventsCreateRequestDto>,
|
ValidatedJson(payload): ValidatedJson<EventsCreateRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(headers, Extension(state), vec![PermissionsEnum::Administrator]).await {
|
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||||
Ok((_claims, state)) => EventsService::create_event(&state, payload).await,
|
EventsService::create_event(&state, payload).await
|
||||||
Err(response) => response,
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -98,12 +97,11 @@ pub async fn patch_update_event(
|
|||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Extension(state): Extension<AppState>,
|
Extension(state): Extension<AppState>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
Json(payload): Json<EventsUpdateRequestDto>,
|
ValidatedJson(payload): ValidatedJson<EventsUpdateRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(headers, Extension(state), vec![PermissionsEnum::Administrator]).await {
|
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||||
Ok((_claims, state)) => EventsService::update_event(&state, id, payload).await,
|
EventsService::update_event(&state, id, payload).await
|
||||||
Err(response) => response,
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -125,8 +123,7 @@ pub async fn delete_event(
|
|||||||
Extension(state): Extension<AppState>,
|
Extension(state): Extension<AppState>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(headers, Extension(state), vec![PermissionsEnum::Administrator]).await {
|
require_permissions!(headers, state, [PermissionsEnum::Administrator], {
|
||||||
Ok((_claims, state)) => EventsService::delete_event(&state, id).await,
|
EventsService::delete_event(&state, id).await
|
||||||
Err(response) => response,
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,12 @@ use super::{
|
|||||||
};
|
};
|
||||||
use axum::extract::{Path, Query};
|
use axum::extract::{Path, Query};
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::{Extension, Json, http::HeaderMap};
|
use axum::{Extension, http::HeaderMap};
|
||||||
use imphnen_iam::UsersDetailQueryDto;
|
use imphnen_iam::{UsersDetailQueryDto, require_auth};
|
||||||
use imphnen_libs::{
|
use imphnen_libs::{
|
||||||
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
|
AppState, MessageResponseDto, MetaRequestDto, ResponseListSuccessDto,
|
||||||
ResponseSuccessDto,
|
ResponseSuccessDto, ValidatedJson,
|
||||||
};
|
};
|
||||||
use imphnen_iam::permissions_guard;
|
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
@@ -73,12 +72,11 @@ pub async fn post_create_testimonial(
|
|||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Extension(state): Extension<AppState>,
|
Extension(state): Extension<AppState>,
|
||||||
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
||||||
Json(payload): Json<TestimonialsCreateRequestDto>,
|
ValidatedJson(payload): ValidatedJson<TestimonialsCreateRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(headers, Extension(state), vec![]).await {
|
require_auth!(headers, state, {
|
||||||
Ok((_claims, state)) => TestimonialsService::create_testimonial(&state, payload, &authenticated_user).await,
|
TestimonialsService::create_testimonial(&state, payload, &authenticated_user).await
|
||||||
Err(response) => response,
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -101,12 +99,11 @@ pub async fn patch_update_testimonial(
|
|||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
Extension(state): Extension<AppState>,
|
Extension(state): Extension<AppState>,
|
||||||
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
||||||
Json(payload): Json<TestimonialsUpdateRequestDto>,
|
ValidatedJson(payload): ValidatedJson<TestimonialsUpdateRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(headers, Extension(state), vec![]).await {
|
require_auth!(headers, state, {
|
||||||
Ok((_claims, state)) => TestimonialsService::update_testimonial(&state, id, payload, &authenticated_user).await,
|
TestimonialsService::update_testimonial(&state, id, payload, &authenticated_user).await
|
||||||
Err(response) => response,
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -129,8 +126,7 @@ pub async fn delete_testimonial(
|
|||||||
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(headers, Extension(state), vec![]).await {
|
require_auth!(headers, state, {
|
||||||
Ok((_claims, state)) => TestimonialsService::delete_testimonial(&state, id, &authenticated_user).await,
|
TestimonialsService::delete_testimonial(&state, id, &authenticated_user).await
|
||||||
Err(response) => response,
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ use super::{
|
|||||||
};
|
};
|
||||||
use crate::v1::mentors::mentors_dto::MentorRegisterResponseDto;
|
use crate::v1::mentors::mentors_dto::MentorRegisterResponseDto;
|
||||||
use ::axum::{
|
use ::axum::{
|
||||||
extract::{Extension, Json, Path, Query},
|
extract::{Extension, Path, Query},
|
||||||
http::HeaderMap,
|
http::HeaderMap,
|
||||||
response::Response,
|
response::Response,
|
||||||
};
|
};
|
||||||
use imphnen_entities::MetaRequestDto;
|
use imphnen_entities::MetaRequestDto;
|
||||||
use imphnen_libs::AppState;
|
use imphnen_libs::{AppState, ValidatedJson};
|
||||||
use imphnen_iam::{PermissionsEnum, permissions_guard};
|
use imphnen_iam::{PermissionsEnum, require_permissions};
|
||||||
use imphnen_utils::extract_email;
|
use imphnen_utils::extract_email;
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -27,7 +27,7 @@ use imphnen_utils::extract_email;
|
|||||||
)]
|
)]
|
||||||
pub async fn post_register_mentor(
|
pub async fn post_register_mentor(
|
||||||
Extension(app_state): Extension<AppState>,
|
Extension(app_state): Extension<AppState>,
|
||||||
Json(dto): Json<MentorUserRegisterRequestDto>,
|
ValidatedJson(dto): ValidatedJson<MentorUserRegisterRequestDto>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
MentorsService::register_mentor(&app_state, dto).await
|
MentorsService::register_mentor(&app_state, dto).await
|
||||||
}
|
}
|
||||||
@@ -56,16 +56,9 @@ pub async fn get_mentor_list(
|
|||||||
Extension(app_state): Extension<AppState>,
|
Extension(app_state): Extension<AppState>,
|
||||||
Query(meta): Query<MetaRequestDto>,
|
Query(meta): Query<MetaRequestDto>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match permissions_guard(
|
require_permissions!(headers, app_state, [PermissionsEnum::ReadListMentors], {
|
||||||
headers,
|
MentorsService::get_mentor_list(&app_state, meta).await
|
||||||
Extension(app_state),
|
})
|
||||||
vec![PermissionsEnum::ReadListMentors],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok((_user, app_state)) => MentorsService::get_mentor_list(&app_state, meta).await,
|
|
||||||
Err(response) => response,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -89,16 +82,9 @@ pub async fn get_mentor_by_id(
|
|||||||
Extension(app_state): Extension<AppState>,
|
Extension(app_state): Extension<AppState>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match permissions_guard(
|
require_permissions!(headers, app_state, [PermissionsEnum::ReadDetailMentors], {
|
||||||
headers,
|
MentorsService::get_mentor_by_id(&app_state, &id).await
|
||||||
Extension(app_state),
|
})
|
||||||
vec![PermissionsEnum::ReadDetailMentors],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok((_user, app_state)) => MentorsService::get_mentor_by_id(&app_state, &id).await,
|
|
||||||
Err(response) => response,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -123,18 +109,11 @@ pub async fn put_update_mentor(
|
|||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Extension(app_state): Extension<AppState>,
|
Extension(app_state): Extension<AppState>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
Json(dto): Json<MentorUpdateRequestDto>,
|
ValidatedJson(dto): ValidatedJson<MentorUpdateRequestDto>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match permissions_guard(
|
require_permissions!(headers, app_state, [PermissionsEnum::UpdateMentors], {
|
||||||
headers,
|
MentorsService::update_mentor(&app_state, &id, dto).await
|
||||||
Extension(app_state),
|
})
|
||||||
vec![PermissionsEnum::UpdateMentors],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok((_user, app_state)) => MentorsService::update_mentor(&app_state, &id, dto).await,
|
|
||||||
Err(response) => response,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -158,16 +137,9 @@ pub async fn delete_mentor(
|
|||||||
Extension(app_state): Extension<AppState>,
|
Extension(app_state): Extension<AppState>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match permissions_guard(
|
require_permissions!(headers, app_state, [PermissionsEnum::DeleteMentors], {
|
||||||
headers,
|
MentorsService::delete_mentor(&app_state, &id).await
|
||||||
Extension(app_state),
|
})
|
||||||
vec![PermissionsEnum::DeleteMentors],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok((_user, app_state)) => MentorsService::delete_mentor(&app_state, &id).await,
|
|
||||||
Err(response) => response,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -192,18 +164,11 @@ pub async fn put_verify_mentor(
|
|||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Extension(app_state): Extension<AppState>,
|
Extension(app_state): Extension<AppState>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
Json(dto): Json<MentorVerifyRequestDto>,
|
ValidatedJson(dto): ValidatedJson<MentorVerifyRequestDto>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match permissions_guard(
|
require_permissions!(headers, app_state, [PermissionsEnum::VerifyMentors], {
|
||||||
headers,
|
MentorsService::verify_mentor(&app_state, &id, dto).await
|
||||||
Extension(app_state),
|
})
|
||||||
vec![PermissionsEnum::VerifyMentors],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok((_user, app_state)) => MentorsService::verify_mentor(&app_state, &id, dto).await,
|
|
||||||
Err(response) => response,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -224,27 +189,18 @@ pub async fn get_mentor_me(
|
|||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Extension(app_state): Extension<AppState>,
|
Extension(app_state): Extension<AppState>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match permissions_guard(
|
require_permissions!(headers.clone(), app_state, [PermissionsEnum::ReadOwnMentorProfile], {
|
||||||
headers.clone(),
|
let email = match extract_email(&headers) {
|
||||||
Extension(app_state),
|
Some(email) => email,
|
||||||
vec![PermissionsEnum::ReadOwnMentorProfile],
|
None => {
|
||||||
)
|
return imphnen_utils::common_response(
|
||||||
.await
|
axum::http::StatusCode::UNAUTHORIZED,
|
||||||
{
|
"Token tidak valid",
|
||||||
Ok((_user, app_state)) => {
|
);
|
||||||
let email = match extract_email(&headers) {
|
}
|
||||||
Some(email) => email,
|
};
|
||||||
None => {
|
MentorsService::get_mentor_me(&app_state, &email).await
|
||||||
return imphnen_utils::common_response(
|
})
|
||||||
axum::http::StatusCode::UNAUTHORIZED,
|
|
||||||
"Token tidak valid",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
MentorsService::get_mentor_me(&app_state, &email).await
|
|
||||||
}
|
|
||||||
Err(response) => response,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -266,29 +222,20 @@ pub async fn get_mentor_me(
|
|||||||
pub async fn put_update_mentor_me(
|
pub async fn put_update_mentor_me(
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Extension(app_state): Extension<AppState>,
|
Extension(app_state): Extension<AppState>,
|
||||||
Json(dto): Json<MentorUpdateRequestDto>,
|
ValidatedJson(dto): ValidatedJson<MentorUpdateRequestDto>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match permissions_guard(
|
require_permissions!(headers.clone(), app_state, [PermissionsEnum::UpdateOwnMentorProfile], {
|
||||||
headers.clone(),
|
let email = match extract_email(&headers) {
|
||||||
Extension(app_state),
|
Some(email) => email,
|
||||||
vec![PermissionsEnum::UpdateOwnMentorProfile],
|
None => {
|
||||||
)
|
return imphnen_utils::common_response(
|
||||||
.await
|
axum::http::StatusCode::UNAUTHORIZED,
|
||||||
{
|
"Token tidak valid",
|
||||||
Ok((_user, app_state)) => {
|
);
|
||||||
let email = match extract_email(&headers) {
|
}
|
||||||
Some(email) => email,
|
};
|
||||||
None => {
|
MentorsService::update_mentor_me(&app_state, &email, dto).await
|
||||||
return imphnen_utils::common_response(
|
})
|
||||||
axum::http::StatusCode::UNAUTHORIZED,
|
|
||||||
"Token tidak valid",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
MentorsService::update_mentor_me(&app_state, &email, dto).await
|
|
||||||
}
|
|
||||||
Err(response) => response,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
put,
|
put,
|
||||||
@@ -324,25 +271,16 @@ pub async fn get_mentor_status(
|
|||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Extension(app_state): Extension<AppState>,
|
Extension(app_state): Extension<AppState>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
match permissions_guard(
|
require_permissions!(headers.clone(), app_state, [PermissionsEnum::ReadOwnMentorStatus], {
|
||||||
headers.clone(),
|
let email = match extract_email(&headers) {
|
||||||
Extension(app_state),
|
Some(email) => email,
|
||||||
vec![PermissionsEnum::ReadOwnMentorStatus],
|
None => {
|
||||||
)
|
return imphnen_utils::common_response(
|
||||||
.await
|
axum::http::StatusCode::UNAUTHORIZED,
|
||||||
{
|
"Token tidak valid",
|
||||||
Ok((_user, app_state)) => {
|
);
|
||||||
let email = match extract_email(&headers) {
|
}
|
||||||
Some(email) => email,
|
};
|
||||||
None => {
|
MentorsService::get_mentor_status(&app_state, &email).await
|
||||||
return imphnen_utils::common_response(
|
})
|
||||||
axum::http::StatusCode::UNAUTHORIZED,
|
|
||||||
"Token tidak valid",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
MentorsService::get_mentor_status(&app_state, &email).await
|
|
||||||
}
|
|
||||||
Err(response) => response,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ use crate::v1::gacha_items::GachaItemDto;
|
|||||||
use crate::v1::gacha_items::gacha_items_dto::{GachaItemRequestDto, GachaItemUpdateRequestDto};
|
use crate::v1::gacha_items::gacha_items_dto::{GachaItemRequestDto, GachaItemUpdateRequestDto};
|
||||||
use crate::v1::gacha_items::gacha_items_service::GachaItemService;
|
use crate::v1::gacha_items::gacha_items_service::GachaItemService;
|
||||||
use axum::{
|
use axum::{
|
||||||
Extension, Json,
|
Extension,
|
||||||
extract::{Path, Query},
|
extract::{Path, Query},
|
||||||
http::HeaderMap,
|
http::HeaderMap,
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
};
|
};
|
||||||
use imphnen_iam::{PermissionsEnum, permissions_guard};
|
use imphnen_iam::{PermissionsEnum, require_permissions};
|
||||||
|
use imphnen_libs::ValidatedJson;
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
@@ -36,16 +37,9 @@ pub async fn get_gacha_item_list(
|
|||||||
Extension(state): Extension<AppState>,
|
Extension(state): Extension<AppState>,
|
||||||
Query(meta): Query<MetaRequestDto>,
|
Query(meta): Query<MetaRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
require_permissions!(headers, state, [PermissionsEnum::ReadListGachaItems], {
|
||||||
headers,
|
GachaItemService::get_gacha_item_list(&state, meta).await
|
||||||
Extension(state),
|
})
|
||||||
vec![PermissionsEnum::ReadListGachaItems],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok((_user, state)) => GachaItemService::get_gacha_item_list(&state, meta).await,
|
|
||||||
Err(response) => response,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -65,16 +59,9 @@ pub async fn get_gacha_item_by_id(
|
|||||||
Extension(state): Extension<AppState>,
|
Extension(state): Extension<AppState>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
require_permissions!(headers, state, [PermissionsEnum::ReadDetailGachaItems], {
|
||||||
headers,
|
GachaItemService::get_gacha_item_by_id(&state, id).await
|
||||||
Extension(state),
|
})
|
||||||
vec![PermissionsEnum::ReadDetailGachaItems],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok((_user, state)) => GachaItemService::get_gacha_item_by_id(&state, id).await,
|
|
||||||
Err(response) => response,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -92,18 +79,11 @@ pub async fn get_gacha_item_by_id(
|
|||||||
pub async fn post_create_gacha_item(
|
pub async fn post_create_gacha_item(
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Extension(state): Extension<AppState>,
|
Extension(state): Extension<AppState>,
|
||||||
Json(payload): Json<GachaItemRequestDto>,
|
ValidatedJson(payload): ValidatedJson<GachaItemRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
require_permissions!(headers, state, [PermissionsEnum::CreateGachaItems], {
|
||||||
headers,
|
GachaItemService::create_gacha_item(&state, payload).await
|
||||||
Extension(state),
|
})
|
||||||
vec![PermissionsEnum::CreateGachaItems],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok((_user, state)) => GachaItemService::create_gacha_item(&state, payload).await,
|
|
||||||
Err(response) => response,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -122,18 +102,11 @@ pub async fn put_update_gacha_item(
|
|||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Extension(state): Extension<AppState>,
|
Extension(state): Extension<AppState>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
Json(payload): Json<GachaItemUpdateRequestDto>,
|
ValidatedJson(payload): ValidatedJson<GachaItemUpdateRequestDto>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
require_permissions!(headers, state, [PermissionsEnum::UpdateGachaItems], {
|
||||||
headers,
|
GachaItemService::update_gacha_item(&state, payload, id).await
|
||||||
Extension(state),
|
})
|
||||||
vec![PermissionsEnum::UpdateGachaItems],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok((_user, state)) => GachaItemService::update_gacha_item(&state, payload, id).await,
|
|
||||||
Err(response) => response,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
@@ -152,14 +125,7 @@ pub async fn delete_gacha_item(
|
|||||||
Extension(state): Extension<AppState>,
|
Extension(state): Extension<AppState>,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match permissions_guard(
|
require_permissions!(headers, state, [PermissionsEnum::DeleteGachaItems], {
|
||||||
headers,
|
GachaItemService::delete_gacha_item(&state, id).await
|
||||||
Extension(state),
|
})
|
||||||
vec![PermissionsEnum::DeleteGachaItems],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok((_user, state)) => GachaItemService::delete_gacha_item(&state, id).await,
|
|
||||||
Err(response) => response,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
use imphnen_entities::{MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto};
|
use imphnen_entities::{MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto};
|
||||||
use imphnen_utils::{common_response, make_thing, success_list_response, success_response, validate_request};
|
use imphnen_utils::{common_response, make_thing, success_list_response, success_response};
|
||||||
use crate::v1::gacha_items::GachaItemDto;
|
use crate::v1::gacha_items::GachaItemDto;
|
||||||
use crate::v1::gacha_items::gacha_items_dto::{GachaItemRequestDto, GachaItemUpdateRequestDto};
|
use crate::v1::gacha_items::gacha_items_dto::{GachaItemRequestDto, GachaItemUpdateRequestDto};
|
||||||
use crate::v1::gacha_items::gacha_items_repository::GachaItemRepository;
|
use crate::v1::gacha_items::gacha_items_repository::GachaItemRepository;
|
||||||
@@ -44,9 +44,7 @@ impl GachaItemService {
|
|||||||
state: &AppState,
|
state: &AppState,
|
||||||
payload: GachaItemRequestDto,
|
payload: GachaItemRequestDto,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
if let Err((status, message)) = validate_request(&payload) {
|
// Validation is now automatic via ValidatedJson extractor
|
||||||
return common_response(status, &message);
|
|
||||||
}
|
|
||||||
let repo = GachaItemRepository::new(state);
|
let repo = GachaItemRepository::new(state);
|
||||||
let schema = GachaItemSchema {
|
let schema = GachaItemSchema {
|
||||||
id: make_thing(&ResourceEnum::GachaItems.to_string(), &payload.name), // Fixed: Use payload.name or some other identifier
|
id: make_thing(&ResourceEnum::GachaItems.to_string(), &payload.name), // Fixed: Use payload.name or some other identifier
|
||||||
@@ -65,9 +63,7 @@ impl GachaItemService {
|
|||||||
payload: GachaItemUpdateRequestDto,
|
payload: GachaItemUpdateRequestDto,
|
||||||
id: String,
|
id: String,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
if let Err((status, message)) = validate_request(&payload) {
|
// Validation is now automatic via ValidatedJson extractor
|
||||||
return common_response(status, &message);
|
|
||||||
}
|
|
||||||
let repo = GachaItemRepository::new(state);
|
let repo = GachaItemRepository::new(state);
|
||||||
|
|
||||||
// Get current gacha item data first
|
// Get current gacha item data first
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ use super::hackathon_schema::SubmissionStatus;
|
|||||||
use crate::v1::hackathon::HackathonRepository;
|
use crate::v1::hackathon::HackathonRepository;
|
||||||
use crate::{AppState, ResponseSuccessDto, ErrorDto};
|
use crate::{AppState, ResponseSuccessDto, ErrorDto};
|
||||||
use imphnen_entities::{PermissionsEnum, UsersDetailQueryDto};
|
use imphnen_entities::{PermissionsEnum, UsersDetailQueryDto};
|
||||||
use imphnen_libs::{MetaRequestDto, ResponseListSuccessDto};
|
use imphnen_libs::{MetaRequestDto, ResponseListSuccessDto, ValidatedJson};
|
||||||
|
use imphnen_iam::require_permissions;
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Extension, Path, Query},
|
extract::{Extension, Path, Query},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ use super::notification_dto::{
|
|||||||
UnreadCountResponseDto,
|
UnreadCountResponseDto,
|
||||||
};
|
};
|
||||||
use super::notification_repository::Repository;
|
use super::notification_repository::Repository;
|
||||||
use super::notification_schema::NotificationSchema;
|
|
||||||
use axum::http::{Response, StatusCode};
|
use axum::http::{Response, StatusCode};
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::body::Body;
|
use axum::body::Body;
|
||||||
use imphnen_entities::common_dto::ResponseSuccessDto;
|
use imphnen_entities::common_dto::ResponseSuccessDto;
|
||||||
use imphnen_libs::AppState;
|
use imphnen_libs::AppState;
|
||||||
use imphnen_utils::{
|
use imphnen_utils::{
|
||||||
extract_id, make_thing, response_format::success_response, validator::validate_request,
|
extract_id, make_thing, response_format::success_response, error_response,
|
||||||
|
validator::validate_request, AppError,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Service<'a> {
|
pub struct Service<'a> {
|
||||||
@@ -28,8 +28,8 @@ impl<'a> Service<'a> {
|
|||||||
user_email: &str,
|
user_email: &str,
|
||||||
query: NotificationListQueryDto,
|
query: NotificationListQueryDto,
|
||||||
) -> Response<Body> {
|
) -> Response<Body> {
|
||||||
if let Err((status, message)) = validate_request(&query) {
|
if let Err((_status, message)) = validate_request(&query) {
|
||||||
return (status, message).into_response();
|
return error_response(AppError::ValidationError(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_id = make_thing("users", user_email);
|
let user_id = make_thing("users", user_email);
|
||||||
@@ -48,7 +48,7 @@ impl<'a> Service<'a> {
|
|||||||
let notifications = match notifications_result {
|
let notifications = match notifications_result {
|
||||||
Ok(notifs) => notifs,
|
Ok(notifs) => notifs,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return (StatusCode::INTERNAL_SERVER_ERROR, err).into_response();
|
return error_response(AppError::InternalServerError(err.to_string()));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ impl<'a> Service<'a> {
|
|||||||
let total = match total_result {
|
let total = match total_result {
|
||||||
Ok(count) => count,
|
Ok(count) => count,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return (StatusCode::INTERNAL_SERVER_ERROR, err).into_response();
|
return error_response(AppError::InternalServerError(err.to_string()));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ impl<'a> Service<'a> {
|
|||||||
let unread_count = match unread_count_result {
|
let unread_count = match unread_count_result {
|
||||||
Ok(count) => count,
|
Ok(count) => count,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return (StatusCode::INTERNAL_SERVER_ERROR, err).into_response();
|
return error_response(AppError::InternalServerError(err.to_string()));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod v1;
|
pub mod v1;
|
||||||
|
pub mod permission_macros;
|
||||||
|
|
||||||
// Re-export core entity types used throughout the IAM module
|
// Re-export core entity types used throughout the IAM module
|
||||||
pub use imphnen_entities::{
|
pub use imphnen_entities::{
|
||||||
@@ -71,6 +72,9 @@ pub use v1::{
|
|||||||
permissions_guard,
|
permissions_guard,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Export permission macros
|
||||||
|
pub use permission_macros::{check_permissions, check_authenticated};
|
||||||
|
|
||||||
// Export IAM-specific types
|
// Export IAM-specific types
|
||||||
pub use v1::auth::{
|
pub use v1::auth::{
|
||||||
AuthRepository, AuthOtpSchema,
|
AuthRepository, AuthOtpSchema,
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
//! Permission guard utilities and macros to reduce boilerplate
|
||||||
|
//!
|
||||||
|
//! This module provides utilities to simplify permission checking in handlers
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
extract::Extension,
|
||||||
|
http::HeaderMap,
|
||||||
|
response::Response,
|
||||||
|
};
|
||||||
|
use imphnen_entities::PermissionsEnum;
|
||||||
|
use crate::AppState;
|
||||||
|
use crate::permissions_guard;
|
||||||
|
use imphnen_libs::jsonwebtoken::Claims;
|
||||||
|
|
||||||
|
/// Result type for permission-guarded handlers
|
||||||
|
pub type PermissionGuardResult<T> = Result<(T, AppState), Response>;
|
||||||
|
|
||||||
|
/// Helper function to extract user and check permissions
|
||||||
|
///
|
||||||
|
/// This is a cleaner wrapper around the existing permissions_guard
|
||||||
|
pub async fn check_permissions(
|
||||||
|
headers: HeaderMap,
|
||||||
|
state: Extension<AppState>,
|
||||||
|
required_permissions: Vec<PermissionsEnum>,
|
||||||
|
) -> PermissionGuardResult<Claims> {
|
||||||
|
match permissions_guard(headers, state, required_permissions).await {
|
||||||
|
Ok((user, state)) => Ok((user, state)),
|
||||||
|
Err(response) => Err(response),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper function for endpoints that don't require specific permissions
|
||||||
|
/// but still need authentication
|
||||||
|
pub async fn check_authenticated(
|
||||||
|
headers: HeaderMap,
|
||||||
|
state: Extension<AppState>,
|
||||||
|
) -> PermissionGuardResult<Claims> {
|
||||||
|
check_permissions(headers, state, vec![]).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Macro to reduce boilerplate in permission-guarded handlers
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust
|
||||||
|
/// use imphnen_iam::require_permissions;
|
||||||
|
/// use imphnen_entities::PermissionsEnum;
|
||||||
|
///
|
||||||
|
/// pub async fn get_user_list(
|
||||||
|
/// headers: HeaderMap,
|
||||||
|
/// Extension(state): Extension<AppState>,
|
||||||
|
/// Query(meta): Query<MetaRequestDto>,
|
||||||
|
/// ) -> Response {
|
||||||
|
/// require_permissions!(headers, state, [PermissionsEnum::ReadListUsers], {
|
||||||
|
/// UsersService::get_user_list(&state, meta).await
|
||||||
|
/// })
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! require_permissions {
|
||||||
|
($headers:expr, $state:expr, [$($perm:expr),*], $body:block) => {
|
||||||
|
{
|
||||||
|
let state_clone = $state.clone();
|
||||||
|
match $crate::permissions_guard(
|
||||||
|
$headers,
|
||||||
|
axum::extract::Extension(state_clone),
|
||||||
|
vec![$($perm),*],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok((_user, _state_inner)) => {
|
||||||
|
let state = &$state;
|
||||||
|
$body
|
||||||
|
}
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Macro for authenticated-only handlers (no specific permissions)
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! require_auth {
|
||||||
|
($headers:expr, $state:expr, $body:block) => {
|
||||||
|
{
|
||||||
|
let state_clone = $state.clone();
|
||||||
|
match $crate::permissions_guard($headers, axum::extract::Extension(state_clone), vec![]).await {
|
||||||
|
Ok((_user, _state_inner)) => {
|
||||||
|
let state = &$state;
|
||||||
|
$body
|
||||||
|
}
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Macro for handlers that need access to the authenticated user
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! with_user {
|
||||||
|
($headers:expr, $state:expr, [$($perm:expr),*], |$user:ident, $state_var:ident| $body:block) => {
|
||||||
|
{
|
||||||
|
let state_clone = $state.clone();
|
||||||
|
match $crate::permissions_guard(
|
||||||
|
$headers,
|
||||||
|
axum::extract::Extension(state_clone),
|
||||||
|
vec![$($perm),*],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(($user, $state_var)) => $body,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -10,6 +10,8 @@ log.workspace = true
|
|||||||
axum.workspace = true
|
axum.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
validator.workspace = true
|
||||||
argon2.workspace = true
|
argon2.workspace = true
|
||||||
lettre.workspace = true
|
lettre.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
|
|||||||
@@ -3,12 +3,16 @@
|
|||||||
//! This module provides utilities for initializing and running an Axum web server
|
//! This module provides utilities for initializing and running an Axum web server
|
||||||
//! with SurrealDB connections for both WebSocket and in-memory databases.
|
//! with SurrealDB connections for both WebSocket and in-memory databases.
|
||||||
|
|
||||||
|
pub mod validated_json;
|
||||||
|
|
||||||
use crate::{surrealdb_init_mem, surrealdb_init_ws, SurrealMemClient, SurrealWsClient};
|
use crate::{surrealdb_init_mem, surrealdb_init_ws, SurrealMemClient, SurrealWsClient};
|
||||||
use axum::{Router, serve};
|
use axum::{Router, serve};
|
||||||
use std::{future::Future, net::SocketAddr};
|
use std::{future::Future, net::SocketAddr};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use crate::environment::ENV;
|
use crate::environment::ENV;
|
||||||
|
|
||||||
|
pub use validated_json::ValidatedJson;
|
||||||
|
|
||||||
/// Initialize and start the Axum server with SurrealDB connections.
|
/// Initialize and start the Axum server with SurrealDB connections.
|
||||||
///
|
///
|
||||||
/// This function sets up both WebSocket and in-memory SurrealDB connections,
|
/// This function sets up both WebSocket and in-memory SurrealDB connections,
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
//! Custom extractor for automatic JSON validation and sanitization
|
||||||
|
//!
|
||||||
|
//! This extractor automatically validates request payloads using the validator crate
|
||||||
|
//! and returns appropriate error responses if validation fails.
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
extract::{rejection::JsonRejection, FromRequest, Request},
|
||||||
|
http::StatusCode,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
Json,
|
||||||
|
};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use serde_json;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
/// Custom extractor that automatically validates JSON payloads
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust
|
||||||
|
/// use validated_json::ValidatedJson;
|
||||||
|
/// use serde::Deserialize;
|
||||||
|
/// use validator::Validate;
|
||||||
|
///
|
||||||
|
/// #[derive(Deserialize, Validate)]
|
||||||
|
/// struct CreateUserRequest {
|
||||||
|
/// #[validate(email)]
|
||||||
|
/// email: String,
|
||||||
|
/// #[validate(length(min = 8))]
|
||||||
|
/// password: String,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// async fn create_user(
|
||||||
|
/// ValidatedJson(payload): ValidatedJson<CreateUserRequest>
|
||||||
|
/// ) -> Response {
|
||||||
|
/// // payload is already validated
|
||||||
|
/// // ... your logic here
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub struct ValidatedJson<T>(pub T);
|
||||||
|
|
||||||
|
impl<T, S> FromRequest<S> for ValidatedJson<T>
|
||||||
|
where
|
||||||
|
T: DeserializeOwned + Validate + 'static,
|
||||||
|
S: Send + Sync,
|
||||||
|
Json<T>: FromRequest<S, Rejection = JsonRejection>,
|
||||||
|
{
|
||||||
|
type Rejection = Response;
|
||||||
|
|
||||||
|
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
// First, extract JSON
|
||||||
|
let Json(value) = match Json::<T>::from_request(req, state).await {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(rejection) => {
|
||||||
|
let error_message = format!("Invalid JSON payload: {}", rejection);
|
||||||
|
return Err((
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Json(serde_json::json!({
|
||||||
|
"error": error_message,
|
||||||
|
"version": env!("CARGO_PKG_VERSION"),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.into_response());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Then, validate it
|
||||||
|
if let Err(errors) = value.validate() {
|
||||||
|
let error_messages: Vec<String> = errors
|
||||||
|
.field_errors()
|
||||||
|
.iter()
|
||||||
|
.flat_map(|(field, errors)| {
|
||||||
|
errors.iter().map(move |error| {
|
||||||
|
format!(
|
||||||
|
"{}: {}",
|
||||||
|
field,
|
||||||
|
error.message.as_ref().map(|m| m.to_string()).unwrap_or_else(|| error.code.to_string())
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
return Err((
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
Json(serde_json::json!({
|
||||||
|
"error": "Validation failed",
|
||||||
|
"details": error_messages,
|
||||||
|
"version": env!("CARGO_PKG_VERSION"),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.into_response());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ValidatedJson(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Validate)]
|
||||||
|
struct TestPayload {
|
||||||
|
#[validate(email)]
|
||||||
|
email: String,
|
||||||
|
#[validate(length(min = 8))]
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: Full integration tests should be done at the application level
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@ pub mod services;
|
|||||||
pub mod surrealdb;
|
pub mod surrealdb;
|
||||||
|
|
||||||
pub use argon::{hash_password, verify_password};
|
pub use argon::{hash_password, verify_password};
|
||||||
pub use axum::axum_init;
|
pub use axum::{axum_init, ValidatedJson};
|
||||||
pub use environment::{ENV, Env};
|
pub use environment::{ENV, Env};
|
||||||
pub use imphnen_entities::{
|
pub use imphnen_entities::{
|
||||||
MessageResponseDto,
|
MessageResponseDto,
|
||||||
|
|||||||
@@ -22,5 +22,6 @@ tracing.workspace = true
|
|||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
sha2.workspace = true
|
sha2.workspace = true
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
|
regex = "1.11"
|
||||||
dotenvy = { workspace = true }
|
dotenvy = { workspace = true }
|
||||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ pub mod query_builder;
|
|||||||
pub mod errors;
|
pub mod errors;
|
||||||
pub mod query_list;
|
pub mod query_list;
|
||||||
pub mod response_format;
|
pub mod response_format;
|
||||||
|
pub mod sanitization;
|
||||||
pub mod serde_helpers;
|
pub mod serde_helpers;
|
||||||
pub mod validator;
|
pub mod validator;
|
||||||
|
|
||||||
@@ -43,6 +44,16 @@ pub use query_builder::{
|
|||||||
pub use query_list::QueryListBuilder;
|
pub use query_list::QueryListBuilder;
|
||||||
pub use errors::AppError;
|
pub use errors::AppError;
|
||||||
pub use response_format::{common_response, success_created_response, success_list_response, success_response, error_response};
|
pub use response_format::{common_response, success_created_response, success_list_response, success_response, error_response};
|
||||||
|
pub use sanitization::{
|
||||||
|
sanitize_html,
|
||||||
|
sanitize_dangerous_patterns,
|
||||||
|
sanitize_filename,
|
||||||
|
sanitize_user_text,
|
||||||
|
sanitize_email,
|
||||||
|
sanitize_url,
|
||||||
|
normalize_whitespace,
|
||||||
|
contains_path_traversal,
|
||||||
|
};
|
||||||
pub use serde_helpers::{
|
pub use serde_helpers::{
|
||||||
deserialize_datetime,
|
deserialize_datetime,
|
||||||
option_thing_or_string,
|
option_thing_or_string,
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
//! Input sanitization utilities for security
|
||||||
|
//!
|
||||||
|
//! This module provides utilities to sanitize user input and prevent
|
||||||
|
//! common security vulnerabilities like XSS, HTML injection, etc.
|
||||||
|
|
||||||
|
use regex::Regex;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
// Note: HTML escaping is done via char-by-char mapping for better performance
|
||||||
|
// No regex needed for basic HTML entity escaping
|
||||||
|
|
||||||
|
/// SQL-like injection patterns (even though we use SurrealDB, be safe)
|
||||||
|
static SQL_INJECTION_PATTERNS: LazyLock<Regex> = LazyLock::new(|| {
|
||||||
|
Regex::new(r"(?i)(union|select|insert|update|delete|drop|create|alter|exec|script|javascript|onerror|onload)").unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Path traversal patterns
|
||||||
|
static PATH_TRAVERSAL_REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||||||
|
Regex::new(r"\.\.(/|\\)").unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Sanitize HTML by escaping special characters
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust
|
||||||
|
/// use imphnen_utils::sanitize_html;
|
||||||
|
///
|
||||||
|
/// let dirty = "<script>alert('xss')</script>";
|
||||||
|
/// let clean = sanitize_html(dirty);
|
||||||
|
/// assert_eq!(clean, "<script>alert('xss')</script>");
|
||||||
|
/// ```
|
||||||
|
pub fn sanitize_html(input: &str) -> String {
|
||||||
|
input
|
||||||
|
.chars()
|
||||||
|
.map(|c| match c {
|
||||||
|
'<' => "<".to_string(),
|
||||||
|
'>' => ">".to_string(),
|
||||||
|
'"' => """.to_string(),
|
||||||
|
'\'' => "'".to_string(),
|
||||||
|
'&' => "&".to_string(),
|
||||||
|
_ => c.to_string(),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sanitize string to prevent potential injection attacks
|
||||||
|
///
|
||||||
|
/// This is a conservative sanitization that removes potentially dangerous patterns
|
||||||
|
pub fn sanitize_dangerous_patterns(input: &str) -> String {
|
||||||
|
SQL_INJECTION_PATTERNS.replace_all(input, "[FILTERED]").into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if string contains path traversal attempts
|
||||||
|
pub fn contains_path_traversal(input: &str) -> bool {
|
||||||
|
PATH_TRAVERSAL_REGEX.is_match(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sanitize a string for safe usage in file names
|
||||||
|
///
|
||||||
|
/// Removes or replaces characters that could cause issues in file systems
|
||||||
|
pub fn sanitize_filename(input: &str) -> String {
|
||||||
|
input
|
||||||
|
.chars()
|
||||||
|
.map(|c| match c {
|
||||||
|
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
|
||||||
|
c if c.is_control() => '_',
|
||||||
|
c => c,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sanitize user input text (removes HTML and dangerous patterns)
|
||||||
|
///
|
||||||
|
/// Use this for fields like names, descriptions, bios, etc.
|
||||||
|
pub fn sanitize_user_text(input: &str) -> String {
|
||||||
|
let without_html = sanitize_html(input);
|
||||||
|
sanitize_dangerous_patterns(&without_html)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trim and normalize whitespace in a string
|
||||||
|
pub fn normalize_whitespace(input: &str) -> String {
|
||||||
|
input
|
||||||
|
.split_whitespace()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ")
|
||||||
|
.trim()
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate and sanitize email format
|
||||||
|
pub fn sanitize_email(email: &str) -> Option<String> {
|
||||||
|
let trimmed = email.trim().to_lowercase();
|
||||||
|
|
||||||
|
// Basic email validation
|
||||||
|
if trimmed.contains('@') && trimmed.contains('.') {
|
||||||
|
Some(trimmed)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sanitize URL to prevent javascript: and data: schemes
|
||||||
|
pub fn sanitize_url(url: &str) -> Option<String> {
|
||||||
|
let trimmed = url.trim();
|
||||||
|
|
||||||
|
// Block dangerous URL schemes
|
||||||
|
let lower = trimmed.to_lowercase();
|
||||||
|
if lower.starts_with("javascript:") || lower.starts_with("data:") || lower.starts_with("vbscript:") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow http, https, and relative URLs
|
||||||
|
if lower.starts_with("http://") || lower.starts_with("https://") || lower.starts_with("/") {
|
||||||
|
Some(trimmed.to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sanitize_html() {
|
||||||
|
assert_eq!(
|
||||||
|
sanitize_html("<script>alert('xss')</script>"),
|
||||||
|
"<script>alert('xss')</script>"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
sanitize_html("Normal text"),
|
||||||
|
"Normal text"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sanitize_dangerous_patterns() {
|
||||||
|
assert!(sanitize_dangerous_patterns("SELECT * FROM users").contains("[FILTERED]"));
|
||||||
|
assert_eq!(
|
||||||
|
sanitize_dangerous_patterns("Normal search query"),
|
||||||
|
"Normal search query"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_path_traversal() {
|
||||||
|
assert!(contains_path_traversal("../../../etc/passwd"));
|
||||||
|
assert!(contains_path_traversal("..\\windows\\system32"));
|
||||||
|
assert!(!contains_path_traversal("normal/path/to/file"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sanitize_filename() {
|
||||||
|
assert_eq!(
|
||||||
|
sanitize_filename("file<name>.txt"),
|
||||||
|
"file_name_.txt"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
sanitize_filename("normal_file.pdf"),
|
||||||
|
"normal_file.pdf"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sanitize_url() {
|
||||||
|
assert_eq!(
|
||||||
|
sanitize_url("https://example.com"),
|
||||||
|
Some("https://example.com".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(sanitize_url("javascript:alert('xss')"), None);
|
||||||
|
assert_eq!(sanitize_url("data:text/html,<script>alert('xss')</script>"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_normalize_whitespace() {
|
||||||
|
assert_eq!(
|
||||||
|
normalize_whitespace(" multiple spaces "),
|
||||||
|
"multiple spaces"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user