2026-03-17 20:42:51 +01:00
|
|
|
use crate::AppState;
|
|
|
|
|
use axum::{
|
|
|
|
|
extract::{Path, Query, State},
|
|
|
|
|
http::StatusCode,
|
|
|
|
|
response::IntoResponse,
|
|
|
|
|
routing::{get, post},
|
|
|
|
|
Json, Router,
|
|
|
|
|
};
|
2026-03-27 21:25:31 +01:00
|
|
|
use contracts::auth_middleware::{AuthUser, require_admin};
|
2026-03-17 20:42:51 +01:00
|
|
|
use db::models::config::{
|
|
|
|
|
ConfigRepository, CreateDashboardConfigPayload, CreateOnboardingConfigPayload,
|
|
|
|
|
CreateRuntimeConfigPayload,
|
|
|
|
|
};
|
feat: commit remaining service files, migrations, and model updates
- gateway, companies, customers, job_seekers apps updated
- users config/mod/mail handlers
- auth middleware and jwt crate updates
- db models: user, config, mod updates
- all remaining migrations: portfolio, notifications, reviews, kb, support, coupons, onboarding states
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 22:59:47 +01:00
|
|
|
use db::models::user::UserRepository;
|
2026-03-17 20:42:51 +01:00
|
|
|
use serde::Deserialize;
|
2026-03-27 21:34:28 +01:00
|
|
|
use std::collections::{BTreeMap, BTreeSet};
|
2026-03-17 20:42:51 +01:00
|
|
|
use uuid::Uuid;
|
|
|
|
|
|
|
|
|
|
pub fn onboarding_router() -> Router<AppState> {
|
|
|
|
|
Router::new()
|
|
|
|
|
.route("/", get(list_onboarding_configs).post(create_onboarding_config))
|
2026-03-22 15:55:29 +01:00
|
|
|
.route("/{role_id}", get(get_active_onboarding_config))
|
|
|
|
|
.route("/by-key/{role_key}", get(get_onboarding_config_by_key))
|
2026-03-17 20:42:51 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn dashboard_router() -> Router<AppState> {
|
|
|
|
|
Router::new()
|
|
|
|
|
.route("/", get(list_dashboard_configs).post(create_dashboard_config))
|
2026-03-22 15:55:29 +01:00
|
|
|
.route("/{role_id}", get(get_active_dashboard_config))
|
|
|
|
|
.route("/by-key/{role_key}", get(get_dashboard_config_by_key))
|
2026-03-17 20:42:51 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn runtime_router() -> Router<AppState> {
|
|
|
|
|
Router::new()
|
|
|
|
|
.route("/", get(get_my_runtime_config).post(create_runtime_config))
|
2026-03-22 15:55:29 +01:00
|
|
|
.route("/{role_id}", get(get_active_runtime_config))
|
2026-03-17 20:42:51 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-27 21:25:31 +01:00
|
|
|
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()))
|
|
|
|
|
}
|
2026-03-17 20:42:51 +01:00
|
|
|
async fn get_my_runtime_config(
|
|
|
|
|
auth: contracts::auth_middleware::AuthUser,
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
2026-03-27 21:34:28 +01:00
|
|
|
let role_key = auth.claims.active_role.clone().to_uppercase();
|
feat: commit remaining service files, migrations, and model updates
- gateway, companies, customers, job_seekers apps updated
- users config/mod/mail handlers
- auth middleware and jwt crate updates
- db models: user, config, mod updates
- all remaining migrations: portfolio, notifications, reviews, kb, support, coupons, onboarding states
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 22:59:47 +01:00
|
|
|
|
2026-03-27 21:34:28 +01:00
|
|
|
let role = sqlx::query!("SELECT id, key, audience FROM roles WHERE key = $1", role_key)
|
|
|
|
|
.fetch_optional(&state.pool)
|
feat: commit remaining service files, migrations, and model updates
- gateway, companies, customers, job_seekers apps updated
- users config/mod/mail handlers
- auth middleware and jwt crate updates
- db models: user, config, mod updates
- all remaining migrations: portfolio, notifications, reviews, kb, support, coupons, onboarding states
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 22:59:47 +01:00
|
|
|
.await
|
2026-03-27 21:34:28 +01:00
|
|
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)))?
|
|
|
|
|
.ok_or((StatusCode::NOT_FOUND, format!("Role '{}' not found", role_key)))?;
|
|
|
|
|
|
|
|
|
|
let config = match ConfigRepository::get_active_runtime_by_role_key(&state.pool, &role_key).await {
|
|
|
|
|
Ok(c) => Some(c),
|
|
|
|
|
Err(sqlx::Error::RowNotFound) => None,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
return Err((
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
format!("Database error: {}", e),
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
};
|
feat: commit remaining service files, migrations, and model updates
- gateway, companies, customers, job_seekers apps updated
- users config/mod/mail handlers
- auth middleware and jwt crate updates
- db models: user, config, mod updates
- all remaining migrations: portfolio, notifications, reviews, kb, support, coupons, onboarding states
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 22:59:47 +01:00
|
|
|
|
|
|
|
|
// Fetch live user data to merge into the response so the `user` field is always fresh.
|
|
|
|
|
let user = UserRepository::get_by_id(&state.pool, auth.user_id)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|_| (StatusCode::UNAUTHORIZED, "User not found".to_string()))?;
|
|
|
|
|
|
|
|
|
|
let roles = UserRepository::get_user_role_keys(&state.pool, auth.user_id)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
|
|
|
|
// Merge the stored config_json with live user data.
|
|
|
|
|
// `config_json` holds role/modules/flags/permissions; we add the `user` sub-object.
|
2026-03-27 21:34:28 +01:00
|
|
|
let mut response = config
|
|
|
|
|
.map(|c| c.config_json)
|
|
|
|
|
.unwrap_or_else(|| serde_json::json!({}));
|
|
|
|
|
if !response.is_object() {
|
|
|
|
|
response = serde_json::json!({});
|
|
|
|
|
}
|
feat: commit remaining service files, migrations, and model updates
- gateway, companies, customers, job_seekers apps updated
- users config/mod/mail handlers
- auth middleware and jwt crate updates
- db models: user, config, mod updates
- all remaining migrations: portfolio, notifications, reviews, kb, support, coupons, onboarding states
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 22:59:47 +01:00
|
|
|
if let Some(obj) = response.as_object_mut() {
|
|
|
|
|
obj.insert(
|
|
|
|
|
"user".to_string(),
|
|
|
|
|
serde_json::json!({
|
|
|
|
|
"id": user.id.to_string(),
|
|
|
|
|
"full_name": user.full_name.unwrap_or_default(),
|
|
|
|
|
"email": user.email,
|
|
|
|
|
"roles": roles,
|
|
|
|
|
"active_role": role_key,
|
|
|
|
|
}),
|
|
|
|
|
);
|
2026-03-27 21:34:28 +01:00
|
|
|
obj.insert("role".to_string(), serde_json::Value::String(role_key.clone()));
|
|
|
|
|
obj.insert("audience".to_string(), serde_json::Value::String(role.audience.clone()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if role.audience == "INTERNAL" {
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
let mut per_module: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
|
|
|
|
|
for key in permission_keys {
|
|
|
|
|
let key = key.trim().to_uppercase();
|
2026-03-30 04:52:27 +02:00
|
|
|
if key.is_empty() {
|
2026-03-27 21:34:28 +01:00
|
|
|
continue;
|
|
|
|
|
}
|
2026-03-30 04:52:27 +02:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
2026-03-27 21:34:28 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(obj) = response.as_object_mut() {
|
|
|
|
|
let enabled_modules = per_module.keys().cloned().collect::<Vec<_>>();
|
|
|
|
|
obj.insert("enabled_modules".to_string(), serde_json::json!(enabled_modules));
|
|
|
|
|
|
|
|
|
|
let mut permissions_obj = serde_json::Map::new();
|
|
|
|
|
for (module, actions) in per_module {
|
|
|
|
|
permissions_obj.insert(module, serde_json::json!(actions.into_iter().collect::<Vec<_>>()));
|
|
|
|
|
}
|
|
|
|
|
obj.insert("permissions".to_string(), serde_json::Value::Object(permissions_obj));
|
|
|
|
|
}
|
2026-03-17 20:42:51 +01:00
|
|
|
}
|
feat: commit remaining service files, migrations, and model updates
- gateway, companies, customers, job_seekers apps updated
- users config/mod/mail handlers
- auth middleware and jwt crate updates
- db models: user, config, mod updates
- all remaining migrations: portfolio, notifications, reviews, kb, support, coupons, onboarding states
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 22:59:47 +01:00
|
|
|
|
|
|
|
|
Ok((StatusCode::OK, Json(response)))
|
2026-03-17 20:42:51 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-30 04:52:27 +02:00
|
|
|
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()))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 20:42:51 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
async fn create_onboarding_config(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Json(payload): Json<CreateOnboardingConfigPayload>,
|
|
|
|
|
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
|
|
|
|
match ConfigRepository::create_onboarding_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 get_active_onboarding_config(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path(role_id): Path<Uuid>,
|
|
|
|
|
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
|
|
|
|
match ConfigRepository::get_active_onboarding_config(&state.pool, role_id).await {
|
|
|
|
|
Ok(config) => Ok((StatusCode::OK, Json(config))),
|
|
|
|
|
Err(sqlx::Error::RowNotFound) => Err((
|
|
|
|
|
StatusCode::NOT_FOUND,
|
|
|
|
|
"Active onboarding config not found".to_string(),
|
|
|
|
|
)),
|
|
|
|
|
Err(e) => Err((
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
format!("Database error: {}", e),
|
|
|
|
|
)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn list_onboarding_configs(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
|
|
|
|
match ConfigRepository::get_all_onboarding_configs(&state.pool).await {
|
|
|
|
|
Ok(configs) => Ok((StatusCode::OK, Json(configs))),
|
|
|
|
|
Err(e) => Err((
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
format!("Database error: {}", e),
|
|
|
|
|
)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn create_dashboard_config(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Json(payload): Json<CreateDashboardConfigPayload>,
|
|
|
|
|
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
|
|
|
|
match ConfigRepository::create_dashboard_config(&state.pool, payload).await {
|
|
|
|
|
Ok(config) => Ok((StatusCode::CREATED, Json(config))),
|
|
|
|
|
Err(e) => Err((
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
format!("Database error: {}", e),
|
|
|
|
|
)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
struct DashboardQuery {
|
|
|
|
|
audience: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn get_active_dashboard_config(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path(role_id): Path<Uuid>,
|
|
|
|
|
Query(query): Query<DashboardQuery>,
|
|
|
|
|
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
|
|
|
|
match ConfigRepository::get_active_dashboard_config(&state.pool, role_id, &query.audience).await {
|
|
|
|
|
Ok(config) => Ok((StatusCode::OK, Json(config))),
|
|
|
|
|
Err(sqlx::Error::RowNotFound) => Err((
|
|
|
|
|
StatusCode::NOT_FOUND,
|
|
|
|
|
"Active dashboard config not found".to_string(),
|
|
|
|
|
)),
|
|
|
|
|
Err(e) => Err((
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
format!("Database error: {}", e),
|
|
|
|
|
)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn list_dashboard_configs(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
|
|
|
|
match ConfigRepository::get_all_dashboard_configs(&state.pool).await {
|
|
|
|
|
Ok(configs) => Ok((StatusCode::OK, Json(configs))),
|
|
|
|
|
Err(e) => Err((
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
format!("Database error: {}", e),
|
|
|
|
|
)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn create_runtime_config(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Json(payload): Json<CreateRuntimeConfigPayload>,
|
|
|
|
|
) -> Result<impl IntoResponse, (StatusCode, 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 get_active_runtime_config(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path(role_id): Path<Uuid>,
|
|
|
|
|
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
|
|
|
|
match ConfigRepository::get_active_runtime_config(&state.pool, role_id).await {
|
|
|
|
|
Ok(config) => Ok((StatusCode::OK, Json(config))),
|
|
|
|
|
Err(sqlx::Error::RowNotFound) => Err((
|
|
|
|
|
StatusCode::NOT_FOUND,
|
|
|
|
|
"Active runtime config not found".to_string(),
|
|
|
|
|
)),
|
|
|
|
|
Err(e) => Err((
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
format!("Database error: {}", e),
|
|
|
|
|
)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn get_onboarding_config_by_key(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path(role_key): Path<String>,
|
|
|
|
|
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
|
|
|
|
match ConfigRepository::get_active_onboarding_by_role_key(&state.pool, &role_key).await {
|
|
|
|
|
Ok(config) => Ok((StatusCode::OK, Json(config))),
|
|
|
|
|
Err(sqlx::Error::RowNotFound) => Err((
|
|
|
|
|
StatusCode::NOT_FOUND,
|
|
|
|
|
format!("Active onboarding config for role '{}' not found", role_key),
|
|
|
|
|
)),
|
|
|
|
|
Err(e) => Err((
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
format!("Database error: {}", e),
|
|
|
|
|
)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn get_dashboard_config_by_key(
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path(role_key): Path<String>,
|
|
|
|
|
Query(query): Query<DashboardQuery>,
|
|
|
|
|
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
|
|
|
|
match ConfigRepository::get_active_dashboard_by_role_key(&state.pool, &role_key, &query.audience).await {
|
|
|
|
|
Ok(config) => Ok((StatusCode::OK, Json(config))),
|
|
|
|
|
Err(sqlx::Error::RowNotFound) => Err((
|
|
|
|
|
StatusCode::NOT_FOUND,
|
|
|
|
|
format!("Active dashboard config for role '{}' not found", role_key),
|
|
|
|
|
)),
|
|
|
|
|
Err(e) => Err((
|
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
format!("Database error: {}", e),
|
|
|
|
|
)),
|
|
|
|
|
}
|
|
|
|
|
}
|