summaryrefslogtreecommitdiff
path: root/makima/src/server/handlers/users.rs
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-04-29 01:10:11 +0100
committerGitHub <noreply@github.com>2026-04-29 01:10:11 +0100
commit4b1d608b839769052634b4facc345b891d468926 (patch)
tree1d5ff45b5b34b2e3e378a4cf69fd62ff39cf12de /makima/src/server/handlers/users.rs
parent5bde7c2d7e099fd9c8b2615602ab1d096bd9b6be (diff)
downloadsoryu-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.rs154
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
// =============================================================================