-
Notifications
You must be signed in to change notification settings - Fork 6
Beacon Configuration Admin View, with CRUD and Regenerate and Revoke Tokens Actions #585
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
035abfa
27ab546
1b77f29
b4cfadd
b4540fa
fb7b72c
16a8485
ee1549b
ca83796
0a53932
9056305
dd6b8e9
b459b7e
b37d46e
4727750
75f1500
07a9fdf
10a48ea
3954d6e
c125a91
a24368c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| class BeaconsController < ApplicationController | ||
| before_action :redirect_contributors | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: add base beacons controller and move this before action there. We will reuse it later |
||
| before_action :set_beacon, only: %i[show edit update regenerate_key revoke_key] | ||
|
|
||
| def index | ||
| @beacons = Beacon.includes(:language, :region, :providers, :topics).order(created_at: :desc) | ||
| end | ||
|
|
||
| def new | ||
| prepare_associations | ||
| end | ||
|
|
||
| def create | ||
| success, @beacon, api_key = Beacons::Creator.new.call(beacon_params) | ||
|
|
||
| case Beacons::Creator.new.call(beacon_params) | ||
| when [true, @beacon, api_key] | ||
|
Check failure on line 17 in app/controllers/beacons_controller.rb
|
||
| flash[:notice] = "Beacon was successfully provisioned. API Key: #{api_key}" | ||
| redirect_to @beacon | ||
| when [false, _, _] | ||
|
Check failure on line 20 in app/controllers/beacons_controller.rb
|
||
| prepare_associations | ||
| render :new, status: :unprocessable_entity | ||
| end | ||
| end | ||
|
|
||
| def show; end | ||
|
|
||
| def edit | ||
| prepare_associations | ||
| end | ||
|
|
||
| def update | ||
| if @beacon.update(beacon_params) | ||
| redirect_to @beacon, notice: "Beacon was successfully updated." | ||
| else | ||
| prepare_associations | ||
| render :edit, status: :unprocessable_entity | ||
| end | ||
| end | ||
|
|
||
| def regenerate_key | ||
| api_key = @beacon.regenerate | ||
| flash[:notice] = "API key has been successfully regenerated. API Key: #{api_key}" | ||
| redirect_to @beacon | ||
|
|
||
| rescue => e | ||
| flash[:alert] = "API key could not be regenerated." | ||
| redirect_to @beacon | ||
| end | ||
|
|
||
| def revoke_key | ||
| api_key = @beacon.revoke! | ||
| flash[:notice] = "API key has been successfully revoked." | ||
| redirect_to @beacon | ||
|
|
||
| rescue => e | ||
| flash[:alert] = "API key could not be revoked." | ||
| redirect_to @beacon | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def set_beacon | ||
| @beacon = Beacon.find(params[:id]) | ||
| end | ||
|
|
||
| def prepare_associations | ||
| @languages = Language.order(:name) | ||
| @providers = Provider.order(:name) | ||
| @regions = Region.order(:name) | ||
| @topics = Topic.active.order(:title) | ||
| end | ||
|
|
||
| def beacon_params | ||
| params.require(:beacon).permit(:name, :language_id, :region_id, provider_ids: [], topic_ids: []) | ||
FionaLMcLaren marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| end | ||
|
|
||
| def redirect_contributors | ||
| redirect_to root_path, alert: "You don't have permission to access this page." unless Current.user&.is_admin? | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,11 +41,64 @@ class Beacon < ApplicationRecord | |
| scope :active, -> { where(revoked_at: nil) } | ||
| scope :revoked, -> { where.not(revoked_at: nil) } | ||
|
|
||
| def regenerate | ||
| _, raw_key = Beacons::KeyRegenerator.new.call(self) | ||
|
|
||
| key_result | ||
| end | ||
|
|
||
| def revoke! | ||
| update!(revoked_at: Time.current) | ||
| end | ||
|
|
||
| def revoked? | ||
| revoked_at.present? | ||
| end | ||
|
|
||
| # Get count of topics that match this beacon's configuration | ||
| def document_count | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this data will be mostly available from manifest JSON that we are going to store in beacon
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this then be removed after merging the Beacon manifest feature, in a future PR?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Manifest API is already in main branch |
||
| scope = Topic.active | ||
|
|
||
| # Filter by beacon's language | ||
| scope = scope.where(language_id: language_id) if language_id.present? | ||
|
|
||
| # If beacon has specific providers selected, filter by those | ||
| if providers.any? | ||
| scope = scope.where(provider_id: providers.pluck(:id)) | ||
| else | ||
| # If no providers selected, filter by providers in the beacon's region | ||
| if region.present? | ||
| provider_ids = region.providers.pluck(:id) | ||
| scope = scope.where(provider_id: provider_ids) | ||
| end | ||
| end | ||
|
|
||
| # If beacon has specific topics selected, filter by those | ||
| if topics.any? | ||
| scope = scope.where(id: topics.pluck(:id)) | ||
| end | ||
|
|
||
| scope.count | ||
| end | ||
|
|
||
| # Get count of actual document files attached to matching topics | ||
| def file_count | ||
| scope = Topic.active | ||
|
|
||
| scope = scope.where(language_id: language_id) if language_id.present? | ||
|
|
||
| if providers.any? | ||
| scope = scope.where(provider_id: providers.pluck(:id)) | ||
| elsif region.present? | ||
| provider_ids = region.providers.pluck(:id) | ||
| scope = scope.where(provider_id: provider_ids) | ||
| end | ||
|
|
||
| if topics.any? | ||
| scope = scope.where(id: topics.pluck(:id)) | ||
| end | ||
|
|
||
| # Count total attached documents | ||
| scope.joins(:documents_attachments).count | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| <%= form_with model: beacon, local: true do |form| %> | ||
| <% if beacon.errors.any? %> | ||
| <div class="bg-red-50 border border-red-200 rounded-lg p-4 mb-6"> | ||
| <div class="flex"> | ||
| <div class="flex-shrink-0"> | ||
| <svg class="h-5 w-5 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/> | ||
| </svg> | ||
| </div> | ||
| <div class="ml-3"> | ||
| <h3 class="text-sm font-medium text-red-800"> | ||
| <%= pluralize(beacon.errors.count, "error") %> prohibited this beacon from being saved: | ||
| </h3> | ||
| <div class="mt-2 text-sm text-red-700"> | ||
| <ul class="list-disc pl-5 space-y-1"> | ||
| <% beacon.errors.full_messages.each do |message| %> | ||
| <li><%= message %></li> | ||
| <% end %> | ||
| </ul> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <% end %> | ||
|
|
||
| <div class="form-grid"> | ||
| <!-- Basic Information Section --> | ||
| <div class="form-grid-full"> | ||
| <h3 class="text-lg font-semibold text-gray-900 mb-4 pb-2 border-b border-gray-200">Basic Information</h3> | ||
| </div> | ||
|
|
||
| <div> | ||
| <%= form.label :name, class: "form-label" do %> | ||
| Beacon Name | ||
| <span class="form-required">*</span> | ||
| <% end %> | ||
| <%= form.text_field :name, class: "form-input", placeholder: "e.g., Kampala-Central", required: true %> | ||
| <p class="form-help-text">A descriptive name for this beacon deployment</p> | ||
| </div> | ||
|
|
||
| <!-- Document Access Filters Section --> | ||
| <div class="form-grid-full"> | ||
| <h3 class="text-lg font-semibold text-gray-900 mb-4 pb-2 border-b border-gray-200 mt-4"> | ||
| Document Access Filters | ||
| </h3> | ||
| <p class="text-sm text-gray-600 mb-4"> | ||
| Select which documents this beacon can access. Leave filters blank to allow access to all documents. | ||
| </p> | ||
| </div> | ||
|
|
||
| <div> | ||
| <%= form.label :language_id, "Language", class: "form-label" do %> | ||
| Language | ||
| <span class="form-required">*</span> | ||
| <% end %> | ||
| <div style="position:relative;"> | ||
| <%= form.select :language_id, | ||
| options_from_collection_for_select(@languages, :id, :name, beacon.language_id), | ||
| { include_blank: "Select a language" }, | ||
| class: "form-select", required: true %> | ||
| <%= render "shared/dropdown_arrow" %> | ||
| </div> | ||
| <p class="form-help-text">Language for documents this beacon will access</p> | ||
| </div> | ||
|
|
||
| <div> | ||
| <%= form.label :region_id, "Region", class: "form-label" do %> | ||
| Region | ||
| <span class="form-required">*</span> | ||
| <% end %> | ||
| <div style="position:relative;"> | ||
| <%= form.select :region_id, | ||
| options_from_collection_for_select(@regions, :id, :name, beacon.region_id), | ||
| { include_blank: "Select a region" }, | ||
| class: "form-select", required: true %> | ||
| <%= render "shared/dropdown_arrow" %> | ||
| </div> | ||
| <p class="form-help-text">Geographic region for this beacon</p> | ||
| </div> | ||
|
|
||
| <div class="form-grid-full" data-controller="select-tags" data-select-tags-dropdown-parent-value="body"> | ||
| <%= form.label :provider_ids, "Providers", class: "form-label" %> | ||
| <%= form.select :provider_ids, | ||
| options_from_collection_for_select(@providers, :id, :name, beacon.provider_ids), | ||
| { prompt: "Start typing to select providers", include_blank: true }, | ||
| { multiple: true, | ||
| data: { | ||
| "allow-new": "false", | ||
| "allow-clear": "true", | ||
| "select-tags-target": "tagList", | ||
| "dropdown-parent": "body" | ||
| } | ||
| } %> | ||
| <p class="form-help-text">Select which content providers this beacon can access</p> | ||
| </div> | ||
|
|
||
| <div class="form-grid-full" data-controller="select-tags" data-select-tags-dropdown-parent-value="body"> | ||
| <%= form.label :topic_ids, "Topics", class: "form-label" %> | ||
| <%= form.select :topic_ids, | ||
| options_from_collection_for_select(@topics, :id, :title, beacon.topic_ids), | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will selected providers/topics belong to selected language/region? |
||
| { prompt: "Start typing to select topics", include_blank: true }, | ||
| { multiple: true, | ||
| data: { | ||
| "allow-new": "false", | ||
| "allow-clear": "true", | ||
| "select-tags-target": "tagList", | ||
| "dropdown-parent": "body" | ||
| } | ||
| } %> | ||
| <p class="form-help-text">Select which topics this beacon can access. Leave blank to allow access to all topics.</p> | ||
| </div> | ||
| </div> | ||
|
|
||
| <!-- Action Buttons --> | ||
| <div class="flex justify-between items-center pt-8 border-t border-gray-100 mt-8"> | ||
| <div class="flex gap-4"> | ||
| <%= form.submit beacon.persisted? ? "Update Beacon" : "Provision Beacon", class: "bg-gradient-green text-white font-semibold py-3 px-8 rounded-lg border-0 cursor-pointer text-sm shadow-md transition-all hover:-translate-y-0.5" %> | ||
| <%= link_to "Back to Beacons", beacons_path, | ||
| class: "bg-gray-100 text-gray-500 font-semibold py-3 px-8 rounded-lg no-underline text-sm transition-all hover:bg-gray-200" %> | ||
| </div> | ||
|
|
||
| <% if beacon.persisted? %> | ||
| <div class="text-gray-500 text-xs"> | ||
| Last updated: <%= beacon.updated_at.strftime("%B %d, %Y at %I:%M %p") %> | ||
| </div> | ||
| <% end %> | ||
| </div> | ||
| <% end %> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| <% content_for :title, "Edit Beacon" %> | ||
|
|
||
| <div class="bg-gradient-page page-container"> | ||
| <div class="content-wrapper"> | ||
|
|
||
| <!-- Header Section --> | ||
| <div class="section-spacing"> | ||
| <div class="flex items-center gap-4 mb-6"> | ||
| <%= link_to beacon_path(@beacon), class: "text-gray-500 hover:text-gray-700 transition-colors" do %> | ||
| <svg class="icon-size-lg" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/> | ||
| </svg> | ||
| <% end %> | ||
| <h1 class="text-3xl font-bold text-gray-800 m-0"> | ||
| Edit Beacon | ||
| </h1> | ||
| </div> | ||
|
|
||
| <!-- Breadcrumb --> | ||
| <nav class="breadcrumb-nav"> | ||
| <ol class="breadcrumb-list"> | ||
| <li><%= link_to "Dashboard", root_path, class: "text-gray-500 no-underline hover:text-gray-700" %></li> | ||
| <li class="breadcrumb-separator">/</li> | ||
| <li><%= link_to "Beacons", beacons_path, class: "text-gray-500 no-underline hover:text-gray-700" %></li> | ||
| <li class="breadcrumb-separator">/</li> | ||
| <li><%= link_to @beacon.name, beacon_path(@beacon), class: "text-gray-500 no-underline hover:text-gray-700" %></li> | ||
| <li class="breadcrumb-separator">/</li> | ||
| <li class="breadcrumb-current">Edit</li> | ||
| </ol> | ||
| </nav> | ||
| </div> | ||
|
|
||
| <!-- Main Form Card --> | ||
| <div class="card-elevated" style="overflow: visible;"> | ||
| <!-- Form Content --> | ||
| <div class="card-body"> | ||
| <%= render "form", beacon: @beacon %> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </div> |
Uh oh!
There was an error while loading. Please reload this page.