113 lines
3.0 KiB
Rust
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 })))
|
|
}
|