diff options
Diffstat (limited to 'makima/src/server/handlers/settings.rs')
| -rw-r--r-- | makima/src/server/handlers/settings.rs | 196 |
1 files changed, 196 insertions, 0 deletions
diff --git a/makima/src/server/handlers/settings.rs b/makima/src/server/handlers/settings.rs new file mode 100644 index 0000000..ae52d5a --- /dev/null +++ b/makima/src/server/handlers/settings.rs @@ -0,0 +1,196 @@ +//! HTTP handlers for user settings (feature flags / preferences). + +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, + Json, +}; + +use crate::db::models::{UpsertUserSettingRequest, UserSettingsResponse}; +use crate::db::repository; +use crate::server::auth::Authenticated; +use crate::server::messages::ApiError; +use crate::server::state::SharedState; + +/// List all settings for the authenticated user. +#[utoipa::path( + get, + path = "/api/v1/settings", + responses( + (status = 200, description = "User settings", body = UserSettingsResponse), + (status = 401, description = "Not authenticated", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError), + ), + security( + ("bearer_auth" = []) + ), + tag = "Settings" +)] +pub async fn list_settings( + State(state): State<SharedState>, + Authenticated(user): Authenticated, +) -> impl IntoResponse { + let Some(ref pool) = state.db_pool else { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), + ) + .into_response(); + }; + + match repository::get_user_settings(pool, user.owner_id).await { + Ok(settings) => Json(UserSettingsResponse { settings }).into_response(), + Err(e) => { + tracing::error!("Failed to list user settings: {}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response() + } + } +} + +/// Get a specific setting by key. +#[utoipa::path( + get, + path = "/api/v1/settings/{key}", + params( + ("key" = String, Path, description = "Setting key") + ), + responses( + (status = 200, description = "User setting", body = crate::db::models::UserSetting), + (status = 401, description = "Not authenticated", body = ApiError), + (status = 404, description = "Setting not found", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError), + ), + security( + ("bearer_auth" = []) + ), + tag = "Settings" +)] +pub async fn get_setting( + State(state): State<SharedState>, + Authenticated(user): Authenticated, + Path(key): Path<String>, +) -> impl IntoResponse { + let Some(ref pool) = state.db_pool else { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), + ) + .into_response(); + }; + + match repository::get_user_setting(pool, user.owner_id, &key).await { + Ok(Some(setting)) => Json(setting).into_response(), + Ok(None) => ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", format!("Setting '{}' not found", key))), + ) + .into_response(), + Err(e) => { + tracing::error!("Failed to get user setting: {}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response() + } + } +} + +/// Upsert a user setting (create or update). +#[utoipa::path( + put, + path = "/api/v1/settings", + request_body = UpsertUserSettingRequest, + responses( + (status = 200, description = "Setting upserted", body = crate::db::models::UserSetting), + (status = 401, description = "Not authenticated", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError), + ), + security( + ("bearer_auth" = []) + ), + tag = "Settings" +)] +pub async fn upsert_setting( + State(state): State<SharedState>, + Authenticated(user): Authenticated, + Json(req): Json<UpsertUserSettingRequest>, +) -> impl IntoResponse { + let Some(ref pool) = state.db_pool else { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), + ) + .into_response(); + }; + + match repository::upsert_user_setting(pool, user.owner_id, &req.key, &req.value).await { + Ok(setting) => Json(setting).into_response(), + Err(e) => { + tracing::error!("Failed to upsert user setting: {}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response() + } + } +} + +/// Delete a user setting by key. +#[utoipa::path( + delete, + path = "/api/v1/settings/{key}", + params( + ("key" = String, Path, description = "Setting key") + ), + responses( + (status = 200, description = "Setting deleted"), + (status = 401, description = "Not authenticated", body = ApiError), + (status = 404, description = "Setting not found", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError), + ), + security( + ("bearer_auth" = []) + ), + tag = "Settings" +)] +pub async fn delete_setting( + State(state): State<SharedState>, + Authenticated(user): Authenticated, + Path(key): Path<String>, +) -> impl IntoResponse { + let Some(ref pool) = state.db_pool else { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), + ) + .into_response(); + }; + + match repository::delete_user_setting(pool, user.owner_id, &key).await { + Ok(true) => StatusCode::OK.into_response(), + Ok(false) => ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", format!("Setting '{}' not found", key))), + ) + .into_response(), + Err(e) => { + tracing::error!("Failed to delete user setting: {}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response() + } + } +} |
