From 8ee2d7d65fd61f46f8c751b215685d9e19d8f415 Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Thu, 5 Mar 2026 12:48:51 -0500 Subject: [PATCH] Add JSON response format to access token creation (#2662) * Add JSON response format to access token creation Allows programmatic clients (e.g., CLI) to create access tokens via the API by returning the token, description, and permission as JSON instead of redirecting to the HTML show page. * Add JSON response format to access token creation Allows programmatic clients (e.g., CLI) to create access tokens via the API by returning the token, description, and permission as JSON instead of redirecting to the HTML show page. * Style --------- Co-authored-by: David Heinemeier Hansson --- .../my/access_tokens_controller.rb | 13 +++++- docs/API.md | 42 +++++++++++++++++++ .../my/access_tokens_controller_test.rb | 35 ++++++++++++++++ 3 files changed, 88 insertions(+), 2 deletions(-) diff --git a/app/controllers/my/access_tokens_controller.rb b/app/controllers/my/access_tokens_controller.rb index 99c87893f..82cd93842 100644 --- a/app/controllers/my/access_tokens_controller.rb +++ b/app/controllers/my/access_tokens_controller.rb @@ -15,9 +15,18 @@ class My::AccessTokensController < ApplicationController def create access_token = my_access_tokens.create!(access_token_params) - expiring_id = verifier.generate access_token.id, expires_in: 10.seconds - redirect_to my_access_token_path(expiring_id) + respond_to do |format| + format.html do + expiring_id = verifier.generate access_token.id, expires_in: 10.seconds + redirect_to my_access_token_path(expiring_id) + end + + format.json do + render status: :created, json: \ + { token: access_token.token, description: access_token.description, permission: access_token.permission } + end + end end def destroy diff --git a/docs/API.md b/docs/API.md index 21bd24b86..e55faf400 100644 --- a/docs/API.md +++ b/docs/API.md @@ -134,6 +134,48 @@ __Response:__ Returns `204 No Content` on success. +#### Create an access token via the API + +You can programmatically create a personal access token using either a session cookie or an existing Bearer token: + +```bash +curl -X POST \ + -H "Content-Type: application/json" \ + -H "Accept: application/json" \ + -H "Cookie: session_token=eyJfcmFpbHMi..." \ + -d '{"access_token": {"description": "Fizzy CLI", "permission": "write"}}' \ + https://app.fizzy.do/1234567/my/access_tokens +``` + +Or with a Bearer token (must have `write` permission): + +```bash +curl -X POST \ + -H "Content-Type: application/json" \ + -H "Accept: application/json" \ + -H "Authorization: Bearer put-your-access-token-here" \ + -d '{"access_token": {"description": "Fizzy CLI", "permission": "write"}}' \ + https://app.fizzy.do/1234567/my/access_tokens +``` + +The `permission` field accepts `read` or `write`. + +__Response:__ + +``` +HTTP/1.1 201 Created +``` + +```json +{ + "token": "4f9Q6d2wXr8Kp1Ls0Vz3BnTa", + "description": "Fizzy CLI", + "permission": "write" +} +``` + +Store the `token` value securely — it won't be retrievable again. Use it as a Bearer token for subsequent API requests. + ## Caching Most endpoints return [ETag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag) and [Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control) headers. You can use these to avoid re-downloading unchanged data. diff --git a/test/controllers/my/access_tokens_controller_test.rb b/test/controllers/my/access_tokens_controller_test.rb index 43a90fac5..176302336 100644 --- a/test/controllers/my/access_tokens_controller_test.rb +++ b/test/controllers/my/access_tokens_controller_test.rb @@ -19,6 +19,41 @@ class My::AccessTokensControllerTest < ActionDispatch::IntegrationTest end end + test "create new token via JSON with session" do + assert_difference -> { identities(:kevin).access_tokens.count }, +1 do + post my_access_tokens_path, params: { access_token: { description: "Fizzy CLI", permission: "write" } }, as: :json + end + assert_response :created + body = @response.parsed_body + assert body["token"].present? + assert_equal "Fizzy CLI", body["description"] + assert_equal "write", body["permission"] + end + + test "create new token via JSON with bearer token" do + sign_out + bearer_token = { "HTTP_AUTHORIZATION" => "Bearer #{identity_access_tokens(:davids_api_token).token}" } + + assert_difference -> { identities(:david).access_tokens.count }, +1 do + post my_access_tokens_path, params: { access_token: { description: "Fizzy CLI", permission: "read" } }, env: bearer_token, as: :json + end + assert_response :created + body = @response.parsed_body + assert body["token"].present? + assert_equal "Fizzy CLI", body["description"] + assert_equal "read", body["permission"] + end + + test "cannot create new token via JSON with read-only bearer token" do + sign_out + bearer_token = { "HTTP_AUTHORIZATION" => "Bearer #{identity_access_tokens(:jasons_api_token).token}" } + + assert_no_difference -> { identities(:jason).access_tokens.count } do + post my_access_tokens_path, params: { access_token: { description: "Fizzy CLI", permission: "read" } }, env: bearer_token, as: :json + end + assert_response :unauthorized + end + test "accessing new token after reveal window redirects to index" do assert_changes -> { identities(:kevin).access_tokens.count }, +1 do post my_access_tokens_path, params: { access_token: { description: "GitHub", permission: "read" } }