rrx_api 8.0.2 → 8.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 147f5fd76c0409efe7206adee475031281c611924bb7a8a252ad9e3bf5255cb8
4
- data.tar.gz: a1d53995cf2bda0935196dd789194ec8c073ac9c82b80a31c6c0a0bd53728dc8
3
+ metadata.gz: 704fe355ce80b8309eda6e6efe7ea7cf554a3a4925ba6877079a3083380227dd
4
+ data.tar.gz: 78e69441af925e8d2144d894c57668c133d3e80c67b4d2bd14bc4f84cb0d6a33
5
5
  SHA512:
6
- metadata.gz: bf4f7082aeae76be23b58a175e73f54d9df20ed0176cd86b12bc0ef1508adbeeea2c1e8fac5ebe07a137cf6afb71dc53ab911b9e8f3427fa39c0f30c5d357746
7
- data.tar.gz: 30aebec9055081d21e0802d894dce6ce0ad5a100cbbe2fa696deb0b5609b2b62f7266ff1daa8d343bfee618688f1f568b7f3371a6a5582e4ed537bc4a20bd7ea
6
+ metadata.gz: d8fe108896f5647852a600926980dc037c4e9e564dceb7cb13ae5ff9f7917a21c7e88930537f2c440cc7f01938120fdfc0ed8c1defd60d62b312b2982316a3ba
7
+ data.tar.gz: 5045a736b9da7313a3993d8da3328408f10e0a7ec2ae1b1819dd252e3cd097b4fa01e30d3299a9f97b98c0f8415bd8f265ab6a1a53ee3501b36ffdb693ffd24a
data/Gemfile.lock CHANGED
@@ -96,7 +96,7 @@ GEM
96
96
  erubi (~> 1.11)
97
97
  rails-dom-testing (~> 2.2)
98
98
  rails-html-sanitizer (~> 1.6)
99
- active_record_query_trace (1.8.2)
99
+ active_record_query_trace (1.9)
100
100
  activerecord (>= 6.0.0)
101
101
  activejob (8.0.2)
102
102
  activesupport (= 8.0.2)
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RrxApi
4
+ # Controller concern providing authentication plumbing.
5
+ #
6
+ # Include this in your ApplicationController (it is included in RrxApi::Controller
7
+ # by default). Then call +authenticate!+ on any controller that requires auth:
8
+ #
9
+ # class Api::V1::PostsController < ApplicationController
10
+ # authenticate!
11
+ # end
12
+ #
13
+ # Override +authenticated_user+ in your ApplicationController to resolve the
14
+ # current user from the request (token, API key, etc.). The default implementation
15
+ # returns +nil+, which means all requests are unauthenticated unless overridden.
16
+ #
17
+ # In test environments, requests may pass an +X-Test-User-Id+ header instead of
18
+ # a real token. Override +test_user+ if your User model has a different lookup.
19
+ module Authenticatable
20
+ extend ActiveSupport::Concern
21
+
22
+ AUTH_HEADER = 'Authorization'
23
+ BEARER_TOKEN = 'Bearer'
24
+
25
+ included do
26
+ rescue_from RrxApi::Auth::TokenExpiredError do
27
+ render json: { error: 'token_expired' }, status: :unauthorized
28
+ end
29
+ end
30
+
31
+ class_methods do
32
+ # Prepend a before_action that calls +authenticate_user!+.
33
+ # @param only [Array<Symbol>, nil]
34
+ # @param except [Array<Symbol>, nil]
35
+ def authenticate!(only: nil, except: nil)
36
+ prepend_before_action :authenticate_user!, only: only, except: except
37
+ end
38
+ end
39
+
40
+ # Returns the authenticated user or +nil+.
41
+ # In test env the +X-Test-User-Id+ header short-circuits real auth.
42
+ def current_user
43
+ @current_user ||= (Rails.env.test? ? test_user : nil) || authenticated_user
44
+ end
45
+
46
+ private
47
+
48
+ # Render 401 unless current_user is present.
49
+ def authenticate_user!
50
+ unauthorized! unless current_user
51
+ end
52
+
53
+ # Render a standardised 401 response.
54
+ # @param message [String]
55
+ def unauthorized!(message: 'Not authenticated')
56
+ render json: { error: message }, status: :unauthorized
57
+ end
58
+
59
+ # Resolve the current user from the request. Override this in your
60
+ # ApplicationController — e.g. look up by API key or bearer token.
61
+ # @return [Object, nil]
62
+ def authenticated_user
63
+ nil
64
+ end
65
+
66
+ # Yields the bearer token string if an +Authorization: Bearer <token>+ header is present.
67
+ def with_bearer_token
68
+ parts = request.headers[AUTH_HEADER]&.split(' ', 2)
69
+ yield parts[1] if parts&.first == BEARER_TOKEN
70
+ end
71
+
72
+ # Returns a user identified by the +X-Test-User-Id+ request header.
73
+ # Only active in the test environment. Override if your lookup differs.
74
+ # @return [Object, nil]
75
+ def test_user
76
+ nil
77
+ end
78
+ end
79
+ end
@@ -3,6 +3,7 @@
3
3
  module RrxApi
