chore: checkpoint current workspace changes
This commit is contained in:
parent
ac27184ae2
commit
b82f294331
10 changed files with 462 additions and 41 deletions
|
|
@ -17,12 +17,12 @@ pub fn router() -> Router<PgPool> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/profile/me", get(get_profile).patch(update_profile))
|
.route("/profile/me", get(get_profile).patch(update_profile))
|
||||||
.route("/jobs", get(list_jobs).post(create_job))
|
.route("/jobs", get(list_jobs).post(create_job))
|
||||||
.route("/jobs/:id", get(get_job).patch(update_job))
|
.route("/jobs/{id}", get(get_job).patch(update_job))
|
||||||
.route("/jobs/:id/submit", post(submit_job))
|
.route("/jobs/{id}/submit", post(submit_job))
|
||||||
.route("/jobs/:id/close", post(close_job))
|
.route("/jobs/{id}/close", post(close_job))
|
||||||
.route("/jobs/:id/applications", get(list_applications))
|
.route("/jobs/{id}/applications", get(list_applications))
|
||||||
.route("/applications/:id/status", patch(update_application_status))
|
.route("/applications/{id}/status", patch(update_application_status))
|
||||||
.route("/applications/:id/contact", get(view_contact))
|
.route("/applications/{id}/contact", get(view_contact))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,11 @@ pub fn router() -> Router<PgPool> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/profile/me", get(get_profile).patch(update_profile))
|
.route("/profile/me", get(get_profile).patch(update_profile))
|
||||||
.route("/requirements", get(list_requirements).post(create_requirement))
|
.route("/requirements", get(list_requirements).post(create_requirement))
|
||||||
.route("/requirements/:id", get(get_requirement).patch(update_requirement))
|
.route("/requirements/{id}", get(get_requirement).patch(update_requirement))
|
||||||
.route("/requirements/:id/submit", post(submit_requirement))
|
.route("/requirements/{id}/submit", post(submit_requirement))
|
||||||
.route("/requirements/:id/requests", get(list_requests))
|
.route("/requirements/{id}/requests", get(list_requests))
|
||||||
.route("/requirements/:id/requests/:lead_id/approve", post(approve_request))
|
.route("/requirements/{id}/requests/{lead_id}/approve", post(approve_request))
|
||||||
.route("/requirements/:id/requests/:lead_id/reject", post(reject_request))
|
.route("/requirements/{id}/requests/{lead_id}/reject", post(reject_request))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,7 @@ impl Services {
|
||||||
|| path.starts_with("/api/runtime-config")
|
|| path.starts_with("/api/runtime-config")
|
||||||
|| path.starts_with("/api/config")
|
|| path.starts_with("/api/config")
|
||||||
|| path.starts_with("/api/admin/roles")
|
|| path.starts_with("/api/admin/roles")
|
||||||
|
|| path.starts_with("/api/admin/permissions")
|
||||||
|| path.starts_with("/api/admin/onboarding-config")
|
|| path.starts_with("/api/admin/onboarding-config")
|
||||||
|| path.starts_with("/api/admin/dashboard-config")
|
|| path.starts_with("/api/admin/dashboard-config")
|
||||||
|| path.starts_with("/api/admin/users")
|
|| path.starts_with("/api/admin/users")
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,6 @@ pub mod auth;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod notifications;
|
pub mod notifications;
|
||||||
pub mod onboarding;
|
pub mod onboarding;
|
||||||
|
pub mod permissions;
|
||||||
pub mod roles;
|
pub mod roles;
|
||||||
pub mod user_roles;
|
pub mod user_roles;
|
||||||
|
|
|
||||||
79
apps/users/src/handlers/permissions.rs
Normal file
79
apps/users/src/handlers/permissions.rs
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
use crate::AppState;
|
||||||
|
use axum::{http::StatusCode, response::IntoResponse, routing::get, Json, Router};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
pub fn router() -> Router<AppState> {
|
||||||
|
Router::new().route("/", get(list_permissions))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct PermissionEntry {
|
||||||
|
key: String,
|
||||||
|
module: String,
|
||||||
|
action: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
const MODULES: &[&str] = &[
|
||||||
|
"Department Management",
|
||||||
|
"Designation Management",
|
||||||
|
"Internal Role Management",
|
||||||
|
"Employee Management",
|
||||||
|
"External Role Management",
|
||||||
|
"External Onboarding Management",
|
||||||
|
"Internal Dashboard Management",
|
||||||
|
"External Dashboard Management",
|
||||||
|
"Verification Management",
|
||||||
|
"Approval Management",
|
||||||
|
"Users Management",
|
||||||
|
"Company Management",
|
||||||
|
"Candidate Management",
|
||||||
|
"Customer Management",
|
||||||
|
"Photographer Management",
|
||||||
|
"Makeup Artist Management",
|
||||||
|
"Tutor Management",
|
||||||
|
"Developer Management",
|
||||||
|
"Fitness Trainer Management",
|
||||||
|
"Graphic Designer Management",
|
||||||
|
"Social Media Management",
|
||||||
|
"Video Editor Management",
|
||||||
|
"Catering Services Management",
|
||||||
|
"Jobs Management",
|
||||||
|
"Leads Management",
|
||||||
|
"Applications Management",
|
||||||
|
"Responses Management",
|
||||||
|
"Review Management",
|
||||||
|
"Pricing Management",
|
||||||
|
"Credit Management",
|
||||||
|
"Coupon Management",
|
||||||
|
"Discount Management",
|
||||||
|
"Tax Management",
|
||||||
|
"Order Management",
|
||||||
|
"Invoice Management",
|
||||||
|
"Ledger Management",
|
||||||
|
"Knowledge Base Management",
|
||||||
|
"Support Management",
|
||||||
|
"Report Management",
|
||||||
|
"Notifications",
|
||||||
|
];
|
||||||
|
|
||||||
|
const ACTIONS: &[&str] = &["View", "Create", "Update", "Delete"];
|
||||||
|
|
||||||
|
async fn list_permissions(
|
||||||
|
_: axum::extract::State<AppState>,
|
||||||
|
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||||
|
let permissions: Vec<PermissionEntry> = MODULES
|
||||||
|
.iter()
|
||||||
|
.flat_map(|module| {
|
||||||
|
ACTIONS.iter().map(|action| {
|
||||||
|
let key = format!("{}:{}", module.replace(' ', "_").to_lowercase(), action.to_lowercase());
|
||||||
|
PermissionEntry {
|
||||||
|
key,
|
||||||
|
module: module.to_string(),
|
||||||
|
action: action.to_string(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(Json(permissions))
|
||||||
|
}
|
||||||
|
|
@ -1,55 +1,376 @@
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, Query, State},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
routing::{get, post},
|
routing::{delete, get, patch, post},
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
use db::models::role::{CreateRolePayload, RoleRepository};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub fn router() -> Router<AppState> {
|
pub fn router() -> Router<AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(list_roles).post(create_role))
|
.route("/", get(list_roles).post(create_role))
|
||||||
.route("/{key}", get(get_role_by_key))
|
.route("/:id", get(get_role).patch(update_role).delete(delete_role))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Query params ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct ListQuery {
|
||||||
|
audience: Option<String>,
|
||||||
|
q: Option<String>,
|
||||||
|
page: Option<i64>,
|
||||||
|
per_page: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Response types ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct RoleRow {
|
||||||
|
id: Uuid,
|
||||||
|
key: String,
|
||||||
|
name: String,
|
||||||
|
audience: String,
|
||||||
|
description: Option<String>,
|
||||||
|
department_id: Option<Uuid>,
|
||||||
|
department_name: Option<String>,
|
||||||
|
is_active: bool,
|
||||||
|
can_approve_requests: bool,
|
||||||
|
can_manage_system_settings: bool,
|
||||||
|
users_assigned: i64,
|
||||||
|
permissions_count: i64,
|
||||||
|
created_at: chrono::DateTime<chrono::Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct ListResponse {
|
||||||
|
roles: Vec<RoleRow>,
|
||||||
|
total: i64,
|
||||||
|
page: i64,
|
||||||
|
per_page: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct RoleDetail {
|
||||||
|
id: Uuid,
|
||||||
|
key: String,
|
||||||
|
name: String,
|
||||||
|
audience: String,
|
||||||
|
description: Option<String>,
|
||||||
|
department_id: Option<Uuid>,
|
||||||
|
department_name: Option<String>,
|
||||||
|
is_active: bool,
|
||||||
|
can_approve_requests: bool,
|
||||||
|
can_manage_system_settings: bool,
|
||||||
|
permission_keys: Vec<String>,
|
||||||
|
created_at: chrono::DateTime<chrono::Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Request types ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct CreateRolePayload {
|
||||||
|
key: String,
|
||||||
|
name: String,
|
||||||
|
audience: String,
|
||||||
|
description: Option<String>,
|
||||||
|
department_id: Option<Uuid>,
|
||||||
|
is_active: Option<bool>,
|
||||||
|
can_approve_requests: Option<bool>,
|
||||||
|
can_manage_system_settings: Option<bool>,
|
||||||
|
permission_keys: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct UpdateRolePayload {
|
||||||
|
name: Option<String>,
|
||||||
|
description: Option<String>,
|
||||||
|
department_id: Option<Uuid>,
|
||||||
|
is_active: Option<bool>,
|
||||||
|
can_approve_requests: Option<bool>,
|
||||||
|
can_manage_system_settings: Option<bool>,
|
||||||
|
permission_keys: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Handlers ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async fn list_roles(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Query(params): Query<ListQuery>,
|
||||||
|
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||||
|
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_id = r.id
|
||||||
|
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<AppState>,
|
||||||
|
Path(id): Path<Uuid>,
|
||||||
|
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||||
|
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<String> = 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(
|
async fn create_role(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(payload): Json<CreateRolePayload>,
|
Json(payload): Json<CreateRolePayload>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||||
match RoleRepository::create(&state.pool, payload).await {
|
let is_active = payload.is_active.unwrap_or(true);
|
||||||
Ok(role) => Ok((StatusCode::CREATED, Json(role))),
|
let can_approve = payload.can_approve_requests.unwrap_or(false);
|
||||||
Err(e) => Err((
|
let can_manage = payload.can_manage_system_settings.unwrap_or(false);
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
format!("Database error: {}", e),
|
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}")))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_roles(
|
let permission_keys: Vec<String> = 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<AppState>,
|
State(state): State<AppState>,
|
||||||
|
Path(id): Path<Uuid>,
|
||||||
|
Json(payload): Json<UpdateRolePayload>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||||
match RoleRepository::get_all(&state.pool).await {
|
// Fetch current values first
|
||||||
Ok(roles) => Ok((StatusCode::OK, Json(roles))),
|
let current = sqlx::query!(
|
||||||
Err(e) => Err((
|
"SELECT name, description, department_id, is_active, can_approve_requests, can_manage_system_settings FROM roles WHERE id = $1",
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
id
|
||||||
format!("Database error: {}", e),
|
)
|
||||||
)),
|
.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,
|
||||||
|
description,
|
||||||
|
department_id,
|
||||||
|
is_active,
|
||||||
|
can_approve,
|
||||||
|
can_manage,
|
||||||
|
id
|
||||||
|
)
|
||||||
|
.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}")))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_role_by_key(
|
// Return updated role
|
||||||
|
get_role(State(state), Path(id)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_role(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(key): Path<String>,
|
Path(id): Path<Uuid>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||||
match RoleRepository::get_by_key(&state.pool, &key).await {
|
let result = sqlx::query!("DELETE FROM roles WHERE id = $1", id)
|
||||||
Ok(role) => Ok((StatusCode::OK, Json(role))),
|
.execute(&state.pool)
|
||||||
Err(sqlx::Error::RowNotFound) => Err((StatusCode::NOT_FOUND, "Role not found".to_string())),
|
.await
|
||||||
Err(e) => Err((
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
format!("Database error: {}", e),
|
if result.rows_affected() == 0 {
|
||||||
)),
|
return Err((StatusCode::NOT_FOUND, "Role not found".to_string()));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(StatusCode::NO_CONTENT)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ async fn main() {
|
||||||
.nest("/api/auth", handlers::auth::router())
|
.nest("/api/auth", handlers::auth::router())
|
||||||
// ── Roles & User Self-Service ─────────────────────────────────────
|
// ── Roles & User Self-Service ─────────────────────────────────────
|
||||||
.nest("/api/admin/roles", handlers::roles::router())
|
.nest("/api/admin/roles", handlers::roles::router())
|
||||||
|
.nest("/api/admin/permissions", handlers::permissions::router())
|
||||||
.nest("/api/me/roles", handlers::user_roles::router())
|
.nest("/api/me/roles", handlers::user_roles::router())
|
||||||
// ── Notifications ─────────────────────────────────────────────────
|
// ── Notifications ─────────────────────────────────────────────────
|
||||||
.nest("/api/me/notifications", handlers::notifications::router())
|
.nest("/api/me/notifications", handlers::notifications::router())
|
||||||
|
|
|
||||||
|
|
@ -134,9 +134,16 @@ pub fn require_role(auth: &AuthUser, expected_role: &str) -> Result<(), AuthErro
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns Ok if the user has the ADMIN role.
|
/// Returns Ok if the user has an internal admin role (ADMIN or SUPER_ADMIN).
|
||||||
pub fn require_admin(auth: &AuthUser) -> Result<(), AuthError> {
|
pub fn require_admin(auth: &AuthUser) -> Result<(), AuthError> {
|
||||||
if auth.claims.roles.contains(&"ADMIN".to_string()) {
|
let active = auth.claims.active_role.as_str();
|
||||||
|
let has_internal_admin =
|
||||||
|
active == "ADMIN"
|
||||||
|
|| active == "SUPER_ADMIN"
|
||||||
|
|| auth.claims.roles.contains(&"ADMIN".to_string())
|
||||||
|
|| auth.claims.roles.contains(&"SUPER_ADMIN".to_string());
|
||||||
|
|
||||||
|
if has_internal_admin {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(AuthError::InsufficientPermissions)
|
Err(AuthError::InsufficientPermissions)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
ALTER TABLE roles
|
||||||
|
DROP COLUMN IF EXISTS description,
|
||||||
|
DROP COLUMN IF EXISTS department_id,
|
||||||
|
DROP COLUMN IF EXISTS can_approve_requests,
|
||||||
|
DROP COLUMN IF EXISTS can_manage_system_settings;
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
-- Extend roles table for internal role management
|
||||||
|
ALTER TABLE roles
|
||||||
|
ADD COLUMN IF NOT EXISTS description TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS department_id UUID REFERENCES departments(id) ON DELETE SET NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS can_approve_requests BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
ADD COLUMN IF NOT EXISTS can_manage_system_settings BOOLEAN NOT NULL DEFAULT false;
|
||||||
Loading…
Add table
Reference in a new issue