Fix role/config schema alignment and external dashboard runtime loading
This commit is contained in:
parent
2a65d79aea
commit
92ded2b43d
4 changed files with 122 additions and 47 deletions
|
|
@ -157,6 +157,55 @@ fn resolve_signup_role_candidates(intent: Option<&str>, profession: Option<&str>
|
|||
vec!["JOB_SEEKER".to_string()]
|
||||
}
|
||||
|
||||
fn role_display_name_from_code(code: &str) -> String {
|
||||
code
|
||||
.split('_')
|
||||
.filter(|part| !part.is_empty())
|
||||
.map(|part| {
|
||||
let lower = part.to_lowercase();
|
||||
let mut chars = lower.chars();
|
||||
match chars.next() {
|
||||
Some(first) => format!("{}{}", first.to_uppercase(), chars.collect::<String>()),
|
||||
None => String::new(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
async fn ensure_role_exists(pool: &sqlx::PgPool, role_code: &str) -> Option<Uuid> {
|
||||
let normalized = normalize_role_key(role_code);
|
||||
if normalized.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Ok(found) = sqlx::query_scalar::<_, Uuid>("SELECT id FROM roles WHERE code = $1")
|
||||
.bind(&normalized)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
{
|
||||
if found.is_some() {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
let display_name = role_display_name_from_code(&normalized);
|
||||
sqlx::query_scalar::<_, Uuid>(
|
||||
r#"
|
||||
INSERT INTO roles (name, code, audience, is_active)
|
||||
VALUES ($1, $2, 'USER', true)
|
||||
ON CONFLICT (code)
|
||||
DO UPDATE SET updated_at = NOW()
|
||||
RETURNING id
|
||||
"#,
|
||||
)
|
||||
.bind(display_name)
|
||||
.bind(normalized)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
// ── Handlers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/// POST /api/auth/check-email
|
||||
|
|
@ -175,11 +224,22 @@ async fn check_email(
|
|||
);
|
||||
}
|
||||
|
||||
let exists = UserRepository::get_by_email(&state.pool, &email).await.is_ok();
|
||||
let user = UserRepository::get_by_email(&state.pool, &email).await.ok();
|
||||
let exists = user.is_some();
|
||||
let roles = if let Some(ref found_user) = user {
|
||||
UserRepository::get_user_role_keys(&state.pool, found_user.id)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let active_role = roles.first().cloned();
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"exists": exists
|
||||
"exists": exists,
|
||||
"active_role": active_role,
|
||||
"roles": roles,
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
|
@ -234,20 +294,27 @@ async fn register(
|
|||
payload.profession.as_deref(),
|
||||
);
|
||||
for role_key in role_candidates {
|
||||
let role = sqlx::query_scalar::<_, Uuid>("SELECT id FROM roles WHERE key = $1")
|
||||
.bind(&role_key)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
if let Some(role_id) = role {
|
||||
let role_id = ensure_role_exists(&state.pool, &role_key).await;
|
||||
if let Some(role_id) = role_id {
|
||||
let _ = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO user_roles (user_id, role_id, status, approved_at)
|
||||
VALUES ($1, $2, 'APPROVED', NOW())
|
||||
ON CONFLICT (user_id, role_id)
|
||||
DO UPDATE SET status = 'APPROVED', approved_at = NOW()
|
||||
UPDATE user_roles
|
||||
SET status = 'APPROVED'
|
||||
WHERE user_id = $1 AND role_id = $2
|
||||
"#,
|
||||
)
|
||||
.bind(user.id)
|
||||
.bind(role_id)
|
||||
.execute(&state.pool)
|
||||
.await;
|
||||
|
||||
let _ = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO user_roles (user_id, role_id, status)
|
||||
SELECT $1, $2, 'APPROVED'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM user_roles WHERE user_id = $1 AND role_id = $2
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.bind(user.id)
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ async fn list_roles(
|
|||
r#"
|
||||
SELECT
|
||||
r.id,
|
||||
r.key,
|
||||
r.code AS key,
|
||||
r.name,
|
||||
r.audience,
|
||||
r.description,
|
||||
|
|
@ -182,10 +182,10 @@ async fn list_roles(
|
|||
COUNT(DISTINCT rp.id) AS permissions_count
|
||||
FROM roles r
|
||||
LEFT JOIN departments d ON d.id = r.department_id
|
||||
LEFT JOIN employees e ON e.role_code = r.key
|
||||
LEFT JOIN employees e ON e.role_code = r.code
|
||||
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 || '%')
|
||||
AND ($2 = '' OR LOWER(r.name) LIKE '%' || $2 || '%' OR LOWER(r.code) LIKE '%' || $2 || '%')
|
||||
GROUP BY r.id, d.name
|
||||
ORDER BY r.created_at DESC
|
||||
LIMIT $3 OFFSET $4
|
||||
|
|
@ -203,7 +203,7 @@ async fn list_roles(
|
|||
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 || '%')
|
||||
AND ($2 = '' OR LOWER(r.name) LIKE '%' || $2 || '%' OR LOWER(r.code) LIKE '%' || $2 || '%')
|
||||
"#,
|
||||
)
|
||||
.bind(&audience)
|
||||
|
|
@ -241,7 +241,7 @@ async fn get_role(
|
|||
let row = sqlx::query_as::<_, RoleDetailRow>(
|
||||
r#"
|
||||
SELECT
|
||||
r.id, r.key, r.name, r.audience, r.description,
|
||||
r.id, r.code AS 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
|
||||
|
|
@ -290,9 +290,9 @@ async fn create_role(
|
|||
|
||||
let role = sqlx::query_as::<_, InsertedRoleRow>(
|
||||
r#"
|
||||
INSERT INTO roles (key, name, audience, description, department_id, is_active, can_approve_requests, can_manage_system_settings)
|
||||
INSERT INTO roles (code, 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
|
||||
RETURNING id, code AS key, name, audience, description, department_id, is_active, can_approve_requests, can_manage_system_settings, created_at
|
||||
"#,
|
||||
)
|
||||
.bind(&payload.key)
|
||||
|
|
|
|||
|
|
@ -32,10 +32,9 @@ pub struct CreateOnboardingConfigPayload {
|
|||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
pub struct DashboardConfigListItem {
|
||||
pub id: Uuid,
|
||||
pub role_id: Uuid,
|
||||
pub role_key: String,
|
||||
pub audience: String,
|
||||
pub version: i32,
|
||||
pub config_json: serde_json::Value,
|
||||
pub is_active: bool,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
|
@ -43,10 +42,9 @@ pub struct DashboardConfigListItem {
|
|||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
pub struct DashboardConfig {
|
||||
pub id: Uuid,
|
||||
pub role_id: Uuid,
|
||||
pub role_key: String,
|
||||
pub audience: String,
|
||||
pub config_json: serde_json::Value,
|
||||
pub version: i32,
|
||||
pub is_active: bool,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
|
@ -174,15 +172,22 @@ impl ConfigRepository {
|
|||
pool: &PgPool,
|
||||
payload: CreateDashboardConfigPayload,
|
||||
) -> Result<DashboardConfig, sqlx::Error> {
|
||||
let role_key = sqlx::query_scalar::<_, String>(
|
||||
"SELECT code FROM roles WHERE id = $1",
|
||||
)
|
||||
.bind(payload.role_id)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
// Soft-disable previous active configs for this role
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE dashboard_configs
|
||||
SET is_active = false
|
||||
WHERE role_id = $1 AND audience = $2::text AND is_active = true
|
||||
WHERE UPPER(role_key) = UPPER($1) AND audience = $2::text AND is_active = true
|
||||
"#,
|
||||
)
|
||||
.bind(payload.role_id)
|
||||
.bind(&role_key)
|
||||
.bind(&payload.audience)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
|
@ -190,18 +195,17 @@ impl ConfigRepository {
|
|||
// Insert new config
|
||||
let config = sqlx::query_as::<_, DashboardConfig>(
|
||||
r#"
|
||||
INSERT INTO dashboard_configs (role_id, audience, config_json, version, is_active)
|
||||
INSERT INTO dashboard_configs (role_key, audience, widgets, is_active)
|
||||
VALUES (
|
||||
$1,
|
||||
$2::text,
|
||||
$3,
|
||||
COALESCE((SELECT MAX(version) FROM dashboard_configs WHERE role_id = $1 AND audience = $2::text), 0) + 1,
|
||||
true
|
||||
)
|
||||
RETURNING id, role_id, audience, config_json, version, is_active, updated_at
|
||||
RETURNING id, role_key, audience, widgets as config_json, is_active, updated_at
|
||||
"#,
|
||||
)
|
||||
.bind(payload.role_id)
|
||||
.bind(&role_key)
|
||||
.bind(&payload.audience)
|
||||
.bind(payload.config_json)
|
||||
.fetch_one(pool)
|
||||
|
|
@ -215,14 +219,21 @@ impl ConfigRepository {
|
|||
role_id: Uuid,
|
||||
audience: &str,
|
||||
) -> Result<DashboardConfig, sqlx::Error> {
|
||||
let config = sqlx::query_as::<_, DashboardConfig>(
|
||||
r#"
|
||||
SELECT id, role_id, audience, config_json, version, is_active, updated_at
|
||||
FROM dashboard_configs
|
||||
WHERE role_id = $1 AND audience = $2 AND is_active = true
|
||||
"#,
|
||||
let role_key = sqlx::query_scalar::<_, String>(
|
||||
"SELECT code FROM roles WHERE id = $1",
|
||||
)
|
||||
.bind(role_id)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
let config = sqlx::query_as::<_, DashboardConfig>(
|
||||
r#"
|
||||
SELECT id, role_key, audience, widgets as config_json, is_active, updated_at
|
||||
FROM dashboard_configs
|
||||
WHERE UPPER(role_key) = UPPER($1) AND audience = $2 AND is_active = true
|
||||
"#,
|
||||
)
|
||||
.bind(role_key)
|
||||
.bind(audience)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
|
@ -236,10 +247,8 @@ impl ConfigRepository {
|
|||
let configs = sqlx::query_as::<_, DashboardConfigListItem>(
|
||||
r#"
|
||||
SELECT
|
||||
c.id, c.role_id, r.key as role_key, c.audience,
|
||||
c.version, c.is_active, c.updated_at
|
||||
c.id, c.role_key, c.audience, c.widgets as config_json, c.is_active, c.updated_at
|
||||
FROM dashboard_configs c
|
||||
JOIN roles r ON c.role_id = r.id
|
||||
ORDER BY c.updated_at DESC
|
||||
"#,
|
||||
)
|
||||
|
|
@ -256,13 +265,12 @@ impl ConfigRepository {
|
|||
) -> Result<DashboardConfig, sqlx::Error> {
|
||||
let config = sqlx::query_as::<_, DashboardConfig>(
|
||||
r#"
|
||||
SELECT c.id, c.role_id, c.audience, c.config_json, c.version, c.is_active, c.updated_at
|
||||
SELECT c.id, c.role_key, c.audience, c.widgets as config_json, c.is_active, c.updated_at
|
||||
FROM dashboard_configs c
|
||||
JOIN roles r ON c.role_id = r.id
|
||||
WHERE r.key = $1 AND c.audience = $2 AND c.is_active = true
|
||||
WHERE UPPER(c.role_key) = UPPER($1) AND c.audience = $2 AND c.is_active = true
|
||||
"#,
|
||||
)
|
||||
.bind(role_key.to_uppercase())
|
||||
.bind(role_key)
|
||||
.bind(audience)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
|
|
|||
|
|
@ -102,15 +102,15 @@ impl UserRepository {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Returns all approved role keys for a user (e.g. ["COMPANY", "JOB_SEEKER"])
|
||||
/// Returns all approved role codes for a user (e.g. ["COMPANY", "DEVELOPER"])
|
||||
pub async fn get_user_role_keys(pool: &PgPool, user_id: Uuid) -> Result<Vec<String>, sqlx::Error> {
|
||||
let rows = sqlx::query_scalar::<_, String>(
|
||||
r#"
|
||||
SELECT r.key
|
||||
SELECT r.code
|
||||
FROM user_roles ur
|
||||
JOIN roles r ON ur.role_id = r.id
|
||||
WHERE ur.user_id = $1 AND ur.status = 'APPROVED'
|
||||
ORDER BY ur.approved_at ASC
|
||||
ORDER BY ur.created_at ASC
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue