feat(api): expose reset status polling (#1598)

* feat(api): expose reset status polling

* fix(api): hide reset enqueue exception details

* fix(api): use stable reset authorization message

* fix(api): narrow reset enqueue error handling

* fix(api): document reset enqueue failures

* docs(api): regenerate reset status OpenAPI

* fix(api): address reset polling review feedback
This commit is contained in:
ghost
2026-05-02 14:56:42 -06:00
committed by GitHub
parent 95c2208bdb
commit a8425a2488
6 changed files with 330 additions and 10 deletions

View File

@@ -1,12 +1,44 @@
# frozen_string_literal: true
class Api::V1::UsersController < Api::V1::BaseController
before_action :ensure_write_scope
before_action :ensure_admin, only: :reset
before_action :ensure_read_scope, only: :reset_status
before_action :ensure_write_scope, except: :reset_status
before_action :ensure_admin, only: %i[reset reset_status]
def reset
FamilyResetJob.perform_later(Current.family)
render json: { message: "Account reset has been initiated" }
family = current_resource_owner.family
begin
job = FamilyResetJob.perform_later(family)
rescue StandardError => e
Rails.logger.error "Failed to enqueue FamilyResetJob for family #{family.id}: #{e.message}"
render json: {
error: "reset_enqueue_failed",
message: "Account reset could not be queued"
}, status: :internal_server_error
return
end
render json: {
message: "Account reset has been initiated",
status: "queued",
job_id: job.job_id,
family_id: family.id,
status_url: api_v1_users_reset_status_path
}
end
def reset_status
family = current_resource_owner.family
counts = reset_target_counts(family)
reset_complete = counts.values.sum.zero?
render json: {
status: reset_complete ? "complete" : "data_remaining",
family_id: family.id,
reset_complete: reset_complete,
counts: counts
}
end
def destroy
@@ -26,10 +58,26 @@ class Api::V1::UsersController < Api::V1::BaseController
authorize_scope!(:write)
end
def ensure_read_scope
authorize_scope!(:read)
end
def ensure_admin
return true if current_resource_owner&.admin?
render_json({ error: "forbidden", message: I18n.t("users.reset.unauthorized") }, status: :forbidden)
render_json({ error: "forbidden", message: "You are not authorized to perform this action" }, status: :forbidden)
false
end
def reset_target_counts(family)
{
accounts: family.accounts.count,
categories: family.categories.count,
tags: family.tags.count,
merchants: family.merchants.count,
plaid_items: family.plaid_items.count,
imports: family.imports.count,
budgets: family.budgets.count
}
end
end