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.
- checksums.yaml +4 -4
- data/.env.example +75 -29
- data/Gemfile.lock +8 -1
- data/app/controllers/spree/admin/user_security_controller.rb +25 -0
- data/app/controllers/spree/api/v2/storefront/access_tokens_controller.rb +10 -0
- data/app/controllers/spree/api/v2/storefront/account/guest_dynamic_fields_controller.rb +82 -0
- data/app/controllers/spree/api/v2/storefront/account/mark_guest_info_complete_controller.rb +39 -0
- data/app/controllers/spree/api/v2/storefront/account/orders_controller_decorator.rb +34 -5
- data/app/controllers/spree/api/v2/storefront/seat_layouts_controller.rb +1 -1
- data/app/controllers/spree/api/v2/tenant/access_tokens_controller.rb +10 -0
- data/app/interactors/spree_cm_commissioner/stock/inventory_item_resetter.rb +1 -1
- data/app/models/concerns/spree_cm_commissioner/event_metadata.rb +39 -0
- data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +27 -21
- data/app/models/concerns/spree_cm_commissioner/store_metadata.rb +97 -0
- data/app/models/spree_cm_commissioner/guest.rb +38 -4
- data/app/models/spree_cm_commissioner/guest_dynamic_field.rb +1 -0
- data/app/models/spree_cm_commissioner/inventory_item.rb +1 -1
- data/app/models/spree_cm_commissioner/invite_team.rb +8 -0
- data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +2 -2
- data/app/models/spree_cm_commissioner/redis_stock/inventory_updater.rb +2 -2
- data/app/models/spree_cm_commissioner/seat_layout.rb +3 -3
- data/app/models/spree_cm_commissioner/taxon_decorator.rb +2 -20
- data/app/models/spree_cm_commissioner/trip.rb +5 -7
- data/app/models/spree_cm_commissioner/user_decorator.rb +16 -0
- data/app/models/spree_cm_commissioner/vehicle.rb +5 -7
- data/app/overrides/spree/admin/taxons/_form/background_color_and_foreground_color.html.erb.deface +6 -6
- data/app/overrides/spree/admin/taxons/_form/check_in_flows.html.erb.deface +14 -8
- data/app/overrides/spree/admin/users/_tabs/tabs.html.erb.deface +6 -0
- data/app/serializers/spree/v2/storefront/order_serializer_decorator.rb +63 -0
- data/app/serializers/spree/v2/storefront/taxon_serializer_decorator.rb +1 -1
- data/app/serializers/spree_cm_commissioner/v2/operator/dashboard_crew_event_serializer.rb +1 -0
- data/app/views/spree/admin/user_security/show.html.erb +25 -0
- data/config/initializers/spree_permitted_attributes.rb +5 -4
- data/config/locales/en.yml +1 -0
- data/config/routes.rb +11 -0
- data/docs/api/scoped-access-token-endpoints.md +84 -0
- data/lib/spree_cm_commissioner/version.rb +1 -1
- data/lib/spree_cm_commissioner.rb +35 -12
- data/spree_cm_commissioner.gemspec +1 -0
- metadata +28 -5
- 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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!(
|
|
34
|
+
layoutable.update!(preload_seat_layout_id: nil)
|
|
35
35
|
else
|
|
36
|
-
layoutable.update!(
|
|
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::
|
|
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
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
|
|
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
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
|
|
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
|
|
data/app/overrides/spree/admin/taxons/_form/background_color_and_foreground_color.html.erb.deface
CHANGED
|
@@ -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 :
|
|
8
|
-
<%= f.text_field :
|
|
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.
|
|
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 :
|
|
15
|
-
<%= f.text_field :
|
|
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.
|
|
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 :
|
|
6
|
-
<%= f.check_box :
|
|
7
|
-
<%= f.label :
|
|
8
|
-
<%= f.error_message_on :
|
|
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 :
|
|
15
|
-
<%= f.check_box :
|
|
16
|
-
<%= f.label :
|
|
17
|
-
<%= f.error_message_on :
|
|
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.
|
|
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'
|
|
@@ -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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
data/config/locales/en.yml
CHANGED
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
|
+
```
|
|
@@ -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
|
|
55
|
-
attr_writer :
|
|
55
|
+
# Allows overriding the default Redis connection pools with custom ones
|
|
56
|
+
attr_writer :inventory_redis_pool, :external_integrations_redis_pool
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
#
|
|
62
|
-
def
|
|
63
|
-
@
|
|
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
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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:
|
|
97
|
+
Redis.new(url: url, timeout: timeout, ssl_params: ssl_params)
|
|
75
98
|
end
|
|
76
99
|
end
|
|
77
100
|
end
|