#[cfg(feature = "portal")]
use axum::{
extract::{Json, Path, State},
http::StatusCode,
response::IntoResponse,
};
#[cfg(feature = "portal")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "portal")]
use uuid::Uuid;
#[cfg(feature = "portal")]
use crate::portal::auth_db::PortalState;
#[cfg(feature = "portal")]
use crate::portal::db::{queries::SettingsRepository, DbError};
#[cfg(feature = "portal")]
use crate::portal::middleware::AuthClaims;
#[cfg(feature = "portal")]
#[derive(Debug, Serialize)]
pub struct SettingsResponse {
pub settings: Vec<SettingItem>,
pub version: i32,
}
#[cfg(feature = "portal")]
#[derive(Debug, Serialize)]
pub struct SettingItem {
pub key: String,
pub value: serde_json::Value,
pub version: i32,
pub updated_at: String,
}
#[cfg(feature = "portal")]
#[derive(Debug, Deserialize)]
pub struct UpdateSettingsRequest {
pub settings: Vec<SettingUpdate>,
}
#[cfg(feature = "portal")]
#[derive(Debug, Deserialize)]
pub struct SettingUpdate {
pub key: String,
pub value: serde_json::Value,
}
#[cfg(feature = "portal")]
#[derive(Debug, Deserialize)]
pub struct SyncRequest {
pub since_version: Option<i32>,
pub changes: Option<Vec<SettingUpdate>>,
}
#[cfg(feature = "portal")]
#[derive(Debug, Serialize)]
pub struct SyncResponse {
pub success: bool,
pub server_changes: Vec<SettingItem>,
pub conflicts: Vec<ConflictItem>,
pub current_version: i32,
}
#[cfg(feature = "portal")]
#[derive(Debug, Serialize)]
pub struct ConflictItem {
pub key: String,
pub client_value: serde_json::Value,
pub server_value: serde_json::Value,
pub server_version: i32,
}
#[cfg(feature = "portal")]
pub async fn get_settings(
State(state): State<PortalState>,
AuthClaims(claims): AuthClaims,
) -> impl IntoResponse {
let user_id = match Uuid::parse_str(&claims.sub) {
Ok(id) => id,
Err(_) => {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": "Invalid user ID"})),
);
}
};
let settings_repo = SettingsRepository::new(state.db.pool());
match settings_repo.get_all(user_id).await {
Ok(settings) => {
let max_version = settings.iter().map(|s| s.version).max().unwrap_or(0);
let items: Vec<SettingItem> = settings
.into_iter()
.map(|s| SettingItem {
key: s.key,
value: s.value,
version: s.version,
updated_at: s.updated_at.to_rfc3339(),
})
.collect();
(
StatusCode::OK,
Json(serde_json::json!({
"settings": items,
"version": max_version
})),
)
}
Err(e) => {
tracing::error!("Database error: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch settings"})),
)
}
}
}
#[cfg(feature = "portal")]
pub async fn get_setting(
State(state): State<PortalState>,
AuthClaims(claims): AuthClaims,
Path(key): Path<String>,
) -> impl IntoResponse {
let user_id = match Uuid::parse_str(&claims.sub) {
Ok(id) => id,
Err(_) => {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": "Invalid user ID"})),
);
}
};
let settings_repo = SettingsRepository::new(state.db.pool());
match settings_repo.get(user_id, &key).await {
Ok(setting) => (
StatusCode::OK,
Json(serde_json::json!({
"key": setting.key,
"value": setting.value,
"version": setting.version,
"updated_at": setting.updated_at.to_rfc3339()
})),
),
Err(DbError::NotFound) => (
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Setting not found"})),
),
Err(e) => {
tracing::error!("Database error: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to fetch setting"})),
)
}
}
}
#[cfg(feature = "portal")]
pub async fn update_settings(
State(state): State<PortalState>,
AuthClaims(claims): AuthClaims,
Json(req): Json<UpdateSettingsRequest>,
) -> impl IntoResponse {
let user_id = match Uuid::parse_str(&claims.sub) {
Ok(id) => id,
Err(_) => {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": "Invalid user ID"})),
);
}
};
let settings_repo = SettingsRepository::new(state.db.pool());
let mut updated = Vec::new();
let mut errors = Vec::new();
for setting in req.settings {
if setting.key.is_empty() || setting.key.len() > 255 {
errors.push(format!("Invalid key: {}", setting.key));
continue;
}
match settings_repo
.set(user_id, &setting.key, setting.value.clone())
.await
{
Ok(s) => {
updated.push(SettingItem {
key: s.key,
value: s.value,
version: s.version,
updated_at: s.updated_at.to_rfc3339(),
});
}
Err(e) => {
tracing::error!("Failed to update setting {}: {}", setting.key, e);
errors.push(format!("Failed to update: {}", setting.key));
}
}
}
let max_version = updated.iter().map(|s| s.version).max().unwrap_or(0);
(
StatusCode::OK,
Json(serde_json::json!({
"success": errors.is_empty(),
"updated": updated,
"errors": errors,
"version": max_version
})),
)
}
#[cfg(feature = "portal")]
pub async fn set_setting(
State(state): State<PortalState>,
AuthClaims(claims): AuthClaims,
Path(key): Path<String>,
Json(value): Json<serde_json::Value>,
) -> impl IntoResponse {
let user_id = match Uuid::parse_str(&claims.sub) {
Ok(id) => id,
Err(_) => {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": "Invalid user ID"})),
);
}
};
if key.is_empty() || key.len() > 255 {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": "Invalid key format"})),
);
}
let settings_repo = SettingsRepository::new(state.db.pool());
match settings_repo.set(user_id, &key, value).await {
Ok(setting) => {
tracing::info!("Setting {} updated for user {}", key, user_id);
(
StatusCode::OK,
Json(serde_json::json!({
"success": true,
"key": setting.key,
"value": setting.value,
"version": setting.version,
"updated_at": setting.updated_at.to_rfc3339()
})),
)
}
Err(e) => {
tracing::error!("Database error: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to update setting"})),
)
}
}
}
#[cfg(feature = "portal")]
pub async fn delete_setting(
State(state): State<PortalState>,
AuthClaims(claims): AuthClaims,
Path(key): Path<String>,
) -> impl IntoResponse {
let user_id = match Uuid::parse_str(&claims.sub) {
Ok(id) => id,
Err(_) => {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": "Invalid user ID"})),
);
}
};
let settings_repo = SettingsRepository::new(state.db.pool());
match settings_repo.delete(user_id, &key).await {
Ok(()) => {
tracing::info!("Setting {} deleted for user {}", key, user_id);
(
StatusCode::OK,
Json(serde_json::json!({
"success": true,
"deleted": key
})),
)
}
Err(e) => {
tracing::error!("Database error: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": "Failed to delete setting"})),
)
}
}
}
#[cfg(feature = "portal")]
pub async fn sync_settings(
State(state): State<PortalState>,
AuthClaims(claims): AuthClaims,
Json(req): Json<SyncRequest>,
) -> impl IntoResponse {
let user_id = match Uuid::parse_str(&claims.sub) {
Ok(id) => id,
Err(_) => {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": "Invalid user ID"})),
);
}
};
let settings_repo = SettingsRepository::new(state.db.pool());
let mut conflicts = Vec::new();
let mut applied = Vec::new();
if let Some(changes) = req.changes {
for change in changes {
if let Ok(existing) = settings_repo.get(user_id, &change.key).await {
let since = req.since_version.unwrap_or(0);
if existing.version > since {
conflicts.push(ConflictItem {
key: change.key.clone(),
client_value: change.value.clone(),
server_value: existing.value,
server_version: existing.version,
});
continue;
}
}
if let Ok(s) = settings_repo.set(user_id, &change.key, change.value).await {
applied.push(s.key);
}
}
}
let since_version = req.since_version.unwrap_or(0);
let server_changes = match settings_repo
.get_changes_since(user_id, since_version)
.await
{
Ok(changes) => changes
.into_iter()
.filter(|s| !applied.contains(&s.key))
.map(|s| SettingItem {
key: s.key,
value: s.value,
version: s.version,
updated_at: s.updated_at.to_rfc3339(),
})
.collect(),
Err(e) => {
tracing::error!("Database error: {}", e);
Vec::new()
}
};
let current_version = match settings_repo.get_all(user_id).await {
Ok(all) => all.iter().map(|s| s.version).max().unwrap_or(0),
Err(_) => 0,
};
(
StatusCode::OK,
Json(serde_json::json!({
"success": conflicts.is_empty(),
"server_changes": server_changes,
"conflicts": conflicts,
"current_version": current_version,
"applied_count": applied.len()
})),
)
}