Skip to content

Commit

Permalink
Extract api_key value to a dedicated model ApiKey
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexander Chebotarov committed Oct 25, 2024
1 parent c9bb7bc commit d2bd8db
Show file tree
Hide file tree
Showing 24 changed files with 342 additions and 46 deletions.
2 changes: 2 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ require:
- rubocop-rails
- rubocop-thread_safety
- rubocop-graphql
- rubocop-factory_bot
- rubocop-rspec_rails
- ./dev/cops/service_call_cop.rb

inherit_gem:
Expand Down
5 changes: 3 additions & 2 deletions app/controllers/api/base_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ def authenticate
true
end

def current_organization(api_key = nil)
@current_organization ||= Organization.find_by(api_key:)
def current_organization(value = nil)
@current_organization ||=
ApiKey.find_by(value:)&.organization || Organization.find_by(api_key: value)
end

def set_context_source
Expand Down
4 changes: 4 additions & 0 deletions app/graphql/types/organizations/current_organization_type.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ class CurrentOrganizationType < BaseOrganizationType
def webhook_url
object.webhook_endpoints.map(&:webhook_url).first
end

def api_key
object.api_keys.first.value
end
end
end
end
43 changes: 43 additions & 0 deletions app/models/api_key.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

class ApiKey < ApplicationRecord
include PaperTrailTraceable

belongs_to :organization

before_create :set_value

def generate_value
value = SecureRandom.uuid
api_key = ApiKey.find_by(value:)

return generate_value if api_key.present?

value
end

private

def set_value
self.value = generate_value
end
end

# == Schema Information
#
# Table name: api_keys
#
# id :uuid not null, primary key
# value :string not null
# created_at :datetime not null
# updated_at :datetime not null
# organization_id :uuid not null
#
# Indexes
#
# index_api_keys_on_organization_id (organization_id)
#
# Foreign Keys
#
# fk_rails_... (organization_id => organizations.id)
#
12 changes: 1 addition & 11 deletions app/models/organization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class Organization < ApplicationRecord
'credit_note.created'
].freeze

has_many :api_keys
has_many :memberships
has_many :users, through: :memberships
has_many :billable_metrics
Expand Down Expand Up @@ -58,8 +59,6 @@ class Organization < ApplicationRecord

enum document_numbering: DOCUMENT_NUMBERINGS

before_create :generate_api_key

validates :country, country_code: true, unless: -> { country.nil? }
validates :default_currency, inclusion: {in: currency_list}
validates :document_locale, language_code: true
Expand Down Expand Up @@ -124,15 +123,6 @@ def auto_dunning_enabled?

private

def generate_api_key
api_key = SecureRandom.uuid
orga = Organization.find_by(api_key:)

return generate_api_key if orga.present?

self.api_key = SecureRandom.uuid
end

# NOTE: After creating an organization, default document_number_prefix needs to be generated.
# Example of expected format is ORG-4321
def generate_document_number_prefix
Expand Down
6 changes: 3 additions & 3 deletions app/models/webhook.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ class Webhook < ApplicationRecord
belongs_to :webhook_endpoint
belongs_to :object, polymorphic: true, optional: true

# TODO: Use relation to be able to eager load
delegate :organization, to: :webhook_endpoint
has_one :organization, through: :webhook_endpoint

enum status: STATUS

Expand Down Expand Up @@ -57,7 +56,8 @@ def jwt_signature
end

def hmac_signature
hmac = OpenSSL::HMAC.digest('sha-256', organization.api_key, payload.to_json)
api_key_value = organization.api_keys.first.value
hmac = OpenSSL::HMAC.digest('sha-256', api_key_value, payload.to_json)
Base64.strict_encode64(hmac)
end

Expand Down
28 changes: 28 additions & 0 deletions app/services/organizations/create_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

module Organizations
class CreateService < BaseService
def initialize(params)
@params = params
super
end

def call
organization = Organization.new(params)

ActiveRecord::Base.transaction do
organization.save!
organization.api_keys.create!
end

result.organization = organization
result
rescue ActiveRecord::RecordInvalid => e
result.record_validation_failure!(record: e.record)
end

private

attr_reader :params
end
end
5 changes: 4 additions & 1 deletion app/services/users_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ def register(email, password, organization_name)

ActiveRecord::Base.transaction do
result.user = User.create!(email:, password:)
result.organization = Organization.create!(name: organization_name, document_numbering: 'per_organization')

result.organization = Organizations::CreateService
.call(name: organization_name, document_numbering: 'per_organization')
.organization

