use crate::AppState; use axum::{ extract::{Path, Query, State}, http::StatusCode, response::IntoResponse, routing::get, Json, Router, }; use serde::{Deserialize, Serialize}; use uuid::Uuid; pub fn router() -> Router { Router::new() .route("/", get(list_roles).post(create_role)) .route("/{id}", get(get_role).patch(update_role).delete(delete_role)) } // ── Query params ───────────────────────────────────────────────────────────── #[derive(Deserialize)] struct ListQuery { audience: Option, q: Option, page: Option, per_page: Option, } // ── Response types ─────────────────────────────────────────────────────────── #[derive(Serialize)] struct RoleRow { id: Uuid, key: String, name: String, audience: String, description: Option, department_id: Option, department_name: Option, is_active: bool, can_approve_requests: bool, can_manage_system_settings: bool, users_assigned: i64, permissions_count: i64, created_at: chrono::DateTime, } #[derive(Serialize)] struct ListResponse { roles: Vec, total: i64, page: i64, per_page: i64, } #[derive(Serialize)] struct RoleDetail { id: Uuid, key: String, name: String, audience: String, description: Option, department_id: Option, department_name: Option, is_active: bool, can_approve_requests: bool, can_manage_system_settings: bool, permission_keys: Vec, created_at: chrono::DateTime, } // ── Request types ──────────────────────────────────────────────────────────── #[derive(Deserialize)] struct CreateRolePayload { key: String, name: String, audience: String, description: Option, department_id: Option, is_active: Option, can_approve_requests: Option, can_manage_system_settings: Option, permission_keys: Option>, } #[derive(Deserialize)] struct UpdateRolePayload { name: Option, description: Option, department_id: Option, is_active: Option, can_approve_requests: Option, can_manage_system_settings: Option, permission_keys: Option>, } // ── Handlers ───────────────────────────────────────────────────────────────── async fn list_roles( State(state): State, Query(params): Query, ) -> Result { let page = params.page.unwrap_or(1).max(1); let per_page = params.per_page.unwrap_or(20).min(100); let offset = (page - 1) * per_page; let search = params.q.as_deref().unwrap_or("").to_lowercase(); let audience = params.audience.as_deref().unwrap_or(""); let rows = sqlx::query!( r#" SELECT r.id, r.key, r.name, r.audience, r.description, r.department_id, d.name AS "department_name?", r.is_active, r.can_approve_requests, r.can_manage_system_settings, r.created_at, COUNT(DISTINCT e.id) AS users_assigned, COUNT(DISTINCT rp.id) AS permissions_count FROM roles r LEFT JOIN departments d ON d.id = r.department_id LEFT JOIN employees e ON e.role_code = r.key LEFT JOIN role_permissions rp ON rp.role_id = r.id WHERE ($1 = '' OR r.audience = $1) AND ($2 = '' OR LOWER(r.name) LIKE '%' || $2 || '%' OR LOWER(r.key) LIKE '%' || $2 || '%') GROUP BY r.id, d.name ORDER BY r.created_at DESC LIMIT $3 OFFSET $4 "#, audience, search, per_page, offset ) .fetch_all(&state.pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; let total: i64 = sqlx::query_scalar!( r#" SELECT COUNT(*) FROM roles r WHERE ($1 = '' OR r.audience = $1) AND ($2 = '' OR LOWER(r.name) LIKE '%' || $2 || '%' OR LOWER(r.key) LIKE '%' || $2 || '%') "#, audience, search ) .fetch_one(&state.pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))? .unwrap_or(0); let roles = rows .into_iter() .map(|r| RoleRow { id: r.id, key: r.key, name: r.name, audience: r.audience, description: r.description, department_id: r.department_id, department_name: r.department_name, is_active: r.is_active, can_approve_requests: r.can_approve_requests, can_manage_system_settings: r.can_manage_system_settings, users_assigned: r.users_assigned.unwrap_or(0), permissions_count: r.permissions_count.unwrap_or(0), created_at: r.created_at, }) .collect(); Ok(Json(ListResponse { roles, total, page, per_page })) } async fn get_role( State(state): State, Path(id): Path, ) -> Result { let row = sqlx::query!( r#" SELECT r.id, r.key, r.name, r.audience, r.description, r.department_id, d.name AS "department_name?", r.is_active, r.can_approve_requests, r.can_manage_system_settings, r.created_at FROM roles r LEFT JOIN departments d ON d.id = r.department_id WHERE r.id = $1 "#, id ) .fetch_optional(&state.pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))? .ok_or((StatusCode::NOT_FOUND, "Role not found".to_string()))?; let permission_keys: Vec = sqlx::query_scalar!( "SELECT permission_key FROM role_permissions WHERE role_id = $1 ORDER BY permission_key", id ) .fetch_all(&state.pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; Ok(Json(RoleDetail { id: row.id, key: row.key, name: row.name, audience: row.audience, description: row.description, department_id: row.department_id, department_name: row.department_name, is_active: row.is_active, can_approve_requests: row.can_approve_requests, can_manage_system_settings: row.can_manage_system_settings, permission_keys, created_at: row.created_at, })) } async fn create_role( State(state): State, Json(payload): Json, ) -> Result { let is_active = payload.is_active.unwrap_or(true); let can_approve = payload.can_approve_requests.unwrap_or(false); let can_manage = payload.can_manage_system_settings.unwrap_or(false); let role = sqlx::query!( r#" INSERT INTO roles (key, name, audience, description, department_id, is_active, can_approve_requests, can_manage_system_settings) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, key, name, audience, description, department_id, is_active, can_approve_requests, can_manage_system_settings, created_at "#, payload.key, payload.name, payload.audience, payload.description, payload.department_id, is_active, can_approve, can_manage, ) .fetch_one(&state.pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; // Insert permission keys if let Some(keys) = &payload.permission_keys { for key in keys { sqlx::query!( "INSERT INTO role_permissions (role_id, permission_key) VALUES ($1, $2) ON CONFLICT DO NOTHING", role.id, key ) .execute(&state.pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; } } let permission_keys: Vec = sqlx::query_scalar!( "SELECT permission_key FROM role_permissions WHERE role_id = $1 ORDER BY permission_key", role.id ) .fetch_all(&state.pool) .await .unwrap_or_default(); Ok(( StatusCode::CREATED, Json(RoleDetail { id: role.id, key: role.key, name: role.name, audience: role.audience, description: role.description, department_id: role.department_id, department_name: None, is_active: role.is_active, can_approve_requests: role.can_approve_requests, can_manage_system_settings: role.can_manage_system_settings, permission_keys, created_at: role.created_at, }), )) } async fn update_role( State(state): State, Path(id): Path, Json(payload): Json, ) -> Result { // Fetch current values first let current = sqlx::query!( "SELECT name, description, department_id, is_active, can_approve_requests, can_manage_system_settings FROM roles WHERE id = $1", id ) .fetch_optional(&state.pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))? .ok_or((StatusCode::NOT_FOUND, "Role not found".to_string()))?; let name = payload.name.unwrap_or(current.name); let description = payload.description.or(current.description); let department_id = payload.department_id.or(current.department_id); let is_active = payload.is_active.unwrap_or(current.is_active); let can_approve = payload.can_approve_requests.unwrap_or(current.can_approve_requests); let can_manage = payload.can_manage_system_settings.unwrap_or(current.can_manage_system_settings); sqlx::query!( r#" UPDATE roles SET name = $1, description = $2, department_id = $3, is_active = $4, can_approve_requests = $5, can_manage_system_settings = $6 WHERE id = $7 "#, name as String, description as Option, department_id as Option, is_active as bool, can_approve as bool, can_manage as bool, id as Uuid ) .execute(&state.pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; // Replace permissions if provided if let Some(keys) = &payload.permission_keys { sqlx::query!("DELETE FROM role_permissions WHERE role_id = $1", id) .execute(&state.pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; for key in keys { sqlx::query!( "INSERT INTO role_permissions (role_id, permission_key) VALUES ($1, $2) ON CONFLICT DO NOTHING", id, key ) .execute(&state.pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; } } // Return updated role get_role(State(state), Path(id)).await } async fn delete_role( State(state): State, Path(id): Path, ) -> Result { let result = sqlx::query!("DELETE FROM roles WHERE id = $1", id) .execute(&state.pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; if result.rows_affected() == 0 { return Err((StatusCode::NOT_FOUND, "Role not found".to_string())); } Ok(StatusCode::NO_CONTENT) }