spree_cm_commissioner 2.1.7 → 2.1.9.pre.pre1

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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +75 -29
  3. data/Gemfile.lock +8 -1
  4. data/app/controllers/spree/admin/user_security_controller.rb +25 -0
  5. data/app/controllers/spree/api/v2/storefront/access_tokens_controller.rb +10 -0
  6. data/app/controllers/spree/api/v2/storefront/account/guest_dynamic_fields_controller.rb +82 -0
  7. data/app/controllers/spree/api/v2/storefront/account/mark_guest_info_complete_controller.rb +39 -0
  8. data/app/controllers/spree/api/v2/storefront/account/orders_controller_decorator.rb +34 -5
  9. data/app/controllers/spree/api/v2/storefront/seat_layouts_controller.rb +1 -1
  10. data/app/controllers/spree/api/v2/tenant/access_tokens_controller.rb +10 -0
  11. data/app/interactors/spree_cm_commissioner/stock/inventory_item_resetter.rb +1 -1
  12. data/app/models/concerns/spree_cm_commissioner/event_metadata.rb +39 -0
  13. data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +27 -21
  14. data/app/models/concerns/spree_cm_commissioner/store_metadata.rb +97 -0
  15. data/app/models/spree_cm_commissioner/guest.rb +38 -4
  16. data/app/models/spree_cm_commissioner/guest_dynamic_field.rb +1 -0
  17. data/app/models/spree_cm_commissioner/inventory_item.rb +1 -1
  18. data/app/models/spree_cm_commissioner/invite_team.rb +8 -0
  19. data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +2 -2
  20. data/app/models/spree_cm_commissioner/redis_stock/inventory_updater.rb +2 -2
  21. data/app/models/spree_cm_commissioner/seat_layout.rb +3 -3
  22. data/app/models/spree_cm_commissioner/taxon_decorator.rb +2 -20
  23. data/app/models/spree_cm_commissioner/trip.rb +5 -7
  24. data/app/models/spree_cm_commissioner/user_decorator.rb +16 -0
  25. data/app/models/spree_cm_commissioner/vehicle.rb +5 -7
  26. data/app/overrides/spree/admin/taxons/_form/background_color_and_foreground_color.html.erb.deface +6 -6
  27. data/app/overrides/spree/admin/taxons/_form/check_in_flows.html.erb.deface +14 -8
  28. data/app/overrides/spree/admin/users/_tabs/tabs.html.erb.deface +6 -0
  29. data/app/serializers/spree/v2/storefront/order_serializer_decorator.rb +63 -0
  30. data/app/serializers/spree/v2/storefront/taxon_serializer_decorator.rb +1 -1
  31. data/app/serializers/spree_cm_commissioner/v2/operator/dashboard_crew_event_serializer.rb +1 -0
  32. data/app/views/spree/admin/user_security/show.html.erb +25 -0
  33. data/config/initializers/spree_permitted_attributes.rb +5 -4
  34. data/config/locales/en.yml +1 -0
  35. data/config/routes.rb +11 -0
  36. data/docs/api/scoped-access-token-endpoints.md +84 -0
  37. data/lib/spree_cm_commissioner/version.rb +1 -1
  38. data/lib/spree_cm_commissioner.rb +35 -12
  39. data/spree_cm_commissioner.gemspec +1 -0
  40. metadata +28 -5
  41. data/app/models/concerns/spree_cm_commissioner/event_check_in_flowable.rb +0 -30
@@ -48,7 +48,7 @@ module SpreeCmCommissioner
48
48
  end
49
49
 
50
50
  def adjust_quantity_in_redis(quantity)
51
- SpreeCmCommissioner.redis_pool.with do |redis|
51
+ SpreeCmCommissioner.inventory_redis_pool.with do |redis|
52
52
  cached_quantity_available = redis.get(redis_key)
53
53
  # ignore if redis doesn't exist
54
54
  return if cached_quantity_available.nil? # rubocop:disable Lint/NonLocalExitFromIterator
