diff options
| author | UMTS at Teleco <crt@teleco.ch> | 2025-12-13 02:48:13 +0100 |
|---|---|---|
| committer | UMTS at Teleco <crt@teleco.ch> | 2025-12-13 02:48:13 +0100 |
| commit | e52b8e1c2e110d0feb74feb7905c2ff064b51d55 (patch) | |
| tree | 3090814e422250e07e72cf1c83241ffd95cf20f7 /src/routes/preferences.rs | |
Diffstat (limited to 'src/routes/preferences.rs')
| -rw-r--r-- | src/routes/preferences.rs | 423 |
1 files changed, 423 insertions, 0 deletions
diff --git a/src/routes/preferences.rs b/src/routes/preferences.rs new file mode 100644 index 0000000..e58d823 --- /dev/null +++ b/src/routes/preferences.rs @@ -0,0 +1,423 @@ +// User preferences routes +use axum::{ + extract::{ConnectInfo, State}, + http::{HeaderMap, StatusCode}, + Json, +}; +use chrono::Utc; +use serde::{Deserialize, Serialize}; +use std::net::SocketAddr; +use tracing::{error, warn}; + +use crate::config::UserSettingsAccess; +use crate::logging::AuditLogger; +use crate::AppState; + +// Request/Response structures matching the query route pattern +#[derive(Debug, Deserialize)] +pub struct PreferencesRequest { + pub action: String, // "get", "set", "reset" + pub user_id: Option<i32>, // For admin access to other users + pub preferences: Option<serde_json::Value>, +} + +#[derive(Debug, Serialize)] +pub struct PreferencesResponse { + pub success: bool, + pub preferences: Option<serde_json::Value>, + pub error: Option<String>, +} + +/// Extract token from Authorization header +fn extract_token(headers: &HeaderMap) -> Option<String> { + headers + .get("Authorization") + .and_then(|header| header.to_str().ok()) + .and_then(|auth_str| { + if auth_str.starts_with("Bearer ") { + Some(auth_str[7..].to_string()) + } else { + None + } + }) +} + +/// POST /preferences - Handle all preference operations (get, set, reset) +pub async fn handle_preferences( + State(state): State<AppState>, + ConnectInfo(addr): ConnectInfo<SocketAddr>, + headers: HeaderMap, + Json(payload): Json<PreferencesRequest>, +) -> Result<Json<PreferencesResponse>, StatusCode> { + let timestamp = Utc::now(); + let client_ip = addr.ip().to_string(); + let request_id = AuditLogger::generate_request_id(); + + // Extract and validate session token + let token = match extract_token(&headers) { + Some(token) => token, + None => { + return Ok(Json(PreferencesResponse { + success: false, + preferences: None, + error: Some( + "Please stop trying to access this resource without signing in".to_string(), + ), + })); + } + }; + + let session = match state.session_manager.get_session(&token) { + Some(session) => session, + None => { + return Ok(Json(PreferencesResponse { + success: false, + preferences: None, + error: Some("Session not found".to_string()), + })); + } + }; + + // Determine target user ID + let target_user_id = payload.user_id.unwrap_or(session.user_id); + + // Get user's permission level for preferences + let user_settings_permission = state + .config + .permissions + .power_levels + .get(&session.power.to_string()) + .map(|p| &p.user_settings_access) + .unwrap_or(&state.config.security.default_user_settings_access); + + // Check permissions for cross-user access + if target_user_id != session.user_id { + if *user_settings_permission != UserSettingsAccess::ReadWriteAll { + // Log security warning + super::log_warning_async( + &state.logging, + &request_id, + &format!("User {} (power {}) attempted to access preferences of user {} without permission", + session.username, session.power, target_user_id), + Some("authorization"), + Some(&session.username), + Some(session.power), + ); + + return Ok(Json(PreferencesResponse { + success: false, + preferences: None, + error: Some("Insufficient permissions".to_string()), + })); + } + } + + // Check write permissions for set/reset actions + if payload.action == "set" || payload.action == "reset" { + if target_user_id == session.user_id { + // Writing own preferences - need at least ReadWriteOwn + if *user_settings_permission == UserSettingsAccess::ReadOwnOnly { + return Ok(Json(PreferencesResponse { + success: false, + preferences: None, + error: Some("Insufficient permissions".to_string()), + })); + } + } else { + // Writing others' preferences - need ReadWriteAll + if *user_settings_permission != UserSettingsAccess::ReadWriteAll { + return Ok(Json(PreferencesResponse { + success: false, + preferences: None, + error: Some("Insufficient permissions".to_string()), + })); + } + } + } + + // Log the request + if let Err(e) = state + .logging + .log_request( + &request_id, + timestamp, + &client_ip, + Some(&session.username), + Some(session.power), + "/preferences", + &serde_json::json!({"action": payload.action, "target_user_id": target_user_id}), + ) + .await + { + error!("[{}] Failed to log request: {}", request_id, e); + } + + // Handle the action + match payload.action.as_str() { + "get" => { + handle_get_preferences( + state, + request_id, + target_user_id, + session.username.clone(), + session.power, + ) + .await + } + "set" => { + handle_set_preferences( + state, + request_id, + target_user_id, + payload.preferences, + session.username.clone(), + session.power, + ) + .await + } + "reset" => { + handle_reset_preferences( + state, + request_id, + target_user_id, + session.username.clone(), + session.power, + ) + .await + } + _ => Ok(Json(PreferencesResponse { + success: false, + preferences: None, + error: Some(format!("Invalid action: {}", payload.action)), + })), + } +} + +async fn handle_get_preferences( + state: AppState, + request_id: String, + user_id: i32, + username: String, + power: i32, +) -> Result<Json<PreferencesResponse>, StatusCode> { + // Cast JSON column to CHAR to get string representation + let query = "SELECT CAST(preferences AS CHAR) FROM users WHERE id = ? AND active = TRUE"; + let row: Option<(Option<String>,)> = sqlx::query_as(query) + .bind(user_id) + .fetch_optional(state.database.pool()) + .await + .map_err(|e| { + let error_msg = format!( + "Database error fetching preferences for user {}: {}", + user_id, e + ); + super::log_error_async( + &state.logging, + &request_id, + &error_msg, + Some("database"), + Some(&username), + Some(power), + ); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + let preferences = match row { + Some((Some(prefs_str),)) => serde_json::from_str(&prefs_str).unwrap_or_else(|e| { + warn!( + "[{}] Failed to parse preferences JSON for user {}: {}", + request_id, user_id, e + ); + super::log_warning_async( + &state.logging, + &request_id, + &format!( + "Failed to parse preferences JSON for user {}: {}", + user_id, e + ), + Some("data_integrity"), + Some(&username), + Some(power), + ); + serde_json::json!({}) + }), + _ => serde_json::json!({}), + }; + + // Log user action + super::log_info_async( + &state.logging, + &request_id, + &format!( + "User {} retrieved preferences for user {}", + username, user_id + ), + Some("user_action"), + Some(&username), + Some(power), + ); + + Ok(Json(PreferencesResponse { + success: true, + preferences: Some(preferences), + error: None, + })) +} + +async fn handle_set_preferences( + state: AppState, + request_id: String, + user_id: i32, + new_preferences: Option<serde_json::Value>, + username: String, + power: i32, +) -> Result<Json<PreferencesResponse>, StatusCode> { + let new_prefs = match new_preferences { + Some(prefs) => prefs, + None => { + return Ok(Json(PreferencesResponse { + success: false, + preferences: None, + error: Some("Missing preferences field".to_string()), + })); + } + }; + + // Get current preferences for merging + let query = "SELECT CAST(preferences AS CHAR) FROM users WHERE id = ? AND active = TRUE"; + let row: Option<(Option<String>,)> = sqlx::query_as(query) + .bind(user_id) + .fetch_optional(state.database.pool()) + .await + .map_err(|e| { + let error_msg = format!( + "Database error fetching preferences for user {}: {}", + user_id, e + ); + super::log_error_async( + &state.logging, + &request_id, + &error_msg, + Some("database"), + Some(&username), + Some(power), + ); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + // Deep merge the preferences + let mut merged_prefs = match row { + Some((Some(prefs_str),)) => { + serde_json::from_str(&prefs_str).unwrap_or_else(|_| serde_json::json!({})) + } + _ => serde_json::json!({}), + }; + + // Merge function + fn merge_json(base: &mut serde_json::Value, update: &serde_json::Value) { + if let (Some(base_obj), Some(update_obj)) = (base.as_object_mut(), update.as_object()) { + for (key, value) in update_obj { + if let Some(base_value) = base_obj.get_mut(key) { + if base_value.is_object() && value.is_object() { + merge_json(base_value, value); + } else { + *base_value = value.clone(); + } + } else { + base_obj.insert(key.clone(), value.clone()); + } + } + } + } + + merge_json(&mut merged_prefs, &new_prefs); + + // Save to database + let prefs_str = serde_json::to_string(&merged_prefs).map_err(|e| { + error!("[{}] Failed to serialize preferences: {}", request_id, e); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + let update_query = "UPDATE users SET preferences = ? WHERE id = ? AND active = TRUE"; + sqlx::query(update_query) + .bind(&prefs_str) + .bind(user_id) + .execute(state.database.pool()) + .await + .map_err(|e| { + let error_msg = format!( + "Database error updating preferences for user {}: {}", + user_id, e + ); + super::log_error_async( + &state.logging, + &request_id, + &error_msg, + Some("database"), + Some(&username), + Some(power), + ); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + // Log user action + super::log_info_async( + &state.logging, + &request_id, + &format!("User {} updated preferences for user {}", username, user_id), + Some("user_action"), + Some(&username), + Some(power), + ); + + Ok(Json(PreferencesResponse { + success: true, + preferences: Some(merged_prefs), + error: None, + })) +} + +async fn handle_reset_preferences( + state: AppState, + request_id: String, + user_id: i32, + username: String, + power: i32, +) -> Result<Json<PreferencesResponse>, StatusCode> { + let query = "UPDATE users SET preferences = NULL WHERE id = ? AND active = TRUE"; + sqlx::query(query) + .bind(user_id) + .execute(state.database.pool()) + .await + .map_err(|e| { + let error_msg = format!( + "Database error resetting preferences for user {}: {}", + user_id, e + ); + super::log_error_async( + &state.logging, + &request_id, + &error_msg, + Some("database"), + Some(&username), + Some(power), + ); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + // Log user action + super::log_info_async( + &state.logging, + &request_id, + &format!("User {} reset preferences for user {}", username, user_id), + Some("user_action"), + Some(&username), + Some(power), + ); + + Ok(Json(PreferencesResponse { + success: true, + preferences: Some(serde_json::json!({})), + error: None, + })) +} |
