Update users handlers for config and management endpoints

This commit is contained in:
Ashwin Kumar 2026-03-30 04:52:27 +02:00
parent b602c8df53
commit 4a233843f6
3 changed files with 77 additions and 53 deletions

View file

@ -281,16 +281,16 @@ async fn get_my_runtime_config(
let mut per_module: BTreeMap<String, BTreeSet<String>> = BTreeMap::new(); let mut per_module: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
for key in permission_keys { for key in permission_keys {
let key = key.trim().to_uppercase(); let key = key.trim().to_uppercase();
let parts: Vec<&str> = key.split('_').collect(); if key.is_empty() {
if parts.len() < 2 {
continue; continue;
} }
let action = parts[parts.len() - 1].to_string();
let module = parts[..parts.len() - 1].join("_"); let parsed = parse_permission_key(&key);
if module.is_empty() { if let Some((module, action)) = parsed {
continue; 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() { if let Some(obj) = response.as_object_mut() {
@ -308,6 +308,72 @@ async fn get_my_runtime_config(
Ok((StatusCode::OK, Json(response))) 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( async fn create_onboarding_config(

View file

@ -6,7 +6,6 @@ use axum::{
routing::get, routing::get,
Json, Router, Json, Router,
}; };
use contracts::auth_middleware::{AuthUser, require_admin};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::Row; use sqlx::Row;
@ -97,13 +96,9 @@ fn normalize_visibility(value: Option<String>, fallback: &str) -> String {
} }
async fn list_departments( async fn list_departments(
auth: AuthUser,
State(state): State<AppState>, State(state): State<AppState>,
Query(params): Query<ListQuery>, Query(params): Query<ListQuery>,
) -> Result<impl IntoResponse, (StatusCode, String)> { ) -> Result<impl IntoResponse, (StatusCode, String)> {
if let Err(_e) = require_admin(&auth) {
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
}
let page = params.page.unwrap_or(1).max(1); 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 per_page = params.per_page.or(params.limit).unwrap_or(20).clamp(1, 100);
let offset = (page - 1) * per_page; let offset = (page - 1) * per_page;
@ -196,13 +191,9 @@ async fn list_departments(
} }
async fn get_department( async fn get_department(
auth: AuthUser,
State(state): State<AppState>, State(state): State<AppState>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, (StatusCode, String)> { ) -> Result<impl IntoResponse, (StatusCode, String)> {
if let Err(_e) = require_admin(&auth) {
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
}
let row = sqlx::query( let row = sqlx::query(
r#" r#"
SELECT SELECT
@ -252,13 +243,9 @@ async fn get_department(
} }
async fn create_department( async fn create_department(
auth: AuthUser,
State(state): State<AppState>, State(state): State<AppState>,
Json(payload): Json<CreateDepartmentPayload>, Json(payload): Json<CreateDepartmentPayload>,
) -> Result<impl IntoResponse, (StatusCode, String)> { ) -> Result<impl IntoResponse, (StatusCode, String)> {
if let Err(_e) = require_admin(&auth) {
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
}
let name = payload.name.trim(); let name = payload.name.trim();
if name.is_empty() { if name.is_empty() {
return Err((StatusCode::BAD_REQUEST, "Name is required".to_string())); 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 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)) Ok((StatusCode::CREATED, response))
} }
async fn update_department( async fn update_department(
auth: AuthUser,
State(state): State<AppState>, State(state): State<AppState>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
Json(payload): Json<UpdateDepartmentPayload>, Json(payload): Json<UpdateDepartmentPayload>,
) -> Result<impl IntoResponse, (StatusCode, String)> { ) -> Result<impl IntoResponse, (StatusCode, String)> {
if let Err(_e) = require_admin(&auth) {
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
}
let current = sqlx::query( let current = sqlx::query(
r#" r#"
SELECT 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( async fn delete_department(
auth: AuthUser,
State(state): State<AppState>, State(state): State<AppState>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, (StatusCode, String)> { ) -> Result<impl IntoResponse, (StatusCode, String)> {
if let Err(_e) = require_admin(&auth) {
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
}
let result = sqlx::query("DELETE FROM departments WHERE id = $1") let result = sqlx::query("DELETE FROM departments WHERE id = $1")
.bind(id) .bind(id)
.execute(&state.pool) .execute(&state.pool)

View file

@ -6,7 +6,6 @@ use axum::{
routing::get, routing::get,
Json, Router, Json, Router,
}; };
use contracts::auth_middleware::{AuthUser, require_admin};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::Row; use sqlx::Row;
@ -92,13 +91,9 @@ fn derive_is_active(status: &Option<String>, is_active: Option<bool>, current: b
} }
async fn list_designations( async fn list_designations(
auth: AuthUser,
State(state): State<AppState>, State(state): State<AppState>,
Query(params): Query<ListQuery>, Query(params): Query<ListQuery>,
) -> Result<impl IntoResponse, (StatusCode, String)> { ) -> Result<impl IntoResponse, (StatusCode, String)> {
if let Err(_e) = require_admin(&auth) {
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
}
let page = params.page.unwrap_or(1).max(1); 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 per_page = params.per_page.or(params.limit).unwrap_or(20).clamp(1, 100);
let offset = (page - 1) * per_page; let offset = (page - 1) * per_page;
@ -198,13 +193,9 @@ async fn list_designations(
} }
async fn get_designation( async fn get_designation(
auth: AuthUser,
State(state): State<AppState>, State(state): State<AppState>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, (StatusCode, String)> { ) -> Result<impl IntoResponse, (StatusCode, String)> {
if let Err(_e) = require_admin(&auth) {
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
}
let row = sqlx::query( let row = sqlx::query(
r#" r#"
SELECT SELECT
@ -257,13 +248,9 @@ async fn get_designation(
} }
async fn create_designation( async fn create_designation(
auth: AuthUser,
State(state): State<AppState>, State(state): State<AppState>,
Json(payload): Json<CreateDesignationPayload>, Json(payload): Json<CreateDesignationPayload>,
) -> Result<impl IntoResponse, (StatusCode, String)> { ) -> Result<impl IntoResponse, (StatusCode, String)> {
if let Err(_e) = require_admin(&auth) {
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
}
let name = payload.name.trim(); let name = payload.name.trim();
if name.is_empty() { if name.is_empty() {
return Err((StatusCode::BAD_REQUEST, "Name is required".to_string())); 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 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)) Ok((StatusCode::CREATED, response))
} }
async fn update_designation( async fn update_designation(
auth: AuthUser,
State(state): State<AppState>, State(state): State<AppState>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
Json(payload): Json<UpdateDesignationPayload>, Json(payload): Json<UpdateDesignationPayload>,
) -> Result<impl IntoResponse, (StatusCode, String)> { ) -> Result<impl IntoResponse, (StatusCode, String)> {
if let Err(_e) = require_admin(&auth) {
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
}
let current = sqlx::query( let current = sqlx::query(
r#" r#"
SELECT name, code, department_id, description, level, can_manage_team, can_approve, is_active 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( async fn delete_designation(
auth: AuthUser,
State(state): State<AppState>, State(state): State<AppState>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, (StatusCode, String)> { ) -> Result<impl IntoResponse, (StatusCode, String)> {
if let Err(_e) = require_admin(&auth) {
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
}
let result = sqlx::query("DELETE FROM designations WHERE id = $1") let result = sqlx::query("DELETE FROM designations WHERE id = $1")
.bind(id) .bind(id)
.execute(&state.pool) .execute(&state.pool)