@@ -14,6 +14,8 @@ module SpreeCmCommissioner
14
14
 
15
15
  validate :validate_roles
16
16
 
17
+ before_validation :normalize_email
18
+
17
19
  after_create :set_expiration
18
20
  after_create :send_team_invite_email
19
21
 
@@ -42,5 +44,11 @@ module SpreeCmCommissioner
42
44
  def url_valid?
43
45
  expires_at.present? && expires_at > Time.current
44
46
  end
47
+
48
+ private
49
+
50
+ def normalize_email
51
+ self.email = email.strip.downcase if email.present?
52
+ end
45
53
  end
46
54
  end
@@ -12,7 +12,7 @@ module SpreeCmCommissioner
12
12
  keys = inventory_items.map { |item| "inventory:#{item.id}" }
13
13
  return [] unless keys.any?
14
14
 
15
- counts = SpreeCmCommissioner.redis_pool.with { |redis| redis.mget(*keys) }
15
+ counts = SpreeCmCommissioner.inventory_redis_pool.with { |redis| redis.mget(*keys) }
16
16
  inventory_items.map.with_index do |inventory_item, i|
17
17
  ::SpreeCmCommissioner::CachedInventoryItem.new(
18
18
  inventory_key: keys[i],
@@ -30,7 +30,7 @@ module SpreeCmCommissioner
30
30
  return count_in_redis.to_i if count_in_redis.present?
31
31
  return inventory_item.quantity_available unless inventory_item.active?
32
32
 
33
- SpreeCmCommissioner.redis_pool.with do |redis|
33
+ SpreeCmCommissioner.inventory_redis_pool.with do |redis|
34
34
  redis.set(key, inventory_item.quantity_available, ex: inventory_item.redis_expired_in)
35
35
  end
36
36
 
@@ -51,13 +51,13 @@ module SpreeCmCommissioner
51
51
  end
52
52
 
53
53
  def unstock(keys, quantities)
54
- SpreeCmCommissioner.redis_pool.with do |redis|
54
+ SpreeCmCommissioner.inventory_redis_pool.with do |redis|
55
55
  redis.eval(unstock_redis_script, keys: keys, argv: quantities)
56
56
  end.positive?
57
57
  end
58
58
 
59
59
  def restock(keys, quantities)
60
- SpreeCmCommissioner.redis_pool.with do |redis|
60
+ SpreeCmCommissioner.inventory_redis_pool.with do |redis|
61
61
  redis.eval(restock_redis_script, keys: keys, argv: quantities)
62
62
  end.positive?
63
63
  end
@@ -27,13 +27,13 @@ module SpreeCmCommissioner
27
27
 
28
28
  private
29
29
 
30
- # layoutable (taxon, vehicle) must have seat_layout_id attribute,
30
+ # layoutable (taxon, vehicle) must have preload_seat_layout_id attribute,
31
31
  # either as a column or as a key in a JSONB column (e.g., metadata)
32
32
  def sync_seat_layout_id_to_layoutable!
33
33
  if destroyed?
34
- layoutable.update!(seat_layout_id: nil)
34
+ layoutable.update!(preload_seat_layout_id: nil)
35
35
  else
36
- layoutable.update!(seat_layout_id: id)
36
+ layoutable.update!(preload_seat_layout_id: id)
37
37
  end
38
38
  end
39
39
  end
@@ -1,20 +1,10 @@
1
1
  module SpreeCmCommissioner
2
2
  module TaxonDecorator
3
3
  def self.prepended(base) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
4
+ base.include SpreeCmCommissioner::StoreMetadata
4
5
  base.include SpreeCmCommissioner::TaxonKind
5
6
  base.include SpreeCmCommissioner::ParticipationTypeBitwise
6
- base.include SpreeCmCommissioner::EventCheckInFlowable
7
-
8
- base.preference :background_color, :string
9
- base.preference :foreground_color, :string
10
-
11
- # seat_layout_id:
12
- # This model has no seat_layout column (polymorphic association), so we store the seat_layout ID in public_metadata.
13
- # This lets us check if a seat layout exists without triggering a database query.
14
- # The ID is automatically updated whenever the seat_layout is saved.
15
- base.store :public_metadata, accessors: %i[
16
- seat_layout_id
17
- ], coder: JSON
7
+ base.include SpreeCmCommissioner::EventMetadata
18
8
 
19
9
  base.has_many :taxon_vendors, class_name: 'SpreeCmCommissioner::TaxonVendor'
20
10
  base.has_many :vendors, through: :taxon_vendors
@@ -104,14 +94,6 @@ module SpreeCmCommissioner
104
94
  end
105
95
  end
106
96
 
107
- def background_color
108
- preferred_background_color
109
- end
110
-
111
- def foreground_color
112
- preferred_foreground_color
113
- end
114
-
115
97
  def set_kind
116
98
  self.kind = taxonomy.kind
117
99
  end
@@ -1,16 +1,14 @@
1
1
  module SpreeCmCommissioner
2
2
  class Trip < Base
3
+ include SpreeCmCommissioner::StoreMetadata
3
4
  include SpreeCmCommissioner::RouteType
4
5
 
5
6
  attr_accessor :hours, :minutes, :seconds
6
7
 
7
- # seat_layout_id:
8
- # This model has no seat_layout column (polymorphic association), so we store the seat_layout ID in public_metadata.
9
- # This lets us check if a seat layout exists without triggering a database query.
10
- # The ID is automatically updated whenever the seat_layout is saved.
11
- store :public_metadata, accessors: %i[
12
- seat_layout_id
13
- ], coder: JSON
8
+ # This model has no seat_layout column (polymorphic association), so we store the preload_seat_layout_id ID in public_metadata.
9
+ # This lets us check if a seat layout exists without triggering a database query.
10
+ # The ID is automatically updated whenever the seat_layout is saved.
11
+ store_public_metadata :preload_seat_layout_id, :integer
14
12
 
15
13
  before_validation :convert_duration_to_seconds
16
14
 
@@ -46,6 +46,10 @@ module SpreeCmCommissioner
46
46
  # Store has_incomplete_guest_info in public_metadata for easy frontend access
47
47
  base.store :public_metadata, accessors: [:has_incomplete_guest_info], coder: JSON
48
48
 
49
+ base.devise :two_factor_authenticatable
50
+
51
+ base.store :private_metadata, accessors: %i[otp_secret otp_required_for_login consumed_timestep], coder: JSON
52
+
49
53
  define_user_places(base)
50
54
 
51
55
  def base.end_users
@@ -63,6 +67,18 @@ module SpreeCmCommissioner
63
67
  end
64
68
  end
65
69
 
70
+ def otp_secret
71
+ private_metadata['otp_secret']
72
+ end
73
+
74
+ def otp_required_for_login
75
+ private_metadata['otp_required_for_login']
76
+ end
77
+
78
+ def consumed_timestep
79
+ private_metadata['consumed_timestep']
80
+ end
81
+
66
82
  def self.define_user_places(base)
67
83
  base.has_many :user_places, class_name: 'SpreeCmCommissioner::UserPlace'
68
84
  base.has_many :places, through: :user_places, class_name: 'SpreeCmCommissioner::Place'
@@ -1,14 +1,12 @@
1
1
  module SpreeCmCommissioner
2
2
  class Vehicle < Base
3
+ include SpreeCmCommissioner::StoreMetadata
3
4
  include SpreeCmCommissioner::VehicleType
4
5
 
5
- # seat_layout_id:
6
- # This model has no seat_layout column (polymorphic association), so we store the seat_layout ID in public_metadata.
7
- # This lets us check if a seat layout exists without triggering a database query.
8
- # The ID is automatically updated whenever the seat_layout is saved.
9
- store :public_metadata, accessors: %i[
10
- seat_layout_id
11
- ], coder: JSON
6
+ # This model has no seat_layout column (polymorphic association), so we store the preload_seat_layout_id ID in public_metadata.
7
+ # This lets us check if a seat layout exists without triggering a database query.
8
+ # The ID is automatically updated whenever the seat_layout is saved.
9
+ store_public_metadata :preload_seat_layout_id, :integer
12
10
 
13
11
  belongs_to :vendor, class_name: 'Spree::Vendor'
14
12
 
@@ -4,17 +4,17 @@
4
4
 
5
5
  <div class="row <%= "d-none" unless @taxon.depth == 1 %>">
6
6
  <div class="col-md-6">
7
- <%= f.label :preferred_background_color, Spree.t('background_color'), class: 'form-label' %>
8
- <%= f.text_field :preferred_background_color, class: 'form-control color-picker', placeholder: '#FFFFFF', value: @object.preferred_background_color %>
7
+ <%= f.label :background_color, Spree.t('background_color'), class: 'form-label' %>
8
+ <%= f.text_field :background_color, class: 'form-control color-picker', placeholder: '#FFFFFF', value: @object.background_color %>
9
9
  <div class="color-preview" style="margin-top: 10px;">
10
- <%= content_tag :div, nil, style: "background-color: #{@object.preferred_background_color}; width: 60px; height: 60px; border: 1px solid #ccc; box-sizing: border-box; display: block;" %>
10
+ <%= content_tag :div, nil, style: "background-color: #{@object.background_color}; width: 60px; height: 60px; border: 1px solid #ccc; box-sizing: border-box; display: block;" %>
11
11
  </div>
12
12
  </div>
13
13
  <div class="col-md-6">
14
- <%= f.label :preferred_foreground_color, Spree.t('foreground_color'), class: 'form-label' %>
15
- <%= f.text_field :preferred_foreground_color, class: 'form-control color-picker', placeholder: '#FFFFFF', value: @object.preferred_foreground_color %>
14
+ <%= f.label :foreground_color, Spree.t('foreground_color'), class: 'form-label' %>
15
+ <%= f.text_field :foreground_color, class: 'form-control color-picker', placeholder: '#FFFFFF', value: @object.foreground_color %>
16
16
  <div class="color-preview" style="margin-top: 10px;">
17
- <%= content_tag :div, nil, style: "background-color: #{@object.preferred_foreground_color}; width: 60px; height: 60px; border: 1px solid #ccc; box-sizing: border-box; display: block;" %>
17
+ <%= content_tag :div, nil, style: "background-color: #{@object.foreground_color}; width: 60px; height: 60px; border: 1px solid #ccc; box-sizing: border-box; display: block;" %>
18
18
  </div>
19
19
  </div>
20
20
  </div>
@@ -2,17 +2,23 @@
2
2
 
3
3
  <%# Sections don't use video banner, so we hide the form here to avoid confusing admin. %>
4
4
 
5
- <%= f.field_container :preferred_group_check_in_enabled, class: ['custom-control', 'custom-checkbox', 'my-4'] do %>
6
- <%= f.check_box :preferred_group_check_in_enabled, class: 'custom-control-input' %>
7
- <%= f.label :preferred_group_check_in_enabled, Spree.t(:enable_group_check_in), class: 'custom-control-label' %>
8
- <%= f.error_message_on :preferred_group_check_in_enabled %>
5
+ <%= f.field_container :group_check_in_enabled, class: ['custom-control', 'custom-checkbox', 'my-4'] do %>
6
+ <%= f.check_box :group_check_in_enabled, class: 'custom-control-input' %>
7
+ <%= f.label :group_check_in_enabled, Spree.t(:enable_group_check_in), class: 'custom-control-label' %>
8
+ <%= f.error_message_on :group_check_in_enabled %>
9
9
  <small class="form-text text-muted">
10
10
  Enabled by default for most events where users can purchase multiple tickets or use group scanning. For large events (like PSK, ASK, etc.) or invitation-based events, you should disable this to ensure each ticket or invitation is scanned individually.
11
11
  </small>
12
12
  <% end if @taxon.event? && @taxon.depth == 1 %> %>
13
13
 
14
- <%= f.field_container :preferred_individual_check_in_enabled, class: ['custom-control', 'custom-checkbox', 'my-4'] do %>
15
- <%= f.check_box :preferred_individual_check_in_enabled, class: 'custom-control-input' %>
16
- <%= f.label :preferred_individual_check_in_enabled, Spree.t(:enable_individual_check_in), class: 'custom-control-label' %>
17
- <%= f.error_message_on :preferred_individual_check_in_enabled %>
14
+ <%= f.field_container :individual_check_in_enabled, class: ['custom-control', 'custom-checkbox', 'my-4'] do %>
15
+ <%= f.check_box :individual_check_in_enabled, class: 'custom-control-input' %>
16
+ <%= f.label :individual_check_in_enabled, Spree.t(:enable_individual_check_in), class: 'custom-control-label' %>
17
+ <%= f.error_message_on :individual_check_in_enabled %>
18
+ <% end if @taxon.event? && @taxon.depth == 1 %> %>
19
+
20
+ <%= f.field_container :allow_manual_search_for_operator, class: ['custom-control', 'custom-checkbox', 'my-4'] do %>
21
+ <%= f.check_box :allow_manual_search_for_operator, class: 'custom-control-input' %>
22
+ <%= f.label :allow_manual_search_for_operator, Spree.t(:allow_manual_search_for_operator), class: 'custom-control-label' %>
23
+ <%= f.error_message_on :allow_manual_search_for_operator %>
18
24
  <% end if @taxon.event? && @taxon.depth == 1 %> %>
@@ -49,4 +49,10 @@
49
49
  admin_user_events_path(user_id: @user.id),
50
50
  class: "nav-link #{'active' if current == :user_events }" %>
51
51
  </li>
52
+ <li class="nav-item">
53
+ <%= link_to_with_icon 'shield-check.svg',
54
+ Spree.t(:security),
55
+ admin_user_security_path(@user),
56
+ class: "nav-link #{'active' if current == :security }" %>
57
+ </li>
52
58
  <% end %>
@@ -0,0 +1,63 @@
1
+ module Spree
2
+ module V2
3
+ module Storefront
4
+ module OrderSerializerDecorator
5
+ def self.prepended(base)
6
+ base.attribute :has_incomplete_guest_info do |order|
7
+ order.user.public_metadata['has_incomplete_guest_info'] || false
8
+ end
9
+
10
+ base.attribute :guest_info_status do |order|
11
+ {
12
+ incomplete: order_has_incomplete_guests?(order),
13
+ can_mark_complete: can_mark_complete?(order)
14
+ }
15
+ end
16
+ end
17
+
18
+ def self.order_has_incomplete_guests?(order)
19
+ order.line_items.any? do |line_item|
20
+ line_item.guests.any? do |guest|
21
+ guest_incomplete?(guest)
22
+ end
23
+ end
24
+ end
25
+
26
+ def self.guest_incomplete?(guest)
27
+ (guest.pre_registration_fields? && !guest.pre_registration_completed?) ||
28
+ (guest.post_registration_fields? && !guest.post_registration_completed?) ||
29
+ (guest.during_check_in_fields? && !guest.check_in_completed?)
30
+ end
31
+
32
+ def self.can_mark_complete?(order)
33
+ event_ended?(order) || all_guest_info_complete?(order)
34
+ end
35
+
36
+ def self.event_ended?(order)
37
+ order.line_items.any? { |line_item| event_ended_for_line_item?(line_item) }
38
+ end
39
+
40
+ def self.event_ended_for_line_item?(line_item)
41
+ event = line_item.event
42
+ event&.to_date.present? && event.to_date < Time.zone.today
43
+ end
44
+
45
+ def self.all_guest_info_complete?(order)
46
+ order.line_items.all? { |line_item| line_item_guests_complete?(line_item) }
47
+ end
48
+
49
+ def self.line_item_guests_complete?(line_item)
50
+ line_item.guests.all? { |guest| guest_complete?(guest) }
51
+ end
52
+
53
+ def self.guest_complete?(guest)
54
+ guest.pre_registration_completed? &&
55
+ (!guest.post_registration_fields? || guest.post_registration_completed?) &&
56
+ (!guest.during_check_in_fields? || guest.check_in_completed?)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ Spree::V2::Storefront::OrderSerializer.prepend(Spree::V2::Storefront::OrderSerializerDecorator)
@@ -18,7 +18,7 @@ module Spree
18
18
  :purchasable_on, :vendor_id, :available_on, :hide_video_banner
19
19
 
20
20
  # To get full seat layout details, call the seat_layouts API with this ID.
21
- base.attributes :seat_layout_id
21
+ base.attribute :seat_layout_id, &:preload_seat_layout_id
22
22
 
23
23
  base.attribute :purchasable_on_app do |taxon|
24
24
  taxon.purchasable_on == 'app' || taxon.purchasable_on == 'both'
@@ -5,6 +5,7 @@ module SpreeCmCommissioner
5
5
  attributes :id, :name, :permalink,
6
6
  :from_date, :to_date,
7
7
  :check_in_flows,
8
+ :allow_manual_search_for_operator,
8
9
  :updated_at
9
10
 
10
11
  has_many :children_classifications, serializer: :classification
@@ -0,0 +1,25 @@
1
+ <%= render partial: 'spree/admin/users/tabs', locals: { current: :security } %>
2
+
3
+ <div class="card shadow-sm">
4
+ <div class="card-header bg-light">
5
+ <h5 class="card-title mb-0 h6">
6
+ <%= Spree.t(:two_factor_authentication) %>
7
+ </h5>
8
+ </div>
9
+
10
+ <div class="card-body text-center">
11
+ <% if @user.otp_secret.present? && @user.otp_required_for_login == true %>
12
+ <p class="text-muted mb-3">
13
+ Two-factor authentication is currently active for this user.
14
+ </p>
15
+ <%= button_to disable_authenticator_admin_user_security_path, method: :delete, class: "btn btn-danger btn-md",
16
+ data: { confirm: "Are you sure?" } do %>
17
+ Disable 2FA
18
+ <% end %>
19
+ <% else %>
20
+ <p class="text-muted mb-0">
21
+ Two-factor authentication is <strong>not enabled</strong> for this user.
22
+ </p>
23
+ <% end %>
24
+ </div>
25
+ </div>
@@ -31,10 +31,11 @@ module Spree
31
31
  from_date
32
32
  kind
33
33
  subtitle
34
- preferred_background_color
35
- preferred_foreground_color
36
- preferred_group_check_in_enabled
37
- preferred_individual_check_in_enabled
34
+ background_color
35
+ foreground_color
36
+ group_check_in_enabled
37
+ individual_check_in_enabled
38
+ allow_manual_search_for_operator
38
39
  show_badge_status
39
40
  purchasable_on
40
41
  available_on
@@ -30,6 +30,7 @@ en:
30
30
  incorrect_password: "Incorrect password"
31
31
  invalid_or_missing_params: "Invalid or missing params"
32
32
  invalid_client_credentials: "Invalid client credentials"
33
+ success_disabled_2fa: "Successfully disabled 2FA"
33
34
 
34
35
  hello: "Hello world"
35
36
 
data/config/routes.rb CHANGED
@@ -91,6 +91,11 @@ Spree::Core::Engine.add_routes do
91
91
  resources :users do
92
92
  resources :device_tokens
93
93
  resources :user_identity_providers
94
+ resource :security, controller: :user_security, only: %i[show] do
95
+ member do
96
+ delete :disable_authenticator
97
+ end
98
+ end
94
99
  end
95
100
 
96
101
  resources :s3_presigned_urls, only: %i[create new]
@@ -472,6 +477,7 @@ Spree::Core::Engine.add_routes do
472
477
  end
473
478
 
474
479
  namespace :tenant do
480
+ resource :token, controller: :access_tokens, only: [:create]
475
481
  resources :vendors
476
482
  resources :products
477
483
  resources :taxons, only: %i[index show], id: /.+/
@@ -555,6 +561,7 @@ Spree::Core::Engine.add_routes do
555
561
  end
556
562
 
557
563
  namespace :storefront do
564
+ resource :token, controller: :access_tokens, only: [:create]
558
565
  resources :waiting_room_sessions, only: :create
559
566
  resources :vattanac_banks, only: %i[create]
560
567
  resource :cart, controller: :cart, only: %i[show create destroy] do
@@ -594,6 +601,10 @@ Spree::Core::Engine.add_routes do
594
601
 
595
602
  namespace :account do
596
603
  resource :preferred_payment_method, controller: :preferred_payment_method, only: %i[show update]
604
+ patch :mark_guest_info_complete, to: 'mark_guest_info_complete#update'
605
+ resources :guests, only: [] do
606
+ patch 'dynamic_fields/:phase', to: 'guest_dynamic_fields#update_dynamic_field', as: :update_dynamic_fields
607
+ end
597
608
  end
598
609
 
599
610
  resource :s3_signed_urls
@@ -0,0 +1,84 @@
1
+ # Scoped Access Token Endpoints
2
+
3
+ This document describes the new scoped access token endpoints that provide better security and clarity for different API usage patterns.
4
+
5
+ ## Overview
6
+
7
+ Previously, all access tokens were retrieved via the generic `/oauth/token` endpoint, which made it difficult to apply security rules and prevent abuse. The new scoped endpoints provide:
8
+
9
+ - **Clear separation** between storefront and tenant API usage
10
+ - **Enhanced security** with application-specific validations
11
+ - **Better monitoring** and logging capabilities
12
+ - **Easier maintenance** and debugging
13
+
14
+ ## Endpoints
15
+
16
+ ### Storefront Access Token Endpoint
17
+
18
+ **POST** `/api/v2/storefront/token`
19
+
20
+ Issues access tokens for storefront applications.
21
+
22
+ #### Example Response
23
+
24
+ ```json
25
+ {
26
+ "access_token": "abc123...",
27
+ "token_type": "Bearer",
28
+ "expires_in": 86400,
29
+ "refresh_token": "def456...",
30
+ "created_at": 1640995200
31
+ }
32
+ ```
33
+
34
+ ### Tenant Access Token Endpoint
35
+
36
+ **POST** `/api/v2/tenant/token`
37
+
38
+ Issues access tokens for tenant applications (third-party integrations, partner apps, etc.).
39
+
40
+ #### Example Response
41
+
42
+ ```json
43
+ {
44
+ "access_token": "xyz789...",
45
+ "token_type": "Bearer",
46
+ "expires_in": 86400,
47
+ "refresh_token": "uvw012...",
48
+ "created_at": 1640995200
49
+ }
50
+ ```
51
+
52
+ ## Migration Guide
53
+
54
+ ### For Storefront Applications
55
+
56
+ 1. **Update endpoint URL**: Change from `/oauth/token` to `/api/v2/storefront/token`
57
+ 2. **No other changes required**: All existing parameters and flows work identically
58
+
59
+ ### For Tenant Applications
60
+
61
+ 1. **Update endpoint URL**: Change from `/oauth/token` to `/api/v2/tenant/token`
62
+ 2. **No other changes required**: All existing parameters and flows work identically
63
+
64
+ ### Legacy Support
65
+
66
+ The original `/oauth/token` endpoint remains available.
67
+
68
+ ## Implementation
69
+
70
+ ### Controller Inheritance
71
+
72
+ Both controllers inherit from `Doorkeeper::TokensController`.
73
+
74
+ ### Route Configuration
75
+
76
+ ```ruby
77
+ namespace :storefront do
78
+ resource :token, controller: :access_tokens, only: [:create]
79
+ end
80
+
81
+ namespace :tenant do
82
+ resource :token, controller: :access_tokens, only: [:create]
83
+ end
84
+ ```
@@ -1,5 +1,5 @@
1
1
  module SpreeCmCommissioner
2
- VERSION = '2.1.7'.freeze
2
+ VERSION = '2.1.9-pre1'.freeze
3
3
 
4
4
  module_function
5
5
 
@@ -46,32 +46,55 @@ require 'premailer/rails'
46
46
  require 'cm_app_logger'
47
47
  require 'counter_culture'
48
48
  require 'paper_trail'
49
+ require 'devise-two-factor'
49
50
 
50
51
  require 'byebug' if Rails.env.development? || Rails.env.test?
51
52
 
52
53
  module SpreeCmCommissioner
53
54
  class << self
54
- # Allows overriding the default Redis connection pool with a custom one
55
- attr_writer :redis_pool
55
+ # Allows overriding the default Redis connection pools with custom ones
56
+ attr_writer :inventory_redis_pool, :external_integrations_redis_pool
56
57
 
57
- def redis_pool
58
- @redis_pool ||= default_redis_pool
58
+ # Inventory Redis pool for inventory management
59
+ def inventory_redis_pool
60
+ @inventory_redis_pool ||= default_inventory_redis_pool
59
61
  end
60
62
 
61
- # Resets the Redis pool, useful for testing or reinitialization
62
- def reset_redis_pool
63
- @redis_pool = nil
63
+ # External integrations Redis pool for external API integrations
64
+ def external_integrations_redis_pool
65
+ @external_integrations_redis_pool ||= default_external_integrations_redis_pool
66
+ end
67
+
68
+ # Resets all Redis pools, useful for testing or reinitialization
69
+ def reset_redis_pools
70
+ @inventory_redis_pool = nil
71
+ @external_integrations_redis_pool = nil
64
72
  end
65
73
 
66
74
  private
67
75
 
68
- def default_redis_pool
69
- pool_size = ENV.fetch('REDIS_POOL_SIZE', '5').to_i
70
- timeout = ENV.fetch('REDIS_TIMEOUT', '5').to_i
71
- redis_url = ENV.fetch('REDIS_URL', 'redis://localhost:6379/12')
76
+ def default_inventory_redis_pool
77
+ create_redis_pool(
78
+ url: ENV.fetch('REDIS_INVENTORY_URL', ENV.fetch('REDIS_URL', 'redis://localhost:6379/12')),
79
+ pool_size: ENV.fetch('REDIS_INVENTORY_POOL_SIZE', ENV.fetch('REDIS_POOL_SIZE', '5')).to_i,
80
+ timeout: ENV.fetch('REDIS_TIMEOUT', '5').to_i
81
+ )
82
+ end
83
+
84
+ def default_external_integrations_redis_pool
85
+ create_redis_pool(
86
+ url: ENV.fetch('REDIS_EXTERNAL_INTEGRATIONS_URL', ENV.fetch('REDIS_URL', 'redis://localhost:6379/13')),
87
+ pool_size: ENV.fetch('REDIS_EXTERNAL_INTEGRATIONS_POOL_SIZE', ENV.fetch('REDIS_POOL_SIZE', '5')).to_i,
88
+ timeout: ENV.fetch('REDIS_TIMEOUT', '5').to_i
89
+ )
90
+ end
91
+
92
+ def create_redis_pool(url:, pool_size:, timeout:)
93
+ ssl_params = {}
94
+ ssl_params.merge!({ verify_mode: OpenSSL::SSL::VERIFY_NONE }) if ENV['HEROKU_APP_ID'].present?
72
95
 
73
96
  ConnectionPool.new(size: pool_size, timeout: timeout) do
74
- Redis.new(url: redis_url, timeout: timeout)
97
+ Redis.new(url: url, timeout: timeout, ssl_params: ssl_params)
75
98
  end
76
99
  end
77
100
  end
@@ -55,6 +55,7 @@ Gem::Specification.new do |s|
55
55
  s.add_dependency "premailer-rails"
56
56
  s.add_dependency 'counter_culture', '~> 3.2'
57
57
  s.add_dependency 'paper_trail', '~> 16.0'
58
+ s.add_dependency 'devise-two-factor'
58
59
 
59
60
  # Redis
60
61
  s.add_dependency 'redis'