aboutsummaryrefslogtreecommitdiff
path: root/src/routes/preferences.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/routes/preferences.rs')
-rw-r--r--src/routes/preferences.rs423
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,
+ }))
+}