feat(runtime-config): derive enabled modules from internal role permissions
This commit is contained in:
parent
7dec3e85fb
commit
b602c8df53
4 changed files with 64 additions and 15 deletions
|
|
@ -13,6 +13,7 @@ use db::models::config::{
|
||||||
};
|
};
|
||||||
use db::models::user::UserRepository;
|
use db::models::user::UserRepository;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub fn onboarding_router() -> Router<AppState> {
|
pub fn onboarding_router() -> Router<AppState> {
|
||||||
|
|
@ -217,17 +218,24 @@ async fn get_my_runtime_config(
|
||||||
auth: contracts::auth_middleware::AuthUser,
|
auth: contracts::auth_middleware::AuthUser,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||||
let role_key = auth.claims.active_role.clone();
|
let role_key = auth.claims.active_role.clone().to_uppercase();
|
||||||
|
|
||||||
let config = ConfigRepository::get_active_runtime_by_role_key(&state.pool, &role_key)
|
let role = sqlx::query!("SELECT id, key, audience FROM roles WHERE key = $1", role_key)
|
||||||
|
.fetch_optional(&state.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| match e {
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)))?
|
||||||
sqlx::Error::RowNotFound => (
|
.ok_or((StatusCode::NOT_FOUND, format!("Role '{}' not found", role_key)))?;
|
||||||
StatusCode::NOT_FOUND,
|
|
||||||
format!("No runtime config found for role {}", role_key),
|
let config = match ConfigRepository::get_active_runtime_by_role_key(&state.pool, &role_key).await {
|
||||||
),
|
Ok(c) => Some(c),
|
||||||
e => (StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {}", e)),
|
Err(sqlx::Error::RowNotFound) => None,
|
||||||
})?;
|
Err(e) => {
|
||||||
|
return Err((
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("Database error: {}", e),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Fetch live user data to merge into the response so the `user` field is always fresh.
|
// 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)
|
let user = UserRepository::get_by_id(&state.pool, auth.user_id)
|
||||||
|
|
@ -240,7 +248,12 @@ async fn get_my_runtime_config(
|
||||||
|
|
||||||
// Merge the stored config_json with live user data.
|
// Merge the stored config_json with live user data.
|
||||||
// `config_json` holds role/modules/flags/permissions; we add the `user` sub-object.
|
// `config_json` holds role/modules/flags/permissions; we add the `user` sub-object.
|
||||||
let mut response = config.config_json.clone();
|
let mut response = config
|
||||||
|
.map(|c| c.config_json)
|
||||||
|
.unwrap_or_else(|| serde_json::json!({}));
|
||||||
|
if !response.is_object() {
|
||||||
|
response = serde_json::json!({});
|
||||||
|
}
|
||||||
if let Some(obj) = response.as_object_mut() {
|
if let Some(obj) = response.as_object_mut() {
|
||||||
obj.insert(
|
obj.insert(
|
||||||
"user".to_string(),
|
"user".to_string(),
|
||||||
|
|
@ -252,8 +265,44 @@ async fn get_my_runtime_config(
|
||||||
"active_role": role_key,
|
"active_role": role_key,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
// Ensure role is always set from the JWT active_role (authoritative)
|
obj.insert("role".to_string(), serde_json::Value::String(role_key.clone()));
|
||||||
obj.insert("role".to_string(), serde_json::Value::String(role_key));
|
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();
|
||||||
|
let parts: Vec<&str> = key.split('_').collect();
|
||||||
|
if parts.len() < 2 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let action = parts[parts.len() - 1].to_string();
|
||||||
|
let module = parts[..parts.len() - 1].join("_");
|
||||||
|
if module.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
per_module.entry(module).or_default().insert(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((StatusCode::OK, Json(response)))
|
Ok((StatusCode::OK, Json(response)))
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use axum::{
|
||||||
extract::{Path, Query, State},
|
extract::{Path, Query, State},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
routing::{delete, get, post, patch},
|
routing::get,
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
use contracts::auth_middleware::{AuthUser, require_admin};
|
use contracts::auth_middleware::{AuthUser, require_admin};
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use axum::{
|
||||||
extract::{Path, Query, State},
|
extract::{Path, Query, State},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
routing::{delete, get, post, put},
|
routing::get,
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use axum::{
|
||||||
extract::{Path, Query, State},
|
extract::{Path, Query, State},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
routing::{delete, get, patch, post},
|
routing::get,
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue