feat: add permission checker module

Checks:
- User is logged in
- Role is allowed for action
- Verification status
- Account status

Includes unit tests.
This commit is contained in:
Tracewebstudio Dev 2026-06-14 19:29:01 +02:00
parent eaec788b2f
commit 89e4809f0b
2 changed files with 182 additions and 0 deletions

View file

@ -7,6 +7,7 @@ mod error;
mod forms;
mod handlers;
mod jobs;
mod permissions;
mod providers;
mod retrieval;
mod routes;

181
src/permissions/mod.rs Normal file
View file

@ -0,0 +1,181 @@
use serde::{Deserialize, Serialize};
use crate::actions::ActionDefinition;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermissionContext {
pub user_id: Option<String>,
pub role: Option<String>,
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<String> {
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<String>) {
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");
}
}