use super::table_access::{ check_field_write_access, check_table_access, filter_fields_by_role, AccessType, UserRoles, }; use crate::core::shared::state::AppState; use crate::core::shared::sanitize_identifier; use crate::core::urls::ApiUrls; use crate::security::error_sanitizer::log_and_sanitize; use crate::security::sql_guard::{ build_safe_count_query, build_safe_select_query, validate_table_name, }; use axum::{ extract::{Path, Query, State}, http::{HeaderMap, StatusCode}, response::IntoResponse, routing::{delete, get, post, put}, Json, Router, }; use diesel::prelude::*; use diesel::sql_query; use log::{info, warn}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::sync::Arc; use uuid::Uuid; fn user_roles_from_headers(headers: &HeaderMap) -> UserRoles { let roles = headers .get("X-User-Roles") .and_then(|v| v.to_str().ok()) .map(|s| { s.split(';') .map(|r| r.trim().to_string()) .filter(|r| !r.is_empty()) .collect() }) .unwrap_or_default(); let user_id = headers .get("X-User-Id") .and_then(|v| v.to_str().ok()) .and_then(|s| Uuid::parse_str(s).ok()); if let Some(uid) = user_id { UserRoles::with_user_id(roles, uid) } else { UserRoles::new(roles) } } #[derive(Debug, Deserialize)] pub struct QueryParams { pub limit: Option, pub offset: Option, pub order_by: Option, pub order_dir: Option, } #[derive(Debug, Serialize)] pub struct ListResponse { pub data: Vec, pub total: i64, pub limit: i32, pub offset: i32, } #[derive(Debug, Serialize)] pub struct RecordResponse { pub success: bool, pub data: Option, pub message: Option, } #[derive(Debug, Serialize)] pub struct DeleteResponse { pub success: bool, pub deleted: i64, pub message: Option, } pub fn configure_db_routes() -> Router> { Router::new() .route( &ApiUrls::DB_TABLE.replace(":table", "{table}"), get(list_records_handler), ) .route( &ApiUrls::DB_TABLE.replace(":table", "{table}"), post(create_record_handler), ) .route( &ApiUrls::DB_TABLE_RECORD .replace(":table", "{table}") .replace(":id", "{id}"), get(get_record_handler), ) .route( &ApiUrls::DB_TABLE_RECORD .replace(":table", "{table}") .replace(":id", "{id}"), put(update_record_handler), ) .route( &ApiUrls::DB_TABLE_RECORD .replace(":table", "{table}") .replace(":id", "{id}"), delete(delete_record_handler), ) .route( &ApiUrls::DB_TABLE_COUNT.replace(":table", "{table}"), get(count_records_handler), ) .route( &ApiUrls::DB_TABLE_SEARCH.replace(":table", "{table}"), post(search_records_handler), ) } pub async fn list_records_handler( State(state): State>, headers: HeaderMap, Path(table): Path, Query(params): Query, ) -> impl IntoResponse { let table_name = sanitize_identifier(&table); let user_roles = user_roles_from_headers(&headers); let limit = params.limit.unwrap_or(20).min(100); let offset = params.offset.unwrap_or(0); // Validate table name against whitelist if let Err(e) = validate_table_name(&table_name) { warn!("Invalid table name attempted: {} - {}", table_name, e); return ( StatusCode::BAD_REQUEST, Json(json!({ "error": "Invalid table name" })), ) .into_response(); } let order_by = params.order_by.as_deref(); let order_dir = params.order_dir.as_deref(); let mut conn = match state.conn.get() { Ok(c) => c, Err(e) => { return ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": format!("Database connection error: {e}") })), ) .into_response() } }; // Check table-level read access let access_info = match check_table_access(&mut conn, &table_name, &user_roles, AccessType::Read) { Ok(info) => info, Err(e) => { warn!( "Access denied to table {} for user {:?}", table_name, user_roles.user_id ); return (StatusCode::FORBIDDEN, Json(json!({ "error": e }))).into_response(); } }; // Build safe queries using sql_guard let query = match build_safe_select_query(&table_name, order_by, order_dir, limit, offset) { Ok(q) => q, Err(e) => { warn!("Failed to build safe query: {}", e); return ( StatusCode::BAD_REQUEST, Json(json!({ "error": "Invalid query parameters" })), ) .into_response(); } }; let count_query = match build_safe_count_query(&table_name) { Ok(q) => q, Err(e) => { warn!("Failed to build count query: {}", e); return ( StatusCode::BAD_REQUEST, Json(json!({ "error": "Invalid table name" })), ) .into_response(); } }; let rows: Result, _> = sql_query(&query).get_results(&mut conn); let total: Result = sql_query(&count_query).get_result(&mut conn); match (rows, total) { (Ok(data), Ok(count_result)) => { // Filter fields based on user roles let filtered_data: Vec = data .into_iter() .map(|r| filter_fields_by_role(r.data, &user_roles, &access_info)) .collect(); let response = ListResponse { data: filtered_data, total: count_result.count, limit, offset, }; (StatusCode::OK, Json(response)).into_response() } (Err(e), _) | (_, Err(e)) => { let sanitized = log_and_sanitize(&e, &format!("list_records_{}", table_name), None); (StatusCode::INTERNAL_SERVER_ERROR, sanitized).into_response() } } } pub async fn get_record_handler( State(state): State>, headers: HeaderMap, Path((table, id)): Path<(String, String)>, ) -> impl IntoResponse { let table_name = sanitize_identifier(&table); let user_roles = user_roles_from_headers(&headers); let Ok(record_id) = Uuid::parse_str(&id) else { return ( StatusCode::BAD_REQUEST, Json(RecordResponse { success: false, data: None, message: Some("Invalid UUID format".to_string()), }), ) .into_response(); }; let Ok(mut conn) = state.conn.get() else { return ( StatusCode::INTERNAL_SERVER_ERROR, Json(RecordResponse { success: false, data: None, message: Some("Database connection error".to_string()), }), ) .into_response(); }; // Check table-level read access let access_info = match check_table_access(&mut conn, &table_name, &user_roles, AccessType::Read) { Ok(info) => info, Err(e) => { return ( StatusCode::FORBIDDEN, Json(RecordResponse { success: false, data: None, message: Some(e), }), ) .into_response(); } }; let query = format!( "SELECT row_to_json(t.*) as data FROM {} t WHERE id = $1", table_name ); let row: Result, _> = sql_query(&query) .bind::(record_id) .get_result(&mut conn) .optional(); match row { Ok(Some(r)) => { // Filter fields based on user roles let filtered_data = filter_fields_by_role(r.data, &user_roles, &access_info); ( StatusCode::OK, Json(RecordResponse { success: true, data: Some(filtered_data), message: None, }), ) .into_response() } Ok(None) => ( StatusCode::NOT_FOUND, Json(RecordResponse { success: false, data: None, message: Some("Record not found".to_string()), }), ) .into_response(), Err(e) => { let sanitized = log_and_sanitize(&e, &format!("get_record_{}", table_name), None); (StatusCode::INTERNAL_SERVER_ERROR, sanitized).into_response() } } } pub async fn create_record_handler( State(state): State>, headers: HeaderMap, Path(table): Path, Json(payload): Json, ) -> impl IntoResponse { let table_name = sanitize_identifier(&table); let user_roles = user_roles_from_headers(&headers); let Some(obj) = payload.as_object() else { return ( StatusCode::BAD_REQUEST, Json(RecordResponse { success: false, data: None, message: Some("Payload must be a JSON object".to_string()), }), ) .into_response(); }; let mut columns: Vec = vec!["id".to_string()]; let mut values: Vec = vec![format!("'{}'", Uuid::new_v4())]; for (key, value) in obj { let col = sanitize_identifier(key); if col.is_empty() || col == "id" { continue; } columns.push(col); values.push(value_to_sql(value)); } let Ok(mut conn) = state.conn.get() else { return ( StatusCode::INTERNAL_SERVER_ERROR, Json(RecordResponse { success: false, data: None, message: Some("Database connection error".to_string()), }), ) .into_response(); }; let access_info = match check_table_access(&mut conn, &table_name, &user_roles, AccessType::Write) { Ok(info) => info, Err(e) => { return ( StatusCode::FORBIDDEN, Json(RecordResponse { success: false, data: None, message: Some(e), }), ) .into_response(); } }; // Check field-level write access for fields being inserted let field_names: Vec = obj .keys() .map(|k| sanitize_identifier(k)) .filter(|k| !k.is_empty() && k != "id") .collect(); if let Err(e) = check_field_write_access(&field_names, &user_roles, &access_info) { return ( StatusCode::FORBIDDEN, Json(RecordResponse { success: false, data: None, message: Some(e), }), ) .into_response(); } let query = format!( "INSERT INTO {} ({}) VALUES ({}) RETURNING row_to_json({}.*)::jsonb as data", table_name, columns.join(", "), values.join(", "), table_name ); let row: Result = sql_query(&query).get_result(&mut conn); match row { Ok(r) => { info!("Created record in {table_name}"); ( StatusCode::CREATED, Json(RecordResponse { success: true, data: Some(r.data), message: None, }), ) .into_response() } Err(e) => { let sanitized = log_and_sanitize(&e, &format!("create_record_{}", table_name), None); (StatusCode::INTERNAL_SERVER_ERROR, sanitized).into_response() } } } pub async fn update_record_handler( State(state): State>, headers: HeaderMap, Path((table, id)): Path<(String, String)>, Json(payload): Json, ) -> impl IntoResponse { let table_name = sanitize_identifier(&table); let user_roles = user_roles_from_headers(&headers); let Ok(record_id) = Uuid::parse_str(&id) else { return ( StatusCode::BAD_REQUEST, Json(RecordResponse { success: false, data: None, message: Some("Invalid UUID format".to_string()), }), ) .into_response(); }; let Some(obj) = payload.as_object() else { return ( StatusCode::BAD_REQUEST, Json(RecordResponse { success: false, data: None, message: Some("Payload must be a JSON object".to_string()), }), ) .into_response(); }; let mut set_clauses: Vec = Vec::new(); for (key, value) in obj { let col = sanitize_identifier(key); if col.is_empty() || col == "id" { continue; } set_clauses.push(format!("{} = {}", col, value_to_sql(value))); } if set_clauses.is_empty() { return ( StatusCode::BAD_REQUEST, Json(RecordResponse { success: false, data: None, message: Some("No valid fields to update".to_string()), }), ) .into_response(); } set_clauses.push("updated_at = NOW()".to_string()); let mut conn = match state.conn.get() { Ok(c) => c, Err(e) => { return ( StatusCode::INTERNAL_SERVER_ERROR, Json(RecordResponse { success: false, data: None, message: Some(format!("Database connection error: {e}")), }), ) .into_response() } }; // Check table-level write access let access_info = match check_table_access(&mut conn, &table_name, &user_roles, AccessType::Write) { Ok(info) => info, Err(e) => { return ( StatusCode::FORBIDDEN, Json(RecordResponse { success: false, data: None, message: Some(e), }), ) .into_response(); } }; // Check field-level write access for fields being updated let field_names: Vec = obj .keys() .map(|k| sanitize_identifier(k)) .filter(|k| !k.is_empty() && k != "id") .collect(); if let Err(e) = check_field_write_access(&field_names, &user_roles, &access_info) { return ( StatusCode::FORBIDDEN, Json(RecordResponse { success: false, data: None, message: Some(e), }), ) .into_response(); } let query = format!( "UPDATE {} SET {} WHERE id = '{}' RETURNING row_to_json({}.*)::jsonb as data", table_name, set_clauses.join(", "), record_id, table_name ); let row: Result = sql_query(&query).get_result(&mut conn); match row { Ok(r) => { info!("Updated record in {table_name}: {record_id}"); ( StatusCode::OK, Json(RecordResponse { success: true, data: Some(r.data), message: None, }), ) .into_response() } Err(e) => { let sanitized = log_and_sanitize(&e, &format!("update_record_{}", table_name), None); (StatusCode::INTERNAL_SERVER_ERROR, sanitized).into_response() } } } pub async fn delete_record_handler( State(state): State>, headers: HeaderMap, Path((table, id)): Path<(String, String)>, ) -> impl IntoResponse { let table_name = sanitize_identifier(&table); let user_roles = user_roles_from_headers(&headers); let Ok(record_id) = Uuid::parse_str(&id) else { return ( StatusCode::BAD_REQUEST, Json(DeleteResponse { success: false, deleted: 0, message: Some("Invalid UUID format".to_string()), }), ) .into_response(); }; let Ok(mut conn) = state.conn.get() else { return ( StatusCode::INTERNAL_SERVER_ERROR, Json(DeleteResponse { success: false, deleted: 0, message: Some("Database connection error".to_string()), }), ) .into_response(); }; if let Err(e) = check_table_access(&mut conn, &table_name, &user_roles, AccessType::Write) { return ( StatusCode::FORBIDDEN, Json(DeleteResponse { success: false, deleted: 0, message: Some(e), }), ) .into_response(); } let query = format!("DELETE FROM {} WHERE id = $1", table_name); let deleted: Result = sql_query(&query) .bind::(record_id) .execute(&mut conn); match deleted { Ok(count) => { info!("Deleted {count} record(s) from {table_name}"); ( StatusCode::OK, Json(DeleteResponse { success: count > 0, deleted: count as i64, message: if count == 0 { Some("Record not found".to_string()) } else { None }, }), ) .into_response() } Err(e) => { let sanitized = log_and_sanitize(&e, &format!("delete_record_{}", table_name), None); (StatusCode::INTERNAL_SERVER_ERROR, sanitized).into_response() } } } pub async fn count_records_handler( State(state): State>, headers: HeaderMap, Path(table): Path, ) -> impl IntoResponse { let table_name = sanitize_identifier(&table); let user_roles = user_roles_from_headers(&headers); let mut conn = match state.conn.get() { Ok(c) => c, Err(e) => { let sanitized = log_and_sanitize(&e, "count_records_db_connection", None); return (StatusCode::INTERNAL_SERVER_ERROR, sanitized).into_response() } }; // Check table-level read access (count requires read permission) if let Err(e) = check_table_access(&mut conn, &table_name, &user_roles, AccessType::Read) { return (StatusCode::FORBIDDEN, Json(json!({ "error": e }))).into_response(); } let query = format!("SELECT COUNT(*) as count FROM {}", table_name); let result: Result = sql_query(&query).get_result(&mut conn); match result { Ok(r) => (StatusCode::OK, Json(json!({ "count": r.count }))).into_response(), Err(e) => { let sanitized = log_and_sanitize(&e, &format!("count_records_{}", table_name), None); (StatusCode::INTERNAL_SERVER_ERROR, sanitized).into_response() } } } #[derive(Debug, Deserialize)] pub struct SearchRequest { pub query: String, pub fields: Option>, pub limit: Option, } pub async fn search_records_handler( State(state): State>, headers: HeaderMap, Path(table): Path, Json(payload): Json, ) -> impl IntoResponse { let table_name = sanitize_identifier(&table); let user_roles = user_roles_from_headers(&headers); let limit = payload.limit.unwrap_or(20).min(100); let search_term = payload.query.replace('\'', "''"); let mut conn = match state.conn.get() { Ok(c) => c, Err(e) => { let sanitized = log_and_sanitize(&e, "search_records_db_connection", None); return (StatusCode::INTERNAL_SERVER_ERROR, sanitized).into_response() } }; // Check table-level read access let access_info = match check_table_access(&mut conn, &table_name, &user_roles, AccessType::Read) { Ok(info) => info, Err(e) => { return (StatusCode::FORBIDDEN, Json(json!({ "error": e }))).into_response(); } }; let query = format!( "SELECT row_to_json(t.*) as data FROM {} t WHERE COALESCE(t.title::text, '') || ' ' || COALESCE(t.name::text, '') || ' ' || COALESCE(t.description::text, '') ILIKE '%{}%' LIMIT {}", table_name, search_term, limit ); let rows: Result, _> = sql_query(&query).get_results(&mut conn); match rows { Ok(data) => { // Filter fields based on user roles let filtered_data: Vec = data .into_iter() .map(|r| filter_fields_by_role(r.data, &user_roles, &access_info)) .collect(); (StatusCode::OK, Json(json!({ "data": filtered_data }))).into_response() } Err(e) => { let sanitized = log_and_sanitize(&e, &format!("search_records_{}", table_name), None); (StatusCode::INTERNAL_SERVER_ERROR, sanitized) .into_response() } } } fn value_to_sql(value: &Value) -> String { match value { Value::Null => "NULL".to_string(), Value::Bool(b) => b.to_string(), Value::Number(n) => n.to_string(), Value::String(s) => format!("'{}'", s.replace('\'', "''")), Value::Array(_) | Value::Object(_) => { format!("'{}'", value.to_string().replace('\'', "''")) } } } #[derive(QueryableByName)] struct JsonRow { #[diesel(sql_type = diesel::sql_types::Jsonb)] data: Value, } #[derive(QueryableByName)] struct CountResult { #[diesel(sql_type = diesel::sql_types::BigInt)] count: i64, }