Add JSON API support for user data exports (#2786)

This commit is contained in:
Rob Zolkos
2026-04-08 08:17:24 -04:00
committed by GitHub
parent 98b6bdcfb7
commit 8201c2a4bd
5 changed files with 183 additions and 3 deletions
@@ -7,11 +7,20 @@ class Users::DataExportsController < ApplicationController
CURRENT_EXPORT_LIMIT = 10 CURRENT_EXPORT_LIMIT = 10
def show def show
respond_to do |format|
format.html
format.json { @export ? render(:show) : head(:not_found) }
end
end end
def create def create
@user.data_exports.create!(account: Current.account).build_later @export = @user.data_exports.create!(account: Current.account)
redirect_to @user, notice: "Export started. You'll receive an email when it's ready." @export.build_later
respond_to do |format|
format.html { redirect_to @user, notice: "Export started. You'll receive an email when it's ready." }
format.json { render :show, status: :created }
end
end end
private private
@@ -28,6 +37,7 @@ class Users::DataExportsController < ApplicationController
end end
def set_export def set_export
@export = @user.data_exports.completed.find_by(id: params[:id]) scope = request.format.json? ? @user.data_exports : @user.data_exports.completed
@export = scope.find_by(id: params[:id])
end end
end end
@@ -0,0 +1,6 @@
json.(@export, :id, :status)
json.created_at @export.created_at.utc
if @export.completed? && @export.file.attached?
json.download_url rails_blob_url(@export.file, disposition: "attachment")
end
+1
View File
@@ -19,6 +19,7 @@ a bot to perform various actions for you.
- [Users](sections/users.md) - [Users](sections/users.md)
- [Notifications](sections/notifications.md) - [Notifications](sections/notifications.md)
- [Rich Text](sections/rich_text.md) - [Rich Text](sections/rich_text.md)
- [Exports](sections/exports.md)
- [Webhooks](sections/webhooks.md) - [Webhooks](sections/webhooks.md)
## Authentication ## Authentication
+77
View File
@@ -0,0 +1,77 @@
# Exports
Exports are asynchronous jobs. Start an export with `POST`, then poll the export resource with `GET` until it reaches `completed`. When the export file is ready, the response includes a temporary `download_url`.
Possible export statuses are:
- `pending`
- `processing`
- `completed`
- `failed`
Completed export files expire after 24 hours. When that happens, request a new export.
## `POST /account/exports`
Starts an account export for the current account. Only account admins and owners can create account exports.
__Response:__
Returns `201 Created` with the export object:
```json
{
"id": "03f8huu0sog76g3s97596abcd",
"status": "pending",
"created_at": "2026-04-02T12:34:56Z"
}
```
## `GET /account/exports/:id`
Returns the status of an account export created by the current user.
__Response:__
```json
{
"id": "03f8huu0sog76g3s97596abcd",
"status": "completed",
"created_at": "2026-04-02T12:34:56Z",
"download_url": "https://app.fizzy.do/rails/active_storage/blobs/redirect/.../fizzy-account-export.zip"
}
```
The `download_url` field is only present when the export is completed and the export file is still available. Download requests still require normal authenticated access as the export owner.
## `POST /:account_slug/users/:user_id/data_exports`
Starts a personal data export for the current user. You can only create exports for your own user record.
__Response:__
Returns `201 Created` with the export object:
```json
{
"id": "03f8huu0sog76g3s97596wxyz",
"status": "pending",
"created_at": "2026-04-02T12:34:56Z"
}
```
## `GET /:account_slug/users/:user_id/data_exports/:id`
Returns the status of one of your personal data exports.
__Response:__
```json
{
"id": "03f8huu0sog76g3s97596wxyz",
"status": "completed",
"created_at": "2026-04-02T12:34:56Z",
"download_url": "https://app.fizzy.do/rails/active_storage/blobs/redirect/.../fizzy-user-data-export.zip"
}
```
The `download_url` field is only present when the export is completed and the export file is still available. Download requests still require normal authenticated access as the export owner.
@@ -67,6 +67,62 @@ class Users::DataExportsControllerTest < ActionDispatch::IntegrationTest
assert_select "h2", "Download Expired" assert_select "h2", "Download Expired"
end end
test "create as JSON" do
assert_difference -> { User::DataExport.count }, 1 do
assert_enqueued_with(job: DataExportJob) do
post user_data_exports_path(@user), as: :json
end
end
assert_response :created
body = @response.parsed_body
assert body["id"].present?
assert_equal "pending", body["status"]
assert_nil body["download_url"]
end
test "show as JSON with completed export" do
export = @user.data_exports.create!(account: Current.account)
export.build
get user_data_export_path(@user, export), as: :json
assert_response :success
body = @response.parsed_body
assert_equal export.id, body["id"]
assert_equal "completed", body["status"]
assert body["download_url"].present?
end
test "show as JSON with pending export" do
export = @user.data_exports.create!(account: Current.account)
get user_data_export_path(@user, export), as: :json
assert_response :success
body = @response.parsed_body
assert_equal export.id, body["id"]
assert_equal "pending", body["status"]
assert_nil body["download_url"]
end
test "show as JSON with failed export" do
export = @user.data_exports.create!(account: Current.account, status: :failed)
get user_data_export_path(@user, export), as: :json
assert_response :success
body = @response.parsed_body
assert_equal export.id, body["id"]
assert_equal "failed", body["status"]
assert_nil body["download_url"]
end
test "show as JSON with missing export" do
get user_data_export_path(@user, "nonexistent"), as: :json
assert_response :not_found
end
test "create is forbidden for other users" do test "create is forbidden for other users" do
other_user = users(:kevin) other_user = users(:kevin)
@@ -75,6 +131,14 @@ class Users::DataExportsControllerTest < ActionDispatch::IntegrationTest
assert_response :forbidden assert_response :forbidden
end end
test "create as JSON is forbidden for other users" do
other_user = users(:kevin)
post user_data_exports_path(other_user), as: :json
assert_response :forbidden
end
test "show is forbidden for other users" do test "show is forbidden for other users" do
other_user = users(:kevin) other_user = users(:kevin)
export = other_user.data_exports.create!(account: Current.account) export = other_user.data_exports.create!(account: Current.account)
@@ -84,4 +148,26 @@ class Users::DataExportsControllerTest < ActionDispatch::IntegrationTest
assert_response :forbidden assert_response :forbidden
end end
test "show as JSON is forbidden for other users" do
other_user = users(:kevin)
export = other_user.data_exports.create!(account: Current.account)
export.build
get user_data_export_path(other_user, export), as: :json
assert_response :forbidden
end
test "create as JSON rejects request when current export limit is reached" do
Users::DataExportsController::CURRENT_EXPORT_LIMIT.times do
@user.data_exports.create!(account: Current.account)
end
assert_no_difference -> { User::DataExport.count } do
post user_data_exports_path(@user), as: :json
end
assert_response :too_many_requests
end
end end