4
4
  class Controller < ActionController::API
5
5
  include AbstractController::Helpers
6
+ include RrxApi::Authenticatable
6
7
 
7
8
  abstract!
8
9
 
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ArelQuery
4
+ extend ActiveSupport::Concern
5
+
6
+ module Cast
7
+ def self.included(mod)
8
+ mod.class_eval do
9
+ alias_method :create_cast, :cast if method_defined?(:cast)
10
+
11
+ def cast(type)
12
+ ::Arel::Nodes::NamedFunction.new "CAST", [self.as(type)]
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ class WithHelper
19
+ attr_reader :name
20
+
21
+ def initialize(name, relation)
22
+ raise ArgumentError, 'Name must be a Symbol or String' unless name.is_a?(Symbol) || name.is_a?(String)
23
+ raise ArgumentError, 'Relation must be an Arel node or select manager' unless relation.is_a?(Arel::Nodes::Node) || relation.is_a?(Arel::SelectManager)
24
+
25
+ @name = name
26
+ @relation = relation
27
+ end
28
+
29
+ def [](column)
30
+ table[column]
31
+ end
32
+
33
+ def table
34
+ @table ||= Arel::Table.new(@name)
35
+ end
36
+
37
+ def to_cte
38
+ Arel::Nodes::Cte.new(@name, @relation)
39
+ end
40
+ end
41
+
42
+ class ArelHelper
43
+ attr_reader :model
44
+
45
+ delegate :star, :sql, to: Arel
46
+ delegate :project, :where, :from, :join, :outer_join, :group, :order, :alias, to: :@table
47
+ delegate :with, to: :from
48
+
49
+ def initialize(model)
50
+ @model = model
51
+ @table = model.arel_table if model.respond_to?(:arel_table)
52
+ end
53
+
54
+ def table(name = nil)
55
+ if name
56
+ Arel::Table.new(name)
57
+ elsif @table
58
+ @table
59
+ else
60
+ raise ArgumentError, 'No table available. Provide a table name.'
61
+ end
62
+ end
63
+
64
+ alias t table
65
+
66
+ def literal(value)
67
+ Arel::Nodes::SqlLiteral.new(value.to_s)
68
+ end
69
+
70
+ alias l literal
71
+ alias lit literal
72
+
73
+ def string(value)
74
+ literal "'#{value}'"
75
+ end
76
+
77
+ alias s string
78
+ alias str string
79
+
80
+ # @return [Arel::SelectManager]
81
+ def select(...)
82
+ Arel::SelectManager.new(...)
83
+ end
84
+
85
+ def as(what, alias_name)
86
+ raise ArgumentError, 'Alias name must be a Symbol or String' unless alias_name.is_a?(Symbol) || alias_name.is_a?(String)
87
+
88
+ Arel::Nodes::As.new(what, literal(alias_name))
89
+ end
90
+
91
+ def for_with(name, relation)
92
+ WithHelper.new(name, relation)
93
+ end
94
+
95
+ def results(query)
96
+ connection.select_all(query)
97
+ end
98
+
99
+ def rows(query)
100
+ results(query).to_a
101
+ end
102
+
103
+ def connection
104
+ ActiveRecord::Base.connection
105
+ end
106
+
107
+ def respond_to_missing?(_method_name, _include_private = false)
108
+ true
109
+ end
110
+
111
+ FUNCTION_METHOD_PATTERN = /\A[a-z]+(_[a-z]+)*\z/
112
+
113
+ def method_missing(method_name, *args, &block)
114
+ node_class = "Arel::Nodes::#{method_name.to_s.camelize}".safe_constantize
115
+
116
+ if node_class
117
+ self.class.define_method method_name do |*method_args|
118
+ node_class.new(*method_args)
119
+ end
120
+ elsif @table&.respond_to?(method_name)
121
+ return @table.send(method_name, *args)
122
+ elsif method_name.to_s =~ FUNCTION_METHOD_PATTERN
123
+ self.class.define_method method_name do |*method_args|
124
+ Arel::Nodes::NamedFunction.new(method_name.to_s, method_args)
125
+ end
126
+ else
127
+ return super
128
+ end
129
+
130
+ send(method_name, *args)
131
+ end
132
+
133
+ def column(name)
134
+ @table[name]
135
+ end
136
+
137
+ alias col column
138
+ alias c column
139
+
140
+ def [](it)
141
+ case it
142
+ when Symbol
143
+ @table[it]
144
+ when String
145
+ string(it)
146
+ else
147
+ literal(it)
148
+ end
149
+ end
150
+ end
151
+
152
+ class_methods do
153
+ def aq(*args)
154
+ @arel_helper ||= ArelHelper.new(self)
155
+ args.empty? ? @arel_helper : @arel_helper.sql(*args)
156
+ end
157
+ end
158
+
159
+ included do
160
+ def aq(...)
161
+ self.class.aq(...)
162
+ end
163
+ end
164
+ end
165
+
166
+ ActiveSupport.on_load :active_record do
167
+ class ::Arel::SelectManager
168
+ def table
169
+ @ast.cores[0].source.left
170
+ end
171
+
172
+ def join_to(other, from: :id, to: :id, on: nil, type: :inner)
173
+ on ||= table[from].eq(other[to])
174
+ join_node_class = case type
175
+ when :inner then Arel::Nodes::InnerJoin
176
+ when :left, :outer then Arel::Nodes::OuterJoin
177
+ when :right then Arel::Nodes::RightOuterJoin
178
+ when :full then Arel::Nodes::FullOuterJoin
179
+ else raise ArgumentError, "Unknown join type: #{type.inspect}"
180
+ end
181
+
182
+ other = other.table if other.respond_to?(:table)
183
+ join(other, join_node_class).on(on)
184
+ end
185
+ end
186
+
187
+ ::Arel::Nodes::Node.include(ArelQuery::Cast)
188
+ ::Arel::Attributes::Attribute.include(ArelQuery::Cast)
189
+ end
@@ -4,6 +4,8 @@ module RrxApi
4
4
  class Record < ActiveRecord::Base
