//! 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, 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, Authenticated(user): Authenticated, Path(key): Path, ) -> 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, Authenticated(user): Authenticated, Json(req): Json, ) -> 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, Authenticated(user): Authenticated, Path(key): Path, ) -> 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() } } }