summaryrefslogtreecommitdiff
path: root/makima/src/server
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/server')
-rw-r--r--makima/src/server/handlers/mod.rs1
-rw-r--r--makima/src/server/handlers/settings.rs196
-rw-r--r--makima/src/server/mod.rs11
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",