spree_cm_commissioner 2.9.0 → 2.9.1.pre.pre2
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/Gemfile.lock +1 -1
- data/app/controllers/spree/api/v2/storefront/waiting_room_sessions_controller.rb +8 -1
- data/app/controllers/spree/api/v2/tenant/user_account_linkages_controller.rb +2 -1
- data/app/controllers/spree/api/v2/tenant/voting_contestants_controller.rb +1 -2
- data/app/controllers/spree/api/v2/tenant/waiting_room_sessions_controller.rb +8 -1
- data/app/controllers/spree_cm_commissioner/admin/orders_controller_decorator.rb +9 -0
- data/app/interactors/spree_cm_commissioner/account_linkage.rb +13 -1
- data/app/interactors/spree_cm_commissioner/fetch_facebook_user_data_from_jwt.rb +90 -0
- data/app/interactors/spree_cm_commissioner/user_fb_jwt_authenticator.rb +37 -0
- data/app/interactors/spree_cm_commissioner/user_fb_jwt_checker.rb +20 -0
- data/app/interactors/spree_cm_commissioner/user_fb_token_authenticator.rb +1 -0
- data/app/interactors/spree_cm_commissioner/waiting_guests_caller.rb +11 -2
- data/app/interactors/spree_cm_commissioner/waiting_room_session_creator.rb +31 -6
- data/app/models/spree_cm_commissioner/order_decorator.rb +6 -0
- data/app/models/spree_cm_commissioner/voting_contestant.rb +6 -5
- data/app/models/spree_cm_commissioner/waiting_room_session.rb +14 -0
- data/app/overrides/spree/admin/shared/_order_summary/internal_note.html.erb.deface +23 -0
- data/app/overrides/spree/admin/shared/_order_summary/internal_note_modal.html.erb.deface +40 -0
- data/app/request_schemas/spree_cm_commissioner/user_account_linkage_request_schema.rb +4 -2
- data/app/serializers/spree/v2/tenant/show_episode_serializer.rb +1 -9
- data/app/services/spree_cm_commissioner/user_authenticator.rb +5 -0
- data/config/locales/en.yml +5 -0
- data/config/locales/km.yml +5 -0
- data/config/routes.rb +1 -0
- data/db/migrate/20260701000001_add_device_info_to_cm_waiting_room_sessions.rb +14 -0
- data/db/migrate/20260703065239_add_position_to_cm_voting_contestants.rb +6 -0
- data/lib/spree_cm_commissioner/version.rb +1 -1
- metadata +11 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 44f0714e8ec1fab5074038112f26db55308feffc75b103ccda9e9d5d5d38409a
|
|
4
|
+
data.tar.gz: 7f963001ae3a8de9f3f829d03eb83f6a2d310cf5a28ddafc477473e1b2fa9678
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2afbdbb418fbdca1c4ebfb9c6327a5fb0394663b87881e3ce56950c1aca7e4a1467708c6249ad2bab098564349d432187866966b6704e662ca87b34966466760
|
|
7
|
+
data.tar.gz: 569d3213e604558bf4d17134aa51fd8cb2e854780f141187a6bff0af167070fe4c53b5f9eda404f4c38c47659aa79fed702ad1f801dc61d9d62d9ceb775e4480
|
data/Gemfile.lock
CHANGED
|
@@ -14,7 +14,8 @@ module Spree
|
|
|
14
14
|
tenant_id: nil,
|
|
15
15
|
remote_ip: request.remote_ip,
|
|
16
16
|
waiting_guest_firebase_doc_id: params[:waiting_guest_firebase_doc_id],
|
|
17
|
-
page_path: params[:page_path]
|
|
17
|
+
page_path: params[:page_path],
|
|
18
|
+
device_attributes: permitted_device_attributes
|
|
18
19
|
)
|
|
19
20
|
|
|
20
21
|
if context.success?
|
|
@@ -27,6 +28,12 @@ module Spree
|
|
|
27
28
|
def resource_serializer
|
|
28
29
|
SpreeCmCommissioner::V2::Storefront::WaitingRoomSessionSerializer
|
|
29
30
|
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def permitted_device_attributes
|
|
35
|
+
params.permit(*SpreeCmCommissioner::WaitingRoomSessionCreator::PERMITTED_DEVICE_ATTRIBUTES)
|
|
36
|
+
end
|
|
30
37
|
end
|
|
31
38
|
end
|
|
32
39
|
end
|
|
@@ -15,7 +15,8 @@ module Spree
|
|
|
15
15
|
context = SpreeCmCommissioner::AccountLinkage.call(
|
|
16
16
|
user: spree_current_user,
|
|
17
17
|
id_token: params[:id_token],
|
|
18
|
-
fb_access_token: params[:fb_access_token]
|
|
18
|
+
fb_access_token: params[:fb_access_token],
|
|
19
|
+
fb_jwt: params[:fb_jwt]
|
|
19
20
|
)
|
|
20
21
|
|
|
21
22
|
if context.success?
|
|
@@ -10,7 +10,8 @@ module Spree
|
|
|
10
10
|
tenant_id: MultiTenant.current_tenant_id,
|
|
11
11
|
remote_ip: request.remote_ip,
|
|
12
12
|
waiting_guest_firebase_doc_id: params[:waiting_guest_firebase_doc_id],
|
|
13
|
-
page_path: params[:page_path]
|
|
13
|
+
page_path: params[:page_path],
|
|
14
|
+
device_attributes: permitted_device_attributes
|
|
14
15
|
)
|
|
15
16
|
|
|
16
17
|
if context.success?
|
|
@@ -23,6 +24,12 @@ module Spree
|
|
|
23
24
|
def resource_serializer
|
|
24
25
|
Spree::V2::Tenant::WaitingRoomSessionSerializer
|
|
25
26
|
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def permitted_device_attributes
|
|
31
|
+
params.permit(*SpreeCmCommissioner::WaitingRoomSessionCreator::PERMITTED_DEVICE_ATTRIBUTES)
|
|
32
|
+
end
|
|
26
33
|
end
|
|
27
34
|
end
|
|
28
35
|
end
|
|
@@ -10,6 +10,7 @@ module SpreeCmCommissioner
|
|
|
10
10
|
close_adjustments cart channel set_channel
|
|
11
11
|
accept_all reject_all alert_request_to_vendor
|
|
12
12
|
notifications fire_notification queue_webhooks_requests update_order_status
|
|
13
|
+
update_internal_note
|
|
13
14
|
]
|
|
14
15
|
|
|
15
16
|
base.before_action :initialize_notification_methods, only: %i[
|
|
@@ -81,6 +82,14 @@ module SpreeCmCommissioner
|
|
|
81
82
|
|
|
82
83
|
def notifications; end
|
|
83
84
|
|
|
85
|
+
def update_internal_note
|
|
86
|
+
internal_note = params.dig(:order, :internal_note).to_s.strip.presence
|
|
87
|
+
@order.update_columns(internal_note: internal_note, updated_at: Time.current) # rubocop:disable Rails/SkipsModelValidations
|
|
88
|
+
|
|
89
|
+
flash[:success] = Spree.t(:internal_note_updated, default: 'Internal note updated')
|
|
90
|
+
redirect_back fallback_location: spree.edit_admin_order_url(@order)
|
|
91
|
+
end
|
|
92
|
+
|
|
84
93
|
def update_order_status
|
|
85
94
|
if @order.next
|
|
86
95
|
flash[:success] = I18n.t('orders.update_order_status.success')
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module SpreeCmCommissioner
|
|
2
2
|
class AccountLinkage
|
|
3
3
|
include Interactor
|
|
4
|
-
delegate :id_token, :fb_access_token, :user, to: :context
|
|
4
|
+
delegate :id_token, :fb_access_token, :fb_jwt, :user, to: :context
|
|
5
5
|
|
|
6
6
|
# id_token:, user:
|
|
7
7
|
def call
|
|
@@ -10,6 +10,8 @@ module SpreeCmCommissioner
|
|
|
10
10
|
handle_id_token
|
|
11
11
|
elsif fb_access_token.present?
|
|
12
12
|
handle_fb_access_token
|
|
13
|
+
elsif fb_jwt.present?
|
|
14
|
+
handle_fb_jwt
|
|
13
15
|
else
|
|
14
16
|
context.fail!(message: 'missing_social_token')
|
|
15
17
|
end
|
|
@@ -37,6 +39,16 @@ module SpreeCmCommissioner
|
|
|
37
39
|
end
|
|
38
40
|
end
|
|
39
41
|
|
|
42
|
+
def handle_fb_jwt
|
|
43
|
+
facebook_context = FetchFacebookUserDataFromJwt.call(fb_jwt: fb_jwt)
|
|
44
|
+
|
|
45
|
+
if facebook_context.success?
|
|
46
|
+
build_user_identify_provider(facebook_context.provider)
|
|
47
|
+
else
|
|
48
|
+
context.fail!(message: facebook_context.message)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
40
52
|
# {
|
|
41
53
|
# provider_name: provider_name,
|
|
42
54
|
# email: email,
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
class FetchFacebookUserDataFromJwt < BaseInteractor
|
|
3
|
+
JWKS_URL = 'https://limited.facebook.com/.well-known/oauth/openid/jwks/'.freeze
|
|
4
|
+
EXPECTED_ISSUER = 'https://www.facebook.com'.freeze
|
|
5
|
+
|
|
6
|
+
delegate :fb_jwt, to: :context
|
|
7
|
+
|
|
8
|
+
def call
|
|
9
|
+
context.fail!(message: 'fb_jwt_missing') if fb_jwt.blank?
|
|
10
|
+
|
|
11
|
+
claim = decode_jwt
|
|
12
|
+
|
|
13
|
+
context.provider = {
|
|
14
|
+
identity_type: SpreeCmCommissioner::UserIdentityProvider.identity_types[:facebook],
|
|
15
|
+
sub: claim['sub'],
|
|
16
|
+
name: claim['name'],
|
|
17
|
+
email: nil, # Facebook Limited Login JWT does not include email
|
|
18
|
+
first_name: claim['given_name'],
|
|
19
|
+
last_name: claim['family_name'],
|
|
20
|
+
picture: claim['picture']
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def decode_jwt
|
|
27
|
+
result = JWT.decode(fb_jwt, nil, true, algorithms: ['RS256']) do |header|
|
|
28
|
+
kid = header['kid']
|
|
29
|
+
jwk_data = find_jwk(fetch_jwks, kid) || find_jwk(refresh_jwks, kid)
|
|
30
|
+
context.fail!(message: 'invalid_facebook_jwt_kid') unless jwk_data
|
|
31
|
+
public_key_from_jwk(jwk_data)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
claim = result[0]
|
|
35
|
+
context.fail!(message: 'invalid_facebook_issuer') if claim['iss'] != EXPECTED_ISSUER
|
|
36
|
+
claim
|
|
37
|
+
rescue JWT::ExpiredSignature
|
|
38
|
+
context.fail!(message: 'fb_jwt_expired')
|
|
39
|
+
rescue JWT::DecodeError => e
|
|
40
|
+
context.fail!(message: e.message)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def find_jwk(jwks, kid)
|
|
44
|
+
jwks['keys'].find { |k| k['kid'] == kid }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def public_key_from_jwk(jwk_data)
|
|
48
|
+
n = OpenSSL::BN.new(Base64.urlsafe_decode64(jwk_data['n']), 2)
|
|
49
|
+
e = OpenSSL::BN.new(Base64.urlsafe_decode64(jwk_data['e']), 2)
|
|
50
|
+
|
|
51
|
+
pkcs1 = OpenSSL::ASN1::Sequence([
|
|
52
|
+
OpenSSL::ASN1::Integer(n),
|
|
53
|
+
OpenSSL::ASN1::Integer(e)
|
|
54
|
+
]
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
spki = OpenSSL::ASN1::Sequence([
|
|
58
|
+
OpenSSL::ASN1::Sequence([
|
|
59
|
+
OpenSSL::ASN1::ObjectId('rsaEncryption'),
|
|
60
|
+
OpenSSL::ASN1::Null(nil)
|
|
61
|
+
]
|
|
62
|
+
),
|
|
63
|
+
OpenSSL::ASN1::BitString(pkcs1.to_der)
|
|
64
|
+
]
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
OpenSSL::PKey::RSA.new(spki.to_der)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def refresh_jwks
|
|
71
|
+
Rails.cache.delete('facebook-limited-jwks')
|
|
72
|
+
fetch_jwks
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def fetch_jwks
|
|
76
|
+
Rails.cache.fetch('facebook-limited-jwks', expires_in: 1.hour) do
|
|
77
|
+
url = URI(JWKS_URL)
|
|
78
|
+
http = Net::HTTP.new(url.host, url.port)
|
|
79
|
+
http.use_ssl = true
|
|
80
|
+
http.open_timeout = 5
|
|
81
|
+
http.read_timeout = 10
|
|
82
|
+
response = http.get(url.request_uri)
|
|
83
|
+
JSON.parse(response.body)
|
|
84
|
+
end
|
|
85
|
+
rescue StandardError => e
|
|
86
|
+
CmAppLogger.error("Facebook JWKS fetch error: #{e.message}")
|
|
87
|
+
context.fail!(message: 'facebook_jwks_fetch_error')
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
class UserFbJwtAuthenticator < BaseInteractor
|
|
3
|
+
delegate :fb_jwt, :tenant_id, to: :context
|
|
4
|
+
|
|
5
|
+
# :fb_jwt, :tenant_id (optional)
|
|
6
|
+
def call
|
|
7
|
+
context.fail!(message: 'fb_jwt_missing') unless fb_jwt
|
|
8
|
+
context.fail!(message: checker.message) if checker.failure?
|
|
9
|
+
|
|
10
|
+
context.user = if checker.user.nil?
|
|
11
|
+
register_user
|
|
12
|
+
else
|
|
13
|
+
checker.user
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
context.fail!(message: 'account_temporarily_deleted') if context.user.soft_deleted?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def checker
|
|
22
|
+
@checker ||= SpreeCmCommissioner::UserFbJwtChecker.call(
|
|
23
|
+
fb_jwt: fb_jwt,
|
|
24
|
+
tenant_id: tenant_id
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def register_user
|
|
29
|
+
register_context = SpreeCmCommissioner::UserRegistrationWithFbToken.call(
|
|
30
|
+
provider: checker.provider,
|
|
31
|
+
tenant_id: tenant_id
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
register_context.user
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
class UserFbJwtChecker < BaseInteractor
|
|
3
|
+
delegate :tenant_id, :fb_jwt, to: :context
|
|
4
|
+
|
|
5
|
+
def call
|
|
6
|
+
fb_data = SpreeCmCommissioner::FetchFacebookUserDataFromJwt.call(fb_jwt: fb_jwt)
|
|
7
|
+
context.fail!(message: fb_data.message) if fb_data.failure?
|
|
8
|
+
|
|
9
|
+
context.provider = fb_data.provider
|
|
10
|
+
|
|
11
|
+
identity_checker = SpreeCmCommissioner::UserIdentityChecker.call(
|
|
12
|
+
identity_type: context.provider[:identity_type],
|
|
13
|
+
sub: context.provider[:sub],
|
|
14
|
+
tenant_id: tenant_id
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
context.user = identity_checker.success? ? identity_checker.user : nil
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -5,6 +5,7 @@ module SpreeCmCommissioner
|
|
|
5
5
|
# :fb_access_token, :tenant_id (optional)
|
|
6
6
|
def call
|
|
7
7
|
context.fail!(message: 'fb_access_token_missing') unless fb_access_token
|
|
8
|
+
context.fail!(message: checker.message) if checker.failure?
|
|
8
9
|
|
|
9
10
|
context.user = if checker.user.nil?
|
|
10
11
|
register_user
|
|
@@ -12,14 +12,23 @@ module SpreeCmCommissioner
|
|
|
12
12
|
# Firestore bounds a batch update by payload size (10 MiB); 500 ops/commit leaves us far under that.
|
|
13
13
|
FIRESTORE_BATCH_SIZE = (ENV['WAITING_ROOM_FIRESTORE_BATCH_SIZE'] || 500).to_i
|
|
14
14
|
|
|
15
|
+
# Release at most this many guests per run, even when far more slots are free. A capacity spike
|
|
16
|
+
# (cold start / lull → active_sessions low → available_slots ≈ max_sessions, e.g. 900) would
|
|
17
|
+
# otherwise release every queued guest at once, dumping 900 simultaneous session-token requests
|
|
18
|
+
# on the server. Capping paces entry at ~MAX_CALLS_PER_RUN/min (the caller runs every minute);
|
|
19
|
+
# the rest stay queued and drain over the following runs. Matches the stamper's MAX_ETA_BATCH_SIZE
|
|
20
|
+
# so the per-batch ETA the app shows reflects the real release rate.
|
|
21
|
+
MAX_CALLS_PER_RUN = (ENV['WAITING_ROOM_MAX_CALLS_PER_RUN'] || 50).to_i
|
|
22
|
+
|
|
15
23
|
def call
|
|
16
24
|
started_at = Time.zone.now
|
|
17
25
|
available_slots = fetch_available_slots
|
|
26
|
+
call_limit = [available_slots, MAX_CALLS_PER_RUN].min
|
|
18
27
|
|
|
19
28
|
# Always run through to mark_as — even with no slots — so the lobby keeps a fresh heartbeat
|
|
20
29
|
# (logs.waiting_guests_caller_job) during peak/full periods instead of going stale
|
|
21
30
|
# and looking like a dead cron.
|
|
22
|
-
long_waiting_guests =
|
|
31
|
+
long_waiting_guests = call_limit.positive? ? fetch_long_waiting_guests(call_limit) : []
|
|
23
32
|
calling_all(long_waiting_guests) if long_waiting_guests.any?
|
|
24
33
|
|
|
25
34
|
mark_as(
|
|
@@ -28,7 +37,7 @@ module SpreeCmCommissioner
|
|
|
28
37
|
has_queue: waiting_exists?([previous_records_path, records_path]),
|
|
29
38
|
avg_wait_to_enter_seconds: compute_avg_wait_to_enter_seconds(long_waiting_guests),
|
|
30
39
|
avg_queue_to_enter_seconds: compute_avg_queue_to_enter_seconds(long_waiting_guests),
|
|
31
|
-
recent_slots_per_call:
|
|
40
|
+
recent_slots_per_call: call_limit.positive? ? call_limit : nil,
|
|
32
41
|
active_sessions: @active_sessions,
|
|
33
42
|
max_sessions: @max_sessions,
|
|
34
43
|
logs: call_logs(started_at: started_at, called_count: long_waiting_guests.size)
|
|
@@ -3,10 +3,21 @@ require 'zlib'
|
|
|
3
3
|
|
|
4
4
|
module SpreeCmCommissioner
|
|
5
5
|
class WaitingRoomSessionCreator < BaseInteractor
|
|
6
|
-
delegate :remote_ip, :waiting_guest_firebase_doc_id, :page_path, :tenant_id,
|
|
6
|
+
delegate :remote_ip, :waiting_guest_firebase_doc_id, :page_path, :tenant_id,
|
|
7
|
+
:device_attributes, to: :context
|
|
7
8
|
|
|
8
9
|
SESSION_CREATION_LOCK_KEY = 'waiting_room_session_creation'.freeze
|
|
9
10
|
|
|
11
|
+
# Device attributes the client may set on the session. device_id and
|
|
12
|
+
# device_fingerprint are real columns; the rest are virtual columns backed by
|
|
13
|
+
# public/private metadata (see WaitingRoomSession). Anything else the client
|
|
14
|
+
# sends is dropped so we never persist arbitrary payloads.
|
|
15
|
+
PERMITTED_DEVICE_ATTRIBUTES = %w[
|
|
16
|
+
device_id device_fingerprint
|
|
17
|
+
os_name os_version device_model device_manufacturer device_type
|
|
18
|
+
device_environment app_version app_build timezone network_type
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
10
21
|
def call
|
|
11
22
|
return context.fail!(message: 'must_provide_waiting_guest_firebase_doc_id') if waiting_guest_firebase_doc_id.blank?
|
|
12
23
|
return context.fail!(message: 'must_provide_remote_ip') if remote_ip.blank?
|
|
@@ -53,15 +64,29 @@ module SpreeCmCommissioner
|
|
|
53
64
|
# create or renew
|
|
54
65
|
context.room_session = SpreeCmCommissioner::WaitingRoomSession.where(guest_identifier: waiting_guest_firebase_doc_id).first_or_initialize
|
|
55
66
|
context.room_session.assign_attributes(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
67
|
+
{
|
|
68
|
+
jwt_token: context.jwt_token,
|
|
69
|
+
expired_at: expired_at,
|
|
70
|
+
remote_ip: remote_ip,
|
|
71
|
+
page_path: page_path,
|
|
72
|
+
tenant_id: tenant_id
|
|
73
|
+
}.merge(sanitized_device_attributes)
|
|
61
74
|
)
|
|
75
|
+
|
|
62
76
|
context.room_session.save!
|
|
63
77
|
end
|
|
64
78
|
|
|
79
|
+
# Whitelisted, flat device attributes. Real columns (device_id,
|
|
80
|
+
# device_fingerprint) and virtual columns (StoreMetadata) are assigned the
|
|
81
|
+
# same way; the model routes each to the right store.
|
|
82
|
+
def sanitized_device_attributes
|
|
83
|
+
attributes = device_attributes
|
|
84
|
+
attributes = attributes.to_unsafe_h if attributes.respond_to?(:to_unsafe_h)
|
|
85
|
+
return {} unless attributes.is_a?(Hash)
|
|
86
|
+
|
|
87
|
+
attributes.symbolize_keys.slice(*PERMITTED_DEVICE_ATTRIBUTES.map(&:to_sym))
|
|
88
|
+
end
|
|
89
|
+
|
|
65
90
|
def log_to_firebase
|
|
66
91
|
SpreeCmCommissioner::WaitingRoomSessionFirebaseLoggerJob.perform_later(
|
|
67
92
|
room_session_id: context.room_session.id,
|
|
@@ -71,6 +71,12 @@ module SpreeCmCommissioner
|
|
|
71
71
|
end
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
+
# override
|
|
75
|
+
def after_resume
|
|
76
|
+
super
|
|
77
|
+
update_columns(canceled_at: nil, canceler_id: nil) # rubocop:disable Rails/SkipsModelValidations
|
|
78
|
+
end
|
|
79
|
+
|
|
74
80
|
# override to allow storing cancellation reason in one DB write
|
|
75
81
|
def canceled_by(user, cancellation_reason: nil)
|
|
76
82
|
transaction do
|
|
@@ -23,19 +23,20 @@ module SpreeCmCommissioner
|
|
|
23
23
|
before_destroy :prevent_if_has_advanced_to
|
|
24
24
|
before_destroy :prevent_if_pre_vote_session
|
|
25
25
|
|
|
26
|
+
acts_as_list scope: :voting_session_id
|
|
27
|
+
|
|
26
28
|
validates :eliminated_via, presence: true, if: :eliminated?
|
|
27
29
|
|
|
28
|
-
# Uniqueness validation: vote_number must be unique
|
|
29
|
-
# Scope:
|
|
30
|
+
# Uniqueness validation: vote_number must be unique among contestants within the same voting_session
|
|
31
|
+
# Scope: voting_session_id
|
|
30
32
|
#
|
|
31
33
|
# | contestant_id | session_id | vote_number | Status | Reason |
|
|
32
34
|
# |---------------|-----------|-------------|--------|---------------------------|
|
|
33
35
|
# | 1 | 1 | 1 | ✓ OK | First record |
|
|
34
|
-
# |
|
|
36
|
+
# | 2 | 1 | 1 | ✗ FAIL | Duplicate in same session |
|
|
35
37
|
# | 1 | 1 | 2 | ✓ OK | Can update/change number |
|
|
36
|
-
# | 2 | 1 | 1 | ✓ OK | Different contestant |
|
|
37
38
|
# | 1 | 2 | 1 | ✓ OK | Different session |
|
|
38
|
-
validates :vote_number, uniqueness: { scope:
|
|
39
|
+
validates :vote_number, uniqueness: { scope: :voting_session_id, allow_nil: true }
|
|
39
40
|
|
|
40
41
|
delegate :name, :contestant_number, :category, :gender, :eliminated_at, to: :show_contestant, allow_nil: true
|
|
41
42
|
delegate :can_vote?, to: :voting_session, allow_nil: true
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
module SpreeCmCommissioner
|
|
2
2
|
class WaitingRoomSession < Base
|
|
3
|
+
include SpreeCmCommissioner::StoreMetadata
|
|
4
|
+
|
|
5
|
+
store_public_metadata :os_name, :string # e.g. "ios", "android", "web"
|
|
6
|
+
store_public_metadata :os_version, :string # e.g. "17.4", "14"
|
|
7
|
+
store_public_metadata :device_model, :string # e.g. "iPhone15,2", "Pixel 8"
|
|
8
|
+
store_public_metadata :device_manufacturer, :string # e.g. "Apple", "Google", "Samsung"
|
|
9
|
+
store_public_metadata :device_type, :string # e.g. "phone", "tablet", "web", "desktop"
|
|
10
|
+
store_public_metadata :device_environment, :string # e.g. "physical", "emulator", "simulator"
|
|
11
|
+
store_public_metadata :app_version, :string # e.g. "2.3.1"
|
|
12
|
+
store_public_metadata :app_build, :string # e.g. "450"
|
|
13
|
+
|
|
14
|
+
store_private_metadata :timezone, :string # e.g. "ICT", "+07", "UTC"
|
|
15
|
+
store_private_metadata :network_type, :string # e.g. "wifi", "cellular", "ethernet", "none"
|
|
16
|
+
|
|
3
17
|
validates :jwt_token, presence: true
|
|
4
18
|
validates :expired_at, presence: true
|
|
5
19
|
validates :remote_ip, presence: true
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<!-- insert_after "li:has(#order_channel)" -->
|
|
2
|
+
|
|
3
|
+
<li class="list-group-item d-flex justify-content-between align-items-center" data-hook="admin_order_tab_internal_note">
|
|
4
|
+
<small><%= Spree.t(:internal_note, default: 'Internal Note') %></small>
|
|
5
|
+
<span class="d-flex align-items-center" style="gap: .35rem; min-width: 0;">
|
|
6
|
+
<% if @order.internal_note.present? %>
|
|
7
|
+
<span class="small text-muted text-truncate text-right" style="max-width: 150px;" title="<%= @order.internal_note %>"><%= @order.internal_note %></span>
|
|
8
|
+
<% if can?(:update, @order) %>
|
|
9
|
+
<%= link_to '#', class: 'btn btn-light btn-sm with-tip icon-link p-1',
|
|
10
|
+
title: Spree.t(:edit),
|
|
11
|
+
data: { toggle: 'modal', target: "#internal-note-modal-#{@order.number}" } do %>
|
|
12
|
+
<%= svg_icon name: 'pencil-square.svg', width: '13', height: '13' %>
|
|
13
|
+
<% end %>
|
|
14
|
+
<% end %>
|
|
15
|
+
<% elsif can?(:update, @order) %>
|
|
16
|
+
<%= link_to Spree.t(:add_internal_note, default: 'Add note'), '#',
|
|
17
|
+
class: 'small',
|
|
18
|
+
data: { toggle: 'modal', target: "#internal-note-modal-#{@order.number}" } %>
|
|
19
|
+
<% else %>
|
|
20
|
+
<span class="small text-muted font-italic"><%= Spree.t(:no_internal_note, default: 'No note') %></span>
|
|
21
|
+
<% end %>
|
|
22
|
+
</span>
|
|
23
|
+
</li>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<!-- insert_after "#order_tab_summary" -->
|
|
2
|
+
|
|
3
|
+
<% if can?(:update, @order) %>
|
|
4
|
+
<div id="internal-note-modal-<%= @order.number %>" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="internalNoteModalLabel-<%= @order.number %>" aria-hidden="true">
|
|
5
|
+
<div class="modal-dialog" role="document">
|
|
6
|
+
<div class="modal-content">
|
|
7
|
+
<div class="modal-header">
|
|
8
|
+
<h5 class="modal-title mr-auto" id="internalNoteModalLabel-<%= @order.number %>"><%= Spree.t(:internal_note, default: 'Internal Note') %></h5>
|
|
9
|
+
<button type="button" class="close" data-dismiss="modal" aria-label="<%= Spree.t(:close) %>">
|
|
10
|
+
<span aria-hidden="true">×</span>
|
|
11
|
+
</button>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<%= form_with model: @order, url: update_internal_note_admin_order_path(@order), method: :patch, local: true do |f| %>
|
|
15
|
+
<div class="modal-body">
|
|
16
|
+
<div class="form-group text-left text-start">
|
|
17
|
+
<%= f.text_area :internal_note, rows: 5, class: 'form-control' %>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="modal-footer d-flex justify-end">
|
|
21
|
+
<button type="button" class="btn btn-light" data-dismiss="modal"><%= Spree.t(:close) %></button>
|
|
22
|
+
<%= f.submit Spree.t(:save_internal_note, default: 'Save Note'), class: 'btn btn-primary' %>
|
|
23
|
+
</div>
|
|
24
|
+
<% end %>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<%# The summary renders inside #contextualSideMenu (position: sticky/fixed), whose %>
|
|
30
|
+
<%# stacking context sits below Bootstrap's body-level .modal-backdrop. Relocate the %>
|
|
31
|
+
<%# modal to <body> so it layers above the backdrop and stays interactive. %>
|
|
32
|
+
<script>
|
|
33
|
+
(function () {
|
|
34
|
+
var modal = document.getElementById('internal-note-modal-<%= @order.number %>');
|
|
35
|
+
if (modal && modal.parentNode !== document.body) {
|
|
36
|
+
document.body.appendChild(modal);
|
|
37
|
+
}
|
|
38
|
+
})();
|
|
39
|
+
</script>
|
|
40
|
+
<% end %>
|
|
@@ -3,13 +3,15 @@ module SpreeCmCommissioner
|
|
|
3
3
|
params do
|
|
4
4
|
optional(:id_token).maybe(:string)
|
|
5
5
|
optional(:fb_access_token).maybe(:string)
|
|
6
|
+
optional(:fb_jwt).maybe(:string)
|
|
6
7
|
end
|
|
7
8
|
|
|
8
|
-
rule(:id_token, :fb_access_token) do
|
|
9
|
+
rule(:id_token, :fb_access_token, :fb_jwt) do
|
|
9
10
|
idt = values[:id_token]
|
|
10
11
|
fbt = values[:fb_access_token]
|
|
12
|
+
fbj = values[:fb_jwt]
|
|
11
13
|
|
|
12
|
-
key(:base).failure('id_token_or_fb_access_token_required') if blank_or_empty?(idt) && blank_or_empty?(fbt)
|
|
14
|
+
key(:base).failure('id_token_or_fb_access_token_required') if blank_or_empty?(idt) && blank_or_empty?(fbt) && blank_or_empty?(fbj)
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
private
|
|
@@ -2,15 +2,7 @@ module Spree
|
|
|
2
2
|
module V2
|
|
3
3
|
module Tenant
|
|
4
4
|
class ShowEpisodeSerializer < BaseSerializer
|
|
5
|
-
attributes :episode_number, :name, :scheduled_at, :ends_at
|
|
6
|
-
|
|
7
|
-
attribute :status do |episode|
|
|
8
|
-
if episode.current?
|
|
9
|
-
'current'
|
|
10
|
-
elsif episode.upcoming?
|
|
11
|
-
'upcoming'
|
|
12
|
-
end
|
|
13
|
-
end
|
|
5
|
+
attributes :episode_number, :name, :scheduled_at, :ends_at, :status, :preview
|
|
14
6
|
|
|
15
7
|
has_many :voting_sessions, serializer: Spree::V2::Tenant::VotingSessionSerializer
|
|
16
8
|
has_many :vote_packages, serializer: Spree::V2::Tenant::VotePackageSerializer
|
|
@@ -4,6 +4,7 @@ module SpreeCmCommissioner
|
|
|
4
4
|
# :username, :password
|
|
5
5
|
# :id_token
|
|
6
6
|
# :fb_access_token
|
|
7
|
+
# :fb_jwt
|
|
7
8
|
# :telegram_init_data, :tg_bot
|
|
8
9
|
# :session_id
|
|
9
10
|
|
|
@@ -42,6 +43,9 @@ module SpreeCmCommissioner
|
|
|
42
43
|
when 'facebook_auth'
|
|
43
44
|
options = { fb_access_token: params[:fb_access_token], tenant_id: tenant_id }
|
|
44
45
|
SpreeCmCommissioner::UserFbTokenAuthenticator.call(options)
|
|
46
|
+
when 'facebook_jwt_auth'
|
|
47
|
+
options = { fb_jwt: params[:fb_jwt], tenant_id: tenant_id }
|
|
48
|
+
SpreeCmCommissioner::UserFbJwtAuthenticator.call(options)
|
|
45
49
|
when 'telegram_web_app_auth'
|
|
46
50
|
options = { telegram_init_data: params[:telegram_init_data], telegram_bot_username: params[:tg_bot] }
|
|
47
51
|
SpreeCmCommissioner::UserTelegramWebAppAuthenticator.call(options)
|
|
@@ -55,6 +59,7 @@ module SpreeCmCommissioner
|
|
|
55
59
|
return 'login_auth' if params.key?(:username) && params.key?(:password)
|
|
56
60
|
return 'social_auth' if params.key?(:id_token)
|
|
57
61
|
return 'facebook_auth' if params.key?(:fb_access_token)
|
|
62
|
+
return 'facebook_jwt_auth' if params.key?(:fb_jwt)
|
|
58
63
|
return 'telegram_web_app_auth' if params.key?(:telegram_init_data) && params.key?(:tg_bot)
|
|
59
64
|
return 'vattanac_bank_web_app_auth' if params.key?(:session_id)
|
|
60
65
|
|
data/config/locales/en.yml
CHANGED
|
@@ -216,6 +216,11 @@ en:
|
|
|
216
216
|
riel: "៛"
|
|
217
217
|
|
|
218
218
|
spree:
|
|
219
|
+
internal_note: "Internal Note"
|
|
220
|
+
add_internal_note: "Add note"
|
|
221
|
+
no_internal_note: "No note"
|
|
222
|
+
save_internal_note: "Save Note"
|
|
223
|
+
internal_note_updated: "Internal note updated"
|
|
219
224
|
voting_contestant_advancer:
|
|
220
225
|
reversion_error: "Cannot eliminate here yet. This contestant appears in a later session."
|
|
221
226
|
is_open_dated: "Is Open Dated Trip?"
|
data/config/locales/km.yml
CHANGED
|
@@ -200,6 +200,11 @@ km:
|
|
|
200
200
|
riel: "៛"
|
|
201
201
|
|
|
202
202
|
spree:
|
|
203
|
+
internal_note: "ចំណាំផ្ទៃក្នុង"
|
|
204
|
+
add_internal_note: "បន្ថែមចំណាំ"
|
|
205
|
+
no_internal_note: "គ្មានចំណាំ"
|
|
206
|
+
save_internal_note: "រក្សាទុកចំណាំ"
|
|
207
|
+
internal_note_updated: "បានរក្សាទុកចំណាំផ្ទៃក្នុង"
|
|
203
208
|
voting_contestant_advancer:
|
|
204
209
|
reversion_error: "មិនអាចដកចេញនៅវគ្គនេះបានទេ។ បេក្ខជននេះមាននៅវគ្គបោះឆ្នោតក្រោយ។"
|
|
205
210
|
registered_by:
|
data/config/routes.rb
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class AddDeviceInfoToCmWaitingRoomSessions < ActiveRecord::Migration[7.0]
|
|
2
|
+
def change
|
|
3
|
+
# Indexed identifiers used for dedup / abuse detection.
|
|
4
|
+
add_column :cm_waiting_room_sessions, :device_id, :string, if_not_exists: true
|
|
5
|
+
add_column :cm_waiting_room_sessions, :device_fingerprint, :string, if_not_exists: true
|
|
6
|
+
|
|
7
|
+
# Typed device attributes live here via SpreeCmCommissioner::StoreMetadata.
|
|
8
|
+
add_column :cm_waiting_room_sessions, :public_metadata, :jsonb, default: {}, if_not_exists: true
|
|
9
|
+
add_column :cm_waiting_room_sessions, :private_metadata, :jsonb, default: {}, if_not_exists: true
|
|
10
|
+
|
|
11
|
+
add_index :cm_waiting_room_sessions, :device_id, if_not_exists: true
|
|
12
|
+
add_index :cm_waiting_room_sessions, :device_fingerprint, if_not_exists: true
|
|
13
|
+
end
|
|
14
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spree_cm_commissioner
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.9.
|
|
4
|
+
version: 2.9.1.pre.pre2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- You
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: spree
|
|
@@ -1243,6 +1243,7 @@ files:
|
|
|
1243
1243
|
- app/interactors/spree_cm_commissioner/event_qr_generator.rb
|
|
1244
1244
|
- app/interactors/spree_cm_commissioner/existing_account_checker.rb
|
|
1245
1245
|
- app/interactors/spree_cm_commissioner/fetch_facebook_user_data.rb
|
|
1246
|
+
- app/interactors/spree_cm_commissioner/fetch_facebook_user_data_from_jwt.rb
|
|
1246
1247
|
- app/interactors/spree_cm_commissioner/firebase_email_fetcher.rb
|
|
1247
1248
|
- app/interactors/spree_cm_commissioner/firebase_email_fetcher_cron_executor.rb
|
|
1248
1249
|
- app/interactors/spree_cm_commissioner/firebase_id_token_provider.rb
|
|
@@ -1319,6 +1320,8 @@ files:
|
|
|
1319
1320
|
- app/interactors/spree_cm_commissioner/user_contact_updater.rb
|
|
1320
1321
|
- app/interactors/spree_cm_commissioner/user_device_token_deregister.rb
|
|
1321
1322
|
- app/interactors/spree_cm_commissioner/user_device_token_register.rb
|
|
1323
|
+
- app/interactors/spree_cm_commissioner/user_fb_jwt_authenticator.rb
|
|
1324
|
+
- app/interactors/spree_cm_commissioner/user_fb_jwt_checker.rb
|
|
1322
1325
|
- app/interactors/spree_cm_commissioner/user_fb_token_authenticator.rb
|
|
1323
1326
|
- app/interactors/spree_cm_commissioner/user_fb_token_checker.rb
|
|
1324
1327
|
- app/interactors/spree_cm_commissioner/user_forgotten_password_updater.rb
|
|
@@ -1828,6 +1831,8 @@ files:
|
|
|
1828
1831
|
- app/overrides/spree/admin/shared/_main_menu/sidebar_customer_notification.html.erb.deface
|
|
1829
1832
|
- app/overrides/spree/admin/shared/_main_menu/sidebar_homepage.html.erb.deface
|
|
1830
1833
|
- app/overrides/spree/admin/shared/_order_summary/adjustment_total.html.erb.deface
|
|
1834
|
+
- app/overrides/spree/admin/shared/_order_summary/internal_note.html.erb.deface
|
|
1835
|
+
- app/overrides/spree/admin/shared/_order_summary/internal_note_modal.html.erb.deface
|
|
1831
1836
|
- app/overrides/spree/admin/shared/_order_summary/order_status.html.erb.deface
|
|
1832
1837
|
- app/overrides/spree/admin/shared/_order_summary/request_state.html.erb.deface
|
|
1833
1838
|
- app/overrides/spree/admin/shared/_order_tabs/guests.html.erb.deface
|
|
@@ -3407,6 +3412,8 @@ files:
|
|
|
3407
3412
|
- db/migrate/20260623082233_create_cm_claims.rb
|
|
3408
3413
|
- db/migrate/20260624175658_add_unique_index_to_cm_claims.rb
|
|
3409
3414
|
- db/migrate/20260629000001_add_term_of_use_to_spree_vendors.rb
|
|
3415
|
+
- db/migrate/20260701000001_add_device_info_to_cm_waiting_room_sessions.rb
|
|
3416
|
+
- db/migrate/20260703065239_add_position_to_cm_voting_contestants.rb
|
|
3410
3417
|
- docker-compose.yml
|
|
3411
3418
|
- docs/api/scoped-access-token-endpoints.md
|
|
3412
3419
|
- docs/option_types/attr_types.md
|
|
@@ -3619,9 +3626,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
3619
3626
|
version: '2.7'
|
|
3620
3627
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
3621
3628
|
requirements:
|
|
3622
|
-
- - "
|
|
3629
|
+
- - ">"
|
|
3623
3630
|
- !ruby/object:Gem::Version
|
|
3624
|
-
version:
|
|
3631
|
+
version: 1.3.1
|
|
3625
3632
|
requirements:
|
|
3626
3633
|
- none
|
|
3627
3634
|
rubygems_version: 3.4.1
|