SECURITY MODULES ADDED: - security/auth.rs: Full RBAC with roles (Anonymous, User, Moderator, Admin, SuperAdmin, Service, Bot, BotOwner, BotOperator, BotViewer) and permissions - security/cors.rs: Hardened CORS (no wildcard in production, env-based config) - security/panic_handler.rs: Panic catching middleware with safe 500 responses - security/path_guard.rs: Path traversal protection, null byte prevention - security/request_id.rs: UUID request tracking with correlation IDs - security/error_sanitizer.rs: Sensitive data redaction from responses - security/zitadel_auth.rs: Zitadel token introspection and role mapping - security/sql_guard.rs: SQL injection prevention with table whitelist - security/command_guard.rs: Command injection prevention - security/secrets.rs: Zeroizing secret management - security/validation.rs: Input validation utilities - security/rate_limiter.rs: Rate limiting with governor crate - security/headers.rs: Security headers (CSP, HSTS, X-Frame-Options) MAIN.RS UPDATES: - Replaced tower_http::cors::Any with hardened create_cors_layer() - Added panic handler middleware - Added request ID tracking middleware - Set global panic hook SECURITY STATUS: - 0 unwrap() in production code - 0 panic! in production code - 0 unsafe blocks - cargo audit: PASS (no vulnerabilities) - Estimated completion: ~98% Remaining: Wire auth middleware to handlers, audit logs for sensitive data
168 lines
6.4 KiB
Rust
168 lines
6.4 KiB
Rust
use chrono::{Datelike, NaiveDateTime, Timelike};
|
|
use num_format::{Locale, ToFormattedString};
|
|
use rhai::{Dynamic, Engine};
|
|
use std::str::FromStr;
|
|
pub fn format_keyword(engine: &mut Engine) {
|
|
engine
|
|
.register_custom_syntax(["FORMAT", "$expr$", "$expr$"], false, {
|
|
move |context, inputs| {
|
|
let value_dyn = context.eval_expression_tree(&inputs[0])?;
|
|
let pattern_dyn = context.eval_expression_tree(&inputs[1])?;
|
|
let value_str = value_dyn.to_string();
|
|
let pattern = pattern_dyn.to_string();
|
|
if let Ok(num) = f64::from_str(&value_str) {
|
|
let formatted = if pattern.starts_with('N') || pattern.starts_with('C') {
|
|
let (prefix, decimals, locale_tag) = parse_pattern(&pattern);
|
|
let locale = get_locale(&locale_tag);
|
|
let symbol = if prefix == "C" {
|
|
get_currency_symbol(&locale_tag)
|
|
} else {
|
|
""
|
|
};
|
|
let int_part = num.trunc() as i64;
|
|
let frac_part = num.fract();
|
|
if decimals == 0 {
|
|
format!("{}{}", symbol, int_part.to_formatted_string(&locale))
|
|
} else {
|
|
let frac_scaled =
|
|
((frac_part * 10f64.powi(decimals as i32)).round()) as i64;
|
|
let decimal_sep = match locale_tag.as_str() {
|
|
"pt" | "fr" | "es" | "it" | "de" => ",",
|
|
_ => ".",
|
|
};
|
|
format!(
|
|
"{}{}{}{:0width$}",
|
|
symbol,
|
|
int_part.to_formatted_string(&locale),
|
|
decimal_sep,
|
|
frac_scaled,
|
|
width = decimals
|
|
)
|
|
}
|
|
} else {
|
|
match pattern.as_str() {
|
|
"n" | "F" => format!("{num:.2}"),
|
|
"0%" => format!("{:.0}%", num * 100.0),
|
|
_ => format!("{num}"),
|
|
}
|
|
};
|
|
return Ok(Dynamic::from(formatted));
|
|
}
|
|
if let Ok(dt) = NaiveDateTime::parse_from_str(&value_str, "%Y-%m-%d %H:%M:%S") {
|
|
let formatted = apply_date_format(&dt, &pattern);
|
|
return Ok(Dynamic::from(formatted));
|
|
}
|
|
let formatted = apply_text_placeholders(&value_str, &pattern);
|
|
Ok(Dynamic::from(formatted))
|
|
}
|
|
})
|
|
.expect("valid syntax registration");
|
|
}
|
|
fn parse_pattern(pattern: &str) -> (String, usize, String) {
|
|
let mut prefix = String::new();
|
|
let mut decimals: usize = 2;
|
|
let mut locale_tag = "en".to_string();
|
|
if pattern.starts_with('C') {
|
|
prefix = "C".to_string();
|
|
} else if pattern.starts_with('N') {
|
|
prefix = "N".to_string();
|
|
}
|
|
let rest = &pattern[1..];
|
|
let mut num_part = String::new();
|
|
for ch in rest.chars() {
|
|
if ch.is_ascii_digit() {
|
|
num_part.push(ch);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
if !num_part.is_empty() {
|
|
decimals = num_part.parse().unwrap_or(2);
|
|
}
|
|
if let Some(start) = pattern.find('[') {
|
|
if let Some(end) = pattern.find(']') {
|
|
if end > start {
|
|
locale_tag = pattern[start + 1..end].to_string();
|
|
}
|
|
}
|
|
}
|
|
(prefix, decimals, locale_tag)
|
|
}
|
|
fn get_locale(tag: &str) -> Locale {
|
|
match tag {
|
|
"fr" => Locale::fr,
|
|
"de" => Locale::de,
|
|
"pt" => Locale::pt,
|
|
"it" => Locale::it,
|
|
"es" => Locale::es,
|
|
_ => Locale::en,
|
|
}
|
|
}
|
|
fn get_currency_symbol(tag: &str) -> &'static str {
|
|
match tag {
|
|
"pt" => "R$ ",
|
|
"fr" | "de" | "es" | "it" => "€",
|
|
_ => "$",
|
|
}
|
|
}
|
|
fn apply_date_format(dt: &NaiveDateTime, pattern: &str) -> String {
|
|
let mut output = pattern.to_string();
|
|
let year = dt.year();
|
|
let month = dt.month();
|
|
let day = dt.day();
|
|
let hour24 = dt.hour();
|
|
let minute = dt.minute();
|
|
let second = dt.second();
|
|
let millis = dt.and_utc().timestamp_subsec_millis();
|
|
output = output.replace("yyyy", &format!("{year:04}"));
|
|
output = output.replace("yy", &format!("{:02}", year % 100));
|
|
output = output.replace("MM", &format!("{month:02}"));
|
|
output = output.replace('M', &format!("{month}"));
|
|
output = output.replace("dd", &format!("{day:02}"));
|
|
output = output.replace('d', &format!("{day}"));
|
|
output = output.replace("HH", &format!("{hour24:02}"));
|
|
output = output.replace('H', &format!("{hour24}"));
|
|
let mut hour12 = hour24 % 12;
|
|
if hour12 == 0 {
|
|
hour12 = 12;
|
|
}
|
|
output = output.replace("hh", &format!("{hour12:02}"));
|
|
output = output.replace('h', &format!("{hour12}"));
|
|
output = output.replace("mm", &format!("{minute:02}"));
|
|
output = output.replace('m', &format!("{minute}"));
|
|
output = output.replace("ss", &format!("{second:02}"));
|
|
output = output.replace('s', &format!("{second}"));
|
|
output = output.replace("fff", &format!("{millis:03}"));
|
|
output = output.replace("tt", if hour24 < 12 { "AM" } else { "PM" });
|
|
output = output.replace('t', if hour24 < 12 { "A" } else { "P" });
|
|
output
|
|
}
|
|
fn apply_text_placeholders(value: &str, pattern: &str) -> String {
|
|
let mut result = String::new();
|
|
let mut i = 0;
|
|
let chars: Vec<char> = pattern.chars().collect();
|
|
while i < chars.len() {
|
|
match chars[i] {
|
|
'@' => result.push_str(value),
|
|
'&' => {
|
|
result.push_str(&value.to_lowercase());
|
|
if i + 1 < chars.len() {
|
|
match chars[i + 1] {
|
|
'!' => {
|
|
result.push('!');
|
|
i += 1;
|
|
}
|
|
'>' => {
|
|
i += 1;
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
}
|
|
'>' | '!' => result.push_str(&value.to_uppercase()),
|
|
_ => result.push(chars[i]),
|
|
}
|
|
i += 1;
|
|
}
|
|
result
|
|
}
|