Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app/controllers/api/v1/base_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module Api
module V1
class BaseController < ActionController::API
end
end
end
9 changes: 9 additions & 0 deletions app/controllers/api/v1/beacons/base_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module Api
module V1
module Beacons
class BaseController < Api::V1::BaseController
include Api::BeaconAuthentication
end
end
end
end
17 changes: 17 additions & 0 deletions app/controllers/api/v1/beacons/statuses_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module Api
module V1
module Beacons
class StatusesController < Beacons::BaseController
def show
render json: {
status: "ok",
beacon: {
id: Current.beacon.id,
name: Current.beacon.name,
},
}
end
end
end
end
end
35 changes: 35 additions & 0 deletions app/controllers/concerns/api/beacon_authentication.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
module Api
module BeaconAuthentication
extend ActiveSupport::Concern

included do
before_action :authenticate_beacon!
end

private

def authenticate_beacon!
token = extract_bearer_token
return render_unauthorized("Missing authorization header") if token.blank?

digest = OpenSSL::Digest::SHA256.hexdigest(token)
beacon = Beacon.find_by(api_key_digest: digest)

return render_unauthorized("Invalid API key") if beacon.nil?
return render_unauthorized("API key has been revoked") if beacon.revoked?

Current.beacon = beacon
end

def extract_bearer_token
header = request.headers["Authorization"]
return nil if header.blank?

header[/\ABearer\s+(.+)\z/, 1]
end

def render_unauthorized(message)
render json: { error: message }, status: :unauthorized
end
end
end
51 changes: 51 additions & 0 deletions app/models/beacon.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# == Schema Information
#
# Table name: beacons
# Database name: primary
#
# id :bigint not null, primary key
# api_key_digest :string not null
# api_key_prefix :string not null
# name :string not null
# revoked_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# language_id :bigint not null
# region_id :bigint not null
#
# Indexes
#
# index_beacons_on_api_key_digest (api_key_digest) UNIQUE
# index_beacons_on_language_id (language_id)
# index_beacons_on_region_id (region_id)
#
# Foreign Keys
#
# fk_rails_... (language_id => languages.id)
# fk_rails_... (region_id => regions.id)
#
class Beacon < ApplicationRecord
belongs_to :language
Copy link
Collaborator Author

@dmitrytrager dmitrytrager Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may be redundant, not sure

belongs_to :region

has_many :beacon_providers, dependent: :destroy
has_many :providers, through: :beacon_providers

has_many :beacon_topics, dependent: :destroy
has_many :topics, through: :beacon_topics

validates :name, presence: true
validates :api_key_digest, presence: true, uniqueness: true
validates :api_key_prefix, presence: true

scope :active, -> { where(revoked_at: nil) }
scope :revoked, -> { where.not(revoked_at: nil) }

def revoke!
update!(revoked_at: Time.current)
end

def revoked?
revoked_at.present?
end
end
26 changes: 26 additions & 0 deletions app/models/beacon_provider.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# == Schema Information
#
# Table name: beacon_providers
# Database name: primary
#
# id :bigint not null, primary key
# created_at :datetime not null
# updated_at :datetime not null
# beacon_id :bigint not null
# provider_id :bigint not null
#
# Indexes
#
# index_beacon_providers_on_beacon_id (beacon_id)
# index_beacon_providers_on_beacon_id_and_provider_id (beacon_id,provider_id) UNIQUE
# index_beacon_providers_on_provider_id (provider_id)
#
# Foreign Keys
#
# fk_rails_... (beacon_id => beacons.id)
# fk_rails_... (provider_id => providers.id)
#
class BeaconProvider < ApplicationRecord
belongs_to :beacon
belongs_to :provider
end
26 changes: 26 additions & 0 deletions app/models/beacon_topic.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# == Schema Information
#
# Table name: beacon_topics
# Database name: primary
#
# id :bigint not null, primary key
# created_at :datetime not null
# updated_at :datetime not null
# beacon_id :bigint not null
# topic_id :bigint not null
#
# Indexes
#
# index_beacon_topics_on_beacon_id (beacon_id)
# index_beacon_topics_on_beacon_id_and_topic_id (beacon_id,topic_id) UNIQUE
# index_beacon_topics_on_topic_id (topic_id)
#
# Foreign Keys
#
# fk_rails_... (beacon_id => beacons.id)
# fk_rails_... (topic_id => topics.id)
#
class BeaconTopic < ApplicationRecord
belongs_to :beacon
belongs_to :topic
end
1 change: 1 addition & 0 deletions app/models/branch.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# == Schema Information
#
# Table name: branches
# Database name: primary
#
# id :bigint not null, primary key
# created_at :datetime not null
Expand Down
1 change: 1 addition & 0 deletions app/models/contributor.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# == Schema Information
#
# Table name: contributors
# Database name: primary
#
# id :bigint not null, primary key
# created_at :datetime not null
Expand Down
1 change: 1 addition & 0 deletions app/models/current.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
class Current < ActiveSupport::CurrentAttributes
attribute :session
attribute :beacon
delegate :user, to: :session, allow_nil: true
end
1 change: 1 addition & 0 deletions app/models/import_error.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# == Schema Information
#
# Table name: import_errors
# Database name: primary
#
# id :bigint not null, primary key
# error_message :text
Expand Down
1 change: 1 addition & 0 deletions app/models/import_report.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# == Schema Information
#
# Table name: import_reports
# Database name: primary
#
# id :bigint not null, primary key
# completed_at :datetime
Expand Down
2 changes: 2 additions & 0 deletions app/models/language.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# == Schema Information
#
# Table name: languages
# Database name: primary
#
# id :bigint not null, primary key
# name :string
Expand All @@ -10,6 +11,7 @@
class Language < ApplicationRecord
has_many :topics, dependent: :destroy
has_many :providers, through: :topics
has_many :beacons, dependent: :destroy

