use axum::extract::State; use axum::routing::{get, patch}; use axum::{Json, Router}; use serde::Deserialize; use serde_json::{json, Value}; use validator::Validate; use crate::auth::session::AuthUser; use crate::error::{AppError, AppResult}; use crate::models::UserSettings; use crate::state::AppState; mod collab; mod lists; mod subs; pub fn router() -> Router { Router::new() .route("/health", get(health)) .route("/settings", patch(update_settings)) .route("/profile", patch(update_profile)) .merge(lists::router()) .merge(collab::router()) .merge(subs::router()) } async fn health() -> Json { Json(json!({ "status": "ok" })) } const ALLOWED_LOCALES: &[&str] = &["de", "en"]; const ALLOWED_THEMES: &[&str] = &["breakcore", "grunge", "minimal"]; #[derive(Debug, Deserialize)] struct SettingsReq { locale: Option, #[serde(default)] currency: Option, theme: Option, notify_email: Option, } async fn update_settings( State(state): State, AuthUser(user): AuthUser, Json(req): Json, ) -> AppResult> { if let Some(loc) = &req.locale { if !ALLOWED_LOCALES.contains(&loc.as_str()) { return Err(AppError::Validation(format!("unsupported locale: {loc}"))); } } if let Some(theme) = &req.theme { if !ALLOWED_THEMES.contains(&theme.as_str()) { return Err(AppError::Validation(format!("unsupported theme: {theme}"))); } } if let Some(cur) = &req.currency { if cur.len() != 3 { return Err(AppError::Validation( "currency must be a 3-letter code".into(), )); } } let settings = sqlx::query_as::<_, UserSettings>( "UPDATE user_settings SET locale = COALESCE($2, locale), currency = COALESCE($3, currency), theme = COALESCE($4, theme), notify_email = COALESCE($5, notify_email) WHERE user_id = $1 RETURNING user_id, locale, currency, theme, notify_email", ) .bind(user.id) .bind(req.locale) .bind(req.currency.map(|c| c.to_uppercase())) .bind(req.theme) .bind(req.notify_email) .fetch_one(&state.pool) .await?; Ok(Json(settings)) } #[derive(Debug, Deserialize, Validate)] struct ProfileReq { #[validate(length(max = 80, message = "display name too long"))] display_name: Option, } async fn update_profile( State(state): State, AuthUser(user): AuthUser, Json(req): Json, ) -> AppResult> { req.validate() .map_err(|e| AppError::Validation(e.to_string()))?; let display = req .display_name .as_deref() .map(str::trim) .filter(|s| !s.is_empty()); sqlx::query("UPDATE users SET display_name = $2 WHERE id = $1") .bind(user.id) .bind(display) .execute(&state.pool) .await?; Ok(Json(json!({ "display_name": display }))) }