Add JSON API support for user data exports (#2786)
This commit is contained in:
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user