feat(users): admin runtime configs + external roles + employees APIs
This commit is contained in:
parent
3b28d9fd36
commit
7dec3e85fb
8 changed files with 896 additions and 5 deletions
|
|
@ -74,6 +74,7 @@ impl Services {
|
|||
|| path.starts_with("/api/runtime-config")
|
||||
|| path.starts_with("/api/config")
|
||||
|| path.starts_with("/api/admin/roles")
|
||||
|| path.starts_with("/api/admin/external-roles")
|
||||
|| path.starts_with("/api/admin/permissions")
|
||||
|| path.starts_with("/api/admin/onboarding-config")
|
||||
|| path.starts_with("/api/admin/dashboard-config")
|
||||
|
|
@ -149,6 +150,10 @@ impl Services {
|
|||
else if path.starts_with("/api/credits") {
|
||||
Some(self.payments_url.clone())
|
||||
}
|
||||
// Admin runtime config management defaults to users service
|
||||
else if path.starts_with("/api/admin/runtime-configs") {
|
||||
Some(self.users_url.clone())
|
||||
}
|
||||
else {
|
||||
None
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use axum::{
|
|||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use contracts::auth_middleware::{AuthUser, require_admin};
|
||||
use db::models::config::{
|
||||
ConfigRepository, CreateDashboardConfigPayload, CreateOnboardingConfigPayload,
|
||||
CreateRuntimeConfigPayload,
|
||||
|
|
@ -34,6 +35,184 @@ pub fn runtime_router() -> Router<AppState> {
|
|||
.route("/{role_id}", get(get_active_runtime_config))
|
||||
}
|
||||
|
||||
pub fn admin_runtime_router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", get(list_runtime_configs).post(admin_create_runtime_config))
|
||||
.route("/{id}", get(get_runtime_config_by_id).delete(delete_runtime_config))
|
||||
.route("/{id}/activate", post(activate_runtime_config))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AdminListQuery {
|
||||
role_id: Option<Uuid>,
|
||||
role_key: Option<String>,
|
||||
}
|
||||
|
||||
async fn list_runtime_configs(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Query(q): Query<AdminListQuery>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
if let Err(_e) = require_admin(&auth) {
|
||||
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
||||
}
|
||||
// Resolve role_id by role_key if provided
|
||||
let role_id = if let Some(id) = q.role_id {
|
||||
Some(id)
|
||||
} else if let Some(key) = q.role_key.clone() {
|
||||
sqlx::query_scalar!("SELECT id FROM roles WHERE key = $1", key.to_uppercase())
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let items = if let Some(rid) = role_id {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
SELECT id, role_id, config_json, version, is_active, updated_at
|
||||
FROM runtime_configs
|
||||
WHERE role_id = $1
|
||||
ORDER BY version DESC
|
||||
"#,
|
||||
rid
|
||||
)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?
|
||||
.into_iter()
|
||||
.map(|r| serde_json::json!({
|
||||
"id": r.id,
|
||||
"role_id": r.role_id,
|
||||
"config_json": r.config_json,
|
||||
"version": r.version,
|
||||
"is_active": r.is_active,
|
||||
"updated_at": r.updated_at,
|
||||
}))
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
SELECT rc.id, rc.role_id, rc.config_json, rc.version, rc.is_active, rc.updated_at
|
||||
FROM runtime_configs rc
|
||||
JOIN roles r ON rc.role_id = r.id
|
||||
WHERE r.audience = 'INTERNAL'
|
||||
ORDER BY rc.updated_at DESC
|
||||
"#
|
||||
)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?
|
||||
.into_iter()
|
||||
.map(|r| serde_json::json!({
|
||||
"id": r.id,
|
||||
"role_id": r.role_id,
|
||||
"config_json": r.config_json,
|
||||
"version": r.version,
|
||||
"is_active": r.is_active,
|
||||
"updated_at": r.updated_at,
|
||||
}))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
Ok((StatusCode::OK, Json(serde_json::json!({ "items": items }))))
|
||||
}
|
||||
|
||||
async fn get_runtime_config_by_id(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
if let Err(_e) = require_admin(&auth) {
|
||||
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
||||
}
|
||||
let r = sqlx::query!(
|
||||
"SELECT id, role_id, config_json, version, is_active, updated_at FROM runtime_configs 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, "Runtime config not found".to_string()))?;
|
||||
let row = serde_json::json!({
|
||||
"id": r.id,
|
||||
"role_id": r.role_id,
|
||||
"config_json": r.config_json,
|
||||
"version": r.version,
|
||||
"is_active": r.is_active,
|
||||
"updated_at": r.updated_at,
|
||||
});
|
||||
Ok((StatusCode::OK, Json(row)))
|
||||
}
|
||||
|
||||
async fn admin_create_runtime_config(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<CreateRuntimeConfigPayload>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
if let Err(_e) = require_admin(&auth) {
|
||||
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
||||
}
|
||||
match ConfigRepository::create_runtime_config(&state.pool, payload).await {
|
||||
Ok(config) => Ok((StatusCode::CREATED, Json(config))),
|
||||
Err(e) => Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Database error: {}", e),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn activate_runtime_config(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
if let Err(_e) = require_admin(&auth) {
|
||||
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
||||
}
|
||||
// Fetch role_id for the target config
|
||||
let role = sqlx::query!("SELECT role_id FROM runtime_configs 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, "Runtime config not found".to_string()))?;
|
||||
// Disable existing active
|
||||
sqlx::query!(
|
||||
"UPDATE runtime_configs SET is_active = false WHERE role_id = $1 AND is_active = true",
|
||||
role.role_id
|
||||
)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
// Activate target
|
||||
sqlx::query!(
|
||||
"UPDATE runtime_configs SET is_active = true WHERE id = $1",
|
||||
id
|
||||
)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
get_runtime_config_by_id(auth, State(state), Path(id)).await
|
||||
}
|
||||
|
||||
async fn delete_runtime_config(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> 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 runtime_configs 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, "Runtime config not found".to_string()));
|
||||
}
|
||||
Ok((StatusCode::NO_CONTENT, "".to_string()))
|
||||
}
|
||||
async fn get_my_runtime_config(
|
||||
auth: contracts::auth_middleware::AuthUser,
|
||||
State(state): State<AppState>,
|
||||
|
|
@ -236,4 +415,3 @@ async fn get_dashboard_config_by_key(
|
|||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use axum::{
|
|||
routing::get,
|
||||
Json, Router,
|
||||
};
|
||||
use contracts::auth_middleware::{AuthUser, require_admin};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::Row;
|
||||
|
|
@ -96,9 +97,13 @@ fn normalize_visibility(value: Option<String>, fallback: &str) -> String {
|
|||
}
|
||||
|
||||
async fn list_departments(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Query(params): Query<ListQuery>,
|
||||
) -> 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 per_page = params.per_page.or(params.limit).unwrap_or(20).clamp(1, 100);
|
||||
let offset = (page - 1) * per_page;
|
||||
|
|
@ -191,9 +196,13 @@ async fn list_departments(
|
|||
}
|
||||
|
||||
async fn get_department(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
if let Err(_e) = require_admin(&auth) {
|
||||
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
||||
}
|
||||
let row = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
|
|
@ -243,9 +252,13 @@ async fn get_department(
|
|||
}
|
||||
|
||||
async fn create_department(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<CreateDepartmentPayload>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
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()));
|
||||
|
|
@ -283,15 +296,19 @@ async fn create_department(
|
|||
})?;
|
||||
|
||||
let id: Uuid = row.get("id");
|
||||
let response = get_department(State(state), Path(id)).await?;
|
||||
let response = get_department(auth, State(state), Path(id)).await?;
|
||||
Ok((StatusCode::CREATED, response))
|
||||
}
|
||||
|
||||
async fn update_department(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(payload): Json<UpdateDepartmentPayload>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
if let Err(_e) = require_admin(&auth) {
|
||||
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
||||
}
|
||||
let current = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
|
|
@ -373,13 +390,17 @@ async fn update_department(
|
|||
}
|
||||
})?;
|
||||
|
||||
get_department(State(state), Path(id)).await
|
||||
get_department(auth, State(state), Path(id)).await
|
||||
}
|
||||
|
||||
async fn delete_department(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> 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")
|
||||
.bind(id)
|
||||
.execute(&state.pool)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use axum::{
|
|||
routing::get,
|
||||
Json, Router,
|
||||
};
|
||||
use contracts::auth_middleware::{AuthUser, require_admin};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::Row;
|
||||
|
|
@ -91,9 +92,13 @@ fn derive_is_active(status: &Option<String>, is_active: Option<bool>, current: b
|
|||
}
|
||||
|
||||
async fn list_designations(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Query(params): Query<ListQuery>,
|
||||
) -> 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 per_page = params.per_page.or(params.limit).unwrap_or(20).clamp(1, 100);
|
||||
let offset = (page - 1) * per_page;
|
||||
|
|
@ -193,9 +198,13 @@ async fn list_designations(
|
|||
}
|
||||
|
||||
async fn get_designation(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
if let Err(_e) = require_admin(&auth) {
|
||||
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
||||
}
|
||||
let row = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
|
|
@ -248,9 +257,13 @@ async fn get_designation(
|
|||
}
|
||||
|
||||
async fn create_designation(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<CreateDesignationPayload>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
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()));
|
||||
|
|
@ -287,15 +300,19 @@ async fn create_designation(
|
|||
})?;
|
||||
|
||||
let id: Uuid = row.get("id");
|
||||
let response = get_designation(State(state), Path(id)).await?;
|
||||
let response = get_designation(auth, State(state), Path(id)).await?;
|
||||
Ok((StatusCode::CREATED, response))
|
||||
}
|
||||
|
||||
async fn update_designation(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(payload): Json<UpdateDesignationPayload>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
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
|
||||
|
|
@ -376,13 +393,17 @@ async fn update_designation(
|
|||
}
|
||||
})?;
|
||||
|
||||
get_designation(State(state), Path(id)).await
|
||||
get_designation(auth, State(state), Path(id)).await
|
||||
}
|
||||
|
||||
async fn delete_designation(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> 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")
|
||||
.bind(id)
|
||||
.execute(&state.pool)
|
||||
|
|
|
|||
289
apps/users/src/handlers/employees.rs
Normal file
289
apps/users/src/handlers/employees.rs
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
use crate::AppState;
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
routing::{delete, get, post, patch},
|
||||
Json, Router,
|
||||
};
|
||||
use contracts::auth_middleware::{AuthUser, require_admin};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::types::Uuid;
|
||||
use sqlx::Row;
|
||||
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", get(list_employees).post(create_employee))
|
||||
.route("/{id}", get(get_employee).patch(update_employee).delete(delete_employee))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ListQuery {
|
||||
q: Option<String>,
|
||||
status: Option<String>, // ACTIVE | INACTIVE | SUSPENDED
|
||||
page: Option<i64>,
|
||||
per_page: Option<i64>,
|
||||
sort: Option<String>, // name_asc | name_desc | joined_asc | joined_desc
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct EmployeeRow {
|
||||
id: Uuid,
|
||||
user_id: Uuid,
|
||||
name: String,
|
||||
email: String,
|
||||
phone: Option<String>,
|
||||
employee_id: Option<String>,
|
||||
department_name: Option<String>,
|
||||
designation_name: Option<String>,
|
||||
role_name: Option<String>,
|
||||
role_key: Option<String>,
|
||||
joining_date: String,
|
||||
status: String,
|
||||
updated_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ListResponse {
|
||||
employees: Vec<EmployeeRow>,
|
||||
total: i64,
|
||||
page: i64,
|
||||
per_page: i64,
|
||||
}
|
||||
|
||||
async fn list_employees(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Query(q): Query<ListQuery>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
if let Err(_e) = require_admin(&auth) {
|
||||
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
||||
}
|
||||
let page = q.page.unwrap_or(1).max(1);
|
||||
let per_page = q.per_page.unwrap_or(20).clamp(1, 100);
|
||||
let offset = (page - 1) * per_page;
|
||||
let search = q.q.unwrap_or_default().to_lowercase();
|
||||
let status = q.status.unwrap_or_default().to_uppercase();
|
||||
let sort_sql = match q.sort.as_deref() {
|
||||
Some("name_asc") => "u.full_name ASC",
|
||||
Some("name_desc") => "u.full_name DESC",
|
||||
Some("joined_asc") => "e.created_at ASC",
|
||||
_ => "e.created_at DESC",
|
||||
};
|
||||
let sql = format!(r#"
|
||||
SELECT
|
||||
e.id,
|
||||
e.user_id,
|
||||
e.employee_code,
|
||||
e.created_at AS joining_date,
|
||||
u.full_name,
|
||||
u.email,
|
||||
u.phone,
|
||||
u.status AS user_status,
|
||||
r.name AS role_name,
|
||||
r.key AS role_key,
|
||||
d.name AS department_name,
|
||||
des.name AS designation_name,
|
||||
e.created_at AS updated_at
|
||||
FROM employees e
|
||||
JOIN users u ON u.id = e.user_id
|
||||
LEFT JOIN roles r ON r.id = e.role_id
|
||||
LEFT JOIN departments d ON d.id = e.department_id
|
||||
LEFT JOIN designations des ON des.id = e.designation_id
|
||||
WHERE ($1 = '' OR LOWER(u.full_name) LIKE '%' || $1 || '%' OR LOWER(u.email) LIKE '%' || $1 || '%' OR LOWER(COALESCE(e.employee_code,'')) LIKE '%' || $1 || '%')
|
||||
AND (
|
||||
$2 = ''
|
||||
OR ($2 = 'ACTIVE' AND u.status = 'ACTIVE')
|
||||
OR ($2 = 'INACTIVE' AND u.status = 'INACTIVE')
|
||||
OR ($2 = 'SUSPENDED' AND u.status = 'SUSPENDED')
|
||||
)
|
||||
ORDER BY {sort}
|
||||
LIMIT $3 OFFSET $4
|
||||
"#, sort = sort_sql);
|
||||
let rows = sqlx::query(&sql)
|
||||
.bind(&search)
|
||||
.bind(&status)
|
||||
.bind(per_page)
|
||||
.bind(offset)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
|
||||
let employees = rows.into_iter().map(|row| {
|
||||
let joining: chrono::DateTime<chrono::Utc> = row.get::<chrono::DateTime<chrono::Utc>, _>("joining_date");
|
||||
EmployeeRow {
|
||||
id: row.get("id"),
|
||||
user_id: row.get("user_id"),
|
||||
name: row.get::<Option<String>, _>("full_name").unwrap_or_default(),
|
||||
email: row.get("email"),
|
||||
phone: row.get::<Option<String>, _>("phone"),
|
||||
employee_id: row.get::<Option<String>, _>("employee_code"),
|
||||
department_name: row.get::<Option<String>, _>("department_name"),
|
||||
designation_name: row.get::<Option<String>, _>("designation_name"),
|
||||
role_name: row.get::<Option<String>, _>("role_name"),
|
||||
role_key: row.get::<Option<String>, _>("role_key"),
|
||||
joining_date: joining.to_rfc3339(),
|
||||
status: row.get::<Option<String>, _>("user_status").unwrap_or_else(|| "ACTIVE".to_string()),
|
||||
updated_at: Some(row.get::<chrono::DateTime<chrono::Utc>, _>("updated_at")),
|
||||
}
|
||||
}).collect::<Vec<_>>();
|
||||
|
||||
let total: i64 = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT COUNT(*)
|
||||
FROM employees e
|
||||
JOIN users u ON u.id = e.user_id
|
||||
WHERE ($1 = '' OR LOWER(u.full_name) LIKE '%' || $1 || '%' OR LOWER(u.email) LIKE '%' || $1 || '%' OR LOWER(COALESCE(e.employee_code,'')) LIKE '%' || $1 || '%')
|
||||
AND (
|
||||
$2 = ''
|
||||
OR ($2 = 'ACTIVE' AND u.status = 'ACTIVE')
|
||||
OR ($2 = 'INACTIVE' AND u.status = 'INACTIVE')
|
||||
OR ($2 = 'SUSPENDED' AND u.status = 'SUSPENDED')
|
||||
)
|
||||
"#,
|
||||
search,
|
||||
status
|
||||
)
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?
|
||||
.unwrap_or(0);
|
||||
|
||||
Ok(Json(ListResponse { employees, total, page, per_page }))
|
||||
}
|
||||
|
||||
async fn get_employee(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
if let Err(_e) = require_admin(&auth) {
|
||||
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
||||
}
|
||||
let sql = r#"
|
||||
SELECT
|
||||
e.id, e.user_id, e.employee_code,
|
||||
e.created_at AS joining_date, e.created_at AS updated_at,
|
||||
u.full_name, u.email, u.phone, u.status AS user_status,
|
||||
r.name AS role_name, r.key AS role_key,
|
||||
d.name AS department_name,
|
||||
des.name AS designation_name
|
||||
FROM employees e
|
||||
JOIN users u ON u.id = e.user_id
|
||||
LEFT JOIN roles r ON r.id = e.role_id
|
||||
LEFT JOIN departments d ON d.id = e.department_id
|
||||
LEFT JOIN designations des ON des.id = e.designation_id
|
||||
WHERE e.id = $1
|
||||
"#;
|
||||
let row = sqlx::query(sql)
|
||||
.bind(id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?
|
||||
.ok_or((StatusCode::NOT_FOUND, "Employee not found".to_string()))?;
|
||||
let joining: chrono::DateTime<chrono::Utc> = row.get::<chrono::DateTime<chrono::Utc>, _>("joining_date");
|
||||
Ok(Json(EmployeeRow {
|
||||
id: row.get("id"),
|
||||
user_id: row.get("user_id"),
|
||||
name: row.get::<Option<String>, _>("full_name").unwrap_or_default(),
|
||||
email: row.get("email"),
|
||||
phone: row.get::<Option<String>, _>("phone"),
|
||||
employee_id: row.get::<Option<String>, _>("employee_code"),
|
||||
department_name: row.get::<Option<String>, _>("department_name"),
|
||||
designation_name: row.get::<Option<String>, _>("designation_name"),
|
||||
role_name: row.get::<Option<String>, _>("role_name"),
|
||||
role_key: row.get::<Option<String>, _>("role_key"),
|
||||
joining_date: joining.to_rfc3339(),
|
||||
status: row.get::<Option<String>, _>("user_status").unwrap_or_else(|| "ACTIVE".to_string()),
|
||||
updated_at: Some(row.get::<chrono::DateTime<chrono::Utc>, _>("updated_at")),
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CreateEmployeePayload {
|
||||
user_id: Uuid,
|
||||
role_id: Uuid,
|
||||
department_id: Option<Uuid>,
|
||||
designation_id: Option<Uuid>,
|
||||
employee_code: Option<String>,
|
||||
}
|
||||
|
||||
async fn create_employee(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Json(p): Json<CreateEmployeePayload>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
if let Err(_e) = require_admin(&auth) {
|
||||
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
||||
}
|
||||
let row = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO employees (user_id, role_id, department_id, designation_id, employee_code)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (user_id) DO UPDATE SET
|
||||
role_id = EXCLUDED.role_id,
|
||||
department_id = EXCLUDED.department_id,
|
||||
designation_id = EXCLUDED.designation_id,
|
||||
employee_code = EXCLUDED.employee_code
|
||||
RETURNING id
|
||||
"#,
|
||||
p.user_id, p.role_id, p.department_id, p.designation_id, p.employee_code
|
||||
)
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
get_employee(auth, State(state), Path(row.id)).await
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct UpdateEmployeePayload {
|
||||
role_id: Option<Uuid>,
|
||||
department_id: Option<Uuid>,
|
||||
designation_id: Option<Uuid>,
|
||||
employee_code: Option<String>,
|
||||
}
|
||||
|
||||
async fn update_employee(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(p): Json<UpdateEmployeePayload>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
if let Err(_e) = require_admin(&auth) {
|
||||
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
||||
}
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE employees
|
||||
SET
|
||||
role_id = COALESCE($1, role_id),
|
||||
department_id = COALESCE($2, department_id),
|
||||
designation_id = COALESCE($3, designation_id),
|
||||
employee_code = COALESCE($4, employee_code)
|
||||
WHERE id = $5
|
||||
"#,
|
||||
p.role_id, p.department_id, p.designation_id, p.employee_code, id
|
||||
)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
get_employee(auth, State(state), Path(id)).await
|
||||
}
|
||||
|
||||
async fn delete_employee(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> 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 employees 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, "Employee not found".to_string()));
|
||||
}
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
372
apps/users/src/handlers/external_roles.rs
Normal file
372
apps/users/src/handlers/external_roles.rs
Normal file
|
|
@ -0,0 +1,372 @@
|
|||
use crate::AppState;
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
routing::{delete, get, post, put},
|
||||
Json, Router,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
use sqlx::types::Uuid;
|
||||
use contracts::auth_middleware::{AuthUser, require_admin};
|
||||
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", get(list_external_roles).post(create_external_role))
|
||||
.route("/{id}", get(get_external_role).put(update_external_role).delete(delete_external_role))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ListQuery {
|
||||
q: Option<String>,
|
||||
status: Option<String>, // ACTIVE | INACTIVE
|
||||
vertical: Option<String>, // jobs | marketplace
|
||||
category: Option<String>, // provider | employer | consumer | specialist
|
||||
page: Option<i64>,
|
||||
per_page: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ExternalRoleRow {
|
||||
id: Uuid,
|
||||
name: String,
|
||||
code: String,
|
||||
vertical: Option<String>,
|
||||
category: Option<String>,
|
||||
onboarding_schema_id: Option<String>,
|
||||
modules: Vec<String>,
|
||||
permissions: serde_json::Map<String, JsonValue>,
|
||||
requires_onboarding_approval: bool,
|
||||
requires_lead_approval: bool,
|
||||
requires_job_approval: bool,
|
||||
feature_limits: JsonValue,
|
||||
status: String,
|
||||
assigned_users: i64,
|
||||
assigned_user_types: Vec<String>,
|
||||
created_date: chrono::DateTime<chrono::Utc>,
|
||||
updated_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ListResponse {
|
||||
roles: Vec<ExternalRoleRow>,
|
||||
total: i64,
|
||||
page: i64,
|
||||
per_page: i64,
|
||||
}
|
||||
|
||||
async fn list_external_roles(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Query(q): Query<ListQuery>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
if let Err(_e) = require_admin(&auth) {
|
||||
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
||||
}
|
||||
let page = q.page.unwrap_or(1).max(1);
|
||||
let per_page = q.per_page.unwrap_or(20).min(100);
|
||||
let offset = (page - 1) * per_page;
|
||||
let search = q.q.unwrap_or_default().to_lowercase();
|
||||
let status = q.status.unwrap_or_default().to_uppercase();
|
||||
let vertical = q.vertical.unwrap_or_default().to_lowercase();
|
||||
let category = q.category.unwrap_or_default().to_lowercase();
|
||||
|
||||
// Join roles with active runtime_config for that role (optional) and count assigned user_roles
|
||||
let rows = sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
r.id,
|
||||
r.name,
|
||||
r.key as code,
|
||||
r.is_active,
|
||||
r.created_at as created_date,
|
||||
rc.updated_at as "updated_at?",
|
||||
rc.config_json as "config_json?"
|
||||
FROM roles r
|
||||
LEFT JOIN runtime_configs rc
|
||||
ON rc.role_id = r.id AND rc.is_active = true
|
||||
WHERE r.audience = 'EXTERNAL'
|
||||
AND ($1 = '' OR LOWER(r.name) LIKE '%' || $1 || '%' OR LOWER(r.key) LIKE '%' || $1 || '%')
|
||||
AND ($2 = '' OR (CASE WHEN $2 = 'ACTIVE' THEN r.is_active ELSE NOT r.is_active END))
|
||||
ORDER BY r.created_at DESC
|
||||
LIMIT $3 OFFSET $4
|
||||
"#,
|
||||
search,
|
||||
status,
|
||||
per_page,
|
||||
offset
|
||||
)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
|
||||
// Compute total with same filters
|
||||
let total: i64 = sqlx::query_scalar!(
|
||||
r#"
|
||||
SELECT COUNT(*)
|
||||
FROM roles r
|
||||
WHERE r.audience = 'EXTERNAL'
|
||||
AND ($1 = '' OR LOWER(r.name) LIKE '%' || $1 || '%' OR LOWER(r.key) LIKE '%' || $1 || '%')
|
||||
AND ($2 = '' OR (CASE WHEN $2 = 'ACTIVE' THEN r.is_active ELSE NOT r.is_active END))
|
||||
"#,
|
||||
search,
|
||||
status
|
||||
)
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?
|
||||
.unwrap_or(0);
|
||||
|
||||
let mut roles: Vec<ExternalRoleRow> = Vec::with_capacity(rows.len());
|
||||
for row in rows {
|
||||
let (mut vertical_v, mut category_v, mut onboarding_schema_id, mut modules, mut permissions, mut requires_onboarding, mut requires_lead, mut requires_job, mut feature_limits, mut assigned_user_types) = (None, None, None, Vec::new(), serde_json::Map::new(), false, false, false, serde_json::json!({}), Vec::new());
|
||||
if let Some(cfg) = row.config_json {
|
||||
vertical_v = cfg.get("vertical").and_then(|v| v.as_str()).map(|s| s.to_string());
|
||||
category_v = cfg.get("category").and_then(|v| v.as_str()).map(|s| s.to_string());
|
||||
onboarding_schema_id = cfg.get("onboarding_schema_id").and_then(|v| v.as_str()).map(|s| s.to_string());
|
||||
if let Some(arr) = cfg.get("modules").and_then(|v| v.as_array()) {
|
||||
modules = arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect();
|
||||
}
|
||||
if let Some(obj) = cfg.get("permissions").and_then(|v| v.as_object()) {
|
||||
permissions = obj.clone();
|
||||
}
|
||||
requires_onboarding = cfg.pointer("/requires/onboarding").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
requires_lead = cfg.pointer("/requires/lead").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
requires_job = cfg.pointer("/requires/job").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
feature_limits = cfg.get("feature_limits").cloned().unwrap_or_else(|| serde_json::json!({}));
|
||||
if let Some(arr) = cfg.get("assigned_user_types").and_then(|v| v.as_array()) {
|
||||
assigned_user_types = arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect();
|
||||
}
|
||||
}
|
||||
// Additional filters by vertical/category after extracting from config
|
||||
if !vertical.is_empty() && vertical_v.as_deref() != Some(vertical.as_str()) {
|
||||
continue;
|
||||
}
|
||||
if !category.is_empty() && category_v.as_deref() != Some(category.as_str()) {
|
||||
continue;
|
||||
}
|
||||
// Count assigned users from user_roles (approved)
|
||||
let assigned_users: i64 = sqlx::query_scalar!(
|
||||
"SELECT COUNT(*) FROM user_roles WHERE role_id = $1 AND status = 'APPROVED'",
|
||||
row.id
|
||||
)
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
.unwrap_or(Some(0))
|
||||
.unwrap_or(0);
|
||||
|
||||
roles.push(ExternalRoleRow {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
code: row.code,
|
||||
vertical: vertical_v,
|
||||
category: category_v,
|
||||
onboarding_schema_id,
|
||||
modules,
|
||||
permissions,
|
||||
requires_onboarding_approval: requires_onboarding,
|
||||
requires_lead_approval: requires_lead,
|
||||
requires_job_approval: requires_job,
|
||||
feature_limits,
|
||||
status: if row.is_active { "ACTIVE".to_string() } else { "INACTIVE".to_string() },
|
||||
assigned_users,
|
||||
assigned_user_types,
|
||||
created_date: row.created_date,
|
||||
updated_at: row.updated_at,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Json(ListResponse { roles, total, page, per_page }))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ExternalRoleDetail {
|
||||
id: Uuid,
|
||||
name: String,
|
||||
code: String,
|
||||
audience: String,
|
||||
is_active: bool,
|
||||
runtime: JsonValue,
|
||||
created_at: chrono::DateTime<chrono::Utc>,
|
||||
updated_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
}
|
||||
|
||||
async fn get_external_role(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
if let Err(_e) = require_admin(&auth) {
|
||||
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
||||
}
|
||||
let row = sqlx::query!(
|
||||
r#"
|
||||
SELECT r.id, r.name, r.key as code, r.audience, r.is_active, r.created_at, rc.updated_at as "updated_at?", rc.config_json as "config_json?"
|
||||
FROM roles r
|
||||
LEFT JOIN runtime_configs rc ON rc.role_id = r.id AND rc.is_active = true
|
||||
WHERE r.id = $1 AND r.audience = 'EXTERNAL'
|
||||
"#,
|
||||
id
|
||||
)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?
|
||||
.ok_or((StatusCode::NOT_FOUND, "External role not found".to_string()))?;
|
||||
|
||||
Ok(Json(ExternalRoleDetail {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
code: row.code,
|
||||
audience: row.audience,
|
||||
is_active: row.is_active,
|
||||
runtime: row.config_json.unwrap_or_else(|| serde_json::json!({})),
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CreateExternalRolePayload {
|
||||
name: String,
|
||||
code: String,
|
||||
is_active: Option<bool>,
|
||||
runtime: JsonValue, // carries vertical/category/modules/permissions/assigned_user_types/requires/feature_limits/onboarding_schema_id
|
||||
}
|
||||
|
||||
async fn create_external_role(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<CreateExternalRolePayload>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
if let Err(_e) = require_admin(&auth) {
|
||||
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
||||
}
|
||||
let is_active = payload.is_active.unwrap_or(true);
|
||||
// Insert role
|
||||
let role = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO roles (key, name, audience, is_active)
|
||||
VALUES ($1, $2, 'EXTERNAL', $3)
|
||||
RETURNING id, key, name, audience, is_active, created_at
|
||||
"#,
|
||||
payload.code.to_uppercase(),
|
||||
payload.name,
|
||||
is_active
|
||||
)
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
|
||||
// Create runtime config version 1
|
||||
let rc = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO runtime_configs (role_id, config_json, version, is_active)
|
||||
VALUES ($1, $2, 1, true)
|
||||
RETURNING updated_at
|
||||
"#,
|
||||
role.id,
|
||||
payload.runtime
|
||||
)
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
Json(ExternalRoleDetail {
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
code: role.key,
|
||||
audience: role.audience,
|
||||
is_active: role.is_active,
|
||||
runtime: payload.runtime,
|
||||
created_at: role.created_at,
|
||||
updated_at: Some(rc.updated_at),
|
||||
}),
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct UpdateExternalRolePayload {
|
||||
name: Option<String>,
|
||||
is_active: Option<bool>,
|
||||
runtime: Option<JsonValue>,
|
||||
}
|
||||
|
||||
async fn update_external_role(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(payload): Json<UpdateExternalRolePayload>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
if let Err(_e) = require_admin(&auth) {
|
||||
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
||||
}
|
||||
// Update role basic fields
|
||||
if payload.name.is_some() || payload.is_active.is_some() {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE roles
|
||||
SET name = COALESCE($1, name),
|
||||
is_active = COALESCE($2, is_active)
|
||||
WHERE id = $3 AND audience = 'EXTERNAL'
|
||||
"#,
|
||||
payload.name,
|
||||
payload.is_active,
|
||||
id
|
||||
)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
}
|
||||
// Create a new runtime config version if provided
|
||||
if let Some(runtime) = payload.runtime {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE runtime_configs
|
||||
SET is_active = false
|
||||
WHERE role_id = $1 AND is_active = true
|
||||
"#,
|
||||
id
|
||||
)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.ok();
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO runtime_configs (role_id, config_json, version, is_active)
|
||||
VALUES (
|
||||
$1,
|
||||
$2,
|
||||
COALESCE((SELECT MAX(version) FROM runtime_configs WHERE role_id = $1), 0) + 1,
|
||||
true
|
||||
)
|
||||
"#,
|
||||
id,
|
||||
runtime
|
||||
)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
}
|
||||
get_external_role(auth, State(state), Path(id)).await
|
||||
}
|
||||
|
||||
async fn delete_external_role(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> 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 roles WHERE id = $1 AND audience = 'EXTERNAL'", 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, "External role not found".to_string()));
|
||||
}
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
|
@ -4,8 +4,10 @@ pub mod config;
|
|||
pub mod dashboard;
|
||||
pub mod departments;
|
||||
pub mod designations;
|
||||
pub mod employees;
|
||||
pub mod notifications;
|
||||
pub mod onboarding;
|
||||
pub mod permissions;
|
||||
pub mod roles;
|
||||
pub mod user_roles;
|
||||
pub mod external_roles;
|
||||
|
|
|
|||
|
|
@ -59,6 +59,8 @@ async fn main() {
|
|||
.nest("/api/admin/permissions", handlers::permissions::router())
|
||||
.nest("/api/admin/departments", handlers::departments::router())
|
||||
.nest("/api/admin/designations", handlers::designations::router())
|
||||
.nest("/api/admin/employees", handlers::employees::router())
|
||||
.nest("/api/admin/external-roles", handlers::external_roles::router())
|
||||
.nest("/api/me/roles", handlers::user_roles::router())
|
||||
// ── Notifications ─────────────────────────────────────────────────
|
||||
.nest("/api/me/notifications", handlers::notifications::router())
|
||||
|
|
@ -72,6 +74,7 @@ async fn main() {
|
|||
.nest("/api/admin/onboarding-config", handlers::config::onboarding_router())
|
||||
.nest("/api/admin/dashboard-config", handlers::config::dashboard_router())
|
||||
.nest("/api/admin/dashboard", handlers::dashboard::router())
|
||||
.nest("/api/admin/runtime-configs", handlers::config::admin_runtime_router())
|
||||
// ── Public Config ─────────────────────────────────────────────────
|
||||
.nest("/api/config/onboarding", handlers::config::onboarding_router())
|
||||
.nest("/api/config/dashboard", handlers::config::dashboard_router())
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue