This commit is contained in:
2026-06-17 00:21:00 +02:00
commit 408e48c568
41 changed files with 6617 additions and 0 deletions
+99
View File
@@ -0,0 +1,99 @@
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;
pub fn router() -> Router<AppState> {
Router::new()
.route("/health", get(health))
.route("/settings", patch(update_settings))
.route("/profile", patch(update_profile))
}
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 })))
}