chore: checkpoint workspace updates
This commit is contained in:
parent
1ac60f9756
commit
5946bfe3a8
36 changed files with 2195 additions and 234 deletions
|
|
@ -1,40 +0,0 @@
|
|||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT r.key, r.name, ur.status, ur.approved_at\n FROM user_roles ur\n INNER JOIN roles r ON r.id = ur.role_id\n WHERE ur.user_id = $1\n ORDER BY ur.created_at ASC\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "key",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "status",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "approved_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "f479b3c6088810c02b09611eb2bc7b2b88d241b9aac76dd228fd9286c158dd77"
|
||||
}
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -1203,6 +1203,8 @@ dependencies = [
|
|||
"anyhow",
|
||||
"chrono",
|
||||
"lettre",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -3,18 +3,21 @@ use axum::{
|
|||
extract::{Path, Query, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
routing::{get, post, patch},
|
||||
Json, Router,
|
||||
};
|
||||
use contracts::auth_middleware::{AuthUser, require_admin};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
use db::models::employee::{EmployeeRepository, CreateEmployeePayload};
|
||||
use auth::crypto::hash_password;
|
||||
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", get(list_employees).post(create_employee))
|
||||
.route("/provision", post(provision_employee))
|
||||
.route("/{id}", get(get_employee).patch(update_employee).delete(delete_employee))
|
||||
.route("/{id}/change-password", patch(change_password))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
|
@ -82,6 +85,49 @@ async fn create_employee(
|
|||
Ok((StatusCode::CREATED, Json(employee)))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ProvisionEmployeePayload {
|
||||
pub email: String,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub phone: Option<String>,
|
||||
pub role_code: String,
|
||||
pub department_id: Option<Uuid>,
|
||||
pub designation_id: Option<Uuid>,
|
||||
pub employee_code: Option<String>,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
async fn provision_employee(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<ProvisionEmployeePayload>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
if let Err(_) = require_admin(&auth) {
|
||||
return Err((StatusCode::FORBIDDEN, "Insufficient permissions".to_string()));
|
||||
}
|
||||
|
||||
let password_hash = hash_password(&payload.password)
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Password hash error: {}", e)))?;
|
||||
|
||||
let create_payload = CreateEmployeePayload {
|
||||
first_name: payload.first_name,
|
||||
last_name: payload.last_name,
|
||||
email: payload.email,
|
||||
phone: payload.phone,
|
||||
password_hash,
|
||||
department_id: payload.department_id,
|
||||
designation_id: payload.designation_id,
|
||||
role_code: payload.role_code,
|
||||
};
|
||||
|
||||
let employee = EmployeeRepository::create_with_code(&state.pool, create_payload, payload.employee_code)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {}", e)))?;
|
||||
|
||||
Ok((StatusCode::CREATED, Json(employee)))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateEmployeePayload {
|
||||
pub first_name: Option<String>,
|
||||
|
|
@ -133,3 +179,28 @@ async fn delete_employee(
|
|||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ChangePasswordPayload {
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
async fn change_password(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(payload): Json<ChangePasswordPayload>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
if let Err(_) = require_admin(&auth) {
|
||||
return Err((StatusCode::FORBIDDEN, "Insufficient permissions".to_string()));
|
||||
}
|
||||
|
||||
let password_hash = hash_password(&payload.password)
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Password hash error: {}", e)))?;
|
||||
|
||||
EmployeeRepository::change_password(&state.pool, id, &password_hash)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {}", e)))?;
|
||||
|
||||
Ok(Json(serde_json::json!({ "message": "Password updated successfully" })))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,11 +49,11 @@ async fn list_users(
|
|||
let sql = if role_filter.is_empty() {
|
||||
// Generic list: users + their approved roles
|
||||
r#"
|
||||
SELECT
|
||||
SELECT
|
||||
u.id, u.email, u.first_name, u.last_name, u.status, u.created_at,
|
||||
COALESCE(array_agg(r.key) FILTER (WHERE r.key IS NOT NULL), '{}') as roles
|
||||
FROM users u
|
||||
LEFT JOIN user_roles ur ON ur.user_id = u.id AND ur.status = 'APPROVED'
|
||||
LEFT JOIN user_role_assignments ur ON ur.user_id = u.id AND ur.status = 'APPROVED'
|
||||
LEFT JOIN roles r ON r.id = ur.role_id
|
||||
WHERE ($1 = '' OR LOWER(u.first_name) LIKE '%' || $1 || '%' OR LOWER(u.last_name) LIKE '%' || $1 || '%' OR LOWER(u.email) LIKE '%' || $1 || '%')
|
||||
GROUP BY u.id
|
||||
|
|
@ -68,14 +68,14 @@ async fn list_users(
|
|||
"TUTOR" => "tutor_profiles",
|
||||
"DEVELOPER" => "developer_profiles",
|
||||
"VIDEO_EDITOR" => "video_editor_profiles",
|
||||
"GRAPHIC_DESIGNER" => "graphic_designer_profiles",
|
||||
"GRAPHIC_DESIGNER" => "graphic_designer_profiles",
|
||||
"SOCIAL_MEDIA_MANAGER" => "social_media_manager_profiles",
|
||||
"FITNESS_TRAINER" => "fitness_trainer_profiles",
|
||||
"CATERING_SERVICES" => "catering_service_profiles",
|
||||
"CUSTOMER" => "customer_profiles",
|
||||
"COMPANY" => "company_profiles",
|
||||
"JOB_SEEKER" => "job_seeker_profiles",
|
||||
_ => "user_roles", // fallback
|
||||
_ => "user_role_assignments", // fallback
|
||||
};
|
||||
|
||||
format!(
|
||||
|
|
@ -110,11 +110,11 @@ async fn list_customers(
|
|||
let search = q.q.unwrap_or_default().to_lowercase();
|
||||
|
||||
let sql = r#"
|
||||
SELECT
|
||||
SELECT
|
||||
u.id, u.email, u.first_name, u.last_name, u.status, u.created_at,
|
||||
ARRAY['CUSTOMER']::text[] as roles
|
||||
FROM users u
|
||||
JOIN user_roles ur ON ur.user_id = u.id AND ur.status = 'APPROVED'
|
||||
JOIN user_role_assignments ur ON ur.user_id = u.id AND ur.status = 'APPROVED'
|
||||
JOIN roles r ON r.id = ur.role_id AND r.key = 'CUSTOMER'
|
||||
WHERE ($1 = '' OR LOWER(u.first_name) LIKE '%' || $1 || '%' OR LOWER(u.last_name) LIKE '%' || $1 || '%' OR LOWER(u.email) LIKE '%' || $1 || '%')
|
||||
ORDER BY u.created_at DESC
|
||||
|
|
@ -138,11 +138,11 @@ async fn list_candidates(
|
|||
let search = q.q.unwrap_or_default().to_lowercase();
|
||||
|
||||
let sql = r#"
|
||||
SELECT
|
||||
SELECT
|
||||
u.id, u.email, u.first_name, u.last_name, u.status, u.created_at,
|
||||
ARRAY['JOB_SEEKER']::text[] as roles
|
||||
FROM users u
|
||||
JOIN user_roles ur ON ur.user_id = u.id AND ur.status = 'APPROVED'
|
||||
JOIN user_role_assignments ur ON ur.user_id = u.id AND ur.status = 'APPROVED'
|
||||
JOIN roles r ON r.id = ur.role_id AND r.key = 'JOB_SEEKER'
|
||||
WHERE ($1 = '' OR LOWER(u.first_name) LIKE '%' || $1 || '%' OR LOWER(u.last_name) LIKE '%' || $1 || '%' OR LOWER(u.email) LIKE '%' || $1 || '%')
|
||||
ORDER BY u.created_at DESC
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ pub fn router() -> Router<AppState> {
|
|||
.route("/templates", get(list_templates))
|
||||
.route("/templates/{name}/preview", get(preview_template))
|
||||
.route("/templates/{name}/test", post(send_test_email))
|
||||
.route("/smtp-config", get(get_smtp_config).post(update_smtp_config))
|
||||
.route("/smtp-test", post(test_smtp_connection))
|
||||
.route("/email-config", get(get_email_config).post(update_email_config))
|
||||
.route("/email-test", post(test_email_connection))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
|
@ -416,17 +416,21 @@ async fn send_test_email(
|
|||
}
|
||||
}
|
||||
|
||||
// ── SMTP Configuration ───────────────────────────────────────────────────────
|
||||
// ── Email Configuration ───────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
struct SmtpConfig {
|
||||
host: String,
|
||||
port: i32,
|
||||
secure: bool,
|
||||
username: String,
|
||||
struct EmailConfig {
|
||||
provider: String,
|
||||
smtp_host: String,
|
||||
smtp_port: i32,
|
||||
smtp_secure: bool,
|
||||
smtp_username: String,
|
||||
#[serde(skip_serializing)]
|
||||
password: Option<String>,
|
||||
smtp_password: Option<String>,
|
||||
zeptomail_api_key: String,
|
||||
zeptomail_from_email: String,
|
||||
zeptomail_from_name: String,
|
||||
from_email: String,
|
||||
from_name: String,
|
||||
reply_to_email: Option<String>,
|
||||
|
|
@ -434,66 +438,93 @@ struct SmtpConfig {
|
|||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SmtpConfigResponse {
|
||||
host: String,
|
||||
port: i32,
|
||||
secure: bool,
|
||||
username: String,
|
||||
struct EmailConfigResponse {
|
||||
provider: String,
|
||||
smtp_host: String,
|
||||
smtp_port: i32,
|
||||
smtp_secure: bool,
|
||||
smtp_username: String,
|
||||
from_email: String,
|
||||
from_name: String,
|
||||
reply_to_email: Option<String>,
|
||||
enabled: bool,
|
||||
zeptomail_configured: bool,
|
||||
}
|
||||
|
||||
async fn get_smtp_config() -> impl IntoResponse {
|
||||
// Return current SMTP configuration from environment
|
||||
let config = SmtpConfigResponse {
|
||||
host: std::env::var("SMTP_HOST").unwrap_or_default(),
|
||||
port: std::env::var("SMTP_PORT").unwrap_or_else(|_| "587".to_string()).parse().unwrap_or(587),
|
||||
secure: std::env::var("SMTP_SECURE").unwrap_or_default().to_lowercase() == "true",
|
||||
username: std::env::var("SMTP_USER").unwrap_or_default(),
|
||||
from_email: std::env::var("SMTP_FROM_EMAIL").unwrap_or_else(|_| "noreply@nxtgauge.com".to_string()),
|
||||
from_name: std::env::var("SMTP_FROM_NAME").unwrap_or_else(|_| "NXTGAUGE".to_string()),
|
||||
reply_to_email: std::env::var("SMTP_REPLY_TO").ok(),
|
||||
enabled: std::env::var("SMTP_HOST").is_ok() && !std::env::var("SMTP_HOST").unwrap_or_default().is_empty(),
|
||||
async fn get_email_config() -> impl IntoResponse {
|
||||
let provider = std::env::var("EMAIL_PROVIDER").unwrap_or_else(|_| "SMTP".to_string());
|
||||
let zeptomail_configured = std::env::var("ZEPTOMAIL_API_KEY").is_ok();
|
||||
|
||||
let config = EmailConfigResponse {
|
||||
provider: provider.clone(),
|
||||
smtp_host: std::env::var("SMTP_HOST").unwrap_or_default(),
|
||||
smtp_port: std::env::var("SMTP_PORT").unwrap_or_else(|_| "587".to_string()).parse().unwrap_or(587),
|
||||
smtp_secure: std::env::var("SMTP_SECURE").unwrap_or_default().to_lowercase() == "true",
|
||||
smtp_username: std::env::var("SMTP_USER").unwrap_or_default(),
|
||||
from_email: if provider == "ZEPTOMAIL" {
|
||||
std::env::var("ZEPTOMAIL_FROM_EMAIL").unwrap_or_else(|_| "noreply@nxtgauge.com".to_string())
|
||||
} else {
|
||||
std::env::var("SMTP_FROM_EMAIL").unwrap_or_else(|_| "noreply@nxtgauge.com".to_string())
|
||||
},
|
||||
from_name: if provider == "ZEPTOMAIL" {
|
||||
std::env::var("ZEPTOMAIL_FROM_NAME").unwrap_or_else(|_| "NXTGAUGE".to_string())
|
||||
} else {
|
||||
std::env::var("SMTP_FROM_NAME").unwrap_or_else(|_| "NXTGAUGE".to_string())
|
||||
},
|
||||
reply_to_email: std::env::var("SMTP_REPLY_TO")
|
||||
.ok()
|
||||
.or_else(|| std::env::var("ZEPTOMAIL_REPLY_TO").ok()),
|
||||
enabled: (provider == "SMTP" && std::env::var("SMTP_HOST").is_ok())
|
||||
|| (provider == "ZEPTOMAIL" && std::env::var("ZEPTOMAIL_API_KEY").is_ok()),
|
||||
zeptomail_configured,
|
||||
};
|
||||
|
||||
|
||||
(StatusCode::OK, Json(config))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
struct UpdateSmtpConfigRequest {
|
||||
host: String,
|
||||
port: i32,
|
||||
secure: bool,
|
||||
username: String,
|
||||
password: Option<String>,
|
||||
struct UpdateEmailConfigRequest {
|
||||
provider: String,
|
||||
smtp_host: String,
|
||||
smtp_port: i32,
|
||||
smtp_secure: bool,
|
||||
smtp_username: String,
|
||||
smtp_password: Option<String>,
|
||||
zeptomail_api_key: String,
|
||||
zeptomail_from_email: String,
|
||||
zeptomail_from_name: String,
|
||||
from_email: String,
|
||||
from_name: String,
|
||||
reply_to_email: Option<String>,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
async fn update_smtp_config(
|
||||
Json(req): Json<UpdateSmtpConfigRequest>,
|
||||
async fn update_email_config(
|
||||
Json(req): Json<UpdateEmailConfigRequest>,
|
||||
) -> impl IntoResponse {
|
||||
// In production, this would update the database or secrets manager
|
||||
// For now, we just return success (env vars need restart to take effect)
|
||||
|
||||
if req.enabled && req.host.is_empty() {
|
||||
return (StatusCode::BAD_REQUEST, Json(serde_json::json!({
|
||||
"error": "SMTP host is required when enabled"
|
||||
})));
|
||||
if req.enabled {
|
||||
if req.provider == "SMTP" && req.smtp_host.is_empty() {
|
||||
return (StatusCode::BAD_REQUEST, Json(serde_json::json!({
|
||||
"error": "SMTP host is required when SMTP provider is enabled"
|
||||
})));
|
||||
}
|
||||
if req.provider == "ZEPTOMAIL" && req.zeptomail_api_key.is_empty() {
|
||||
return (StatusCode::BAD_REQUEST, Json(serde_json::json!({
|
||||
"error": "Zeptomail API key is required when Zeptomail provider is enabled"
|
||||
})));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
(StatusCode::OK, Json(serde_json::json!({
|
||||
"message": "SMTP configuration updated. Restart services to apply changes.",
|
||||
"message": "Email configuration updated. Restart services to apply changes.",
|
||||
"config": {
|
||||
"host": req.host,
|
||||
"port": req.port,
|
||||
"secure": req.secure,
|
||||
"username": req.username,
|
||||
"provider": req.provider,
|
||||
"smtp_host": req.smtp_host,
|
||||
"smtp_port": req.smtp_port,
|
||||
"smtp_secure": req.smtp_secure,
|
||||
"smtp_username": req.smtp_username,
|
||||
"zeptomail_api_key": if req.zeptomail_api_key.is_empty() { "[hidden]".to_string() } else { "[configured]".to_string() },
|
||||
"from_email": req.from_email,
|
||||
"from_name": req.from_name,
|
||||
"reply_to_email": req.reply_to_email,
|
||||
|
|
@ -503,37 +534,39 @@ async fn update_smtp_config(
|
|||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SmtpTestRequest {
|
||||
struct EmailTestRequest {
|
||||
to_email: String,
|
||||
config: Option<SmtpTestConfig>,
|
||||
provider: Option<String>,
|
||||
config: Option<EmailTestConfig>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
struct SmtpTestConfig {
|
||||
host: String,
|
||||
port: i32,
|
||||
secure: bool,
|
||||
username: String,
|
||||
password: String,
|
||||
struct EmailTestConfig {
|
||||
provider: String,
|
||||
smtp_host: String,
|
||||
smtp_port: i32,
|
||||
smtp_secure: bool,
|
||||
smtp_username: String,
|
||||
smtp_password: String,
|
||||
zeptomail_api_key: String,
|
||||
from_email: String,
|
||||
from_name: String,
|
||||
}
|
||||
|
||||
async fn test_smtp_connection(
|
||||
async fn test_email_connection(
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<SmtpTestRequest>,
|
||||
Json(req): Json<EmailTestRequest>,
|
||||
) -> impl IntoResponse {
|
||||
// Send a test email using current or provided config
|
||||
let result = if let Some(test_config) = req.config {
|
||||
// Create temporary mailer with test config
|
||||
let test_mailer = create_test_mailer(test_config).await;
|
||||
test_mailer.send_test_email(&req.to_email).await
|
||||
// For now, just use the existing mailer - test config would require recreating mailer
|
||||
state.mail.send_test_email(&req.to_email).await
|
||||
} else {
|
||||
// Use existing mailer
|
||||
state.mail.send_test_email(&req.to_email).await
|
||||
};
|
||||
|
||||
|
||||
match result {
|
||||
Ok(_) => (StatusCode::OK, Json(serde_json::json!({
|
||||
"message": "Test email sent successfully",
|
||||
|
|
@ -544,9 +577,3 @@ async fn test_smtp_connection(
|
|||
}))),
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_test_mailer(_config: SmtpTestConfig) -> email::Mailer {
|
||||
// This is a simplified version - in production you'd create a new Mailer instance
|
||||
// For now, we just return the default mailer
|
||||
email::Mailer::new()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@ async fn activate_profile_after_final_approval(
|
|||
|
||||
if let Ok(role) = RoleRepository::get_by_key(&state.pool, &role_key).await {
|
||||
sqlx::query(
|
||||
"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()",
|
||||
"INSERT INTO user_role_assignments (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()",
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(role.id)
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ async fn list_runtime_configs(
|
|||
sqlx::query_as::<_, RcRow>(
|
||||
r#"
|
||||
SELECT id, role_id, config_json, version, is_active, updated_at
|
||||
FROM runtime_configs
|
||||
FROM role_runtime_configs
|
||||
WHERE role_id = $1
|
||||
ORDER BY version DESC
|
||||
"#,
|
||||
|
|
@ -107,7 +107,7 @@ async fn list_runtime_configs(
|
|||
sqlx::query_as::<_, RcRow>(
|
||||
r#"
|
||||
SELECT rc.id, rc.role_id, rc.config_json, rc.version, rc.is_active, rc.updated_at
|
||||
FROM runtime_configs rc
|
||||
FROM role_runtime_configs rc
|
||||
JOIN roles r ON rc.role_id = r.id
|
||||
WHERE r.audience = 'INTERNAL'
|
||||
ORDER BY rc.updated_at DESC
|
||||
|
|
@ -149,7 +149,7 @@ async fn get_runtime_config_by_id(
|
|||
updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
let r = sqlx::query_as::<_, RcDetailRow>(
|
||||
"SELECT id, role_id, config_json, version, is_active, updated_at FROM runtime_configs WHERE id = $1",
|
||||
"SELECT id, role_id, config_json, version, is_active, updated_at FROM role_runtime_configs WHERE id = $1",
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(&state.pool)
|
||||
|
|
@ -193,20 +193,20 @@ async fn activate_runtime_config(
|
|||
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
||||
}
|
||||
// Fetch role_id for the target config
|
||||
let role_id: Uuid = sqlx::query_scalar::<_, Uuid>("SELECT role_id FROM runtime_configs WHERE id = $1")
|
||||
let role_id: Uuid = sqlx::query_scalar::<_, Uuid>("SELECT role_id FROM role_runtime_configs WHERE id = $1")
|
||||
.bind(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")
|
||||
sqlx::query("UPDATE role_runtime_configs SET is_active = false WHERE role_id = $1 AND is_active = true")
|
||||
.bind(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")
|
||||
sqlx::query("UPDATE role_runtime_configs SET is_active = true WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
|
|
@ -222,7 +222,7 @@ async fn delete_runtime_config(
|
|||
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")
|
||||
let result = sqlx::query("DELETE FROM role_runtime_configs WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
|
|
@ -232,11 +232,21 @@ async fn delete_runtime_config(
|
|||
}
|
||||
Ok((StatusCode::NO_CONTENT, "".to_string()))
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct RuntimeConfigQuery {
|
||||
role: Option<String>,
|
||||
}
|
||||
|
||||
async fn get_my_runtime_config(
|
||||
auth: contracts::auth_middleware::AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Query(q): Query<RuntimeConfigQuery>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let role_key = auth.claims.active_role.clone().to_uppercase();
|
||||
// Allow frontend to override role via ?role= query param (falls back to JWT claim)
|
||||
let role_key = q.role
|
||||
.map(|r| r.to_uppercase())
|
||||
.filter(|r| !r.is_empty())
|
||||
.unwrap_or_else(|| auth.claims.active_role.clone().to_uppercase());
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
#[allow(dead_code)]
|
||||
|
|
@ -297,7 +307,7 @@ async fn get_my_runtime_config(
|
|||
|
||||
if role.audience == "INTERNAL" {
|
||||
let permission_keys: Vec<String> = sqlx::query_scalar::<_, String>(
|
||||
"SELECT permission_key FROM role_permissions WHERE role_id = $1 ORDER BY permission_key",
|
||||
"SELECT permission_key FROM role_admin_permissions WHERE role_id = $1 ORDER BY permission_key",
|
||||
)
|
||||
.bind(role.id)
|
||||
.fetch_all(&state.pool)
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ struct ExternalRoleRow {
|
|||
id: Uuid,
|
||||
name: String,
|
||||
code: String,
|
||||
persona_type: Option<String>,
|
||||
vertical: Option<String>,
|
||||
category: Option<String>,
|
||||
onboarding_schema_id: Option<String>,
|
||||
|
|
@ -61,6 +62,7 @@ struct ExternalRoleListRow {
|
|||
id: Uuid,
|
||||
name: String,
|
||||
code: String,
|
||||
persona_type: Option<String>,
|
||||
is_active: bool,
|
||||
created_date: chrono::DateTime<chrono::Utc>,
|
||||
updated_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
|
|
@ -89,13 +91,13 @@ async fn list_external_roles(
|
|||
r.id,
|
||||
r.name,
|
||||
r.key as code,
|
||||
r.persona_type,
|
||||
r.is_active,
|
||||
r.created_at as created_date,
|
||||
rc.updated_at as "updated_at",
|
||||
rc.config_json as "config_json"
|
||||
FROM roles r
|
||||
JOIN external_roles er ON er.role_id = r.id
|
||||
LEFT JOIN runtime_configs rc ON rc.role_id = r.id AND rc.is_active = true
|
||||
LEFT JOIN role_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))
|
||||
|
|
@ -115,7 +117,6 @@ async fn list_external_roles(
|
|||
r#"
|
||||
SELECT COUNT(*)
|
||||
FROM roles r
|
||||
JOIN external_roles er ON er.role_id = r.id
|
||||
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))
|
||||
|
|
@ -155,7 +156,7 @@ async fn list_external_roles(
|
|||
continue;
|
||||
}
|
||||
let assigned_users: i64 = sqlx::query_scalar::<_, i64>(
|
||||
"SELECT COUNT(*) FROM user_roles WHERE role_id = $1 AND status = 'APPROVED'",
|
||||
"SELECT COUNT(*) FROM user_role_assignments WHERE role_id = $1 AND status = 'APPROVED'",
|
||||
)
|
||||
.bind(row.id)
|
||||
.fetch_one(&state.pool)
|
||||
|
|
@ -166,6 +167,7 @@ async fn list_external_roles(
|
|||
id: row.id,
|
||||
name: row.name,
|
||||
code: row.code,
|
||||
persona_type: row.persona_type.or(vertical_v.clone()),
|
||||
vertical: vertical_v,
|
||||
category: category_v,
|
||||
onboarding_schema_id,
|
||||
|
|
@ -223,8 +225,7 @@ async fn get_external_role(
|
|||
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
|
||||
JOIN external_roles er ON er.role_id = r.id
|
||||
LEFT JOIN runtime_configs rc ON rc.role_id = r.id AND rc.is_active = true
|
||||
LEFT JOIN role_runtime_configs rc ON rc.role_id = r.id AND rc.is_active = true
|
||||
WHERE r.id = $1 AND r.audience = 'EXTERNAL'
|
||||
"#,
|
||||
)
|
||||
|
|
@ -251,7 +252,8 @@ struct CreateExternalRolePayload {
|
|||
name: String,
|
||||
code: String,
|
||||
is_active: Option<bool>,
|
||||
runtime: JsonValue,
|
||||
persona_type: Option<String>,
|
||||
runtime: Option<JsonValue>,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
|
|
@ -280,35 +282,29 @@ async fn create_external_role(
|
|||
let is_active = payload.is_active.unwrap_or(true);
|
||||
let role = sqlx::query_as::<_, InsertedRole>(
|
||||
r#"
|
||||
INSERT INTO roles (key, name, audience, is_active)
|
||||
VALUES ($1, $2, 'EXTERNAL', $3)
|
||||
INSERT INTO roles (key, name, audience, is_active, persona_type)
|
||||
VALUES ($1, $2, 'EXTERNAL', $3, $4)
|
||||
RETURNING id, key, name, audience, is_active, created_at
|
||||
"#,
|
||||
)
|
||||
.bind(payload.code.to_uppercase())
|
||||
.bind(&payload.name)
|
||||
.bind(is_active)
|
||||
.bind(&payload.persona_type)
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO external_roles (role_id) VALUES ($1)",
|
||||
)
|
||||
.bind(role.id)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
|
||||
let runtime = payload.runtime.unwrap_or_else(|| serde_json::json!({}));
|
||||
let rc = sqlx::query_as::<_, InsertedRc>(
|
||||
r#"
|
||||
INSERT INTO runtime_configs (role_id, config_json, version, is_active)
|
||||
INSERT INTO role_runtime_configs (role_id, config_json, version, is_active)
|
||||
VALUES ($1, $2, 1, true)
|
||||
RETURNING updated_at
|
||||
"#,
|
||||
)
|
||||
.bind(role.id)
|
||||
.bind(&payload.runtime)
|
||||
.bind(&runtime)
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
|
|
@ -321,7 +317,7 @@ async fn create_external_role(
|
|||
code: role.key,
|
||||
audience: role.audience,
|
||||
is_active: role.is_active,
|
||||
runtime: payload.runtime,
|
||||
runtime,
|
||||
created_at: role.created_at,
|
||||
updated_at: Some(rc.updated_at),
|
||||
}),
|
||||
|
|
@ -363,7 +359,7 @@ async fn update_external_role(
|
|||
if let Some(runtime) = payload.runtime {
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE runtime_configs
|
||||
UPDATE role_runtime_configs
|
||||
SET is_active = false
|
||||
WHERE role_id = $1 AND is_active = true
|
||||
"#,
|
||||
|
|
@ -374,11 +370,11 @@ async fn update_external_role(
|
|||
.ok();
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO runtime_configs (role_id, config_json, version, is_active)
|
||||
INSERT INTO role_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,
|
||||
COALESCE((SELECT MAX(version) FROM role_runtime_configs WHERE role_id = $1), 0) + 1,
|
||||
true
|
||||
)
|
||||
"#,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ pub mod config;
|
|||
pub mod coupons;
|
||||
pub mod dashboard;
|
||||
pub mod kb;
|
||||
pub mod modules;
|
||||
pub mod notifications;
|
||||
pub mod onboarding;
|
||||
pub mod permissions;
|
||||
|
|
|
|||
263
apps/users/src/handlers/modules.rs
Normal file
263
apps/users/src/handlers/modules.rs
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
use crate::AppState;
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
Json, Router,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::types::Uuid;
|
||||
use contracts::auth_middleware::AuthUser;
|
||||
|
||||
pub fn persona_types_router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/api/admin/persona-types", get(list_persona_types))
|
||||
}
|
||||
|
||||
pub fn modules_router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/api/admin/modules", get(list_modules))
|
||||
}
|
||||
|
||||
pub fn role_modules_router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/api/admin/roles/{id}/modules", get(get_role_modules).post(add_role_module))
|
||||
.route("/api/admin/roles/{id}/modules/{module_id}", axum::routing::delete(remove_role_module))
|
||||
.route("/api/admin/roles/{id}/permissions", get(get_role_permissions).put(update_role_permission))
|
||||
}
|
||||
|
||||
#[derive(Serialize, sqlx::FromRow)]
|
||||
struct PersonaTypeRow {
|
||||
id: Uuid,
|
||||
code: String,
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
is_active: bool,
|
||||
}
|
||||
|
||||
async fn list_persona_types(
|
||||
_auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let rows = sqlx::query_as::<_, PersonaTypeRow>(
|
||||
"SELECT id, code, name, description, is_active FROM persona_types WHERE is_active = true ORDER BY name",
|
||||
)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
|
||||
Ok(Json(rows))
|
||||
}
|
||||
|
||||
#[derive(Serialize, sqlx::FromRow)]
|
||||
struct ModuleRow {
|
||||
id: Uuid,
|
||||
module_key: String,
|
||||
module_name: String,
|
||||
category: String,
|
||||
description: Option<String>,
|
||||
backend_domain: Option<String>,
|
||||
default_route: Option<String>,
|
||||
default_sidebar_label: Option<String>,
|
||||
icon_key: Option<String>,
|
||||
is_core: bool,
|
||||
is_active: bool,
|
||||
}
|
||||
|
||||
async fn list_modules(
|
||||
_auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let rows = sqlx::query_as::<_, ModuleRow>(
|
||||
r#"
|
||||
SELECT id, module_key, module_name, category, description,
|
||||
backend_domain, default_route, default_sidebar_label,
|
||||
icon_key, is_core, is_active
|
||||
FROM modules
|
||||
WHERE is_active = true
|
||||
ORDER BY category, module_name
|
||||
"#,
|
||||
)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
|
||||
Ok(Json(rows))
|
||||
}
|
||||
|
||||
#[derive(Serialize, sqlx::FromRow)]
|
||||
struct RoleModuleAccessRow {
|
||||
id: Uuid,
|
||||
module_id: Uuid,
|
||||
module_key: String,
|
||||
module_name: String,
|
||||
is_enabled: bool,
|
||||
is_sidebar_visible: bool,
|
||||
sidebar_label_override: Option<String>,
|
||||
route_override: Option<String>,
|
||||
}
|
||||
|
||||
async fn get_role_modules(
|
||||
_auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(role_id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let rows = sqlx::query_as::<_, RoleModuleAccessRow>(
|
||||
r#"
|
||||
SELECT rma.id, rma.module_id, m.module_key, m.module_name,
|
||||
rma.is_enabled, rma.is_sidebar_visible,
|
||||
rma.sidebar_label_override, rma.route_override
|
||||
FROM role_module_access rma
|
||||
JOIN modules m ON m.id = rma.module_id
|
||||
WHERE rma.role_id = $1
|
||||
ORDER BY m.category, m.module_name
|
||||
"#,
|
||||
)
|
||||
.bind(role_id)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
|
||||
Ok(Json(rows))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AddModulePayload {
|
||||
module_id: Uuid,
|
||||
is_enabled: Option<bool>,
|
||||
is_sidebar_visible: Option<bool>,
|
||||
sidebar_label_override: Option<String>,
|
||||
route_override: Option<String>,
|
||||
}
|
||||
|
||||
async fn add_role_module(
|
||||
_auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(role_id): Path<Uuid>,
|
||||
Json(payload): Json<AddModulePayload>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let is_enabled = payload.is_enabled.unwrap_or(true);
|
||||
let is_sidebar_visible = payload.is_sidebar_visible.unwrap_or(true);
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO role_module_access (role_id, module_id, is_enabled, is_sidebar_visible, sidebar_label_override, route_override)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT (role_id, module_id) DO UPDATE SET
|
||||
is_enabled = EXCLUDED.is_enabled,
|
||||
is_sidebar_visible = EXCLUDED.is_sidebar_visible,
|
||||
sidebar_label_override = EXCLUDED.sidebar_label_override,
|
||||
route_override = EXCLUDED.route_override
|
||||
"#,
|
||||
)
|
||||
.bind(role_id)
|
||||
.bind(payload.module_id)
|
||||
.bind(is_enabled)
|
||||
.bind(is_sidebar_visible)
|
||||
.bind(&payload.sidebar_label_override)
|
||||
.bind(&payload.route_override)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
|
||||
Ok(StatusCode::CREATED)
|
||||
}
|
||||
|
||||
async fn remove_role_module(
|
||||
_auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path((role_id, module_id)): Path<(Uuid, Uuid)>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let result = sqlx::query(
|
||||
"DELETE FROM role_module_access WHERE role_id = $1 AND module_id = $2",
|
||||
)
|
||||
.bind(role_id)
|
||||
.bind(module_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, "Module access not found".to_string()));
|
||||
}
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
#[derive(Serialize, sqlx::FromRow)]
|
||||
struct RolePermissionRow {
|
||||
id: Uuid,
|
||||
module_id: Uuid,
|
||||
module_key: String,
|
||||
module_name: String,
|
||||
category: String,
|
||||
can_view: bool,
|
||||
can_list: bool,
|
||||
can_create: bool,
|
||||
can_update: bool,
|
||||
can_delete: bool,
|
||||
}
|
||||
|
||||
async fn get_role_permissions(
|
||||
_auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(role_id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let rows = sqlx::query_as::<_, RolePermissionRow>(
|
||||
r#"
|
||||
SELECT rmp.id, rmp.module_id, m.module_key, m.module_name, m.category,
|
||||
rmp.can_view, rmp.can_list, rmp.can_create, rmp.can_update, rmp.can_delete
|
||||
FROM role_module_permissions rmp
|
||||
JOIN modules m ON m.id = rmp.module_id
|
||||
WHERE rmp.role_id = $1
|
||||
ORDER BY m.category, m.module_name
|
||||
"#,
|
||||
)
|
||||
.bind(role_id)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
|
||||
Ok(Json(rows))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct UpdatePermissionPayload {
|
||||
module_key: String,
|
||||
permission: String,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
async fn update_role_permission(
|
||||
_auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(role_id): Path<Uuid>,
|
||||
Json(payload): Json<UpdatePermissionPayload>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let permission_col = match payload.permission.as_str() {
|
||||
"view" => "can_view",
|
||||
"list" => "can_list",
|
||||
"create" => "can_create",
|
||||
"update" => "can_update",
|
||||
"delete" => "can_delete",
|
||||
_ => return Err((StatusCode::BAD_REQUEST, "Invalid permission type".to_string())),
|
||||
};
|
||||
|
||||
sqlx::query(&format!(
|
||||
r#"
|
||||
UPDATE role_module_permissions
|
||||
SET {} = $1
|
||||
WHERE role_id = $2 AND module_id = (SELECT id FROM modules WHERE module_key = $3)
|
||||
"#,
|
||||
permission_col
|
||||
))
|
||||
.bind(payload.enabled)
|
||||
.bind(role_id)
|
||||
.bind(&payload.module_key)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
|
@ -209,7 +209,7 @@ async fn submit(
|
|||
// 3. Mark the user_role as PENDING (awaiting admin review of onboarding)
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE user_roles
|
||||
UPDATE user_role_assignments
|
||||
SET status = 'PENDING'
|
||||
WHERE user_id = $1 AND role_id = $2
|
||||
"#,
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ const MODULES: &[&str] = &[
|
|||
"Social Media Management",
|
||||
"Video Editor Management",
|
||||
"Catering Services Management",
|
||||
"UGC Content Creator Management",
|
||||
"Jobs Management",
|
||||
"Leads Management",
|
||||
"Applications Management",
|
||||
|
|
@ -49,11 +50,15 @@ const MODULES: &[&str] = &[
|
|||
"Tax Management",
|
||||
"Order Management",
|
||||
"Invoice Management",
|
||||
"Payment Gateway Management",
|
||||
"Ledger Management",
|
||||
"Knowledge Base Management",
|
||||
"Support Management",
|
||||
"Report Management",
|
||||
"SMTP Management",
|
||||
"Email Management",
|
||||
"Notifications",
|
||||
"Dashboard",
|
||||
];
|
||||
|
||||
const ACTIONS: &[&str] = &["View", "Create", "Update", "Delete"];
|
||||
|
|
|
|||
|
|
@ -342,7 +342,7 @@ async fn submit_for_verification(
|
|||
// Mark user_role as PENDING
|
||||
if let Ok(role) = RoleRepository::get_by_key(&state.pool, &role_key).await {
|
||||
sqlx::query(
|
||||
"UPDATE user_roles SET status = 'PENDING' WHERE user_id = $1 AND role_id = $2",
|
||||
"UPDATE user_role_assignments SET status = 'PENDING' WHERE user_id = $1 AND role_id = $2",
|
||||
)
|
||||
.bind(auth.user_id)
|
||||
.bind(role.id)
|
||||
|
|
|
|||
|
|
@ -164,10 +164,10 @@ async fn list_roles(
|
|||
COUNT(DISTINCT e.id) AS users_assigned,
|
||||
COUNT(DISTINCT rp.id) AS permissions_count
|
||||
FROM roles r
|
||||
JOIN internal_roles ir ON ir.role_id = r.id
|
||||
JOIN internal_role_details ir ON ir.role_id = r.id
|
||||
LEFT JOIN departments d ON d.id = ir.department_id
|
||||
LEFT JOIN employees e ON e.role_code = r.key
|
||||
LEFT JOIN role_permissions rp ON rp.role_id = r.id
|
||||
LEFT JOIN role_admin_permissions rp ON rp.role_id = r.id
|
||||
WHERE r.audience = 'INTERNAL'
|
||||
AND ($1 = '' OR LOWER(r.name) LIKE '%' || $1 || '%' OR LOWER(r.key) LIKE '%' || $1 || '%')
|
||||
GROUP BY r.id, ir.description, ir.department_id, ir.can_approve_requests, ir.can_manage_system_settings, d.name
|
||||
|
|
@ -185,7 +185,7 @@ async fn list_roles(
|
|||
let total: i64 = sqlx::query_scalar::<_, i64>(
|
||||
r#"
|
||||
SELECT COUNT(*) FROM roles r
|
||||
JOIN internal_roles ir ON ir.role_id = r.id
|
||||
JOIN internal_role_details ir ON ir.role_id = r.id
|
||||
WHERE r.audience = 'INTERNAL'
|
||||
AND ($1 = '' OR LOWER(r.name) LIKE '%' || $1 || '%' OR LOWER(r.key) LIKE '%' || $1 || '%')
|
||||
"#,
|
||||
|
|
@ -232,7 +232,7 @@ async fn get_role(
|
|||
COALESCE(ir.can_manage_system_settings, false) AS can_manage_system_settings,
|
||||
r.created_at
|
||||
FROM roles r
|
||||
JOIN internal_roles ir ON ir.role_id = r.id
|
||||
JOIN internal_role_details ir ON ir.role_id = r.id
|
||||
LEFT JOIN departments d ON d.id = ir.department_id
|
||||
WHERE r.id = $1 AND r.audience = 'INTERNAL'
|
||||
"#,
|
||||
|
|
@ -244,7 +244,7 @@ async fn get_role(
|
|||
.ok_or((StatusCode::NOT_FOUND, "Role not found".to_string()))?;
|
||||
|
||||
let permission_keys: Vec<String> = sqlx::query_scalar::<_, String>(
|
||||
"SELECT permission_key FROM role_permissions WHERE role_id = $1 ORDER BY permission_key",
|
||||
"SELECT permission_key FROM role_admin_permissions WHERE role_id = $1 ORDER BY permission_key",
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_all(&state.pool)
|
||||
|
|
@ -291,7 +291,7 @@ async fn create_role(
|
|||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO internal_roles (role_id, description, department_id, can_approve_requests, can_manage_system_settings)
|
||||
INSERT INTO internal_role_details (role_id, description, department_id, can_approve_requests, can_manage_system_settings)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
"#,
|
||||
)
|
||||
|
|
@ -307,7 +307,7 @@ async fn create_role(
|
|||
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",
|
||||
"INSERT INTO role_admin_permissions (role_id, permission_key) VALUES ($1, $2) ON CONFLICT DO NOTHING",
|
||||
)
|
||||
.bind(role.id)
|
||||
.bind(key)
|
||||
|
|
@ -318,7 +318,7 @@ async fn create_role(
|
|||
}
|
||||
|
||||
let permission_keys: Vec<String> = sqlx::query_scalar::<_, String>(
|
||||
"SELECT permission_key FROM role_permissions WHERE role_id = $1 ORDER BY permission_key",
|
||||
"SELECT permission_key FROM role_admin_permissions WHERE role_id = $1 ORDER BY permission_key",
|
||||
)
|
||||
.bind(role.id)
|
||||
.fetch_all(&state.pool)
|
||||
|
|
@ -355,7 +355,7 @@ async fn update_role(
|
|||
COALESCE(ir.can_approve_requests, false) AS can_approve_requests,
|
||||
COALESCE(ir.can_manage_system_settings, false) AS can_manage_system_settings
|
||||
FROM roles r
|
||||
JOIN internal_roles ir ON ir.role_id = r.id
|
||||
JOIN internal_role_details ir ON ir.role_id = r.id
|
||||
WHERE r.id = $1 AND r.audience = 'INTERNAL'
|
||||
"#,
|
||||
)
|
||||
|
|
@ -385,7 +385,7 @@ async fn update_role(
|
|||
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE internal_roles SET
|
||||
UPDATE internal_role_details SET
|
||||
description = $1,
|
||||
department_id = $2,
|
||||
can_approve_requests = $3,
|
||||
|
|
@ -403,7 +403,7 @@ async fn update_role(
|
|||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
|
||||
if let Some(keys) = &payload.permission_keys {
|
||||
sqlx::query("DELETE FROM role_permissions WHERE role_id = $1")
|
||||
sqlx::query("DELETE FROM role_admin_permissions WHERE role_id = $1")
|
||||
.bind(id)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
|
|
@ -411,7 +411,7 @@ async fn update_role(
|
|||
|
||||
for key in keys {
|
||||
sqlx::query(
|
||||
"INSERT INTO role_permissions (role_id, permission_key) VALUES ($1, $2) ON CONFLICT DO NOTHING",
|
||||
"INSERT INTO role_admin_permissions (role_id, permission_key) VALUES ($1, $2) ON CONFLICT DO NOTHING",
|
||||
)
|
||||
.bind(id)
|
||||
.bind(key)
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ async fn list_my_roles(
|
|||
let rows = sqlx::query_as::<_, UserRoleRow>(
|
||||
r#"
|
||||
SELECT r.key, r.name, ur.status, ur.approved_at
|
||||
FROM user_roles ur
|
||||
FROM user_role_assignments ur
|
||||
INNER JOIN roles r ON r.id = ur.role_id
|
||||
WHERE ur.user_id = $1
|
||||
ORDER BY ur.created_at ASC
|
||||
|
|
@ -100,7 +100,7 @@ async fn register_role(
|
|||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO user_roles (user_id, role_id, status, approved_at)
|
||||
INSERT INTO user_role_assignments (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()
|
||||
|
|
|
|||
|
|
@ -60,6 +60,9 @@ async fn main() {
|
|||
.nest("/api/admin/roles", handlers::roles::router())
|
||||
.nest("/api/admin/permissions", handlers::permissions::router())
|
||||
.nest("/api/admin/external-roles", handlers::external_roles::router())
|
||||
.merge(handlers::modules::persona_types_router())
|
||||
.merge(handlers::modules::modules_router())
|
||||
.merge(handlers::modules::role_modules_router())
|
||||
.nest("/api/admin/users", handlers::admin::router())
|
||||
.nest("/api/me/roles", handlers::user_roles::router())
|
||||
// ── Notifications ─────────────────────────────────────────────────
|
||||
|
|
|
|||
15
crates/auth/examples/hash_gen.rs
Normal file
15
crates/auth/examples/hash_gen.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
use argon2::{
|
||||
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, SaltString},
|
||||
Argon2,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
let password = std::env::args().nth(1).unwrap_or_default();
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let argon2 = Argon2::default();
|
||||
let hashed = argon2
|
||||
.hash_password(password.as_bytes(), &salt)
|
||||
.unwrap()
|
||||
.to_string();
|
||||
println!("{}", hashed);
|
||||
}
|
||||
|
|
@ -312,7 +312,7 @@ async fn list_portfolio(State(state): State<ProfessionState>, auth: AuthUser) ->
|
|||
Ok(items) => (StatusCode::OK, Json(serde_json::json!({ "data": items }))).into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
},
|
||||
Err(_) => (StatusCode::NOT_FOUND, "Professional profile not found").into_response(),
|
||||
Err(_) => (StatusCode::OK, Json(serde_json::json!({ "data": [] }))).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -322,13 +322,17 @@ async fn list_services(State(state): State<ProfessionState>, auth: AuthUser) ->
|
|||
Ok(items) => (StatusCode::OK, Json(serde_json::json!({ "data": items }))).into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
},
|
||||
Err(_) => (StatusCode::NOT_FOUND, "Professional profile not found").into_response(),
|
||||
Err(_) => (StatusCode::OK, Json(serde_json::json!({ "data": [] }))).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn wallet_balance(State(state): State<ProfessionState>, auth: AuthUser) -> impl IntoResponse {
|
||||
let _ = ProfessionalRepository::ensure_wallet(&state.pool, auth.user_id).await;
|
||||
match ProfessionalRepository::get_wallet(&state.pool, auth.user_id).await {
|
||||
Ok(w) => (StatusCode::OK, Json(w)).into_response(),
|
||||
Err(sqlx::Error::RowNotFound) => {
|
||||
(StatusCode::OK, Json(serde_json::json!({ "balance": 0, "reserved": 0 }))).into_response()
|
||||
}
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
|
@ -349,7 +353,13 @@ async fn my_requests(
|
|||
) -> impl IntoResponse {
|
||||
let prof = match ProfessionalRepository::get_by_user_id(&state.pool, auth.user_id).await {
|
||||
Ok(p) => p,
|
||||
Err(_) => return (StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "Professional profile not found" }))).into_response(),
|
||||
Err(_) => return (
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"data": [],
|
||||
"pagination": { "page": 1, "limit": 20, "total": 0, "total_pages": 1 }
|
||||
}))
|
||||
).into_response(),
|
||||
};
|
||||
|
||||
let page = q.page.unwrap_or(1).max(1);
|
||||
|
|
@ -381,7 +391,7 @@ async fn my_requests(
|
|||
LEFT JOIN requirements r ON r.id = lr.requirement_id
|
||||
LEFT JOIN customers c ON c.id = r.customer_id
|
||||
LEFT JOIN users u ON u.id = c.user_id
|
||||
WHERE lr.professional_id = $1 AND lr.status = $2
|
||||
WHERE lr.user_role_profile_id = $1 AND lr.status = $2
|
||||
ORDER BY lr.requested_at DESC LIMIT $3 OFFSET $4
|
||||
"#
|
||||
)
|
||||
|
|
@ -397,7 +407,7 @@ async fn my_requests(
|
|||
LEFT JOIN requirements r ON r.id = lr.requirement_id
|
||||
LEFT JOIN customers c ON c.id = r.customer_id
|
||||
LEFT JOIN users u ON u.id = c.user_id
|
||||
WHERE lr.professional_id = $1
|
||||
WHERE lr.user_role_profile_id = $1
|
||||
ORDER BY lr.requested_at DESC LIMIT $2 OFFSET $3
|
||||
"#
|
||||
)
|
||||
|
|
@ -405,10 +415,10 @@ async fn my_requests(
|
|||
};
|
||||
|
||||
let total: i64 = if let Some(ref status) = q.status {
|
||||
sqlx::query_scalar("SELECT COUNT(*) FROM lead_requests WHERE professional_id = $1 AND status = $2")
|
||||
sqlx::query_scalar("SELECT COUNT(*) FROM lead_requests WHERE user_role_profile_id = $1 AND status = $2")
|
||||
.bind(prof.id).bind(status).fetch_one(&state.pool).await.unwrap_or(0)
|
||||
} else {
|
||||
sqlx::query_scalar("SELECT COUNT(*) FROM lead_requests WHERE professional_id = $1")
|
||||
sqlx::query_scalar("SELECT COUNT(*) FROM lead_requests WHERE user_role_profile_id = $1")
|
||||
.bind(prof.id).fetch_one(&state.pool).await.unwrap_or(0)
|
||||
};
|
||||
|
||||
|
|
@ -478,7 +488,13 @@ async fn accepted_leads(
|
|||
) -> impl IntoResponse {
|
||||
let user_role_profile = match UserRoleProfileRepository::get_by_user_and_role(&state.pool, auth.user_id, "PHOTOGRAPHER").await {
|
||||
Ok(Some(p)) => p,
|
||||
Ok(None) => return (StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "Professional profile not found" }))).into_response(),
|
||||
Ok(None) => return (
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"data": [],
|
||||
"pagination": { "page": 1, "limit": 20, "total": 0, "total_pages": 1 }
|
||||
}))
|
||||
).into_response(),
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() }))).into_response(),
|
||||
};
|
||||
|
||||
|
|
@ -575,7 +591,7 @@ async fn accepted_lead_detail(
|
|||
INNER JOIN customers c ON c.id = r.customer_id
|
||||
INNER JOIN users u ON u.id = c.user_id
|
||||
WHERE lr.id = $1
|
||||
AND lr.professional_id = $2
|
||||
AND lr.user_role_profile_id = $2
|
||||
AND lr.status = 'ACCEPTED'
|
||||
"#
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
-- Phase 1: External Role Management Module System
|
||||
-- Creates base schema for persona_types, external_roles, modules, role_module_access, module_actions, role_module_permissions
|
||||
|
||||
-- ============================================
|
||||
-- persona_types
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS persona_types (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
code varchar(50) UNIQUE NOT NULL,
|
||||
name varchar(100) NOT NULL,
|
||||
description text,
|
||||
is_active boolean DEFAULT true,
|
||||
created_at timestamptz DEFAULT NOW(),
|
||||
updated_at timestamptz DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- external_roles
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS external_roles (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
role_code varchar(50) UNIQUE NOT NULL,
|
||||
role_name varchar(100) NOT NULL,
|
||||
persona_type_id uuid REFERENCES persona_types(id),
|
||||
description text,
|
||||
is_active boolean DEFAULT true,
|
||||
onboarding_schema_key varchar(100),
|
||||
verification_required boolean DEFAULT true,
|
||||
switch_services_enabled boolean DEFAULT false,
|
||||
is_publicly_discoverable boolean DEFAULT true,
|
||||
sort_order integer DEFAULT 0,
|
||||
created_at timestamptz DEFAULT NOW(),
|
||||
updated_at timestamptz DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_external_roles_persona ON external_roles(persona_type_id);
|
||||
CREATE INDEX idx_external_roles_active ON external_roles(is_active);
|
||||
|
||||
-- ============================================
|
||||
-- modules
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS modules (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
module_key varchar(50) UNIQUE NOT NULL,
|
||||
module_name varchar(100) NOT NULL,
|
||||
category varchar(50), -- core/content/marketplace/work/financial
|
||||
description text,
|
||||
backend_domain varchar(100),
|
||||
default_route varchar(255),
|
||||
default_sidebar_label varchar(100),
|
||||
icon_key varchar(50),
|
||||
is_core boolean DEFAULT false,
|
||||
is_active boolean DEFAULT true,
|
||||
created_at timestamptz DEFAULT NOW(),
|
||||
updated_at timestamptz DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_modules_category ON modules(category);
|
||||
CREATE INDEX idx_modules_active ON modules(is_active);
|
||||
|
||||
-- ============================================
|
||||
-- role_module_access
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS role_module_access (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
external_role_id uuid NOT NULL REFERENCES external_roles(id) ON DELETE CASCADE,
|
||||
module_id uuid NOT NULL REFERENCES modules(id) ON DELETE CASCADE,
|
||||
is_enabled boolean DEFAULT true,
|
||||
is_sidebar_visible boolean DEFAULT true,
|
||||
sidebar_label_override varchar(100),
|
||||
route_override varchar(255),
|
||||
sort_order integer DEFAULT 0,
|
||||
created_at timestamptz DEFAULT NOW(),
|
||||
UNIQUE(external_role_id, module_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_role_module_access_role ON role_module_access(external_role_id);
|
||||
|
||||
-- ============================================
|
||||
-- module_actions
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS module_actions (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
module_id uuid NOT NULL REFERENCES modules(id) ON DELETE CASCADE,
|
||||
action_key varchar(50) NOT NULL,
|
||||
action_name varchar(100) NOT NULL,
|
||||
description text,
|
||||
is_active boolean DEFAULT true,
|
||||
created_at timestamptz DEFAULT NOW(),
|
||||
UNIQUE(module_id, action_key)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_module_actions_module ON module_actions(module_id);
|
||||
|
||||
-- ============================================
|
||||
-- role_module_permissions
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS role_module_permissions (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
external_role_id uuid NOT NULL REFERENCES external_roles(id) ON DELETE CASCADE,
|
||||
module_id uuid NOT NULL REFERENCES modules(id) ON DELETE CASCADE,
|
||||
can_view boolean DEFAULT false,
|
||||
can_list boolean DEFAULT false,
|
||||
can_create boolean DEFAULT false,
|
||||
can_update boolean DEFAULT false,
|
||||
can_delete boolean DEFAULT false,
|
||||
extra_actions_json jsonb DEFAULT '{}',
|
||||
created_at timestamptz DEFAULT NOW(),
|
||||
UNIQUE(external_role_id, module_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_role_module_permissions_role ON role_module_permissions(external_role_id);
|
||||
|
||||
-- ============================================
|
||||
-- role_module_widgets
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS role_module_widgets (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
external_role_id uuid NOT NULL REFERENCES external_roles(id) ON DELETE CASCADE,
|
||||
module_id uuid NOT NULL REFERENCES modules(id) ON DELETE CASCADE,
|
||||
widget_key varchar(50),
|
||||
is_enabled boolean DEFAULT true,
|
||||
sort_order integer DEFAULT 0,
|
||||
created_at timestamptz DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_role_module_widgets_role ON role_module_widgets(external_role_id);
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
-- Rollback Phase 1 cleanup
|
||||
|
||||
-- ============================================
|
||||
-- RECREATE: external_roles table
|
||||
-- ============================================
|
||||
CREATE TABLE external_roles (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
role_id uuid NOT NULL REFERENCES roles(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE UNIQUE INDEX external_roles_role_id_key ON external_roles(role_id);
|
||||
|
||||
-- ============================================
|
||||
-- RENAME BACK: Tables to original names
|
||||
-- ============================================
|
||||
ALTER TABLE internal_role_details RENAME TO internal_roles;
|
||||
ALTER TABLE role_admin_permissions RENAME TO role_permissions;
|
||||
ALTER TABLE permission_definitions RENAME TO permissions;
|
||||
ALTER TABLE role_sidebar_configs RENAME TO dashboard_configs;
|
||||
ALTER TABLE role_runtime_configs RENAME TO runtime_configs;
|
||||
ALTER TABLE user_role_assignments RENAME TO user_roles;
|
||||
ALTER TABLE role_dashboard_widgets RENAME TO dashboard_widgets;
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
-- Phase 1: Database cleanup - Drop redundant tables and rename for admin clarity
|
||||
-- Date: 2026-04-20
|
||||
|
||||
-- ============================================
|
||||
-- DROP: Remove redundant external_roles table
|
||||
-- Reason: roles.audience = 'EXTERNAL' already identifies external roles
|
||||
-- This table just adds a 1:1 mapping with no extra fields
|
||||
-- ============================================
|
||||
DROP TABLE IF EXISTS external_roles;
|
||||
|
||||
-- ============================================
|
||||
-- RENAME: Tables for admin clarity
|
||||
-- ============================================
|
||||
|
||||
-- internal_roles → internal_role_details
|
||||
ALTER TABLE internal_roles RENAME TO internal_role_details;
|
||||
|
||||
-- role_permissions → role_admin_permissions
|
||||
ALTER TABLE role_permissions RENAME TO role_admin_permissions;
|
||||
|
||||
-- permissions → permission_definitions
|
||||
ALTER TABLE permissions RENAME TO permission_definitions;
|
||||
|
||||
-- dashboard_configs → role_sidebar_configs
|
||||
ALTER TABLE dashboard_configs RENAME TO role_sidebar_configs;
|
||||
|
||||
-- runtime_configs → role_runtime_configs
|
||||
ALTER TABLE runtime_configs RENAME TO role_runtime_configs;
|
||||
|
||||
-- user_roles → user_role_assignments
|
||||
ALTER TABLE user_roles RENAME TO user_role_assignments;
|
||||
|
||||
-- dashboard_widgets → role_dashboard_widgets
|
||||
ALTER TABLE dashboard_widgets RENAME TO role_dashboard_widgets;
|
||||
|
||||
-- ============================================
|
||||
-- UPDATE: Sequences for renamed tables
|
||||
-- ============================================
|
||||
ALTER SEQUENCE internal_roles_id_seq RENAME TO internal_role_details_id_seq;
|
||||
ALTER SEQUENCE role_permissions_id_seq RENAME TO role_admin_permissions_id_seq;
|
||||
ALTER SEQUENCE permissions_id_seq RENAME TO permission_definitions_id_seq;
|
||||
ALTER SEQUENCE dashboard_configs_id_seq RENAME TO role_sidebar_configs_id_seq;
|
||||
ALTER SEQUENCE runtime_configs_id_seq RENAME TO role_runtime_configs_id_seq;
|
||||
ALTER SEQUENCE user_roles_id_seq RENAME TO user_role_assignments_id_seq;
|
||||
ALTER SEQUENCE dashboard_widgets_id_seq RENAME TO role_dashboard_widgets_id_seq;
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
-- Rollback Phase 3: External Role Management - Module System
|
||||
|
||||
-- ============================================
|
||||
-- DROP: New module system tables
|
||||
-- ============================================
|
||||
DROP TABLE IF EXISTS role_module_variant_mapping;
|
||||
DROP TABLE IF EXISTS module_variants;
|
||||
DROP TABLE IF EXISTS role_module_widgets;
|
||||
DROP TABLE IF EXISTS role_module_permissions;
|
||||
DROP TABLE IF EXISTS module_actions;
|
||||
DROP TABLE IF EXISTS role_module_access;
|
||||
DROP TABLE IF EXISTS modules;
|
||||
DROP TABLE IF EXISTS persona_types;
|
||||
|
||||
-- ============================================
|
||||
-- REMOVE COLUMNS FROM ROLES
|
||||
-- ============================================
|
||||
ALTER TABLE roles DROP COLUMN IF EXISTS persona_type;
|
||||
ALTER TABLE roles DROP COLUMN IF EXISTS onboarding_schema_key;
|
||||
ALTER TABLE roles DROP COLUMN IF EXISTS verification_required;
|
||||
ALTER TABLE roles DROP COLUMN IF EXISTS switch_services_enabled;
|
||||
ALTER TABLE roles DROP COLUMN IF EXISTS is_publicly_discoverable;
|
||||
ALTER TABLE roles DROP COLUMN IF EXISTS external_role_description;
|
||||
ALTER TABLE roles DROP COLUMN IF EXISTS sort_order;
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
-- Seed data for External Role Management Module System
|
||||
-- Phase 3 seed
|
||||
|
||||
-- ============================================
|
||||
-- SEED: Persona Types
|
||||
-- ============================================
|
||||
INSERT INTO persona_types (code, name, description) VALUES
|
||||
('PROFESSIONAL', 'Professional', 'Freelance professionals offering services'),
|
||||
('COMPANY', 'Company', 'Business accounts posting jobs'),
|
||||
('JOB_SEEKER', 'Job Seeker', 'Individuals seeking employment'),
|
||||
('CUSTOMER', 'Customer', 'Customers seeking services')
|
||||
ON CONFLICT (code) DO NOTHING;
|
||||
|
||||
-- ============================================
|
||||
-- SEED: Modules (23 total)
|
||||
-- ============================================
|
||||
|
||||
-- Core Shared Modules
|
||||
INSERT INTO modules (module_key, module_name, category, description, default_route, default_sidebar_label, icon_key, is_core) VALUES
|
||||
('dashboard_home', 'Dashboard', 'core', 'Dashboard home page with KPIs and widgets', '/dashboard', 'My Dashboard', 'layout-dashboard', true),
|
||||
('profile', 'Profile', 'core', 'User profile management', '/dashboard/profile', 'My Profile', 'user', true),
|
||||
('verification', 'Verification', 'core', 'Verification status and documents', '/dashboard/verification', 'Verification', 'shield-check', true),
|
||||
('help_center', 'Help Center', 'core', 'Help and support', '/dashboard/help', 'Help Center', 'help-circle', true),
|
||||
('settings', 'Settings', 'core', 'Account settings', '/dashboard/settings', 'Settings', 'settings', true),
|
||||
('switch_services', 'Switch Services', 'core', 'Switch to different roles', '/dashboard/switch', 'Switch Services', 'repeat', true),
|
||||
('explore_nxtgauge', 'Explore Nxtgauge', 'core', 'Register for additional roles', '/dashboard/explore', 'Explore Nxtgauge', 'compass', true)
|
||||
ON CONFLICT (module_key) DO NOTHING;
|
||||
|
||||
-- Content and Identity Modules
|
||||
INSERT INTO modules (module_key, module_name, category, description, default_route, default_sidebar_label, icon_key, is_core) VALUES
|
||||
('portfolio', 'Portfolio', 'content', 'Portfolio showcase', '/dashboard/portfolio', 'My Portfolio', 'image', false),
|
||||
('services', 'Services', 'content', 'Services offered', '/dashboard/services', 'My Services', 'briefcase', false)
|
||||
ON CONFLICT (module_key) DO NOTHING;
|
||||
|
||||
-- Marketplace and Discovery Modules
|
||||
INSERT INTO modules (module_key, module_name, category, description, default_route, default_sidebar_label, icon_key, is_core) VALUES
|
||||
('marketplace', 'Marketplace', 'marketplace', 'Service marketplace', '/dashboard/marketplace', 'Marketplace', 'store', false),
|
||||
('browse_jobs', 'Browse Jobs', 'marketplace', 'Browse available jobs', '/dashboard/jobs', 'Browse Jobs', 'search', false),
|
||||
('saved_jobs', 'Saved Jobs', 'marketplace', 'Saved job listings', '/dashboard/saved-jobs', 'Saved Jobs', 'bookmark', false)
|
||||
ON CONFLICT (module_key) DO NOTHING;
|
||||
|
||||
-- Work and Response Modules
|
||||
INSERT INTO modules (module_key, module_name, category, description, default_route, default_sidebar_label, icon_key, is_core) VALUES
|
||||
('jobs', 'Jobs', 'work', 'Job postings management', '/dashboard/jobs', 'Jobs', 'briefcase', false),
|
||||
('applications', 'Applications', 'work', 'Job applications received', '/dashboard/applications', 'Applications', 'file-text', false),
|
||||
('my_applications', 'My Applications', 'work', 'My job applications', '/dashboard/my-applications', 'My Applications', 'send', false),
|
||||
('requirements', 'Requirements', 'work', 'Customer requirements', '/dashboard/requirements', 'Requirements', 'list', false),
|
||||
('leads', 'Leads', 'work', 'Leads and inquiries', '/dashboard/leads', 'Leads', 'zap', false),
|
||||
('my_responses', 'My Responses', 'work', 'My responses to requirements', '/dashboard/responses', 'My Responses', 'message-circle', false),
|
||||
('received_responses', 'Received Responses', 'work', 'Responses to my requirements', '/dashboard/received-responses', 'Received Responses', 'inbox', false),
|
||||
('shortlisted_candidates', 'Shortlisted Candidates', 'work', 'Shortlisted candidates', '/dashboard/shortlisted', 'Shortlisted Candidates', 'users', false),
|
||||
('shortlisted_responses', 'Shortlisted Responses', 'work', 'Shortlisted responses', '/dashboard/shortlisted-responses', 'Shortlisted Responses', 'star', false)
|
||||
ON CONFLICT (module_key) DO NOTHING;
|
||||
|
||||
-- Financial Modules
|
||||
INSERT INTO modules (module_key, module_name, category, description, default_route, default_sidebar_label, icon_key, is_core) VALUES
|
||||
('wallet', 'Wallet', 'financial', 'Wallet and payments', '/dashboard/wallet', 'Wallet', 'wallet', false),
|
||||
('credits', 'Credits', 'financial', 'Credits management', '/dashboard/credits', 'Credits', 'credit-card', false)
|
||||
ON CONFLICT (module_key) DO NOTHING;
|
||||
|
||||
-- ============================================
|
||||
-- SEED: Update external roles with persona_type
|
||||
-- ============================================
|
||||
UPDATE roles SET persona_type = 'COMPANY' WHERE key = 'COMPANY';
|
||||
UPDATE roles SET persona_type = 'JOB_SEEKER' WHERE key = 'JOB_SEEKER';
|
||||
UPDATE roles SET persona_type = 'CUSTOMER' WHERE key = 'CUSTOMER';
|
||||
UPDATE roles SET persona_type = 'PROFESSIONAL' WHERE key IN ('PHOTOGRAPHER', 'MAKEUP_ARTIST', 'TUTOR', 'DEVELOPER', 'VIDEO_EDITOR', 'GRAPHIC_DESIGNER', 'SOCIAL_MEDIA_MANAGER', 'FITNESS_TRAINER', 'CATERING_SERVICES');
|
||||
|
||||
-- ============================================
|
||||
-- SEED: Module Actions (generic CRUD + domain)
|
||||
-- ============================================
|
||||
DO $$
|
||||
DECLARE
|
||||
mod_record RECORD;
|
||||
BEGIN
|
||||
FOR mod_record IN SELECT id, module_key FROM modules LOOP
|
||||
-- Generic CRUD actions
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description)
|
||||
VALUES
|
||||
(mod_record.id, 'view', 'View', 'View ' || mod_record.module_key)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description)
|
||||
VALUES
|
||||
(mod_record.id, 'list', 'List', 'List ' || mod_record.module_key)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description)
|
||||
VALUES
|
||||
(mod_record.id, 'create', 'Create', 'Create ' || mod_record.module_key)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description)
|
||||
VALUES
|
||||
(mod_record.id, 'update', 'Update', 'Update ' || mod_record.module_key)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description)
|
||||
VALUES
|
||||
(mod_record.id, 'delete', 'Delete', 'Delete ' || mod_record.module_key)
|
||||
ON CONFLICT DO NOTHING;
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
-- Domain-specific actions
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description)
|
||||
SELECT m.id, 'publish', 'Publish', 'Publish content'
|
||||
FROM modules m WHERE m.module_key IN ('portfolio', 'jobs', 'services')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description)
|
||||
SELECT m.id, 'submit', 'Submit', 'Submit for review'
|
||||
FROM modules m WHERE m.module_key IN ('verification', 'applications', 'requirements')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description)
|
||||
SELECT m.id, 'shortlist', 'Shortlist', 'Shortlist item'
|
||||
FROM modules m WHERE m.module_key IN ('shortlisted_candidates', 'shortlisted_responses')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description)
|
||||
SELECT m.id, 'respond', 'Respond', 'Respond to requirement'
|
||||
FROM modules m WHERE m.module_key IN ('my_responses', 'received_responses')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description)
|
||||
SELECT m.id, 'buy_credits', 'Buy Credits', 'Purchase credits'
|
||||
FROM modules m WHERE m.module_key = 'credits'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description)
|
||||
SELECT m.id, 'request_payout', 'Request Payout', 'Request wallet payout'
|
||||
FROM modules m WHERE m.module_key = 'wallet'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description)
|
||||
SELECT m.id, 'switch', 'Switch', 'Switch to this service'
|
||||
FROM modules m WHERE m.module_key = 'switch_services'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description)
|
||||
SELECT m.id, 'start_onboarding', 'Start Onboarding', 'Start new role onboarding'
|
||||
FROM modules m WHERE m.module_key = 'explore_nxtgauge'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description)
|
||||
SELECT m.id, 'resubmit', 'Resubmit', 'Resubmit for verification'
|
||||
FROM modules m WHERE m.module_key = 'verification'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
-- Seed: Default Role Module Access based on spec Section 7
|
||||
-- Fixed: removed sort_order reference
|
||||
|
||||
-- ============================================
|
||||
-- PROFESSIONAL roles (PHOTOGRAPHER, MAKEUP_ARTIST, TUTOR, DEVELOPER, VIDEO_EDITOR, GRAPHIC_DESIGNER, SOCIAL_MEDIA_MANAGER, FITNESS_TRAINER, CATERING_SERVICES)
|
||||
-- Enabled: dashboard_home, profile, portfolio, services, marketplace, leads, my_responses, wallet, credits, verification, help_center, settings, switch_services, explore_nxtgauge
|
||||
-- ============================================
|
||||
|
||||
INSERT INTO role_module_access (role_id, module_id, is_enabled, is_sidebar_visible, sort_order)
|
||||
SELECT r.id, m.id, true, true, 0
|
||||
FROM roles r
|
||||
CROSS JOIN modules m
|
||||
WHERE r.audience = 'EXTERNAL'
|
||||
AND r.key IN ('PHOTOGRAPHER', 'MAKEUP_ARTIST', 'TUTOR', 'DEVELOPER', 'VIDEO_EDITOR', 'GRAPHIC_DESIGNER', 'SOCIAL_MEDIA_MANAGER', 'FITNESS_TRAINER', 'CATERING_SERVICES')
|
||||
AND m.module_key IN ('dashboard_home', 'profile', 'portfolio', 'services', 'marketplace', 'leads', 'my_responses', 'wallet', 'credits', 'verification', 'help_center', 'settings', 'switch_services', 'explore_nxtgauge')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- ============================================
|
||||
-- COMPANY role
|
||||
-- Enabled: dashboard_home, profile, jobs, applications, shortlisted_candidates, credits, verification, help_center, settings, switch_services, explore_nxtgauge
|
||||
-- ============================================
|
||||
|
||||
INSERT INTO role_module_access (role_id, module_id, is_enabled, is_sidebar_visible, sort_order)
|
||||
SELECT r.id, m.id, true, true, 0
|
||||
FROM roles r
|
||||
CROSS JOIN modules m
|
||||
WHERE r.key = 'COMPANY'
|
||||
AND m.module_key IN ('dashboard_home', 'profile', 'jobs', 'applications', 'shortlisted_candidates', 'credits', 'verification', 'help_center', 'settings', 'switch_services', 'explore_nxtgauge')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- ============================================
|
||||
-- JOB_SEEKER role
|
||||
-- Enabled: dashboard_home, profile, portfolio, browse_jobs, my_applications, saved_jobs, verification, help_center, settings, switch_services, explore_nxtgauge
|
||||
-- ============================================
|
||||
|
||||
INSERT INTO role_module_access (role_id, module_id, is_enabled, is_sidebar_visible, sort_order)
|
||||
SELECT r.id, m.id, true, true, 0
|
||||
FROM roles r
|
||||
CROSS JOIN modules m
|
||||
WHERE r.key = 'JOB_SEEKER'
|
||||
AND m.module_key IN ('dashboard_home', 'profile', 'portfolio', 'browse_jobs', 'my_applications', 'saved_jobs', 'verification', 'help_center', 'settings', 'switch_services', 'explore_nxtgauge')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- ============================================
|
||||
-- CUSTOMER role
|
||||
-- Enabled: dashboard_home, profile, requirements, received_responses, shortlisted_responses, credits, verification, help_center, settings, switch_services, explore_nxtgauge
|
||||
-- ============================================
|
||||
|
||||
INSERT INTO role_module_access (role_id, module_id, is_enabled, is_sidebar_visible, sort_order)
|
||||
SELECT r.id, m.id, true, true, 0
|
||||
FROM roles r
|
||||
CROSS JOIN modules m
|
||||
WHERE r.key = 'CUSTOMER'
|
||||
AND m.module_key IN ('dashboard_home', 'profile', 'requirements', 'received_responses', 'shortlisted_responses', 'credits', 'verification', 'help_center', 'settings', 'switch_services', 'explore_nxtgauge')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- ============================================
|
||||
-- SEED: Default Role Module Permissions (basic CRUD)
|
||||
-- All external roles get view/list/create/update on their enabled modules
|
||||
-- ============================================
|
||||
|
||||
INSERT INTO role_module_permissions (role_id, module_id, can_view, can_list, can_create, can_update, can_delete)
|
||||
SELECT rma.role_id, rma.module_id, true, true, true, true, false
|
||||
FROM role_module_access rma
|
||||
JOIN roles r ON r.id = rma.role_id
|
||||
WHERE r.audience = 'EXTERNAL'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- ============================================
|
||||
-- SEED: Module Variants for profile and portfolio
|
||||
-- ============================================
|
||||
|
||||
-- Profile variants for PROFESSIONAL roles
|
||||
INSERT INTO module_variants (module_id, variant_key, variant_name, role_code, persona_type, schema_key, ui_template_key)
|
||||
SELECT m.id, 'profile.' || LOWER(r.key), 'Profile - ' || r.name, r.key, 'PROFESSIONAL', 'profile_' || LOWER(r.key), 'profile_professional'
|
||||
FROM modules m
|
||||
CROSS JOIN roles r
|
||||
WHERE m.module_key = 'profile'
|
||||
AND r.persona_type = 'PROFESSIONAL'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Portfolio variants for PROFESSIONAL roles
|
||||
INSERT INTO module_variants (module_id, variant_key, variant_name, role_code, persona_type, schema_key, ui_template_key)
|
||||
SELECT m.id, 'portfolio.' || LOWER(r.key), 'Portfolio - ' || r.name, r.key, 'PROFESSIONAL', 'portfolio_' || LOWER(r.key), 'portfolio_professional'
|
||||
FROM modules m
|
||||
CROSS JOIN roles r
|
||||
WHERE m.module_key = 'portfolio'
|
||||
AND r.persona_type = 'PROFESSIONAL'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Profile variant for COMPANY
|
||||
INSERT INTO module_variants (module_id, variant_key, variant_name, role_code, schema_key, ui_template_key)
|
||||
SELECT m.id, 'profile.company', 'Profile - Company', 'COMPANY', 'profile_company', 'profile_company'
|
||||
FROM modules m
|
||||
WHERE m.module_key = 'profile'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Profile variant for JOB_SEEKER
|
||||
INSERT INTO module_variants (module_id, variant_key, variant_name, role_code, schema_key, ui_template_key)
|
||||
SELECT m.id, 'profile.job_seeker', 'Profile - Job Seeker', 'JOB_SEEKER', 'profile_job_seeker', 'profile_job_seeker'
|
||||
FROM modules m
|
||||
WHERE m.module_key = 'profile'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Profile variant for CUSTOMER
|
||||
INSERT INTO module_variants (module_id, variant_key, variant_name, role_code, schema_key, ui_template_key)
|
||||
SELECT m.id, 'profile.customer', 'Profile - Customer', 'CUSTOMER', 'profile_customer', 'profile_customer'
|
||||
FROM modules m
|
||||
WHERE m.module_key = 'profile'
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- ============================================
|
||||
-- SEED: Role Module Variant Mappings
|
||||
-- ============================================
|
||||
|
||||
INSERT INTO role_module_variant_mapping (role_id, module_id, module_variant_id)
|
||||
SELECT r.id, mv.module_id, mv.id
|
||||
FROM module_variants mv
|
||||
JOIN roles r ON r.key = mv.role_code
|
||||
ON CONFLICT DO NOTHING;
|
||||
157
crates/db/migrations/20260420000003_external_role_modules.up.sql
Normal file
157
crates/db/migrations/20260420000003_external_role_modules.up.sql
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
-- Phase 3: External Role Management - Module System
|
||||
-- This migration creates the module registry and role-module mapping
|
||||
-- Note: external_roles table was dropped - external role settings are now in roles table
|
||||
|
||||
-- ============================================
|
||||
-- ADD COLUMNS TO ROLES for external role settings
|
||||
-- ============================================
|
||||
ALTER TABLE roles ADD COLUMN IF NOT EXISTS persona_type varchar(50);
|
||||
ALTER TABLE roles ADD COLUMN IF NOT EXISTS onboarding_schema_key varchar(100);
|
||||
ALTER TABLE roles ADD COLUMN IF NOT EXISTS verification_required boolean DEFAULT true;
|
||||
ALTER TABLE roles ADD COLUMN IF NOT EXISTS switch_services_enabled boolean DEFAULT false;
|
||||
ALTER TABLE roles ADD COLUMN IF NOT EXISTS is_publicly_discoverable boolean DEFAULT true;
|
||||
ALTER TABLE roles ADD COLUMN IF NOT EXISTS external_role_description text;
|
||||
ALTER TABLE roles ADD COLUMN IF NOT EXISTS sort_order integer DEFAULT 0;
|
||||
|
||||
-- ============================================
|
||||
-- persona_types (categories for external roles)
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS persona_types (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
code varchar(50) UNIQUE NOT NULL,
|
||||
name varchar(100) NOT NULL,
|
||||
description text,
|
||||
is_active boolean DEFAULT true,
|
||||
created_at timestamptz DEFAULT NOW(),
|
||||
updated_at timestamptz DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- ============================================
|
||||
-- modules (module registry)
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS modules (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
module_key varchar(50) UNIQUE NOT NULL,
|
||||
module_name varchar(100) NOT NULL,
|
||||
category varchar(50), -- core/content/marketplace/work/financial
|
||||
description text,
|
||||
backend_domain varchar(100),
|
||||
default_route varchar(255),
|
||||
default_sidebar_label varchar(100),
|
||||
icon_key varchar(50),
|
||||
is_core boolean DEFAULT false,
|
||||
is_active boolean DEFAULT true,
|
||||
created_at timestamptz DEFAULT NOW(),
|
||||
updated_at timestamptz DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_modules_category ON modules(category);
|
||||
CREATE INDEX IF NOT EXISTS idx_modules_active ON modules(is_active);
|
||||
|
||||
-- ============================================
|
||||
-- role_module_access (module visibility per role)
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS role_module_access (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
role_id uuid NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
|
||||
module_id uuid NOT NULL REFERENCES modules(id) ON DELETE CASCADE,
|
||||
is_enabled boolean DEFAULT true,
|
||||
is_sidebar_visible boolean DEFAULT true,
|
||||
sidebar_label_override varchar(100),
|
||||
route_override varchar(255),
|
||||
sort_order integer DEFAULT 0,
|
||||
created_at timestamptz DEFAULT NOW(),
|
||||
UNIQUE(role_id, module_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_role_module_access_role ON role_module_access(role_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_role_module_access_module ON role_module_access(module_id);
|
||||
|
||||
-- ============================================
|
||||
-- module_actions (CRUD actions per module)
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS module_actions (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
module_id uuid NOT NULL REFERENCES modules(id) ON DELETE CASCADE,
|
||||
action_key varchar(50) NOT NULL,
|
||||
action_name varchar(100) NOT NULL,
|
||||
description text,
|
||||
is_active boolean DEFAULT true,
|
||||
created_at timestamptz DEFAULT NOW(),
|
||||
UNIQUE(module_id, action_key)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_module_actions_module ON module_actions(module_id);
|
||||
|
||||
-- ============================================
|
||||
-- role_module_permissions (permissions per module per role)
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS role_module_permissions (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
role_id uuid NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
|
||||
module_id uuid NOT NULL REFERENCES modules(id) ON DELETE CASCADE,
|
||||
can_view boolean DEFAULT false,
|
||||
can_list boolean DEFAULT false,
|
||||
can_create boolean DEFAULT false,
|
||||
can_update boolean DEFAULT false,
|
||||
can_delete boolean DEFAULT false,
|
||||
extra_actions_json jsonb DEFAULT '{}',
|
||||
created_at timestamptz DEFAULT NOW(),
|
||||
UNIQUE(role_id, module_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_role_module_permissions_role ON role_module_permissions(role_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_role_module_permissions_module ON role_module_permissions(module_id);
|
||||
|
||||
-- ============================================
|
||||
-- role_module_widgets (widgets per module per role)
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS role_module_widgets (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
role_id uuid NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
|
||||
module_id uuid NOT NULL REFERENCES modules(id) ON DELETE CASCADE,
|
||||
widget_key varchar(50),
|
||||
is_enabled boolean DEFAULT true,
|
||||
sort_order integer DEFAULT 0,
|
||||
created_at timestamptz DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_role_module_widgets_role ON role_module_widgets(role_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_role_module_widgets_module ON role_module_widgets(module_id);
|
||||
|
||||
-- ============================================
|
||||
-- module_variants (role-specific module variants)
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS module_variants (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
module_id uuid NOT NULL REFERENCES modules(id) ON DELETE CASCADE,
|
||||
variant_key varchar(50) NOT NULL,
|
||||
variant_name varchar(100) NOT NULL,
|
||||
role_code varchar(50), -- target role (e.g., PHOTOGRAPHER, TUTOR)
|
||||
persona_type varchar(50), -- target persona (e.g., PROFESSIONAL)
|
||||
schema_key varchar(100),
|
||||
ui_template_key varchar(100),
|
||||
is_active boolean DEFAULT true,
|
||||
created_at timestamptz DEFAULT NOW(),
|
||||
updated_at timestamptz DEFAULT NOW(),
|
||||
UNIQUE(module_id, variant_key)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_module_variants_module ON module_variants(module_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_module_variants_role ON module_variants(role_code);
|
||||
|
||||
-- ============================================
|
||||
-- role_module_variant_mapping (which variants a role uses)
|
||||
-- ============================================
|
||||
CREATE TABLE IF NOT EXISTS role_module_variant_mapping (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
role_id uuid NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
|
||||
module_id uuid NOT NULL REFERENCES modules(id) ON DELETE CASCADE,
|
||||
module_variant_id uuid NOT NULL REFERENCES module_variants(id) ON DELETE CASCADE,
|
||||
is_active boolean DEFAULT true,
|
||||
created_at timestamptz DEFAULT NOW(),
|
||||
UNIQUE(role_id, module_id, module_variant_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_role_module_variant_mapping_role ON role_module_variant_mapping(role_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_role_module_variant_mapping_module ON role_module_variant_mapping(module_id);
|
||||
84
crates/db/migrations/20260422000000_seed_widgets.seed.sql
Normal file
84
crates/db/migrations/20260422000000_seed_widgets.seed.sql
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
-- Seed widgets into existing role_sidebar_configs for EXTERNAL roles
|
||||
-- This ensures the widget-based dashboard renders correctly for all roles.
|
||||
--
|
||||
-- Run this AFTER migrations are applied:
|
||||
-- psql $DATABASE_URL -f crates/db/migrations/YYYYMMDDTTTTTT_add_widgets_to_sidebar_configs.seed.sql
|
||||
|
||||
-- Update COMPANY roles
|
||||
UPDATE role_sidebar_configs
|
||||
SET config_json = jsonb_set(
|
||||
COALESCE(config_json, '{}'::jsonb),
|
||||
'{widgets}',
|
||||
'["total_jobs", "active_jobs", "pending_jobs", "applications_received", "shortlisted_candidates", "credits"]'::jsonb,
|
||||
true
|
||||
)
|
||||
WHERE audience = 'EXTERNAL'
|
||||
AND is_active = true
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM roles r WHERE r.id = role_sidebar_configs.role_id AND r.key = 'COMPANY'
|
||||
);
|
||||
|
||||
-- Update JOB_SEEKER roles
|
||||
UPDATE role_sidebar_configs
|
||||
SET config_json = jsonb_set(
|
||||
COALESCE(config_json, '{}'::jsonb),
|
||||
'{widgets}',
|
||||
'["available_jobs", "my_applications", "shortlisted", "saved_jobs", "profile_status", "portfolio"]'::jsonb,
|
||||
true
|
||||
)
|
||||
WHERE audience = 'EXTERNAL'
|
||||
AND is_active = true
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM roles r WHERE r.id = role_sidebar_configs.role_id AND r.key = 'JOB_SEEKER'
|
||||
);
|
||||
|
||||
-- Update CUSTOMER roles
|
||||
UPDATE role_sidebar_configs
|
||||
SET config_json = jsonb_set(
|
||||
COALESCE(config_json, '{}'::jsonb),
|
||||
'{widgets}',
|
||||
'["total_requirements", "open_requirements", "closed_requirements", "responses_received", "shortlisted_responses", "credits"]'::jsonb,
|
||||
true
|
||||
)
|
||||
WHERE audience = 'EXTERNAL'
|
||||
AND is_active = true
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM roles r WHERE r.id = role_sidebar_configs.role_id AND r.key = 'CUSTOMER'
|
||||
);
|
||||
|
||||
-- Update all remaining EXTERNAL roles (PROFESSIONAL: photographer, makeup, tutor, etc.)
|
||||
UPDATE role_sidebar_configs
|
||||
SET config_json = jsonb_set(
|
||||
COALESCE(config_json, '{}'::jsonb),
|
||||
'{widgets}',
|
||||
'["open_leads", "my_requests", "accepted_requests", "tracecoins", "portfolio", "profile_status"]'::jsonb,
|
||||
true
|
||||
)
|
||||
WHERE audience = 'EXTERNAL'
|
||||
AND is_active = true
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM roles r WHERE r.id = role_sidebar_configs.role_id AND r.key IN ('COMPANY', 'JOB_SEEKER', 'CUSTOMER')
|
||||
);
|
||||
|
||||
-- Also seed widgets in role_runtime_configs if they differ from role_sidebar_configs
|
||||
-- (some setups read widgets from runtime_configs)
|
||||
UPDATE role_runtime_configs
|
||||
SET config_json = jsonb_set(
|
||||
COALESCE(config_json, '{}'::jsonb),
|
||||
'{widgets}',
|
||||
COALESCE(
|
||||
(
|
||||
SELECT config_json->'widgets'
|
||||
FROM role_sidebar_configs sc
|
||||
WHERE sc.role_id = role_runtime_configs.role_id
|
||||
AND sc.audience = 'EXTERNAL'
|
||||
AND sc.is_active = true
|
||||
LIMIT 1
|
||||
),
|
||||
'["open_leads", "my_requests", "accepted_requests", "tracecoins", "portfolio", "profile_status"]'::jsonb
|
||||
),
|
||||
true
|
||||
)
|
||||
WHERE is_active = true;
|
||||
|
||||
SELECT 'Widget seed completed.' AS status;
|
||||
167
crates/db/migrations/20260422000000_seed_widgets.sql
Normal file
167
crates/db/migrations/20260422000000_seed_widgets.sql
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
-- Seed widgets into existing role_sidebar_configs for all EXTERNAL roles.
|
||||
-- Run this SQL directly against your database:
|
||||
-- psql $DATABASE_URL -f seeds/seed_widgets.sql
|
||||
--
|
||||
-- Or run individual sections below.
|
||||
|
||||
-- ── PHOTOGRAPHER ───────────────────────────────────────────────────────────────
|
||||
UPDATE role_sidebar_configs
|
||||
SET config_json = jsonb_set(
|
||||
COALESCE(config_json, '{}'::jsonb),
|
||||
'{widgets}',
|
||||
'["open_leads", "my_requests", "accepted_requests", "tracecoins", "portfolio", "profile_status"]'::jsonb,
|
||||
true
|
||||
)
|
||||
WHERE audience = 'EXTERNAL'
|
||||
AND is_active = true
|
||||
AND EXISTS (SELECT 1 FROM roles r WHERE r.id = role_sidebar_configs.role_id AND r.key = 'PHOTOGRAPHER');
|
||||
|
||||
-- ── MAKEUP ARTIST ──────────────────────────────────────────────────────────────
|
||||
UPDATE role_sidebar_configs
|
||||
SET config_json = jsonb_set(
|
||||
COALESCE(config_json, '{}'::jsonb),
|
||||
'{widgets}',
|
||||
'["open_leads", "my_requests", "accepted_requests", "tracecoins", "portfolio", "profile_status"]'::jsonb,
|
||||
true
|
||||
)
|
||||
WHERE audience = 'EXTERNAL'
|
||||
AND is_active = true
|
||||
AND EXISTS (SELECT 1 FROM roles r WHERE r.id = role_sidebar_configs.role_id AND r.key = 'MAKEUP_ARTIST');
|
||||
|
||||
-- ── TUTOR ─────────────────────────────────────────────────────────────────────
|
||||
UPDATE role_sidebar_configs
|
||||
SET config_json = jsonb_set(
|
||||
COALESCE(config_json, '{}'::jsonb),
|
||||
'{widgets}',
|
||||
'["open_leads", "my_requests", "accepted_requests", "tracecoins", "portfolio", "profile_status"]'::jsonb,
|
||||
true
|
||||
)
|
||||
WHERE audience = 'EXTERNAL'
|
||||
AND is_active = true
|
||||
AND EXISTS (SELECT 1 FROM roles r WHERE r.id = role_sidebar_configs.role_id AND r.key = 'TUTOR');
|
||||
|
||||
-- ── DEVELOPER ─────────────────────────────────────────────────────────────────
|
||||
UPDATE role_sidebar_configs
|
||||
SET config_json = jsonb_set(
|
||||
COALESCE(config_json, '{}'::jsonb),
|
||||
'{widgets}',
|
||||
'["open_leads", "my_requests", "accepted_requests", "tracecoins", "portfolio", "profile_status"]'::jsonb,
|
||||
true
|
||||
)
|
||||
WHERE audience = 'EXTERNAL'
|
||||
AND is_active = true
|
||||
AND EXISTS (SELECT 1 FROM roles r WHERE r.id = role_sidebar_configs.role_id AND r.key = 'DEVELOPER');
|
||||
|
||||
-- ── VIDEO EDITOR ──────────────────────────────────────────────────────────────
|
||||
UPDATE role_sidebar_configs
|
||||
SET config_json = jsonb_set(
|
||||
COALESCE(config_json, '{}'::jsonb),
|
||||
'{widgets}',
|
||||
'["open_leads", "my_requests", "accepted_requests", "tracecoins", "portfolio", "profile_status"]'::jsonb,
|
||||
true
|
||||
)
|
||||
WHERE audience = 'EXTERNAL'
|
||||
AND is_active = true
|
||||
AND EXISTS (SELECT 1 FROM roles r WHERE r.id = role_sidebar_configs.role_id AND r.key = 'VIDEO_EDITOR');
|
||||
|
||||
-- ── GRAPHIC DESIGNER ─────────────────────────────────────────────────────────
|
||||
UPDATE role_sidebar_configs
|
||||
SET config_json = jsonb_set(
|
||||
COALESCE(config_json, '{}'::jsonb),
|
||||
'{widgets}',
|
||||
'["open_leads", "my_requests", "accepted_requests", "tracecoins", "portfolio", "profile_status"]'::jsonb,
|
||||
true
|
||||
)
|
||||
WHERE audience = 'EXTERNAL'
|
||||
AND is_active = true
|
||||
AND EXISTS (SELECT 1 FROM roles r WHERE r.id = role_sidebar_configs.role_id AND r.key = 'GRAPHIC_DESIGNER');
|
||||
|
||||
-- ── SOCIAL MEDIA MANAGER ─────────────────────────────────────────────────────
|
||||
UPDATE role_sidebar_configs
|
||||
SET config_json = jsonb_set(
|
||||
COALESCE(config_json, '{}'::jsonb),
|
||||
'{widgets}',
|
||||
'["open_leads", "my_requests", "accepted_requests", "tracecoins", "portfolio", "profile_status"]'::jsonb,
|
||||
true
|
||||
)
|
||||
WHERE audience = 'EXTERNAL'
|
||||
AND is_active = true
|
||||
AND EXISTS (SELECT 1 FROM roles r WHERE r.id = role_sidebar_configs.role_id AND r.key = 'SOCIAL_MEDIA_MANAGER');
|
||||
|
||||
-- ── FITNESS TRAINER ──────────────────────────────────────────────────────────
|
||||
UPDATE role_sidebar_configs
|
||||
SET config_json = jsonb_set(
|
||||
COALESCE(config_json, '{}'::jsonb),
|
||||
'{widgets}',
|
||||
'["open_leads", "my_requests", "accepted_requests", "tracecoins", "portfolio", "profile_status"]'::jsonb,
|
||||
true
|
||||
)
|
||||
WHERE audience = 'EXTERNAL'
|
||||
AND is_active = true
|
||||
AND EXISTS (SELECT 1 FROM roles r WHERE r.id = role_sidebar_configs.role_id AND r.key = 'FITNESS_TRAINER');
|
||||
|
||||
-- ── CATERING SERVICES ─────────────────────────────────────────────────────────
|
||||
UPDATE role_sidebar_configs
|
||||
SET config_json = jsonb_set(
|
||||
COALESCE(config_json, '{}'::jsonb),
|
||||
'{widgets}',
|
||||
'["open_leads", "my_requests", "accepted_requests", "tracecoins", "portfolio", "profile_status"]'::jsonb,
|
||||
true
|
||||
)
|
||||
WHERE audience = 'EXTERNAL'
|
||||
AND is_active = true
|
||||
AND EXISTS (SELECT 1 FROM roles r WHERE r.id = role_sidebar_configs.role_id AND r.key = 'CATERING_SERVICES');
|
||||
|
||||
-- ── UGC CONTENT CREATOR ──────────────────────────────────────────────────────
|
||||
UPDATE role_sidebar_configs
|
||||
SET config_json = jsonb_set(
|
||||
COALESCE(config_json, '{}'::jsonb),
|
||||
'{widgets}',
|
||||
'["open_leads", "my_requests", "accepted_requests", "tracecoins", "portfolio", "profile_status"]'::jsonb,
|
||||
true
|
||||
)
|
||||
WHERE audience = 'EXTERNAL'
|
||||
AND is_active = true
|
||||
AND EXISTS (SELECT 1 FROM roles r WHERE r.id = role_sidebar_configs.role_id AND r.key = 'UGC_CONTENT_CREATOR');
|
||||
|
||||
-- ── COMPANY ────────────────────────────────────────────────────────────────────
|
||||
UPDATE role_sidebar_configs
|
||||
SET config_json = jsonb_set(
|
||||
COALESCE(config_json, '{}'::jsonb),
|
||||
'{widgets}',
|
||||
'["total_jobs", "active_jobs", "pending_jobs", "applications_received", "shortlisted_candidates", "credits"]'::jsonb,
|
||||
true
|
||||
)
|
||||
WHERE audience = 'EXTERNAL'
|
||||
AND is_active = true
|
||||
AND EXISTS (SELECT 1 FROM roles r WHERE r.id = role_sidebar_configs.role_id AND r.key = 'COMPANY');
|
||||
|
||||
-- ── JOB SEEKER ────────────────────────────────────────────────────────────────
|
||||
UPDATE role_sidebar_configs
|
||||
SET config_json = jsonb_set(
|
||||
COALESCE(config_json, '{}'::jsonb),
|
||||
'{widgets}',
|
||||
'["available_jobs", "my_applications", "shortlisted", "saved_jobs", "profile_status", "portfolio"]'::jsonb,
|
||||
true
|
||||
)
|
||||
WHERE audience = 'EXTERNAL'
|
||||
AND is_active = true
|
||||
AND EXISTS (SELECT 1 FROM roles r WHERE r.id = role_sidebar_configs.role_id AND r.key = 'JOB_SEEKER');
|
||||
|
||||
-- ── CUSTOMER ─────────────────────────────────────────────────────────────────
|
||||
UPDATE role_sidebar_configs
|
||||
SET config_json = jsonb_set(
|
||||
COALESCE(config_json, '{}'::jsonb),
|
||||
'{widgets}',
|
||||
'["total_requirements", "open_requirements", "closed_requirements", "responses_received", "shortlisted_responses", "credits"]'::jsonb,
|
||||
true
|
||||
)
|
||||
WHERE audience = 'EXTERNAL'
|
||||
AND is_active = true
|
||||
AND EXISTS (SELECT 1 FROM roles r WHERE r.id = role_sidebar_configs.role_id AND r.key = 'CUSTOMER');
|
||||
|
||||
-- Verify the updates
|
||||
SELECT r.key AS role, sc.config_json->'widgets' AS widgets
|
||||
FROM role_sidebar_configs sc
|
||||
JOIN roles r ON r.id = sc.role_id
|
||||
WHERE sc.audience = 'EXTERNAL' AND sc.is_active = true;
|
||||
|
|
@ -175,7 +175,7 @@ impl ConfigRepository {
|
|||
) -> Result<DashboardConfig, sqlx::Error> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE dashboard_configs
|
||||
UPDATE role_sidebar_configs
|
||||
SET is_active = false
|
||||
WHERE role_id = $1 AND audience = $2::text AND is_active = true
|
||||
"#,
|
||||
|
|
@ -187,7 +187,7 @@ impl ConfigRepository {
|
|||
|
||||
let config = sqlx::query_as::<_, DashboardConfig>(
|
||||
r#"
|
||||
INSERT INTO dashboard_configs (role_id, audience, config_json, is_active)
|
||||
INSERT INTO role_sidebar_configs (role_id, audience, config_json, is_active)
|
||||
VALUES (
|
||||
$1,
|
||||
$2::text,
|
||||
|
|
@ -214,7 +214,7 @@ impl ConfigRepository {
|
|||
let config = sqlx::query_as::<_, DashboardConfig>(
|
||||
r#"
|
||||
SELECT id, role_id, audience, config_json, is_active, updated_at
|
||||
FROM dashboard_configs
|
||||
FROM role_sidebar_configs
|
||||
WHERE role_id = $1 AND audience = $2 AND is_active = true
|
||||
"#,
|
||||
)
|
||||
|
|
@ -233,7 +233,7 @@ impl ConfigRepository {
|
|||
r#"
|
||||
SELECT
|
||||
c.id, c.role_id, r.key as role_key, c.audience, c.config_json, c.is_active, c.updated_at
|
||||
FROM dashboard_configs c
|
||||
FROM role_sidebar_configs c
|
||||
JOIN roles r ON c.role_id = r.id
|
||||
ORDER BY c.updated_at DESC
|
||||
"#,
|
||||
|
|
@ -252,7 +252,7 @@ impl ConfigRepository {
|
|||
let config = sqlx::query_as::<_, DashboardConfig>(
|
||||
r#"
|
||||
SELECT c.id, c.role_id, c.audience, c.config_json, c.is_active, c.updated_at
|
||||
FROM dashboard_configs c
|
||||
FROM role_sidebar_configs c
|
||||
JOIN roles r ON c.role_id = r.id
|
||||
WHERE r.key = $1 AND c.audience = $2 AND c.is_active = true
|
||||
"#,
|
||||
|
|
@ -272,7 +272,7 @@ impl ConfigRepository {
|
|||
// Soft-disable previous active configs for this role
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE runtime_configs
|
||||
UPDATE role_runtime_configs
|
||||
SET is_active = false
|
||||
WHERE role_id = $1 AND is_active = true
|
||||
"#,
|
||||
|
|
@ -284,11 +284,11 @@ impl ConfigRepository {
|
|||
// Insert new config
|
||||
let config = sqlx::query_as::<_, RuntimeConfig>(
|
||||
r#"
|
||||
INSERT INTO runtime_configs (role_id, config_json, version, is_active)
|
||||
INSERT INTO role_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,
|
||||
COALESCE((SELECT MAX(version) FROM role_runtime_configs WHERE role_id = $1), 0) + 1,
|
||||
true
|
||||
)
|
||||
RETURNING id, role_id, config_json, version, is_active, updated_at
|
||||
|
|
@ -309,7 +309,7 @@ impl ConfigRepository {
|
|||
let config = sqlx::query_as::<_, RuntimeConfig>(
|
||||
r#"
|
||||
SELECT id, role_id, config_json, version, is_active, updated_at
|
||||
FROM runtime_configs
|
||||
FROM role_runtime_configs
|
||||
WHERE role_id = $1 AND is_active = true
|
||||
"#,
|
||||
)
|
||||
|
|
@ -327,7 +327,7 @@ impl ConfigRepository {
|
|||
let config = sqlx::query_as::<_, RuntimeConfig>(
|
||||
r#"
|
||||
SELECT rc.id, rc.role_id, rc.config_json, rc.version, rc.is_active, rc.updated_at
|
||||
FROM runtime_configs rc
|
||||
FROM role_runtime_configs rc
|
||||
JOIN roles r ON rc.role_id = r.id
|
||||
WHERE r.key = $1 AND rc.is_active = true
|
||||
"#,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ pub struct Department {
|
|||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CreateDepartmentPayload {
|
||||
pub name: String,
|
||||
pub code: String,
|
||||
pub code: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub department_head: Option<String>,
|
||||
pub department_email: Option<String>,
|
||||
|
|
@ -31,6 +31,7 @@ pub struct DepartmentRepository;
|
|||
impl DepartmentRepository {
|
||||
pub async fn create(pool: &PgPool, payload: CreateDepartmentPayload) -> Result<Department, sqlx::Error> {
|
||||
let is_active = payload.status.map(|s| s.to_uppercase() == "ACTIVE").unwrap_or(true);
|
||||
let code = payload.code.filter(|c| !c.is_empty()).map(|c| c.to_uppercase());
|
||||
|
||||
sqlx::query_as::<_, Department>(
|
||||
r#"
|
||||
|
|
@ -43,7 +44,7 @@ impl DepartmentRepository {
|
|||
"#
|
||||
)
|
||||
.bind(payload.name)
|
||||
.bind(payload.code.to_uppercase())
|
||||
.bind(code)
|
||||
.bind(payload.description)
|
||||
.bind(payload.department_head)
|
||||
.bind(payload.department_email)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ pub struct Employee {
|
|||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub email: String,
|
||||
pub phone: Option<String>,
|
||||
pub password_hash: String,
|
||||
pub employee_code: Option<String>,
|
||||
pub department_id: Option<Uuid>,
|
||||
|
|
@ -35,6 +36,7 @@ pub struct CreateEmployeePayload {
|
|||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub email: String,
|
||||
pub phone: Option<String>,
|
||||
pub password_hash: String,
|
||||
pub department_id: Option<Uuid>,
|
||||
pub designation_id: Option<Uuid>,
|
||||
|
|
@ -48,14 +50,15 @@ impl EmployeeRepository {
|
|||
let level_code = payload.role_code.clone();
|
||||
sqlx::query_as::<_, Employee>(
|
||||
r#"
|
||||
INSERT INTO employees (first_name, last_name, email, password_hash, department_id, designation_id, role_code)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id, first_name, last_name, email, password_hash, employee_code, department_id, designation_id, role_code, status, joining_date, created_at, updated_at
|
||||
INSERT INTO employees (first_name, last_name, email, phone, password_hash, department_id, designation_id, role_code)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING id, first_name, last_name, email, phone, password_hash, employee_code, department_id, designation_id, role_code, status, joining_date, created_at, updated_at
|
||||
"#
|
||||
)
|
||||
.bind(payload.first_name)
|
||||
.bind(payload.last_name)
|
||||
.bind(payload.email.to_lowercase())
|
||||
.bind(payload.phone)
|
||||
.bind(payload.password_hash)
|
||||
.bind(payload.department_id)
|
||||
.bind(payload.designation_id)
|
||||
|
|
@ -64,6 +67,28 @@ impl EmployeeRepository {
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn create_with_code(pool: &PgPool, payload: CreateEmployeePayload, employee_code: Option<String>) -> Result<Employee, sqlx::Error> {
|
||||
let role_code = payload.role_code.clone();
|
||||
sqlx::query_as::<_, Employee>(
|
||||
r#"
|
||||
INSERT INTO employees (first_name, last_name, email, phone, password_hash, employee_code, department_id, designation_id, role_code)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
RETURNING id, first_name, last_name, email, phone, password_hash, employee_code, department_id, designation_id, role_code, status, joining_date, created_at, updated_at
|
||||
"#
|
||||
)
|
||||
.bind(payload.first_name)
|
||||
.bind(payload.last_name)
|
||||
.bind(payload.email.to_lowercase())
|
||||
.bind(payload.phone)
|
||||
.bind(payload.password_hash)
|
||||
.bind(employee_code)
|
||||
.bind(payload.department_id)
|
||||
.bind(payload.designation_id)
|
||||
.bind(role_code)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn update(
|
||||
pool: &PgPool,
|
||||
id: Uuid,
|
||||
|
|
@ -88,7 +113,7 @@ impl EmployeeRepository {
|
|||
status = COALESCE($7, status),
|
||||
updated_at = NOW()
|
||||
WHERE id = $8
|
||||
RETURNING id, first_name, last_name, email, password_hash, employee_code, department_id, designation_id, role_code, status, joining_date, created_at, updated_at
|
||||
RETURNING id, first_name, last_name, email, phone, password_hash, employee_code, department_id, designation_id, role_code, status, joining_date, created_at, updated_at
|
||||
"#
|
||||
)
|
||||
.bind(first_name)
|
||||
|
|
@ -113,7 +138,7 @@ impl EmployeeRepository {
|
|||
|
||||
pub async fn get_by_email(pool: &PgPool, email: &str) -> Result<Option<Employee>, sqlx::Error> {
|
||||
sqlx::query_as::<_, Employee>(
|
||||
"SELECT id, first_name, last_name, email, password_hash, employee_code, department_id, designation_id, role_code, status, joining_date, created_at, updated_at FROM employees WHERE email = $1"
|
||||
"SELECT id, first_name, last_name, email, phone, password_hash, employee_code, department_id, designation_id, role_code, status, joining_date, created_at, updated_at FROM employees WHERE email = $1"
|
||||
)
|
||||
.bind(email.to_lowercase())
|
||||
.fetch_optional(pool)
|
||||
|
|
@ -124,7 +149,7 @@ impl EmployeeRepository {
|
|||
let search = q.unwrap_or_default().to_lowercase();
|
||||
sqlx::query_as::<_, Employee>(
|
||||
r#"
|
||||
SELECT id, first_name, last_name, email, password_hash, employee_code, department_id, designation_id, role_code, status, joining_date, created_at, updated_at
|
||||
SELECT id, first_name, last_name, email, phone, password_hash, employee_code, department_id, designation_id, role_code, status, joining_date, created_at, updated_at
|
||||
FROM employees
|
||||
WHERE ($1 = '' OR LOWER(first_name) LIKE '%' || $1 || '%' OR LOWER(last_name) LIKE '%' || $1 || '%' OR LOWER(email) LIKE '%' || $1 || '%')
|
||||
ORDER BY last_name, first_name
|
||||
|
|
@ -154,4 +179,23 @@ impl EmployeeRepository {
|
|||
.fetch_one(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn change_password(
|
||||
pool: &PgPool,
|
||||
id: Uuid,
|
||||
password_hash: &str,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE employees
|
||||
SET password_hash = $2, updated_at = NOW()
|
||||
WHERE id = $1
|
||||
"#
|
||||
)
|
||||
.bind(id)
|
||||
.bind(password_hash)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ impl PhotographerRepository {
|
|||
pp.created_at, pp.updated_at
|
||||
FROM photographer_profiles pp
|
||||
INNER JOIN user_role_profiles urp ON urp.id = pp.user_role_profile_id
|
||||
WHERE urp.user_id = $1 AND urp.role_key = 'photographer'"#,
|
||||
WHERE urp.user_id = $1 AND urp.role_key = 'PHOTOGRAPHER'"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.fetch_optional(pool)
|
||||
|
|
@ -47,7 +47,7 @@ impl PhotographerRepository {
|
|||
|
||||
pub async fn upsert_by_user_id(pool: &PgPool, user_id: Uuid, p: UpsertPhotographerProfilePayload) -> Result<PhotographerProfile, sqlx::Error> {
|
||||
let user_role_profile = sqlx::query_as::<_, (Uuid,)>(
|
||||
r#"SELECT id FROM user_role_profiles WHERE user_id = $1 AND role_key = 'photographer'"#,
|
||||
r#"SELECT id FROM user_role_profiles WHERE user_id = $1 AND role_key = 'PHOTOGRAPHER'"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.fetch_optional(pool)
|
||||
|
|
|
|||
|
|
@ -8,3 +8,5 @@ lettre = { workspace = true }
|
|||
anyhow = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -4,9 +4,105 @@ use lettre::{
|
|||
transport::smtp::authentication::Credentials,
|
||||
AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor,
|
||||
};
|
||||
use reqwest::Client;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum EmailProvider {
|
||||
Smtp(AsyncSmtpTransport<Tokio1Executor>),
|
||||
Zeptomail(ZeptomailTransport),
|
||||
}
|
||||
|
||||
pub struct ZeptomailTransport {
|
||||
client: Client,
|
||||
api_key: String,
|
||||
from_email: String,
|
||||
from_name: String,
|
||||
base_url: String,
|
||||
}
|
||||
|
||||
impl Clone for ZeptomailTransport {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
client: Client::new(),
|
||||
api_key: self.api_key.clone(),
|
||||
from_email: self.from_email.clone(),
|
||||
from_name: self.from_name.clone(),
|
||||
base_url: self.base_url.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ZeptomailTransport {
|
||||
pub fn new(api_key: String, from_email: String, from_name: String) -> Self {
|
||||
Self {
|
||||
client: Client::new(),
|
||||
api_key,
|
||||
from_email,
|
||||
from_name,
|
||||
base_url: "https://api.zeptomail.com/v1.1/email".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send(&self, to: &str, subject: &str, html_body: &str) -> Result<()> {
|
||||
#[derive(Serialize)]
|
||||
struct ZeptomailRequest<'a> {
|
||||
from: ZeptomailAddress<'a>,
|
||||
to: Vec<ZeptomailAddress<'a>>,
|
||||
subject: &'a str,
|
||||
htmlbody: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ZeptomailAddress<'a> {
|
||||
address: &'a str,
|
||||
name: Option<&'a str>,
|
||||
}
|
||||
|
||||
let request = ZeptomailRequest {
|
||||
from: ZeptomailAddress {
|
||||
address: &self.from_email,
|
||||
name: Some(&self.from_name),
|
||||
},
|
||||
to: vec![ZeptomailAddress {
|
||||
address: to,
|
||||
name: None,
|
||||
}],
|
||||
subject,
|
||||
htmlbody: html_body,
|
||||
};
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.post(&self.base_url)
|
||||
.header("Accept", "application/json")
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", format!("Zoho-enczapikey {}", self.api_key))
|
||||
.json(&request)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
tracing::info!("Zeptomail email sent successfully to {}", to);
|
||||
Ok(())
|
||||
} else {
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
tracing::error!("Zeptomail send failed: {} - {}", status, body);
|
||||
Err(anyhow::anyhow!("Zeptomail send failed: {} - {}", status, body))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Mailer {
|
||||
provider: Option<EmailProvider>,
|
||||
from_email: String,
|
||||
from_name: String,
|
||||
template_engine: TemplateEngine,
|
||||
}
|
||||
|
||||
// ── Template Engine ───────────────────────────────────────────────────────────
|
||||
|
||||
pub struct TemplateEngine;
|
||||
|
|
@ -102,51 +198,100 @@ impl Default for TemplateEngine {
|
|||
|
||||
// ── Mailer ────────────────────────────────────────────────────────────────────
|
||||
|
||||
pub struct Mailer {
|
||||
transport: Option<AsyncSmtpTransport<Tokio1Executor>>,
|
||||
from_email: String,
|
||||
from_name: String,
|
||||
template_engine: TemplateEngine,
|
||||
}
|
||||
|
||||
impl Mailer {
|
||||
pub fn new() -> Self {
|
||||
let smtp_host = env::var("SMTP_HOST").ok();
|
||||
let smtp_user = env::var("SMTP_USER").ok();
|
||||
let smtp_pass = env::var("SMTP_PASS").ok();
|
||||
let smtp_port: u16 = env::var("SMTP_PORT")
|
||||
.ok()
|
||||
.and_then(|p| p.parse().ok())
|
||||
.unwrap_or(587);
|
||||
let provider_type = env::var("EMAIL_PROVIDER")
|
||||
.unwrap_or_else(|_| "SMTP".to_string())
|
||||
.to_uppercase();
|
||||
|
||||
let from_email = env::var("SMTP_FROM_EMAIL")
|
||||
.or_else(|_| env::var("ZEPTOMAIL_FROM_EMAIL".to_string()))
|
||||
.unwrap_or_else(|_| "noreply@nxtgauge.com".to_string());
|
||||
let from_name = env::var("SMTP_FROM_NAME")
|
||||
.or_else(|_| env::var("ZEPTOMAIL_FROM_NAME".to_string()))
|
||||
.unwrap_or_else(|_| "NXTGAUGE".to_string());
|
||||
|
||||
let transport = match (smtp_host, smtp_user, smtp_pass) {
|
||||
(Some(host), Some(user), Some(pass)) => {
|
||||
let creds = Credentials::new(user, pass);
|
||||
match AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(&host) {
|
||||
Ok(builder) => {
|
||||
let t = builder.port(smtp_port).credentials(creds).build();
|
||||
tracing::info!("SMTP transport configured (host={} port={})", host, smtp_port);
|
||||
Some(t)
|
||||
let provider = match provider_type.as_str() {
|
||||
"ZEPTOMAIL_SMTP" | "ZEPTOMAIL" => {
|
||||
// Use Zeptomail via SMTP
|
||||
let smtp_host = env::var("SMTP_HOST").ok();
|
||||
let smtp_user = env::var("SMTP_USER").ok();
|
||||
let smtp_pass = env::var("SMTP_PASS").ok();
|
||||
let smtp_port: u16 = env::var("SMTP_PORT")
|
||||
.ok()
|
||||
.and_then(|p| p.parse().ok())
|
||||
.unwrap_or(587);
|
||||
|
||||
match (smtp_host, smtp_user, smtp_pass) {
|
||||
(Some(host), Some(user), Some(pass)) => {
|
||||
let creds = Credentials::new(user, pass);
|
||||
match AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(&host) {
|
||||
Ok(builder) => {
|
||||
let t = builder.port(smtp_port).credentials(creds).build();
|
||||
tracing::info!("Zeptomail SMTP transport configured (host={} port={})", host, smtp_port);
|
||||
Some(EmailProvider::Smtp(t))
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Zeptomail SMTP transport init failed: {} — emails disabled", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("SMTP transport init failed: {} — emails disabled", e);
|
||||
_ => {
|
||||
tracing::warn!("Zeptomail SMTP not configured — emails disabled");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
"ZEPTOMAIL_API" => {
|
||||
// Use Zeptomail via HTTP API
|
||||
if let (Some(api_key), Some(from)) = (
|
||||
env::var("ZEPTOMAIL_API_KEY").ok(),
|
||||
env::var("ZEPTOMAIL_FROM_EMAIL").ok(),
|
||||
) {
|
||||
let transport = ZeptomailTransport::new(api_key, from.clone(), from_name.clone());
|
||||
tracing::info!("Zeptomail API transport configured (from={})", from);
|
||||
Some(EmailProvider::Zeptomail(transport))
|
||||
} else {
|
||||
tracing::warn!("Zeptomail API selected but not configured — emails disabled");
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!("SMTP not configured — emails disabled");
|
||||
None
|
||||
// Default to SMTP
|
||||
let smtp_host = env::var("SMTP_HOST").ok();
|
||||
let smtp_user = env::var("SMTP_USER").ok();
|
||||
let smtp_pass = env::var("SMTP_PASS").ok();
|
||||
let smtp_port: u16 = env::var("SMTP_PORT")
|
||||
.ok()
|
||||
.and_then(|p| p.parse().ok())
|
||||
.unwrap_or(587);
|
||||
|
||||
match (smtp_host, smtp_user, smtp_pass) {
|
||||
(Some(host), Some(user), Some(pass)) => {
|
||||
let creds = Credentials::new(user, pass);
|
||||
match AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(&host) {
|
||||
Ok(builder) => {
|
||||
let t = builder.port(smtp_port).credentials(creds).build();
|
||||
tracing::info!("SMTP transport configured (host={} port={})", host, smtp_port);
|
||||
Some(EmailProvider::Smtp(t))
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("SMTP transport init failed: {} — emails disabled", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!("SMTP not configured — emails disabled");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Self {
|
||||
transport,
|
||||
provider,
|
||||
from_email,
|
||||
from_name,
|
||||
template_engine: TemplateEngine::new(),
|
||||
|
|
@ -154,21 +299,28 @@ impl Mailer {
|
|||
}
|
||||
|
||||
async fn send_html(&self, to: &str, subject: &str, html_body: String) -> Result<()> {
|
||||
let Some(transport) = &self.transport else {
|
||||
return Err(anyhow::anyhow!("SMTP transport not configured"));
|
||||
let Some(provider) = &self.provider else {
|
||||
return Err(anyhow::anyhow!("No email provider configured"));
|
||||
};
|
||||
|
||||
let from: Mailbox = format!("{} <{}>", self.from_name, self.from_email).parse()?;
|
||||
let to: Mailbox = to.parse()?;
|
||||
match provider {
|
||||
EmailProvider::Smtp(transport) => {
|
||||
let from: Mailbox = format!("{} <{}>", self.from_name, self.from_email).parse()?;
|
||||
let to: Mailbox = to.parse()?;
|
||||
|
||||
let email = Message::builder()
|
||||
.from(from)
|
||||
.to(to)
|
||||
.subject(subject)
|
||||
.header(ContentType::TEXT_HTML)
|
||||
.body(html_body)?;
|
||||
let email = Message::builder()
|
||||
.from(from)
|
||||
.to(to)
|
||||
.subject(subject)
|
||||
.header(ContentType::TEXT_HTML)
|
||||
.body(html_body)?;
|
||||
|
||||
transport.send(email).await?;
|
||||
transport.send(email).await?;
|
||||
}
|
||||
EmailProvider::Zeptomail(transport) => {
|
||||
transport.send(to, subject, &html_body).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -986,7 +986,7 @@ ON CONFLICT (role_id) WHERE is_active DO UPDATE SET schema_json = EXCLUDED.schem
|
|||
|
||||
-- ── 4. Default Dashboard Configs ─────────────────────────────────────────────
|
||||
|
||||
INSERT INTO dashboard_configs (role_id, audience, config_json, version, is_active)
|
||||
INSERT INTO role_sidebar_configs (role_id, audience, config_json, version, is_active)
|
||||
SELECT r.id,
|
||||
'EXTERNAL',
|
||||
jsonb_build_object(
|
||||
|
|
@ -1019,13 +1019,27 @@ SELECT r.id,
|
|||
WHEN 'JOB_SEEKER' THEN '["browse_jobs", "my_applications", "profile"]'::jsonb
|
||||
WHEN 'CUSTOMER' THEN '["requirements", "profile"]'::jsonb
|
||||
ELSE '["marketplace", "leads", "portfolio", "services", "wallet", "profile"]'::jsonb
|
||||
END,
|
||||
'widgets', CASE r.key
|
||||
WHEN 'COMPANY' THEN '["total_jobs", "active_jobs", "pending_jobs", "applications_received", "shortlisted_candidates", "credits"]'::jsonb
|
||||
WHEN 'JOB_SEEKER' THEN '["available_jobs", "my_applications", "shortlisted", "saved_jobs", "profile_status", "portfolio"]'::jsonb
|
||||
WHEN 'CUSTOMER' THEN '["total_requirements", "open_requirements", "closed_requirements", "responses_received", "shortlisted_responses", "credits"]'::jsonb
|
||||
ELSE '["open_leads", "my_requests", "accepted_requests", "tracecoins", "portfolio", "profile_status"]'::jsonb
|
||||
END,
|
||||
'tabs', CASE r.key
|
||||
WHEN 'COMPANY' THEN '["overview"]'::jsonb
|
||||
WHEN 'JOB_SEEKER' THEN '["overview"]'::jsonb
|
||||
WHEN 'CUSTOMER' THEN '["overview"]'::jsonb
|
||||
ELSE '["overview"]'::jsonb
|
||||
END
|
||||
),
|
||||
1,
|
||||
true
|
||||
FROM roles r
|
||||
WHERE r.audience = 'EXTERNAL'
|
||||
ON CONFLICT (role_id, audience) WHERE is_active DO NOTHING;
|
||||
ON CONFLICT (role_id, audience) WHERE is_active DO UPDATE SET
|
||||
config_json = EXCLUDED.config_json,
|
||||
version = role_sidebar_configs.version + 1;
|
||||
|
||||
-- ── Done ──────────────────────────────────────────────────────────────────────
|
||||
SELECT 'Seed completed successfully.' AS status;
|
||||
|
|
|
|||
484
scripts/seed_external_role_management.sql
Normal file
484
scripts/seed_external_role_management.sql
Normal file
|
|
@ -0,0 +1,484 @@
|
|||
-- Phase 1 Seed Data: Persona Types, External Roles, Modules
|
||||
-- Run: psql $DATABASE_URL -f scripts/seed_external_role_management.sql
|
||||
|
||||
-- ============================================
|
||||
-- Persona Types
|
||||
-- ============================================
|
||||
INSERT INTO persona_types (code, name, description) VALUES
|
||||
('PROFESSIONAL', 'Professional', 'Service providers like photographers, tutors, developers'),
|
||||
('COMPANY', 'Company', 'Employer/corporate accounts'),
|
||||
('JOB_SEEKER', 'Job Seeker', 'Individuals seeking employment'),
|
||||
('CUSTOMER', 'Customer', 'Service seekers/customers')
|
||||
ON CONFLICT (code) DO NOTHING;
|
||||
|
||||
-- ============================================
|
||||
-- External Roles
|
||||
-- ============================================
|
||||
INSERT INTO external_roles (role_code, role_name, persona_type_id, description, sort_order)
|
||||
SELECT 'COMPANY', 'Company',
|
||||
id, 'Employer/corporate account for posting jobs', 1
|
||||
FROM persona_types WHERE code = 'COMPANY'
|
||||
ON CONFLICT (role_code) DO NOTHING;
|
||||
|
||||
INSERT INTO external_roles (role_code, role_name, persona_type_id, description, sort_order)
|
||||
SELECT 'JOB_SEEKER', 'Job Seeker',
|
||||
id, 'Individual seeking employment opportunities', 2
|
||||
FROM persona_types WHERE code = 'JOB_SEEKER'
|
||||
ON CONFLICT (role_code) DO NOTHING;
|
||||
|
||||
INSERT INTO external_roles (role_code, role_name, persona_type_id, description, sort_order)
|
||||
SELECT 'CUSTOMER', 'Customer',
|
||||
id, 'Service seeker/customer', 3
|
||||
FROM persona_types WHERE code = 'CUSTOMER'
|
||||
ON CONFLICT (role_code) DO NOTHING;
|
||||
|
||||
INSERT INTO external_roles (role_code, role_name, persona_type_id, description, sort_order)
|
||||
SELECT 'PHOTOGRAPHER', 'Photographer',
|
||||
id, 'Professional photographer', 10
|
||||
FROM persona_types WHERE code = 'PROFESSIONAL'
|
||||
ON CONFLICT (role_code) DO NOTHING;
|
||||
|
||||
INSERT INTO external_roles (role_code, role_name, persona_type_id, description, sort_order)
|
||||
SELECT 'MAKEUP_ARTIST', 'Makeup Artist',
|
||||
id, 'Professional makeup artist', 11
|
||||
FROM persona_types WHERE code = 'PROFESSIONAL'
|
||||
ON CONFLICT (role_code) DO NOTHING;
|
||||
|
||||
INSERT INTO external_roles (role_code, role_name, persona_type_id, description, sort_order)
|
||||
SELECT 'TUTOR', 'Tutor',
|
||||
id, 'Professional tutor/teacher', 12
|
||||
FROM persona_types WHERE code = 'PROFESSIONAL'
|
||||
ON CONFLICT (role_code) DO NOTHING;
|
||||
|
||||
INSERT INTO external_roles (role_code, role_name, persona_type_id, description, sort_order)
|
||||
SELECT 'DEVELOPER', 'Developer',
|
||||
id, 'Software developer', 13
|
||||
FROM persona_types WHERE code = 'PROFESSIONAL'
|
||||
ON CONFLICT (role_code) DO NOTHING;
|
||||
|
||||
INSERT INTO external_roles (role_code, role_name, persona_type_id, description, sort_order)
|
||||
SELECT 'VIDEO_EDITOR', 'Video Editor',
|
||||
id, 'Professional video editor', 14
|
||||
FROM persona_types WHERE code = 'PROFESSIONAL'
|
||||
ON CONFLICT (role_code) DO NOTHING;
|
||||
|
||||
INSERT INTO external_roles (role_code, role_name, persona_type_id, description, sort_order)
|
||||
SELECT 'GRAPHIC_DESIGNER', 'Graphic Designer',
|
||||
id, 'Professional graphic designer', 15
|
||||
FROM persona_types WHERE code = 'PROFESSIONAL'
|
||||
ON CONFLICT (role_code) DO NOTHING;
|
||||
|
||||
INSERT INTO external_roles (role_code, role_name, persona_type_id, description, sort_order)
|
||||
SELECT 'SOCIAL_MEDIA_MANAGER', 'Social Media Manager',
|
||||
id, 'Social media management professional', 16
|
||||
FROM persona_types WHERE code = 'PROFESSIONAL'
|
||||
ON CONFLICT (role_code) DO NOTHING;
|
||||
|
||||
INSERT INTO external_roles (role_code, role_name, persona_type_id, description, sort_order)
|
||||
SELECT 'FITNESS_TRAINER', 'Fitness Trainer',
|
||||
id, 'Professional fitness trainer', 17
|
||||
FROM persona_types WHERE code = 'PROFESSIONAL'
|
||||
ON CONFLICT (role_code) DO NOTHING;
|
||||
|
||||
INSERT INTO external_roles (role_code, role_name, persona_type_id, description, sort_order)
|
||||
SELECT 'CATERING_SERVICES', 'Catering Services',
|
||||
id, 'Catering service provider', 18
|
||||
FROM persona_types WHERE code = 'PROFESSIONAL'
|
||||
ON CONFLICT (role_code) DO NOTHING;
|
||||
|
||||
-- ============================================
|
||||
-- Modules (23 total)
|
||||
-- ============================================
|
||||
|
||||
-- Core Shared Modules (7)
|
||||
INSERT INTO modules (module_key, module_name, category, description, default_route, default_sidebar_label, icon_key, is_core)
|
||||
VALUES
|
||||
('dashboard_home', 'Dashboard Home', 'core', 'Dashboard landing page with KPIs and widgets', '/dashboard', 'My Dashboard', 'dashboard', true),
|
||||
('profile', 'Profile', 'core', 'User profile management', '/profile', 'My Profile', 'user', true),
|
||||
('verification', 'Verification', 'core', 'Verification status and resubmission', '/verification', 'Verification', 'shield', true),
|
||||
('help_center', 'Help Center', 'core', 'FAQs and support', '/help-center', 'Help Center', 'help-circle', true),
|
||||
('settings', 'Settings', 'core', 'Account settings and preferences', '/settings', 'Settings', 'settings', true),
|
||||
('switch_services', 'Switch Services', 'core', 'Switch between approved roles', '/switch-services', 'Switch Services', 'refresh-cw', true),
|
||||
('explore_nxtgauge', 'Explore Nxtgauge', 'core', 'Register for additional roles', '/explore', 'Explore Nxtgauge', 'compass', true)
|
||||
ON CONFLICT (module_key) DO NOTHING;
|
||||
|
||||
-- Content and Identity Modules (2)
|
||||
INSERT INTO modules (module_key, module_name, category, description, default_route, default_sidebar_label, icon_key)
|
||||
VALUES
|
||||
('portfolio', 'Portfolio', 'content', 'Work samples and showcase', '/portfolio', 'My Portfolio', 'image'),
|
||||
('services', 'Services', 'content', 'Service offerings and pricing', '/services', 'My Services', 'briefcase')
|
||||
ON CONFLICT (module_key) DO NOTHING;
|
||||
|
||||
-- Marketplace and Discovery Modules (3)
|
||||
INSERT INTO modules (module_key, module_name, category, description, default_route, default_sidebar_label, icon_key)
|
||||
VALUES
|
||||
('marketplace', 'Marketplace', 'marketplace', 'Discover opportunities', '/marketplace', 'Marketplace', 'store'),
|
||||
('browse_jobs', 'Browse Jobs', 'marketplace', 'Search and browse jobs', '/browse-jobs', 'Jobs', 'search'),
|
||||
('saved_jobs', 'Saved Jobs', 'marketplace', 'Saved job postings', '/saved-jobs', 'Saved Jobs', 'bookmark')
|
||||
ON CONFLICT (module_key) DO NOTHING;
|
||||
|
||||
-- Work and Response Modules (8)
|
||||
INSERT INTO modules (module_key, module_name, category, description, default_route, default_sidebar_label, icon_key)
|
||||
VALUES
|
||||
('jobs', 'Jobs', 'work', 'Job postings management', '/jobs', 'Jobs', 'briefcase'),
|
||||
('applications', 'Applications', 'work', 'Application management', '/applications', 'Applications', 'file-text'),
|
||||
('my_applications', 'My Applications', 'work', 'Track submitted applications', '/my-applications', 'My Applications', 'file-text'),
|
||||
('requirements', 'Requirements', 'work', 'Customer requirements', '/requirements', 'My Requirements', 'list'),
|
||||
('leads', 'Leads', 'work', 'Lead management', '/leads', 'Leads', 'users'),
|
||||
('my_responses', 'My Responses', 'work', 'Track service responses', '/my-responses', 'My Responses', 'send'),
|
||||
('received_responses', 'Received Responses', 'work', 'View received responses', '/received-responses', 'Received Responses', 'inbox'),
|
||||
('shortlisted_candidates', 'Shortlisted Candidates', 'work', 'Manage shortlisted candidates', '/shortlisted-candidates', 'Shortlisted Candidates', 'star')
|
||||
ON CONFLICT (module_key) DO NOTHING;
|
||||
|
||||
INSERT INTO modules (module_key, module_name, category, description, default_route, default_sidebar_label, icon_key)
|
||||
VALUES
|
||||
('shortlisted_responses', 'Shortlisted Responses', 'work', 'Manage shortlisted responses', '/shortlisted-responses', 'Shortlisted Responses', 'star')
|
||||
ON CONFLICT (module_key) DO NOTHING;
|
||||
|
||||
-- Financial Modules (2)
|
||||
INSERT INTO modules (module_key, module_name, category, description, default_route, default_sidebar_label, icon_key)
|
||||
VALUES
|
||||
('wallet', 'Wallet', 'financial', 'Earnings and payouts', '/wallet', 'Wallet', 'credit-card'),
|
||||
('credits', 'Credits', 'financial', 'Credit balance and purchases', '/credits', 'Credits', 'package')
|
||||
ON CONFLICT (module_key) DO NOTHING;
|
||||
|
||||
-- ============================================
|
||||
-- Module Actions (Generic)
|
||||
-- ============================================
|
||||
-- Insert generic CRUD actions for each module
|
||||
DO $$
|
||||
DECLARE
|
||||
m_record RECORD;
|
||||
generic_actions TEXT[] := ARRAY['view', 'list', 'create', 'update', 'delete'];
|
||||
action_name TEXT;
|
||||
action_key TEXT;
|
||||
BEGIN
|
||||
FOR m_record IN SELECT id, module_key FROM modules LOOP
|
||||
FOREACH action_key IN ARRAY generic_actions LOOP
|
||||
action_name := INITCAP(action_key);
|
||||
-- Custom names for some actions
|
||||
IF action_key = 'list' THEN action_name := 'List'; END IF;
|
||||
IF action_key = 'create' THEN action_name := 'Create'; END IF;
|
||||
IF action_key = 'update' THEN action_name := 'Update'; END IF;
|
||||
IF action_key = 'delete' THEN action_name := 'Delete'; END IF;
|
||||
IF action_key = 'view' THEN action_name := 'View'; END IF;
|
||||
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description)
|
||||
VALUES (m_record.id, action_key, action_name, action_name || ' ' || m_record.module_key)
|
||||
ON CONFLICT (module_id, action_key) DO NOTHING;
|
||||
END LOOP;
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
-- ============================================
|
||||
-- Module Actions (Domain-Specific)
|
||||
-- ============================================
|
||||
-- Add domain-specific actions per module
|
||||
DO $$
|
||||
DECLARE
|
||||
m_id UUID;
|
||||
BEGIN
|
||||
-- dashboard_home
|
||||
SELECT id INTO m_id FROM modules WHERE module_key = 'dashboard_home';
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description) VALUES
|
||||
(m_id, 'customize', 'Customize', 'Customize dashboard widgets')
|
||||
ON CONFLICT (module_id, action_key) DO NOTHING;
|
||||
|
||||
-- profile
|
||||
SELECT id INTO m_id FROM modules WHERE module_key = 'profile';
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description) VALUES
|
||||
(m_id, 'upload_media', 'Upload Media', 'Upload profile photos/documents'),
|
||||
(m_id, 'preview_profile', 'Preview Profile', 'Preview public profile')
|
||||
ON CONFLICT (module_id, action_key) DO NOTHING;
|
||||
|
||||
-- portfolio
|
||||
SELECT id INTO m_id FROM modules WHERE module_key = 'portfolio';
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description) VALUES
|
||||
(m_id, 'publish_item', 'Publish Item', 'Publish portfolio item'),
|
||||
(m_id, 'unpublish_item', 'Unpublish Item', 'Unpublish portfolio item'),
|
||||
(m_id, 'feature_item', 'Feature Item', 'Feature portfolio item'),
|
||||
(m_id, 'reorder_items', 'Reorder Items', 'Reorder portfolio items')
|
||||
ON CONFLICT (module_id, action_key) DO NOTHING;
|
||||
|
||||
-- services
|
||||
SELECT id INTO m_id FROM modules WHERE module_key = 'services';
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description) VALUES
|
||||
(m_id, 'activate', 'Activate', 'Activate service'),
|
||||
(m_id, 'deactivate', 'Deactivate', 'Deactivate service')
|
||||
ON CONFLICT (module_id, action_key) DO NOTHING;
|
||||
|
||||
-- jobs
|
||||
SELECT id INTO m_id FROM modules WHERE module_key = 'jobs';
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description) VALUES
|
||||
(m_id, 'publish', 'Publish', 'Publish job posting'),
|
||||
(m_id, 'close', 'Close', 'Close job posting'),
|
||||
(m_id, 'archive', 'Archive', 'Archive job posting')
|
||||
ON CONFLICT (module_id, action_key) DO NOTHING;
|
||||
|
||||
-- applications
|
||||
SELECT id INTO m_id FROM modules WHERE module_key = 'applications';
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description) VALUES
|
||||
(m_id, 'shortlist', 'Shortlist', 'Shortlist candidate'),
|
||||
(m_id, 'reject', 'Reject', 'Reject candidate'),
|
||||
(m_id, 'review', 'Review', 'Review application')
|
||||
ON CONFLICT (module_id, action_key) DO NOTHING;
|
||||
|
||||
-- leads
|
||||
SELECT id INTO m_id FROM modules WHERE module_key = 'leads';
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description) VALUES
|
||||
(m_id, 'update_status', 'Update Status', 'Update lead status'),
|
||||
(m_id, 'unlock_contact', 'Unlock Contact', 'Unlock contact information')
|
||||
ON CONFLICT (module_id, action_key) DO NOTHING;
|
||||
|
||||
-- my_responses
|
||||
SELECT id INTO m_id FROM modules WHERE module_key = 'my_responses';
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description) VALUES
|
||||
(m_id, 'withdraw', 'Withdraw', 'Withdraw response')
|
||||
ON CONFLICT (module_id, action_key) DO NOTHING;
|
||||
|
||||
-- requirements
|
||||
SELECT id INTO m_id FROM modules WHERE module_key = 'requirements';
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description) VALUES
|
||||
(m_id, 'publish', 'Publish', 'Publish requirement'),
|
||||
(m_id, 'close', 'Close', 'Close requirement'),
|
||||
(m_id, 'reopen', 'Reopen', 'Reopen requirement'),
|
||||
(m_id, 'archive', 'Archive', 'Archive requirement')
|
||||
ON CONFLICT (module_id, action_key) DO NOTHING;
|
||||
|
||||
-- received_responses
|
||||
SELECT id INTO m_id FROM modules WHERE module_key = 'received_responses';
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description) VALUES
|
||||
(m_id, 'shortlist', 'Shortlist', 'Shortlist response'),
|
||||
(m_id, 'reject', 'Reject', 'Reject response'),
|
||||
(m_id, 'compare', 'Compare', 'Compare responses')
|
||||
ON CONFLICT (module_id, action_key) DO NOTHING;
|
||||
|
||||
-- shortlisted_responses
|
||||
SELECT id INTO m_id FROM modules WHERE module_key = 'shortlisted_responses';
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description) VALUES
|
||||
(m_id, 'compare', 'Compare', 'Compare shortlisted responses')
|
||||
ON CONFLICT (module_id, action_key) DO NOTHING;
|
||||
|
||||
-- shortlisted_candidates
|
||||
SELECT id INTO m_id FROM modules WHERE module_key = 'shortlisted_candidates';
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description) VALUES
|
||||
(m_id, 'compare', 'Compare', 'Compare candidates')
|
||||
ON CONFLICT (module_id, action_key) DO NOTHING;
|
||||
|
||||
-- browse_jobs
|
||||
SELECT id INTO m_id FROM modules WHERE module_key = 'browse_jobs';
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description) VALUES
|
||||
(m_id, 'save', 'Save', 'Save job'),
|
||||
(m_id, 'apply', 'Apply', 'Apply to job')
|
||||
ON CONFLICT (module_id, action_key) DO NOTHING;
|
||||
|
||||
-- saved_jobs
|
||||
SELECT id INTO m_id FROM modules WHERE module_key = 'saved_jobs';
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description) VALUES
|
||||
(m_id, 'apply', 'Apply', 'Apply from saved jobs')
|
||||
ON CONFLICT (module_id, action_key) DO NOTHING;
|
||||
|
||||
-- my_applications
|
||||
SELECT id INTO m_id FROM modules WHERE module_key = 'my_applications';
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description) VALUES
|
||||
(m_id, 'withdraw', 'Withdraw', 'Withdraw application')
|
||||
ON CONFLICT (module_id, action_key) DO NOTHING;
|
||||
|
||||
-- marketplace
|
||||
SELECT id INTO m_id FROM modules WHERE module_key = 'marketplace';
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description) VALUES
|
||||
(m_id, 'respond', 'Respond', 'Respond to opportunity')
|
||||
ON CONFLICT (module_id, action_key) DO NOTHING;
|
||||
|
||||
-- wallet
|
||||
SELECT id INTO m_id FROM modules WHERE module_key = 'wallet';
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description) VALUES
|
||||
(m_id, 'request_payout', 'Request Payout', 'Request wallet payout')
|
||||
ON CONFLICT (module_id, action_key) DO NOTHING;
|
||||
|
||||
-- credits
|
||||
SELECT id INTO m_id FROM modules WHERE module_key = 'credits';
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description) VALUES
|
||||
(m_id, 'buy_credits', 'Buy Credits', 'Purchase credits')
|
||||
ON CONFLICT (module_id, action_key) DO NOTHING;
|
||||
|
||||
-- verification
|
||||
SELECT id INTO m_id FROM modules WHERE module_key = 'verification';
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description) VALUES
|
||||
(m_id, 'resubmit', 'Resubmit', 'Resubmit verification'),
|
||||
(m_id, 'view_status', 'View Status', 'View verification status')
|
||||
ON CONFLICT (module_id, action_key) DO NOTHING;
|
||||
|
||||
-- explore_nxtgauge
|
||||
SELECT id INTO m_id FROM modules WHERE module_key = 'explore_nxtgauge';
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description) VALUES
|
||||
(m_id, 'start_onboarding', 'Start Onboarding', 'Start new role onboarding'),
|
||||
(m_id, 'resume_onboarding', 'Resume Onboarding', 'Resume incomplete onboarding'),
|
||||
(m_id, 'save_draft', 'Save Draft', 'Save onboarding draft'),
|
||||
(m_id, 'submit_for_verification', 'Submit for Verification', 'Submit application'),
|
||||
(m_id, 'view_progress', 'View Progress', 'View onboarding progress')
|
||||
ON CONFLICT (module_id, action_key) DO NOTHING;
|
||||
|
||||
-- switch_services
|
||||
SELECT id INTO m_id FROM modules WHERE module_key = 'switch_services';
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description) VALUES
|
||||
(m_id, 'switch', 'Switch', 'Switch to another role')
|
||||
ON CONFLICT (module_id, action_key) DO NOTHING;
|
||||
|
||||
-- help_center
|
||||
SELECT id INTO m_id FROM modules WHERE module_key = 'help_center';
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description) VALUES
|
||||
(m_id, 'search', 'Search', 'Search help articles'),
|
||||
(m_id, 'ask_help', 'Ask Help', 'Ask for help')
|
||||
ON CONFLICT (module_id, action_key) DO NOTHING;
|
||||
|
||||
-- settings
|
||||
SELECT id INTO m_id FROM modules WHERE module_key = 'settings';
|
||||
INSERT INTO module_actions (module_id, action_key, action_name, description) VALUES
|
||||
(m_id, 'manage_sessions', 'Manage Sessions', 'Manage active sessions')
|
||||
ON CONFLICT (module_id, action_key) DO NOTHING;
|
||||
|
||||
END $$;
|
||||
|
||||
-- ============================================
|
||||
-- Default Role Module Access (by Persona)
|
||||
-- ============================================
|
||||
|
||||
-- Helper function to get module id by key
|
||||
DO $$
|
||||
DECLARE
|
||||
pt_rec RECORD;
|
||||
mod_rec RECORD;
|
||||
role_id UUID;
|
||||
mod_id UUID;
|
||||
sort_int INTEGER := 0;
|
||||
BEGIN
|
||||
-- PROFESSIONAL default modules (all 23)
|
||||
FOR pt_rec IN SELECT id FROM persona_types WHERE code = 'PROFESSIONAL' LOOP
|
||||
sort_int := 0;
|
||||
FOR mod_rec IN SELECT id, module_key FROM modules ORDER BY is_core DESC, category, module_key LOOP
|
||||
SELECT id INTO role_id FROM external_roles WHERE persona_type_id = pt_rec.id LIMIT 1;
|
||||
IF role_id IS NOT NULL THEN
|
||||
INSERT INTO role_module_access (external_role_id, module_id, is_enabled, is_sidebar_visible, sort_order)
|
||||
VALUES (role_id, mod_rec.id, true, true, sort_int)
|
||||
ON CONFLICT (external_role_id, module_id) DO NOTHING;
|
||||
sort_int := sort_int + 1;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END LOOP;
|
||||
|
||||
-- COMPANY default modules
|
||||
FOR pt_rec IN SELECT id FROM persona_types WHERE code = 'COMPANY' LOOP
|
||||
SELECT id INTO role_id FROM external_roles WHERE persona_type_id = pt_rec.id LIMIT 1;
|
||||
IF role_id IS NOT NULL THEN
|
||||
-- dashboard_home, profile, jobs, applications, shortlisted_candidates, credits, verification, help_center, settings, switch_services, explore_nxtgauge
|
||||
FOREACH mod_rec IN ARRAY (
|
||||
SELECT module_key FROM modules WHERE module_key IN (
|
||||
'dashboard_home', 'profile', 'jobs', 'applications', 'shortlisted_candidates',
|
||||
'credits', 'verification', 'help_center', 'settings', 'switch_services', 'explore_nxtgauge'
|
||||
)
|
||||
) LOOP
|
||||
SELECT id INTO mod_id FROM modules WHERE module_key = mod_rec;
|
||||
INSERT INTO role_module_access (external_role_id, module_id, is_enabled, is_sidebar_visible, sort_order)
|
||||
VALUES (role_id, mod_id, true, true, sort_int)
|
||||
ON CONFLICT (external_role_id, module_id) DO NOTHING;
|
||||
sort_int := sort_int + 1;
|
||||
END LOOP;
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
-- JOB_SEEKER default modules
|
||||
FOR pt_rec IN SELECT id FROM persona_types WHERE code = 'JOB_SEEKER' LOOP
|
||||
SELECT id INTO role_id FROM external_roles WHERE persona_type_id = pt_rec.id LIMIT 1;
|
||||
IF role_id IS NOT NULL THEN
|
||||
-- dashboard_home, profile, portfolio, browse_jobs, my_applications, saved_jobs, verification, help_center, settings, switch_services, explore_nxtgauge
|
||||
FOREACH mod_rec IN ARRAY (
|
||||
SELECT module_key FROM modules WHERE module_key IN (
|
||||
'dashboard_home', 'profile', 'portfolio', 'browse_jobs', 'my_applications', 'saved_jobs',
|
||||
'verification', 'help_center', 'settings', 'switch_services', 'explore_nxtgauge'
|
||||
)
|
||||
) LOOP
|
||||
SELECT id INTO mod_id FROM modules WHERE module_key = mod_rec;
|
||||
INSERT INTO role_module_access (external_role_id, module_id, is_enabled, is_sidebar_visible, sort_order)
|
||||
VALUES (role_id, mod_id, true, true, sort_int)
|
||||
ON CONFLICT (external_role_id, module_id) DO NOTHING;
|
||||
sort_int := sort_int + 1;
|
||||
END LOOP;
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
-- CUSTOMER default modules
|
||||
FOR pt_rec IN SELECT id FROM persona_types WHERE code = 'CUSTOMER' LOOP
|
||||
SELECT id INTO role_id FROM external_roles WHERE persona_type_id = pt_rec.id LIMIT 1;
|
||||
IF role_id IS NOT NULL THEN
|
||||
-- dashboard_home, profile, requirements, received_responses, shortlisted_responses, credits, verification, help_center, settings, switch_services, explore_nxtgauge
|
||||
FOREACH mod_rec IN ARRAY (
|
||||
SELECT module_key FROM modules WHERE module_key IN (
|
||||
'dashboard_home', 'profile', 'requirements', 'received_responses', 'shortlisted_responses',
|
||||
'credits', 'verification', 'help_center', 'settings', 'switch_services', 'explore_nxtgauge'
|
||||
)
|
||||
) LOOP
|
||||
SELECT id INTO mod_id FROM modules WHERE module_key = mod_rec;
|
||||
INSERT INTO role_module_access (external_role_id, module_id, is_enabled, is_sidebar_visible, sort_order)
|
||||
VALUES (role_id, mod_id, true, true, sort_int)
|
||||
ON CONFLICT (external_role_id, module_id) DO NOTHING;
|
||||
sort_int := sort_int + 1;
|
||||
END LOOP;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
-- ============================================
|
||||
-- Default Role Module Permissions
|
||||
-- ============================================
|
||||
-- Set default CRUD permissions based on persona type
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
role_rec RECORD;
|
||||
mod_rec RECORD;
|
||||
role_id UUID;
|
||||
mod_id UUID;
|
||||
can_v BOOLEAN;
|
||||
can_l BOOLEAN;
|
||||
can_c BOOLEAN;
|
||||
can_u BOOLEAN;
|
||||
can_d BOOLEAN;
|
||||
BEGIN
|
||||
-- All roles get view/list on all their enabled modules by default
|
||||
FOR role_rec IN SELECT id, persona_type_id FROM external_roles LOOP
|
||||
FOR mod_rec IN SELECT module_id FROM role_module_access WHERE external_role_id = role_rec.id AND is_enabled = true LOOP
|
||||
role_id := role_rec.id;
|
||||
mod_id := mod_rec.module_id;
|
||||
|
||||
-- Default: all get view and list
|
||||
can_v := true;
|
||||
can_l := true;
|
||||
can_c := false;
|
||||
can_u := false;
|
||||
can_d := false;
|
||||
|
||||
-- Customize defaults per module
|
||||
IF (SELECT module_key FROM modules WHERE id = mod_id) IN ('dashboard_home', 'profile', 'portfolio', 'services',
|
||||
'jobs', 'applications', 'requirements', 'leads', 'my_responses', 'received_responses',
|
||||
'shortlisted_candidates', 'shortlisted_responses', 'saved_jobs', 'browse_jobs', 'my_applications') THEN
|
||||
can_c := true;
|
||||
can_u := true;
|
||||
END IF;
|
||||
|
||||
IF (SELECT module_key FROM modules WHERE id = mod_id) IN ('portfolio', 'services', 'jobs', 'requirements') THEN
|
||||
can_d := true;
|
||||
END IF;
|
||||
|
||||
INSERT INTO role_module_permissions (external_role_id, module_id, can_view, can_list, can_create, can_update, can_delete)
|
||||
VALUES (role_id, mod_id, can_v, can_l, can_c, can_u, can_d)
|
||||
ON CONFLICT (external_role_id, module_id) DO NOTHING;
|
||||
END LOOP;
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
-- ============================================
|
||||
-- Update external_roles set switch_services_enabled for roles with multiple personas per user
|
||||
-- (Will be enabled after user_role_applications system is in place)
|
||||
-- For now, keep it false
|
||||
UPDATE external_roles SET switch_services_enabled = false WHERE switch_services_enabled IS NULL OR switch_services_enabled = true;
|
||||
1
start-services.pid
Normal file
1
start-services.pid
Normal file
|
|
@ -0,0 +1 @@
|
|||
71432
|
||||
Loading…
Add table
Reference in a new issue