Files
consume-rs/backend/src/routes/mod.rs
T

113 lines
3.0 KiB
Rust

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<AppState> {
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<Value> {
Json(json!({ "status": "ok" }))
}
const ALLOWED_LOCALES: &[&str] = &["de", "en"];
const ALLOWED_THEMES: &[&str] = &["breakcore", "grunge", "minimal"];
#[derive(Debug, Deserialize)]
struct SettingsReq {
locale: Option<String>,
#[serde(default)]
currency: Option<String>,
theme: Option<String>,
notify_email: Option<bool>,
}
async fn update_settings(
State(state): State<AppState>,
AuthUser(user): AuthUser,
Json(req): Json<SettingsReq>,
) -> AppResult<Json<UserSettings>> {
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<String>,
}
async fn update_profile(
State(state): State<AppState>,
AuthUser(user): AuthUser,
Json(req): Json<ProfileReq>,
) -> AppResult<Json<Value>> {
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 })))
}