5
5
  self.abstract_class = true
6
6
 
7
+ include ArelQuery
8
+
7
9
  before_create :set_new_id
8
10
 
9
11
  # @return [RrxLogging::Logger]
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RrxApi
4
+ module Auth
5
+ class TokenExpiredError < StandardError
6
+ def initialize
7
+ super('User token expired')
8
+ end
9
+ end
10
+
11
+ # Abstract authentication provider base class.
12
+ # Subclass and override {#id_from_token} (and optionally {#user_info}) to implement a provider.
13
+ class Base
14
+ # Verify a bearer token and return the provider user ID.
15
+ # @param _token [String]
16
+ # @return [String]
17
+ def id_from_token(_token)
18
+ raise NotImplementedError, "#{self.class}#id_from_token is not implemented"
19
+ end
20
+
21
+ # Fetch user info for a given provider UID (optional).
22
+ # @param _uid [String]
23
+ # @return [Object, nil]
24
+ def user_info(_uid)
25
+ nil
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module RrxApi
6
+ module Auth
7
+ # Firebase authentication provider.
8
+ #
9
+ # Requires the +firebase-admin-sdk+ gem. It is intentionally not listed as a
10
+ # dependency of rrx_api — add it to your application's Gemfile when you want
11
+ # Firebase auth:
12
+ #
13
+ # gem 'firebase-admin-sdk'
14
+ #
15
+ # Configuration is read from +RrxConfig.firebase+ (project_id, client_email,
16
+ # private_key, etc.).
17
+ class Firebase < Base
18
+ def id_from_token(id_token)
19
+ require_firebase!
20
+ verified = auth.verify_id_token(id_token)
21
+ verified['sub']
22
+ rescue LoadError
23
+ raise
24
+ rescue => e
25
+ # Lazily match the expired-token error so this file can be parsed before
26
+ # the firebase-admin-sdk gem is loaded.
27
+ raise TokenExpiredError if e.class.name == 'Firebase::Admin::Auth::ExpiredTokenError'
28
+
29
+ raise
30
+ end
31
+
32
+ def user_info(uid)
33
+ require_firebase!
34
+ auth.get_user(uid)
35
+ end
36
+
37
+ private
38
+
39
+ def require_firebase!
40
+ require 'firebase-admin-sdk'
41
+ rescue LoadError
42
+ raise LoadError,
43
+ "firebase-admin-sdk is required for Firebase authentication. " \
44
+ "Add `gem 'firebase-admin-sdk'` to your Gemfile."
45
+ end
46
+
47
+ def credentials
48
+ @credentials ||= ::Firebase::Admin::Credentials.from_json(config.to_json)
49
+ end
50
+
51
+ def app
52
+ @app ||= begin
53
+ app_config = ::Firebase::Admin::Config.new(
54
+ project_id: config.project_id,
55
+ service_account_id: config.client_email
56
+ )
57
+ ::Firebase::Admin::App.new(credentials:, config: app_config)
58
+ end
59
+ end
60
+
61
+ def auth
62
+ @auth ||= ::Firebase::Admin::Auth::Client.new(app)
63
+ end
64
+
65
+ def config
66
+ RrxConfig.firebase
67
+ end
68
+ end
69
+ end
70
+ end
@@ -7,12 +7,35 @@ require 'action_view/railtie'
7
7
  require 'jbuilder'