result.membership = Membership.create!(
user: result.user,
Expand Down
41 changes: 41 additions & 0 deletions db/migrate/20241021140054_create_api_keys.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

class CreateApiKeys < ActiveRecord::Migration[7.1]
def up
create_table :api_keys, id: :uuid do |t|
t.references :organization, type: :uuid, null: false, foreign_key: true

t.string :value, null: false

t.timestamps
end

safety_assured do
execute <<-SQL
INSERT INTO api_keys (value, organization_id, created_at, updated_at)
SELECT organizations.api_key, organizations.id, organizations.created_at, organizations.created_at
FROM organizations
SQL
end
end

def down
safety_assured do
execute <<-SQL
UPDATE organizations
SET api_key = first_api_key.value
FROM (
SELECT DISTINCT ON (organization_id)
organization_id,
value
FROM api_keys
ORDER BY organization_id, id ASC
) first_api_key
WHERE organizations.id = first_api_key.organization_id
AND organizations.api_key IS NULL
SQL
end

drop_table :api_keys
end
end
11 changes: 10 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions db/seeds/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
organization = Organization.find_or_create_by!(name: 'Hooli')
organization.premium_integrations = Organization::PREMIUM_INTEGRATIONS
organization.save!
ApiKey.find_or_create_by!(organization:)
Membership.find_or_create_by!(user:, organization:, role: :admin)

# NOTE: define a billing model
Expand Down
1 change: 1 addition & 0 deletions db/seeds/webhooks.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require 'factory_bot_rails'

organization = Organization.find_or_create_by!(name: 'Hooli')
ApiKey.find_or_create_by!(organization:)

webhook_endpoint = WebhookEndpoint.find_or_create_by!(organization:, webhook_url: 'http://test.lago.dev/webhook')

Expand Down
10 changes: 8 additions & 2 deletions lib/tasks/signup.rake
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,15 @@ namespace :signup do
.find_or_create_by!(email: ENV['LAGO_ORG_USER_EMAIL'])
organization = Organization.find_or_create_by!(name: ENV['LAGO_ORG_NAME'])
raise "Couldn't find LAGO_ORG_API_KEY in environement variables" if ENV['LAGO_ORG_API_KEY'].blank?

organization.api_key = ENV['LAGO_ORG_API_KEY']
organization.save!

existing_api_key = ApiKey.find_by(organization:, value: ENV['LAGO_ORG_API_KEY'])

unless existing_api_key
api_key = ApiKey.create!(organization:)
api_key.update!(value: ENV['LAGO_ORG_API_KEY'])
end

Membership.find_or_create_by!(user:, organization:, role: :admin)

pp 'ending seeding environment'
Expand Down
7 changes: 7 additions & 0 deletions spec/factories/api_keys.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

FactoryBot.define do
factory :api_key do
organization { association(:organization) }
end
end
6 changes: 5 additions & 1 deletion spec/factories/organizations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@
end

after(:create) do |organization, evaluator|
organization.webhook_endpoints.create(webhook_url: evaluator.webhook_url)
if evaluator.webhook_url
organization.webhook_endpoints.create!(webhook_url: evaluator.webhook_url)
end

organization.api_keys.create!
end
end
end
2 changes: 1 addition & 1 deletion spec/graphql/resolvers/organization_resolver_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@

aggregate_failures do
expect(data['taxIdentificationNumber']).to eq(organization.tax_identification_number)
expect(data['apiKey']).to eq(organization.api_key)
expect(data['apiKey']).to eq(organization.api_keys.first.value)
expect(data['webhookUrl']).to eq(organization.webhook_endpoints.first.webhook_url)
expect(data['billingConfiguration']['invoiceFooter']).to eq(organization.invoice_footer)
expect(data['emailSettings']).to eq(organization.email_settings.map { _1.tr('.', '_') })
Expand Down
61 changes: 61 additions & 0 deletions spec/models/api_key_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe ApiKey, type: :model do
it { is_expected.to belong_to(:organization) }

describe '#save' do
subject { api_key.save! }

before do
allow(api_key).to receive(:set_value).and_call_original
subject
end

context 'with a new record' do
let(:api_key) { build(:api_key) }

it 'calls #set_value' do
expect(api_key).to have_received(:set_value)
end
end

context 'with a persisted record' do
let(:api_key) { create(:api_key) }

it 'does not call #set_value' do
expect(api_key).not_to have_received(:set_value)
end
end
end

describe '#set_value' do
subject { api_key.send(:set_value) }

let(:api_key) { build(:api_key) }
let(:unique_value) { SecureRandom.uuid }

before { allow(api_key).to receive(:generate_value).and_return(unique_value) }

it 'sets result of #generate_value to the value' do
expect { subject }.to change(api_key, :value).to unique_value
end
end

describe '#generate_value' do
subject { api_key.generate_value }

let(:api_key) { build(:api_key) }
let(:used_value) { create(:api_key).value }
let(:unique_value) { SecureRandom.uuid }

before do
allow(SecureRandom).to receive(:uuid).and_return(used_value, unique_value)
end

it 'returns unique value between all ApiKeys' do
expect(subject).to eq unique_value
end
end
end
Loading

0 comments on commit d2bd8db

Please sign in to comment.