From 23cfab7f20b996950c690d2a7d77626416f867f8 Mon Sep 17 00:00:00 2001 From: Mat Manna Date: Sun, 1 Mar 2026 22:23:42 -0500 Subject: [PATCH 1/7] feat: add hackatime normal token revocation --- .../internal/revocations_normal_controller.rb | 52 +++++++++++++++++++ config/routes.rb | 1 + 2 files changed, 53 insertions(+) create mode 100644 app/controllers/api/internal/revocations_normal_controller.rb diff --git a/app/controllers/api/internal/revocations_normal_controller.rb b/app/controllers/api/internal/revocations_normal_controller.rb new file mode 100644 index 000000000..63bd72e03 --- /dev/null +++ b/app/controllers/api/internal/revocations_normal_controller.rb @@ -0,0 +1,52 @@ +module Api + module Internal + class RevocationsNormalController < Api::Internal::ApplicationController + def create + token = params[:token] + + return head 400 unless token.present? + + masked_token = mask_token(token) + + api_key = ApiKey.find_by(token:) + + unless api_key.present? + return render json: { success: false } + end + + api_key.update!( + token: SecureRandom.uuid_v4, + name: "#{api_key.name}_revoked_#{SecureRandom.hex(8)}" + ) + + user = api_key.user + + new_token_mask = mask_token(api_key.token) + + render json: { + success: true, + owner_email: user.email_addresses.first&.email, + key_name: api_key.name + }.compact_blank + end + + private + + def mask_token(token) + return nil unless token + t = token.to_s + return t if t.length <= 8 + "#{t[0,4]}...#{t[-4,4]}" + end + + private def authenticate! + res = authenticate_with_http_token do |token, _| + ActiveSupport::SecurityUtils.secure_compare(token, ENV["HKA_REVOCATION_KEY"]) + end + unless res + redirect_to "https://www.youtube.com/watch?v=dQw4w9WgXcQ", allow_other_host: true + end + end + end + end +end diff --git a/config/routes.rb b/config/routes.rb index 2f9d13741..d6d17cab4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -310,6 +310,7 @@ def matches?(request) namespace :internal do post "revoke", to: "revocations#create" + post "revoke_normal", to: "revocations_normal#create" end end From 3a8ab492f21e7d2d4933aecf420fd79c712cb39a Mon Sep 17 00:00:00 2001 From: Mat Manna Date: Sun, 1 Mar 2026 22:32:15 -0500 Subject: [PATCH 2/7] chore: make linter not hate me (its always whitespace) <3 --- app/controllers/api/internal/revocations_normal_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/internal/revocations_normal_controller.rb b/app/controllers/api/internal/revocations_normal_controller.rb index 63bd72e03..13e0713d2 100644 --- a/app/controllers/api/internal/revocations_normal_controller.rb +++ b/app/controllers/api/internal/revocations_normal_controller.rb @@ -36,7 +36,7 @@ def mask_token(token) return nil unless token t = token.to_s return t if t.length <= 8 - "#{t[0,4]}...#{t[-4,4]}" + "#{t[0, 4]}...#{t[-4, 4]}" end private def authenticate! From afb38bb5c9a939649e4a4f576a2245c614015a2c Mon Sep 17 00:00:00 2001 From: Mat Manna <91392083+matmanna@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:53:49 +0000 Subject: [PATCH 3/7] fix: combine both revocation apis into one (as requested by mahad) --- .../api/internal/revocations_controller.rb | 38 +++++++++++--- .../internal/revocations_normal_controller.rb | 52 ------------------- config/routes.rb | 1 - 3 files changed, 32 insertions(+), 59 deletions(-) delete mode 100644 app/controllers/api/internal/revocations_normal_controller.rb diff --git a/app/controllers/api/internal/revocations_controller.rb b/app/controllers/api/internal/revocations_controller.rb index c42f06e84..ebbeeb36a 100644 --- a/app/controllers/api/internal/revocations_controller.rb +++ b/app/controllers/api/internal/revocations_controller.rb @@ -1,26 +1,52 @@ module Api module Internal class RevocationsController < Api::Internal::ApplicationController + REGULAR_KEY_REGEX = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i + ADMIN_KEY_REGEX = /\Ahka_[0-9a-f]{64}\z/ + def create token = params[:token] return head 400 unless token.present? - admin_api_key = AdminApiKey.active.find_by(token:) - - return render json: { success: false } unless admin_api_key.present? + key, user = revocable_key_and_owner(token) - admin_api_key.revoke! + return render json: { success: false } unless key.present? - user = admin_api_key.user + revoke_key!(key) render json: { success: true, owner_email: user.email_addresses.first&.email, - key_name: admin_api_key.name + key_name: key.name }.compact_blank end + private + + def revocable_key_and_owner(token) + if token.match?(ADMIN_KEY_REGEX) + key = AdminApiKey.active.find_by(token:) + return [ key, key&.user ] + end + + if token.match?(REGULAR_KEY_REGEX) + key = ApiKey.find_by(token:) + return [ key, key&.user ] + end + + [ nil, nil ] + end + + def revoke_key!(key) + return key.revoke! if key.is_a?(AdminApiKey) + + key.update!( + token: SecureRandom.uuid_v4, + name: "#{key.name}_revoked_#{SecureRandom.hex(8)}" + ) + end + private def authenticate! res = authenticate_with_http_token do |token, _| ActiveSupport::SecurityUtils.secure_compare(token, ENV["HKA_REVOCATION_KEY"]) diff --git a/app/controllers/api/internal/revocations_normal_controller.rb b/app/controllers/api/internal/revocations_normal_controller.rb deleted file mode 100644 index 13e0713d2..000000000 --- a/app/controllers/api/internal/revocations_normal_controller.rb +++ /dev/null @@ -1,52 +0,0 @@ -module Api - module Internal - class RevocationsNormalController < Api::Internal::ApplicationController - def create - token = params[:token] - - return head 400 unless token.present? - - masked_token = mask_token(token) - - api_key = ApiKey.find_by(token:) - - unless api_key.present? - return render json: { success: false } - end - - api_key.update!( - token: SecureRandom.uuid_v4, - name: "#{api_key.name}_revoked_#{SecureRandom.hex(8)}" - ) - - user = api_key.user - - new_token_mask = mask_token(api_key.token) - - render json: { - success: true, - owner_email: user.email_addresses.first&.email, - key_name: api_key.name - }.compact_blank - end - - private - - def mask_token(token) - return nil unless token - t = token.to_s - return t if t.length <= 8 - "#{t[0, 4]}...#{t[-4, 4]}" - end - - private def authenticate! - res = authenticate_with_http_token do |token, _| - ActiveSupport::SecurityUtils.secure_compare(token, ENV["HKA_REVOCATION_KEY"]) - end - unless res - redirect_to "https://www.youtube.com/watch?v=dQw4w9WgXcQ", allow_other_host: true - end - end - end - end -end diff --git a/config/routes.rb b/config/routes.rb index d6d17cab4..2f9d13741 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -310,7 +310,6 @@ def matches?(request) namespace :internal do post "revoke", to: "revocations#create" - post "revoke_normal", to: "revocations_normal#create" end end From 8e10693bf32223d6cb2dd9d7d4893eaba60db7d1 Mon Sep 17 00:00:00 2001 From: Mat Manna <91392083+matmanna@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:11:35 +0000 Subject: [PATCH 4/7] chore: add HKA_REVOCATION_KEY to .env.example --- .env.example | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.env.example b/.env.example index 2a7808848..1085cee63 100644 --- a/.env.example +++ b/.env.example @@ -63,3 +63,6 @@ S3_ACCESS_KEY_ID=your_s3_access_key_id_here S3_SECRET_ACCESS_KEY=your_s3_secret_access_key_here S3_BUCKET=your_s3_bucket_name_here S3_ENDPOINT=https://.r2.cloudflarestorage.com + +# Revocation key for token revocation +HKA_REVOCATION_KEY=your_hka_revocation_key_here \ No newline at end of file From c4cc7090228145b3d2104d463850c685d8866bc6 Mon Sep 17 00:00:00 2001 From: Mat Manna Date: Sun, 1 Mar 2026 22:23:42 -0500 Subject: [PATCH 5/7] feat: add hackatime normal token revocation --- .../internal/revocations_normal_controller.rb | 52 +++++++++++++++++++ config/routes.rb | 1 + 2 files changed, 53 insertions(+) create mode 100644 app/controllers/api/internal/revocations_normal_controller.rb diff --git a/app/controllers/api/internal/revocations_normal_controller.rb b/app/controllers/api/internal/revocations_normal_controller.rb new file mode 100644 index 000000000..63bd72e03 --- /dev/null +++ b/app/controllers/api/internal/revocations_normal_controller.rb @@ -0,0 +1,52 @@ +module Api + module Internal + class RevocationsNormalController < Api::Internal::ApplicationController + def create + token = params[:token] + + return head 400 unless token.present? + + masked_token = mask_token(token) + + api_key = ApiKey.find_by(token:) + + unless api_key.present? + return render json: { success: false } + end + + api_key.update!( + token: SecureRandom.uuid_v4, + name: "#{api_key.name}_revoked_#{SecureRandom.hex(8)}" + ) + + user = api_key.user + + new_token_mask = mask_token(api_key.token) + + render json: { + success: true, + owner_email: user.email_addresses.first&.email, + key_name: api_key.name + }.compact_blank + end + + private + + def mask_token(token) + return nil unless token + t = token.to_s + return t if t.length <= 8 + "#{t[0,4]}...#{t[-4,4]}" + end + + private def authenticate! + res = authenticate_with_http_token do |token, _| + ActiveSupport::SecurityUtils.secure_compare(token, ENV["HKA_REVOCATION_KEY"]) + end + unless res + redirect_to "https://www.youtube.com/watch?v=dQw4w9WgXcQ", allow_other_host: true + end + end + end + end +end diff --git a/config/routes.rb b/config/routes.rb index 2f9d13741..d6d17cab4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -310,6 +310,7 @@ def matches?(request) namespace :internal do post "revoke", to: "revocations#create" + post "revoke_normal", to: "revocations_normal#create" end end From bcb1ae1d01feefe9a704116fa99ce83da91e2f5a Mon Sep 17 00:00:00 2001 From: Mat Manna Date: Sun, 1 Mar 2026 22:32:15 -0500 Subject: [PATCH 6/7] chore: make linter not hate me (its always whitespace) <3 --- app/controllers/api/internal/revocations_normal_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/internal/revocations_normal_controller.rb b/app/controllers/api/internal/revocations_normal_controller.rb index 63bd72e03..13e0713d2 100644 --- a/app/controllers/api/internal/revocations_normal_controller.rb +++ b/app/controllers/api/internal/revocations_normal_controller.rb @@ -36,7 +36,7 @@ def mask_token(token) return nil unless token t = token.to_s return t if t.length <= 8 - "#{t[0,4]}...#{t[-4,4]}" + "#{t[0, 4]}...#{t[-4, 4]}" end private def authenticate! From 8e5aabdbd67ab37b6a375c478e33dabe9a828d67 Mon Sep 17 00:00:00 2001 From: Mat Manna <91392083+matmanna@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:53:49 +0000 Subject: [PATCH 7/7] fix: combine both revocation apis into one (as requested by mahad) --- .../internal/revocations_normal_controller.rb | 52 ------------------- config/routes.rb | 1 - 2 files changed, 53 deletions(-) delete mode 100644 app/controllers/api/internal/revocations_normal_controller.rb diff --git a/app/controllers/api/internal/revocations_normal_controller.rb b/app/controllers/api/internal/revocations_normal_controller.rb deleted file mode 100644 index 13e0713d2..000000000 --- a/app/controllers/api/internal/revocations_normal_controller.rb +++ /dev/null @@ -1,52 +0,0 @@ -module Api - module Internal - class RevocationsNormalController < Api::Internal::ApplicationController - def create - token = params[:token] - - return head 400 unless token.present? - - masked_token = mask_token(token) - - api_key = ApiKey.find_by(token:) - - unless api_key.present? - return render json: { success: false } - end - - api_key.update!( - token: SecureRandom.uuid_v4, - name: "#{api_key.name}_revoked_#{SecureRandom.hex(8)}" - ) - - user = api_key.user - - new_token_mask = mask_token(api_key.token) - - render json: { - success: true, - owner_email: user.email_addresses.first&.email, - key_name: api_key.name - }.compact_blank - end - - private - - def mask_token(token) - return nil unless token - t = token.to_s - return t if t.length <= 8 - "#{t[0, 4]}...#{t[-4, 4]}" - end - - private def authenticate! - res = authenticate_with_http_token do |token, _| - ActiveSupport::SecurityUtils.secure_compare(token, ENV["HKA_REVOCATION_KEY"]) - end - unless res - redirect_to "https://www.youtube.com/watch?v=dQw4w9WgXcQ", allow_other_host: true - end - end - end - end -end diff --git a/config/routes.rb b/config/routes.rb index d6d17cab4..2f9d13741 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -310,7 +310,6 @@ def matches?(request) namespace :internal do post "revoke", to: "revocations#create" - post "revoke_normal", to: "revocations_normal#create" end end