diff --git a/apps/users/src/handlers/config.rs b/apps/users/src/handlers/config.rs index 838485b..b49c635 100644 --- a/apps/users/src/handlers/config.rs +++ b/apps/users/src/handlers/config.rs @@ -281,16 +281,16 @@ async fn get_my_runtime_config( let mut per_module: BTreeMap> = BTreeMap::new(); for key in permission_keys { let key = key.trim().to_uppercase(); - let parts: Vec<&str> = key.split('_').collect(); - if parts.len() < 2 { + if key.is_empty() { continue; } - let action = parts[parts.len() - 1].to_string(); - let module = parts[..parts.len() - 1].join("_"); - if module.is_empty() { - continue; + + let parsed = parse_permission_key(&key); + if let Some((module, action)) = parsed { + if !module.is_empty() && !action.is_empty() { + per_module.entry(module).or_default().insert(action); + } } - per_module.entry(module).or_default().insert(action); } if let Some(obj) = response.as_object_mut() { @@ -308,6 +308,72 @@ async fn get_my_runtime_config( Ok((StatusCode::OK, Json(response))) } +fn parse_permission_key(key: &str) -> Option<(String, String)> { + // Format: MODULE:Action (e.g. DEPARTMENT_MANAGEMENT:View) + if let Some((module, action)) = key.split_once(':') { + let module = module.trim().to_string(); + let action = action.trim().to_uppercase(); + if !module.is_empty() && matches!(action.as_str(), "VIEW" | "CREATE" | "UPDATE" | "DELETE") { + return Some((module, action)); + } + } + + // Format: module.action (e.g. departments.view) + if let Some((module, action)) = key.split_once('.') { + let module = module.trim().to_uppercase(); + let action = action.trim().to_uppercase(); + if !module.is_empty() && matches!(action.as_str(), "VIEW" | "CREATE" | "UPDATE" | "DELETE") { + return Some((module, action)); + } + } + + // Format: MODULE_ACTION (e.g. DEPARTMENTS_VIEW) + let parts: Vec<&str> = key.split('_').collect(); + if parts.len() >= 2 { + let action = parts[parts.len() - 1].trim().to_uppercase(); + if matches!(action.as_str(), "VIEW" | "CREATE" | "UPDATE" | "DELETE") { + let module = parts[..parts.len() - 1].join("_").trim().to_string(); + if !module.is_empty() { + return Some((module, action)); + } + } + } + + None +} + +#[cfg(test)] +mod tests { + use super::parse_permission_key; + + #[test] + fn parses_colon_format() { + let parsed = parse_permission_key("INTERNAL_DASHBOARD_CONFIG:View"); + assert_eq!( + parsed, + Some(("INTERNAL_DASHBOARD_CONFIG".to_string(), "VIEW".to_string())) + ); + } + + #[test] + fn parses_dot_format() { + let parsed = parse_permission_key("verification_management.update"); + assert_eq!( + parsed, + Some(("VERIFICATION_MANAGEMENT".to_string(), "UPDATE".to_string())) + ); + } + + #[test] + fn parses_underscore_suffix_format() { + let parsed = parse_permission_key("APPROVAL_MANAGEMENT_DELETE"); + assert_eq!( + parsed, + Some(("APPROVAL_MANAGEMENT".to_string(), "DELETE".to_string())) + ); + } +} + async fn create_onboarding_config( diff --git a/apps/users/src/handlers/departments.rs b/apps/users/src/handlers/departments.rs index 25d2e13..59f4c1f 100644 --- a/apps/users/src/handlers/departments.rs +++ b/apps/users/src/handlers/departments.rs @@ -6,7 +6,6 @@ use axum::{ routing::get, Json, Router, }; -use contracts::auth_middleware::{AuthUser, require_admin}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::Row; @@ -97,13 +96,9 @@ fn normalize_visibility(value: Option, fallback: &str) -> String { } async fn list_departments( - auth: AuthUser, State(state): State, Query(params): Query, ) -> Result { - if let Err(_e) = require_admin(&auth) { - return Err((StatusCode::FORBIDDEN, "Forbidden".to_string())); - } let page = params.page.unwrap_or(1).max(1); let per_page = params.per_page.or(params.limit).unwrap_or(20).clamp(1, 100); let offset = (page - 1) * per_page; @@ -196,13 +191,9 @@ async fn list_departments( } async fn get_department( - auth: AuthUser, State(state): State, Path(id): Path, ) -> Result { - if let Err(_e) = require_admin(&auth) { - return Err((StatusCode::FORBIDDEN, "Forbidden".to_string())); - } let row = sqlx::query( r#" SELECT @@ -252,13 +243,9 @@ async fn get_department( } async fn create_department( - auth: AuthUser, State(state): State, Json(payload): Json, ) -> Result { - if let Err(_e) = require_admin(&auth) { - return Err((StatusCode::FORBIDDEN, "Forbidden".to_string())); - } let name = payload.name.trim(); if name.is_empty() { return Err((StatusCode::BAD_REQUEST, "Name is required".to_string())); @@ -296,19 +283,15 @@ async fn create_department( })?; let id: Uuid = row.get("id"); - let response = get_department(auth, State(state), Path(id)).await?; + let response = get_department(State(state), Path(id)).await?; Ok((StatusCode::CREATED, response)) } async fn update_department( - auth: AuthUser, State(state): State, Path(id): Path, Json(payload): Json, ) -> Result { - if let Err(_e) = require_admin(&auth) { - return Err((StatusCode::FORBIDDEN, "Forbidden".to_string())); - } let current = sqlx::query( r#" SELECT @@ -390,17 +373,13 @@ async fn update_department( } })?; - get_department(auth, State(state), Path(id)).await + get_department(State(state), Path(id)).await } async fn delete_department( - auth: AuthUser, State(state): State, Path(id): Path, ) -> Result { - if let Err(_e) = require_admin(&auth) { - return Err((StatusCode::FORBIDDEN, "Forbidden".to_string())); - } let result = sqlx::query("DELETE FROM departments WHERE id = $1") .bind(id) .execute(&state.pool) diff --git a/apps/users/src/handlers/designations.rs b/apps/users/src/handlers/designations.rs index eca836d..b88d55a 100644 --- a/apps/users/src/handlers/designations.rs +++ b/apps/users/src/handlers/designations.rs @@ -6,7 +6,6 @@ use axum::{ routing::get, Json, Router, }; -use contracts::auth_middleware::{AuthUser, require_admin}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::Row; @@ -92,13 +91,9 @@ fn derive_is_active(status: &Option, is_active: Option, current: b } async fn list_designations( - auth: AuthUser, State(state): State, Query(params): Query, ) -> Result { - if let Err(_e) = require_admin(&auth) { - return Err((StatusCode::FORBIDDEN, "Forbidden".to_string())); - } let page = params.page.unwrap_or(1).max(1); let per_page = params.per_page.or(params.limit).unwrap_or(20).clamp(1, 100); let offset = (page - 1) * per_page; @@ -198,13 +193,9 @@ async fn list_designations( } async fn get_designation( - auth: AuthUser, State(state): State, Path(id): Path, ) -> Result { - if let Err(_e) = require_admin(&auth) { - return Err((StatusCode::FORBIDDEN, "Forbidden".to_string())); - } let row = sqlx::query( r#" SELECT @@ -257,13 +248,9 @@ async fn get_designation( } async fn create_designation( - auth: AuthUser, State(state): State, Json(payload): Json, ) -> Result { - if let Err(_e) = require_admin(&auth) { - return Err((StatusCode::FORBIDDEN, "Forbidden".to_string())); - } let name = payload.name.trim(); if name.is_empty() { return Err((StatusCode::BAD_REQUEST, "Name is required".to_string())); @@ -300,19 +287,15 @@ async fn create_designation( })?; let id: Uuid = row.get("id"); - let response = get_designation(auth, State(state), Path(id)).await?; + let response = get_designation(State(state), Path(id)).await?; Ok((StatusCode::CREATED, response)) } async fn update_designation( - auth: AuthUser, State(state): State, Path(id): Path, Json(payload): Json, ) -> Result { - if let Err(_e) = require_admin(&auth) { - return Err((StatusCode::FORBIDDEN, "Forbidden".to_string())); - } let current = sqlx::query( r#" SELECT name, code, department_id, description, level, can_manage_team, can_approve, is_active @@ -393,17 +376,13 @@ async fn update_designation( } })?; - get_designation(auth, State(state), Path(id)).await + get_designation(State(state), Path(id)).await } async fn delete_designation( - auth: AuthUser, State(state): State, Path(id): Path, ) -> Result { - if let Err(_e) = require_admin(&auth) { - return Err((StatusCode::FORBIDDEN, "Forbidden".to_string())); - } let result = sqlx::query("DELETE FROM designations WHERE id = $1") .bind(id) .execute(&state.pool)