<%= @beacon.created_at.strftime("%B %d, %Y at %I:%M %p") %>
+
+
+
Updated
+
<%= @beacon.updated_at.strftime("%B %d, %Y at %I:%M %p") %>
+
+
+
+
+
+
+
+
+
+
+ API Key
+
+
+
+ <% if @api_key_display %>
+
+
+ ✓ Beacon Provisioned - API Key Generated
+
+
+ <%= @api_key_display %>
+
+
+
+
+
+
+
+
Save This Key
+
+ Copy this API key and configure it in your beacon deployment. This page will only show the full key immediately after creation. You can regenerate a new key anytime if needed.
+
+
+
+
+ <% else %>
+
+
+ API Key Prefix
+
+ <%= @beacon.api_key_prefix %>***
+
+
+
+
+
+
+
+
API Key Active
+
+ The API key for this beacon is active (shown by prefix above). The full key was displayed when the beacon was first created. If you need a new key, use the regenerate button below.
+
+
+
+
+
+ <%= button_to regenerate_key_beacon_path(@beacon), method: :post,
+ class: "w-full bg-amber-600 hover:bg-amber-700 text-white font-semibold py-3 px-4 rounded-lg transition-colors inline-flex items-center justify-center gap-2",
+ data: { turbo_confirm: "Are you sure? This will invalidate the current API key and generate a new one. Any beacons using the old key will stop working." } do %>
+
+ Regenerate API Key
+ <% end %>
+
+ <%= button_to revoke_key_beacon_path(@beacon), method: :post,
+ class: "w-full bg-red-600 hover:bg-red-700 text-white font-semibold py-3 px-4 rounded-lg transition-colors inline-flex items-center justify-center gap-2",
+ data: { turbo_confirm: "Are you sure? This will invalidate the current API key. Any beacons using the old key will stop working." } do %>
+
+ Revoke API Key
+ <% end %>
+
+ <% end %>
+
+
+
+
+
+
+
+
+
+ Document Access Configuration
+
+
+
+
+
+
Basic Configuration
+
+
+
Language
+
+
+ <%= @beacon.language.name %>
+
+
+
+
+
Region
+
+
+ <%= @beacon.region.name %>
+
+
+
+
+
+
+
Access Filters
+
+
+
Providers
+
+ <% if @beacon.providers.any? %>
+
+ <% @beacon.providers.each do |provider| %>
+
+ <%= provider.name %>
+
+ <% end %>
+
+ <% else %>
+ All Providers
+ <% end %>
+
+
+
+
Topics
+
+ <% if @beacon.topics.any? %>
+
+ <% @beacon.topics.each do |topic| %>
+
+ <%= topic.title %>
+
+ <% end %>
+
diff --git a/config/routes.rb b/config/routes.rb
index bfa18754..513a604c 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -17,6 +17,12 @@
resources :tags, only: %i[index], controller: "topics/tags"
end
resources :import_reports, only: %i[index show]
+ resources :beacons, except: :destroy do
+ member do
+ post :regenerate_key
+ post :revoke_key
+ end
+ end
resource :settings, only: [] do
put :provider, on: :collection
end
diff --git a/db/schema.rb b/db/schema.rb
index 46aaf864..d813d56b 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[8.1].define(version: 2026_02_03_100004) do
+ActiveRecord::Schema[8.1].define(version: 2026_02_03_100134) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"
diff --git a/spec/models/beacon_spec.rb b/spec/models/beacon_spec.rb
index f397a629..9a86164c 100644
--- a/spec/models/beacon_spec.rb
+++ b/spec/models/beacon_spec.rb
@@ -62,6 +62,22 @@
end
end
+ describe "#regenerate" do
+ it "reassigns the API key's prefix and digest" do
+ beacon = create(:beacon)
+
+ expect { beacon.regenerate }.to change { beacon.api_key_digest }
+ .and change { beacon.api_key_prefix }
+ end
+
+ it "sets revoked_at to nil" do
+ beacon = create(:beacon, :revoked)
+
+ beacon.regenerate
+ expect(beacon.revoked_at).to be_nil
+ end
+ end
+
describe "#revoke!" do
include ActiveSupport::Testing::TimeHelpers
diff --git a/spec/requests/beacons_spec.rb b/spec/requests/beacons_spec.rb
new file mode 100644
index 00000000..6bb5cd82
--- /dev/null
+++ b/spec/requests/beacons_spec.rb
@@ -0,0 +1,173 @@
+require "rails_helper"
+
+RSpec.describe "/beacons", type: :request do
+ let(:user) { create(:user, :admin) }
+ let(:region) { create(:region) }
+ let(:language) { create(:language) }
+ let(:valid_attributes) do
+ { name: "New Beacon",
+ language_id: language.id,
+ region_id: region.id,
+ }
+ end
+ let(:invalid_attributes) { { name: "" } }
+
+ before do
+ sign_in(user)
+ end
+
+ describe "GET /index" do
+ it "renders a successful response" do
+ create(:beacon)
+
+ get beacons_url
+
+ expect(response).to be_successful
+ end
+ end
+
+ describe "GET /show" do
+ it "renders a successful response" do
+ beacon = create(:beacon)
+
+ get beacon_url(beacon)
+
+ expect(response).to be_successful
+ end
+ end
+
+ describe "GET /new" do
+ it "renders a successful response" do
+ get new_beacon_url
+
+ expect(response).to be_successful
+ end
+ end
+
+ describe "GET /edit" do
+ let(:beacon) { create(:beacon) }
+
+ it "renders a successful response" do
+ get edit_beacon_url(beacon)
+
+ expect(response).to be_successful
+ end
+ end
+
+ describe "POST /create" do
+ context "with valid parameters" do
+ it "creates a new beacon" do
+ expect {
+ post beacons_url, params: { beacon: valid_attributes }
+ }.to change(Beacon, :count).by(1)
+ end
+
+ it "redirects to the created beacon" do
+ post beacons_url, params: { beacon: valid_attributes }
+
+ expect(response).to redirect_to(beacon_url(Beacon.last))
+ end
+ end
+
+ context "with invalid parameters" do
+ it "does not create a new beacon" do
+ expect {
+ post beacons_url, params: { beacon: invalid_attributes }
+ }.to change(Beacon, :count).by(0)
+ end
+
+ it "renders a response with 422 status (i.e. to display the 'new' template)" do
+ post beacons_url, params: { beacon: invalid_attributes }
+
+ expect(response).to have_http_status(:unprocessable_content)
+ end
+ end
+ end
+
+ describe "PATCH /update" do
+ context "with valid parameters" do
+ let(:updated_region) { create(:region) }
+ let(:updated_language) { create(:language) }
+ let(:new_attributes) do
+ { name: "Updated Beacon",
+ language_id: updated_language.id,
+ region_id: updated_region.id,
+ }
+ end
+
+ it "updates the requested beacon" do
+ beacon = create(:beacon)
+
+ patch beacon_url(beacon), params: { beacon: new_attributes }
+ beacon.reload
+
+ expect(beacon.name).to eq("Updated Beacon")
+ expect(beacon.language).to eq(updated_language)
+ expect(beacon.region).to eq(updated_region)
+ end
+
+ it "redirects to the beacon" do
+ beacon = create(:beacon)
+
+ patch beacon_url(beacon), params: { beacon: new_attributes }
+ beacon.reload
+
+ expect(response).to redirect_to(beacon_url(beacon))
+ end
+ end
+
+ context "with invalid parameters" do
+ it "renders a response with 422 status (i.e. to display the 'edit' template)" do
+ beacon = create(:beacon)
+
+ patch beacon_url(beacon), params: { beacon: invalid_attributes }
+
+ expect(response).to have_http_status(:unprocessable_content)
+ end
+ end
+ end
+
+ describe "POST /regenerate_key" do
+ let(:beacon) { create(:beacon) }
+ subject { post regenerate_key_beacon_url(beacon) }
+
+ it "redirects to the beacon" do
+ subject
+
+ expect(response).to redirect_to(beacon_url(beacon))
+ end
+
+ context "when there is an error" do
+ before { allow(beacon).to receive(:regenerate).and_raise("Error") }
+
+ it "redirects to the beacon" do
+ subject
+
+ expect(response).to redirect_to(beacon_url(beacon))
+ end
+ end
+ end
+
+ describe "POST /revoke_key" do
+ include ActiveSupport::Testing::TimeHelpers
+
+ let(:beacon) { create(:beacon) }
+ subject { post revoke_key_beacon_url(beacon) }
+
+ it "redirects to the beacon" do
+ subject
+
+ expect(response).to redirect_to(beacon_url(beacon))
+ end
+
+ context "when there is an error" do
+ before { allow(beacon).to receive(:revoke!).and_raise("Error") }
+
+ it "redirects to the beacon" do
+ subject
+
+ expect(response).to redirect_to(beacon_url(beacon))
+ end
+ end
+ end
+end