refactor: migrate to clean architecture with trait-based DI (v0.2.0)
Complete architectural overhaul across all 12 crates: - Replace validator crate with zod-rs for all DTO validation - Replace manual pagination with paginator-rs/paginator-sea-orm - Migrate all modules (iam, cms, gacha, dimentorin) to clean architecture: domain → application → infrastructure layers - Introduce trait-based DI (Arc<dyn Trait>) at every layer for repositories and services - Delete all v1/ legacy SurrealDB-era code across every crate - Replace opaque response helpers with typed IntoResponse structs (ApiSuccess, ApiCreated, ApiPaginated, ApiMessage) - Remove dual_mode_repository, migration_validation_errors, validator.rs dead code - Zero cargo clippy warnings; release build clean Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
1b3366d735
commit
e432a1a743
@@ -1,30 +1,29 @@
|
||||
[package]
|
||||
name = "imphnen-middleware"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
imphnen-libs.workspace = true
|
||||
imphnen-utils.workspace = true
|
||||
imphnen-entities.workspace = true
|
||||
sea-orm.workspace = true
|
||||
uuid.workspace = true
|
||||
axum.workspace = true
|
||||
axum-extra.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
utoipa.workspace = true
|
||||
lazy_static.workspace = true
|
||||
regex.workspace = true
|
||||
validator.workspace = true
|
||||
axum-test.workspace = true
|
||||
rand.workspace = true
|
||||
base64.workspace = true
|
||||
tokio.workspace = true
|
||||
chrono.workspace = true
|
||||
log.workspace = true
|
||||
anyhow.workspace = true
|
||||
tower-http.workspace = true
|
||||
futures.workspace = true
|
||||
tower.workspace = true
|
||||
utoipa-swagger-ui.workspace = true
|
||||
[package]
|
||||
name = "imphnen-middleware"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
imphnen-libs.workspace = true
|
||||
imphnen-utils.workspace = true
|
||||
imphnen-entities.workspace = true
|
||||
sea-orm.workspace = true
|
||||
uuid.workspace = true
|
||||
axum.workspace = true
|
||||
axum-extra.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
utoipa.workspace = true
|
||||
lazy_static.workspace = true
|
||||
regex.workspace = true
|
||||
axum-test.workspace = true
|
||||
rand.workspace = true
|
||||
base64.workspace = true
|
||||
tokio.workspace = true
|
||||
chrono.workspace = true
|
||||
log.workspace = true
|
||||
anyhow.workspace = true
|
||||
tower-http.workspace = true
|
||||
futures.workspace = true
|
||||
tower.workspace = true
|
||||
utoipa-swagger-ui.workspace = true
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use axum::{
|
||||
Extension, extract::Request, http::StatusCode, middleware::Next,
|
||||
response::Response,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use imphnen_libs::{AppState, jsonwebtoken::decode_access_token};
|
||||
use axum_extra::headers::{authorization::Bearer, Authorization, HeaderMapExt};
|
||||
use std::convert::Infallible;
|
||||
use uuid::Uuid;
|
||||
use imphnen_utils::response_format::common_response;
|
||||
use imphnen_utils::response_format::ApiMessage;
|
||||
|
||||
pub async fn auth_middleware(
|
||||
Extension(state): Extension<AppState>,
|
||||
@@ -17,20 +17,20 @@ pub async fn auth_middleware(
|
||||
.headers()
|
||||
.typed_get::<Authorization<Bearer>>() {
|
||||
Some(header) => header,
|
||||
None => return Ok(common_response(
|
||||
None => return Ok(ApiMessage::new(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"Invalid or missing authorization token",
|
||||
)),
|
||||
).into_response()),
|
||||
};
|
||||
|
||||
let token = auth_header.token();
|
||||
|
||||
let claims = match decode_access_token(token) {
|
||||
Ok(token_data) => token_data.claims,
|
||||
Err(_) => return Ok(common_response(
|
||||
Err(_) => return Ok(ApiMessage::new(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"Invalid or expired token",
|
||||
)),
|
||||
).into_response()),
|
||||
};
|
||||
|
||||
let user_id = claims.user_id.clone();
|
||||
@@ -38,17 +38,17 @@ pub async fn auth_middleware(
|
||||
// Validate UUID format
|
||||
let user_uuid = match Uuid::parse_str(&user_id) {
|
||||
Ok(uuid) => uuid,
|
||||
Err(_) => return Ok(common_response(
|
||||
Err(_) => return Ok(ApiMessage::new(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"Invalid user identifier format",
|
||||
)),
|
||||
).into_response()),
|
||||
};
|
||||
|
||||
// Use UserLookupService to fetch full user details including roles/permissions
|
||||
// This ensures consistency and populates the DTO expected by controllers
|
||||
let user_info = match state.user_lookup_service.get_user_by_id(user_uuid, &state).await {
|
||||
Ok(info) => info,
|
||||
Err(_) => return Ok(common_response(StatusCode::UNAUTHORIZED, "User not found or inactive")),
|
||||
Err(_) => return Ok(ApiMessage::new(StatusCode::UNAUTHORIZED, "User not found or inactive").into_response()),
|
||||
};
|
||||
|
||||
// Insert the Model (reconstructed or fetched? Wait, UserLookupService returns ExtendedUserInfo)
|
||||
|
||||
@@ -5,7 +5,8 @@ use axum::{
|
||||
use futures::future::BoxFuture;
|
||||
use imphnen_entities::PermissionsEnum;
|
||||
use imphnen_libs::{AppState, services::ExtendedUserInfo};
|
||||
use imphnen_utils::response_format::common_response;
|
||||
use imphnen_utils::response_format::ApiMessage;
|
||||
use axum::response::IntoResponse;
|
||||
use imphnen_utils::{extract_email, extract_email_async};
|
||||
use std::task::{Context, Poll};
|
||||
use tower::{Layer, Service};
|
||||
@@ -77,19 +78,19 @@ where
|
||||
// Extract user email from authorization headers
|
||||
let email = extract_user_email(headers).await
|
||||
.ok_or_else(|| {
|
||||
common_response(
|
||||
ApiMessage::new(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"Invalid or missing authorization token",
|
||||
)
|
||||
).into_response()
|
||||
})?;
|
||||
|
||||
|
||||
// Get user data with permissions from user lookup service
|
||||
let user = app_state.user_lookup_service.get_user_by_email(&email, &app_state).await
|
||||
.map_err(|_| {
|
||||
common_response(
|
||||
ApiMessage::new(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"User session expired or not found",
|
||||
)
|
||||
).into_response()
|
||||
})?;
|
||||
|
||||
// Extract user permissions from role
|
||||
@@ -100,10 +101,10 @@ where
|
||||
|
||||
// Check if user has required permissions
|
||||
if !has_required_permissions(&user_permissions, &permissions) {
|
||||
return Err(common_response(
|
||||
return Err(ApiMessage::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
"You don't have the required permissions",
|
||||
));
|
||||
).into_response());
|
||||
}
|
||||
|
||||
inner.call(req).await
|
||||
@@ -174,27 +175,27 @@ pub async fn check_permissions(
|
||||
) -> Result<(), Response<Body>> {
|
||||
let email = extract_user_email(headers).await
|
||||
.ok_or_else(|| {
|
||||
common_response(
|
||||
ApiMessage::new(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"Invalid or missing authorization token",
|
||||
)
|
||||
).into_response()
|
||||
})?;
|
||||
|
||||
|
||||
let user = app_state.user_lookup_service.get_user_by_email(&email, app_state).await
|
||||
.map_err(|_| {
|
||||
common_response(
|
||||
ApiMessage::new(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"User session expired or not found",
|
||||
)
|
||||
).into_response()
|
||||
})?;
|
||||
|
||||
|
||||
let user_permissions = extract_user_permissions(&user);
|
||||
|
||||
|
||||
if !has_required_permissions(&user_permissions, &required_permissions) {
|
||||
return Err(common_response(
|
||||
return Err(ApiMessage::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
"You don't have the required permissions",
|
||||
));
|
||||
).into_response());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user