diff options
| author | soryu <soryu@soryu.co> | 2026-04-29 01:10:11 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-04-29 01:10:11 +0100 |
| commit | 4b1d608b839769052634b4facc345b891d468926 (patch) | |
| tree | 1d5ff45b5b34b2e3e378a4cf69fd62ff39cf12de /makima/src/server/handlers/users.rs | |
| parent | 5bde7c2d7e099fd9c8b2615602ab1d096bd9b6be (diff) | |
| download | soryu-4b1d608b839769052634b4facc345b891d468926.tar.gz soryu-4b1d608b839769052634b4facc345b891d468926.zip | |
feat: document-mode directive UI proof of concept (Lexical) (#101)
* WIP: heartbeat checkpoint
* feat: soryu-co/soryu - makima: Backend: feature flag + goal-edit interrupt messaging
* WIP: heartbeat checkpoint
* WIP: heartbeat checkpoint
* feat: soryu-co/soryu - makima: Frontend: Lexical document editor with step blocks, context menu, countdown
Diffstat (limited to 'makima/src/server/handlers/users.rs')
| -rw-r--r-- | makima/src/server/handlers/users.rs | 154 |
1 files changed, 154 insertions, 0 deletions
diff --git a/makima/src/server/handlers/users.rs b/makima/src/server/handlers/users.rs index 0b2ccdd..0b86592 100644 --- a/makima/src/server/handlers/users.rs +++ b/makima/src/server/handlers/users.rs @@ -928,6 +928,160 @@ pub async fn delete_account_handler( } // ============================================================================= +// User Settings (per-user feature flags) +// ============================================================================= + +/// User settings response. +#[derive(Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct UserSettingsResponse { + /// Whether the new "document mode" UI is enabled for this user. + pub document_mode_enabled: bool, +} + +/// Update user settings request. +#[derive(Debug, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct UpdateUserSettingsRequest { + /// Whether to enable the new "document mode" UI for this user. + pub document_mode_enabled: bool, +} + +/// Get the authenticated user's settings (feature flags). +/// +/// Returns the user's per-user settings, currently consisting of feature flags +/// used by the frontend to decide which UI to show. +#[utoipa::path( + get, + path = "/api/v1/users/me/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 = "Users" +)] +pub async fn get_user_settings_handler( + State(state): State<SharedState>, + UserOnly(user): UserOnly, +) -> 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 sqlx::query_scalar::<_, bool>( + "SELECT document_mode_enabled FROM users WHERE id = $1", + ) + .bind(user.user_id) + .fetch_optional(pool) + .await + { + Ok(Some(document_mode_enabled)) => Json(UserSettingsResponse { + document_mode_enabled, + }) + .into_response(), + Ok(None) => { + // User row missing — fall back to defaults rather than 404 since the + // user is authenticated. + tracing::warn!(user_id = %user.user_id, "User row not found when fetching settings — returning defaults"); + Json(UserSettingsResponse { + document_mode_enabled: false, + }) + .into_response() + } + Err(e) => { + tracing::error!("Failed to fetch user settings: {}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response() + } + } +} + +/// Update the authenticated user's settings (feature flags). +/// +/// Replaces the user's settings record with the provided values. +#[utoipa::path( + put, + path = "/api/v1/users/me/settings", + request_body = UpdateUserSettingsRequest, + responses( + (status = 200, description = "Updated user settings", body = UserSettingsResponse), + (status = 401, description = "Not authenticated", body = ApiError), + (status = 404, description = "User not found", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError), + ), + security( + ("bearer_auth" = []) + ), + tag = "Users" +)] +pub async fn update_user_settings_handler( + State(state): State<SharedState>, + UserOnly(user): UserOnly, + Json(req): Json<UpdateUserSettingsRequest>, +) -> impl IntoResponse { + let Some(ref pool) = state.db_pool else { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), + ) + .into_response(); + }; + + let result = sqlx::query_scalar::<_, bool>( + r#" + UPDATE users + SET document_mode_enabled = $1, updated_at = NOW() + WHERE id = $2 + RETURNING document_mode_enabled + "#, + ) + .bind(req.document_mode_enabled) + .bind(user.user_id) + .fetch_optional(pool) + .await; + + match result { + Ok(Some(document_mode_enabled)) => { + tracing::info!( + user_id = %user.user_id, + document_mode_enabled = document_mode_enabled, + "Updated user settings" + ); + Json(UserSettingsResponse { + document_mode_enabled, + }) + .into_response() + } + Ok(None) => ( + StatusCode::NOT_FOUND, + Json(ApiError::new("USER_NOT_FOUND", "User not found")), + ) + .into_response(), + Err(e) => { + tracing::error!("Failed to update user settings: {}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response() + } + } +} + +// ============================================================================= // Tests // ============================================================================= |
