diff options
Diffstat (limited to 'makima/src/server')
| -rw-r--r-- | makima/src/server/handlers/mod.rs | 1 | ||||
| -rw-r--r-- | makima/src/server/handlers/settings.rs | 196 | ||||
| -rw-r--r-- | makima/src/server/mod.rs | 11 |
3 files changed, 207 insertions, 1 deletions
diff --git a/makima/src/server/handlers/mod.rs b/makima/src/server/handlers/mod.rs index 4bdb424..b3c433b 100644 --- a/makima/src/server/handlers/mod.rs +++ b/makima/src/server/handlers/mod.rs @@ -20,6 +20,7 @@ pub mod mesh_merge; pub mod mesh_supervisor; pub mod mesh_ws; pub mod repository_history; +pub mod settings; pub mod speak; pub mod templates; pub mod voice; 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() + } + } +} diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs index b382f04..025ec85 100644 --- a/makima/src/server/mod.rs +++ b/makima/src/server/mod.rs @@ -18,7 +18,7 @@ use tower_http::trace::TraceLayer; use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; -use crate::server::handlers::{api_keys, chat, contract_chat, contract_daemon, contract_discuss, contracts, daemon_download, directives, file_ws, files, history, listen, mesh, mesh_chat, mesh_daemon, mesh_merge, mesh_supervisor, mesh_ws, orders, repository_history, speak, templates, transcript_analysis, users, versions}; +use crate::server::handlers::{api_keys, chat, contract_chat, contract_daemon, contract_discuss, contracts, daemon_download, directives, file_ws, files, history, listen, mesh, mesh_chat, mesh_daemon, mesh_merge, mesh_supervisor, mesh_ws, orders, repository_history, settings, speak, templates, transcript_analysis, users, versions}; use crate::server::openapi::ApiDoc; use crate::server::state::SharedState; @@ -281,6 +281,15 @@ pub fn make_router(state: SharedState) -> Router { .route("/timeline", get(history::get_timeline)) // Contract type templates (built-in only) .route("/contract-types", get(templates::list_contract_types)) + // User settings (feature flags) endpoints + .route( + "/user-settings", + get(settings::list_settings).put(settings::upsert_setting), + ) + .route( + "/user-settings/{key}", + get(settings::get_setting).delete(settings::delete_setting), + ) // Settings endpoints .route( "/settings/repository-history", |