8
8
  require 'rack/cors'
9
9
  require 'actionpack/action_caching'
10
+ require_relative 'auth/base'
11
+ require_relative 'auth/firebase'
10
12
 
11
13
  module RrxApi
12
14
  class Engine < ::Rails::Engine
13
15
  CORS_LOCALHOST_PATTERN = /\Ahttp:\/\/localhost(?::\d{4})?\z/.freeze
14
16
 
15
17
  config.cors_origins = []
18
+
19
+ # Checks whether +source+ matches any of the configured CORS origins.
20
+ # Each entry in +cors_origins+ may be:
21
+ # - a String for exact match (e.g. "https://app.example.com")
22
+ # - a String with a leading wildcard (e.g. "*.example.com" matches "https://foo.example.com")
23
+ # - a Regexp (e.g. /\.example\.com\z/)
24
+ def self.cors_origin_allowed?(source, origins)
25
+ origins.any? do |origin|
26
+ case origin
27
+ when Regexp then origin.match?(source)
28
+ when String
29
+ if origin.start_with?('*.')
30
+ # Wildcard subdomain: *.example.com matches any scheme+subdomain of example.com
31
+ suffix = origin[1..] # => ".example.com"
32
+ source.end_with?(suffix)
33
+ else
34
+ source == origin
35
+ end
36
+ end
37
+ end
38
+ end
16
39
  config.healthcheck = nil
17
40
  config.healthcheck_route = 'healthcheck'
18
41
 
@@ -40,7 +63,7 @@ module RrxApi
40
63
  if Rails.env.development?
41
64
  CORS_LOCALHOST_PATTERN.match? source
42
65
  else
43
- app.config.cors_origins.include?(source)
66
+ Engine.cors_origin_allowed?(source, app.config.cors_origins)
44
67
  end
45
68
  end
46
69
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RrxApi
4
- VERSION = '8.0.2'
4
+ VERSION = '8.0.3'
5
5
  DEPENDENCY_VERSION = "~> #{VERSION}"
6
6
  RAILS_VERSION = DEPENDENCY_VERSION
7
7
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rrx_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.0.2
4
+ version: 8.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dan Drew
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-11-17 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: actionpack-action_caching
@@ -128,42 +127,42 @@ dependencies:
128
127
  requirements:
129
128
  - - "~>"
130
129
  - !ruby/object:Gem::Version
131
- version: 8.0.2
130
+ version: 8.0.3
132
131
  type: :runtime
133
132
  prerelease: false
134
133
  version_requirements: !ruby/object:Gem::Requirement
