diff --git a/src/main.rs b/src/main.rs index abc2f56..0a5ec42 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ mod error; mod forms; mod handlers; mod jobs; +mod permissions; mod providers; mod retrieval; mod routes; diff --git a/src/permissions/mod.rs b/src/permissions/mod.rs new file mode 100644 index 0000000..1d471dc --- /dev/null +++ b/src/permissions/mod.rs @@ -0,0 +1,181 @@ +use serde::{Deserialize, Serialize}; + +use crate::actions::ActionDefinition; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PermissionContext { + pub user_id: Option, + pub role: Option, + pub is_verified: bool, + pub account_status: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PermissionResult { + pub allowed: bool, + pub reason_code: String, + pub message: String, +} + +pub struct PermissionChecker; + +impl PermissionChecker { + pub fn check(context: &PermissionContext, action: &ActionDefinition) -> PermissionResult { + if !context.user_id.is_some() && action.requires_login { + return PermissionResult { + allowed: false, + reason_code: "NOT_LOGGED_IN".to_string(), + message: "You must be logged in to perform this action.".to_string(), + }; + } + + if let Some(role) = &context.role { + if !action.allowed_roles.contains(role) { + return PermissionResult { + allowed: false, + reason_code: "ROLE_NOT_ALLOWED".to_string(), + message: format!( + "This action is not available for your role ({}).", + role + ), + }; + } + } else if action.requires_login { + return PermissionResult { + allowed: false, + reason_code: "NO_ROLE_SELECTED".to_string(), + message: "Please select a role before performing this action.".to_string(), + }; + } + + if action.requires_verification && !context.is_verified { + return PermissionResult { + allowed: false, + reason_code: "VERIFICATION_REQUIRED".to_string(), + message: "You need to complete verification before using this feature.".to_string(), + }; + } + + if context.account_status != "ACTIVE" { + return PermissionResult { + allowed: false, + reason_code: "ACCOUNT_NOT_ACTIVE".to_string(), + message: format!( + "Your account is {}. Please contact support.", + context.account_status.to_lowercase() + ), + }; + } + + PermissionResult { + allowed: true, + reason_code: "ALLOWED".to_string(), + message: "Permission granted.".to_string(), + } + } + + pub fn get_missing_fields( + provided_fields: &[String], + required_fields: &[String], + ) -> Vec { + required_fields + .iter() + .filter(|f| !provided_fields.contains(f)) + .cloned() + .collect() + } + + pub fn validate_fields( + fields: &serde_json::Value, + required_fields: &[String], + optional_fields: &[String], + ) -> (bool, Vec) { + let mut missing = Vec::new(); + let obj = fields.as_object().cloned().unwrap_or_default(); + + for field in required_fields { + if !obj.contains_key(field) || obj.get(field).map(|v| v.is_null()).unwrap_or(false) { + missing.push(field.clone()); + } + } + + (missing.is_empty(), missing) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn get_test_action() -> ActionDefinition { + ActionDefinition { + action_code: "test_action".to_string(), + intent: "test_action".to_string(), + allowed_roles: vec!["COMPANY".to_string(), "JOB_SEEKER".to_string()], + requires_login: true, + requires_verification: true, + requires_confirmation: true, + required_fields: vec!["field1".to_string(), "field2".to_string()], + optional_fields: vec!["opt1".to_string()], + feature_code: "test".to_string(), + ai_action_cost: 1, + uses_llm: true, + backend_handler: "test.handler".to_string(), + } + } + + #[test] + fn test_allows_verified_company_user() { + let context = PermissionContext { + user_id: Some("user123".to_string()), + role: Some("COMPANY".to_string()), + is_verified: true, + account_status: "ACTIVE".to_string(), + }; + let action = get_test_action(); + let result = PermissionChecker::check(&context, &action); + assert!(result.allowed); + } + + #[test] + fn test_blocks_unverified_user() { + let context = PermissionContext { + user_id: Some("user123".to_string()), + role: Some("COMPANY".to_string()), + is_verified: false, + account_status: "ACTIVE".to_string(), + }; + let action = get_test_action(); + let result = PermissionChecker::check(&context, &action); + assert!(!result.allowed); + assert_eq!(result.reason_code, "VERIFICATION_REQUIRED"); + } + + #[test] + fn test_blocks_wrong_role() { + let context = PermissionContext { + user_id: Some("user123".to_string()), + role: Some("ADMIN".to_string()), + is_verified: true, + account_status: "ACTIVE".to_string(), + }; + let action = get_test_action(); + let result = PermissionChecker::check(&context, &action); + assert!(!result.allowed); + assert_eq!(result.reason_code, "ROLE_NOT_ALLOWED"); + } + + #[test] + fn test_requires_login() { + let context = PermissionContext { + user_id: None, + role: None, + is_verified: false, + account_status: "ACTIVE".to_string(), + }; + let action = get_test_action(); + let result = PermissionChecker::check(&context, &action); + assert!(!result.allowed); + assert_eq!(result.reason_code, "NOT_LOGGED_IN"); + } +}