validates :name, presence: true, uniqueness: true, length: { minimum: 2 }

Expand Down
3 changes: 3 additions & 0 deletions app/models/provider.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# == Schema Information
#
# Table name: providers
# Database name: primary
#
# id :bigint not null, primary key
# file_name_prefix :string
Expand All @@ -20,6 +21,8 @@ class Provider < ApplicationRecord
has_many :contributors
has_many :users, through: :contributors
has_many :topics
has_many :beacon_providers, dependent: :destroy
has_many :beacons, through: :beacon_providers

validates :name, :provider_type, presence: true
validates :name, uniqueness: true
Expand Down
2 changes: 2 additions & 0 deletions app/models/region.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# == Schema Information
#
# Table name: regions
# Database name: primary
#
# id :bigint not null, primary key
# name :string
Expand All @@ -10,6 +11,7 @@
class Region < ApplicationRecord
has_many :branches, dependent: :destroy
has_many :providers, through: :branches
has_many :beacons, dependent: :destroy

validates :name, presence: true
end
1 change: 1 addition & 0 deletions app/models/session.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# == Schema Information
#
# Table name: sessions
# Database name: primary
#
# id :bigint not null, primary key
# ip_address :string
Expand Down
1 change: 1 addition & 0 deletions app/models/tag.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# == Schema Information
#
# Table name: tags
# Database name: primary
#
# id :bigint not null, primary key
# name :string
Expand Down
1 change: 1 addition & 0 deletions app/models/tag_cognate.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# == Schema Information
#
# Table name: tag_cognates
# Database name: primary
#
# id :bigint not null, primary key
# created_at :datetime not null
Expand Down
3 changes: 3 additions & 0 deletions app/models/topic.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# == Schema Information
#
# Table name: topics
# Database name: primary
#
# id :bigint not null, primary key
# description :text
Expand Down Expand Up @@ -35,6 +36,8 @@ class Topic < ApplicationRecord

belongs_to :language
belongs_to :provider
has_many :beacon_topics, dependent: :destroy
has_many :beacons, through: :beacon_topics
has_many_attached :documents, dependent: :purge

validates :title, :language_id, :provider_id, :published_at, presence: true
Expand Down
1 change: 1 addition & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# == Schema Information
#
# Table name: users
# Database name: primary
#
# id :bigint not null, primary key
# email :string not null
Expand Down
15 changes: 15 additions & 0 deletions app/services/beacons/api_key_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module Beacons
class ApiKeyGenerator
PREFIX = "sk_live_"

def call
raw_key = "#{PREFIX}#{SecureRandom.hex(16)}"
digest = OpenSSL::Digest::SHA256.hexdigest(raw_key)
prefix = raw_key.delete_prefix(PREFIX)[0, 8]

Result.new(raw_key: raw_key, digest: digest, prefix: prefix)
end

Result = Data.define(:raw_key, :digest, :prefix)
end
end
25 changes: 25 additions & 0 deletions app/services/beacons/creator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module Beacons
class Creator
attr_reader :key_generator

def initialize(key_generator: ApiKeyGenerator.new)
@key_generator = key_generator
end

def call(params)
key_result = key_generator.call

beacon = Beacon.new(
**params,
api_key_digest: key_result.digest,
api_key_prefix: key_result.prefix,
)

if beacon.save
[ true, beacon, key_result.raw_key ]
else
[ false, beacon, nil ]
end
end
end
end
21 changes: 21 additions & 0 deletions app/services/beacons/key_regenerator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module Beacons
class KeyRegenerator
attr_reader :key_generator

def initialize(key_generator: ApiKeyGenerator.new)
@key_generator = key_generator
end

def call(beacon)
key_result = key_generator.call

beacon.update!(
api_key_digest: key_result.digest,
api_key_prefix: key_result.prefix,
revoked_at: nil,
)

[ beacon, key_result.raw_key ]
end
end
end
4 changes: 4 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@
namespace :api do
namespace :v1 do
resources :tags, only: %i[index show]

namespace :beacons do
resource :status, only: :show
end
end
end

Expand Down
Loading