135
134
  requirements:
136
135
  - - "~>"
137
136
  - !ruby/object:Gem::Version
138
- version: 8.0.2
137
+ version: 8.0.3
139
138
  - !ruby/object:Gem::Dependency
140
139
  name: rrx_config
141
140
  requirement: !ruby/object:Gem::Requirement
142
141
  requirements:
143
142
  - - "~>"
144
143
  - !ruby/object:Gem::Version
145
- version: 8.0.2
144
+ version: 8.0.3
146
145
  type: :runtime
147
146
  prerelease: false
148
147
  version_requirements: !ruby/object:Gem::Requirement
149
148
  requirements:
150
149
  - - "~>"
151
150
  - !ruby/object:Gem::Version
152
- version: 8.0.2
151
+ version: 8.0.3
153
152
  - !ruby/object:Gem::Dependency
154
153
  name: rrx_logging
155
154
  requirement: !ruby/object:Gem::Requirement
156
155
  requirements:
157
156
  - - "~>"
158
157
  - !ruby/object:Gem::Version
159
- version: 8.0.2
158
+ version: 8.0.3
160
159
  type: :runtime
161
160
  prerelease: false
162
161
  version_requirements: !ruby/object:Gem::Requirement
163
162
  requirements:
164
163
  - - "~>"
165
164
  - !ruby/object:Gem::Version
166
- version: 8.0.2
165
+ version: 8.0.3
167
166
  - !ruby/object:Gem::Dependency
168
167
  name: rswag-api
169
168
  requirement: !ruby/object:Gem::Requirement
@@ -254,15 +253,14 @@ dependencies:
254
253
  requirements:
255
254
  - - "~>"
256
255
  - !ruby/object:Gem::Version
257
- version: 8.0.2
256
+ version: 8.0.3
258
257
  type: :development
259
258
  prerelease: false
260
259
  version_requirements: !ruby/object:Gem::Requirement
261
260
  requirements:
262
261
  - - "~>"
263
262
  - !ruby/object:Gem::Version
264
- version: 8.0.2
265
- description:
263
+ version: 8.0.3
266
264
  email:
267
265
  - dan.drew@hotmail.com
268
266
  executables: []
@@ -278,8 +276,10 @@ files:
278
276
  - LICENSE.txt
279
277
  - README.md
280
278
  - Rakefile
279
+ - app/controllers/concerns/rrx_api/authenticatable.rb
281
280
  - app/controllers/rrx_api/controller.rb
282
281
  - app/controllers/rrx_api/health_controller.rb
282
+ - app/models/concerns/arel_query.rb
283
283
  - app/models/rrx_api/record.rb
284
284
  - config/routes.rb
285
285
  - lib/generators/rrx_api/base.rb
@@ -294,6 +294,8 @@ files:
294
294
  - lib/generators/rrx_api/templates/terraform/aws/service.tf.tt
295
295
  - lib/generators/rrx_api/terraform_generator.rb
296
296
  - lib/rrx_api.rb
297
+ - lib/rrx_api/auth/base.rb
298
+ - lib/rrx_api/auth/firebase.rb
297
299
  - lib/rrx_api/engine.rb
298
300
  - lib/rrx_api/version.rb
299
301
  - sig/rrx_api.rbs
@@ -303,23 +305,21 @@ licenses:
303
305
  metadata:
304
306
  homepage_uri: https://github.com/rails-rrx/rrx_api
305
307
  source_code_uri: https://github.com/rails-rrx/rrx_api
306
- post_install_message:
307
308
  rdoc_options: []
308
309
  require_paths:
309
310
  - lib
310
311
  required_ruby_version: !ruby/object:Gem::Requirement
311
312
  requirements:
312
- - - ">="
313
+ - - "~>"
313
314
  - !ruby/object:Gem::Version
314
- version: '3.1'
315
+ version: 3.4.0
315
316
  required_rubygems_version: !ruby/object:Gem::Requirement
316
317
  requirements:
317
318
  - - ">="
318
319
  - !ruby/object:Gem::Version
319
320
  version: '0'
320
321
  requirements: []
321
- rubygems_version: 3.4.19
322
- signing_key:
322
+ rubygems_version: 3.6.7
323
323
  specification_version: 4
324
324
  summary: Ruby on Rails core API support
325
325
  test_files: []