<%= @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 params[:api_key] %>
+
+
+ ✓ Beacon Provisioned - API Key Generated
+
+
+ <%= params[:api_key] %>
+
+
+
+
+
+
+
+
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 %>
+
+ <% else %>
+ All Topics
+ <% end %>
+
+
+
+
+
+
+
+
+
+
diff --git a/app/views/layouts/_sidebar.html.erb b/app/views/layouts/_sidebar.html.erb
index 0a4528c3..66bb9110 100644
--- a/app/views/layouts/_sidebar.html.erb
+++ b/app/views/layouts/_sidebar.html.erb
@@ -59,6 +59,13 @@
<% end %>
+
+ <%= link_to beacons_path, class: "sidebar__link #{active_link_class(beacons_path)}" do %>
+ 📡
+ Beacon Management
+ <% end %>
+
diff --git a/config/routes.rb b/config/routes.rb
index bfa18754..92c12cb5 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -21,6 +21,13 @@
put :provider, on: :collection
end
+ resources :beacons, except: :destroy, module: :beacons do
+ member do
+ post :regenerate_key
+ post :revoke_key
+ end
+ end
+
# Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb)
# get "manifest" => "rails/pwa#manifest", as: :pwa_manifest
# get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker
diff --git a/spec/helpers/beacons_helper_spec.rb b/spec/helpers/beacons_helper_spec.rb
new file mode 100644
index 00000000..4420b3e2
--- /dev/null
+++ b/spec/helpers/beacons_helper_spec.rb
@@ -0,0 +1,17 @@
+require "rails_helper"
+
+RSpec.describe BeaconsHelper, type: :helper do
+ describe "#status_string" do
+ subject { helper.status_string(beacon) }
+
+ context "with an active beacon" do
+ let(:beacon) { create(:beacon) }
+ it { is_expected.to eq("Active") }
+ end
+
+ context "with a revoked beacon" do
+ let(:beacon) { create(:beacon, :revoked) }
+ it { is_expected.to eq("Revoked") }
+ end
+ end
+end
diff --git a/spec/requests/beacons_spec.rb b/spec/requests/beacons_spec.rb
new file mode 100644
index 00000000..656c79f2
--- /dev/null
+++ b/spec/requests/beacons_spec.rb
@@ -0,0 +1,176 @@
+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 have_http_status(302)
+ expect(assigns(:beacon)).to eq(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 have_http_status(302)
+ expect(assigns(:beacon)).to eq(beacon)
+ end
+
+ context "when there is an error" do
+ before { allow(Beacons::KeyRegenerator.new).to receive(:call).and_raise("Error") }
+
+ it "redirects to the beacon" do
+ subject
+
+ expect(response).to have_http_status(302)
+ expect(assigns(:beacon)).to eq(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