spree_cm_commissioner 2.13.3 → 2.13.4
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 +0 -1
- data/Gemfile.lock +1 -1
- data/app/assets/config/spree_cm_commissioner_manifest.js +0 -1
- data/app/assets/javascripts/spree_cm_commissioner/backend.js +0 -8
- data/app/assets/stylesheets/spree_cm_commissioner/backend/commissioner_admin.css.scss +0 -1
- data/app/controllers/concerns/spree_cm_commissioner/content_cachable.rb +10 -8
- data/app/controllers/spree/admin/base_controller_decorator.rb +0 -12
- data/app/controllers/spree/admin/oauth_application_api_keys_controller.rb +118 -0
- data/app/controllers/spree/admin/oauth_applications_controller.rb +10 -0
- data/app/controllers/spree/api/v2/storefront/dynamic_field_options_controller.rb +12 -0
- data/app/controllers/spree/api/v2/storefront/trip_search_controller.rb +0 -5
- data/app/controllers/spree/api/v2/tenant/trip_search_controller.rb +0 -5
- data/app/helpers/spree/{billing → admin}/navigation_helper_decorator.rb +2 -9
- data/app/interactors/spree_cm_commissioner/user_acleda_bank_web_app_authenticator.rb +38 -0
- data/app/interactors/spree_cm_commissioner/user_wing_bank_web_app_authenticator.rb +39 -0
- data/app/models/concerns/spree_cm_commissioner/oauth_application/mini_app.rb +72 -0
- data/app/models/spree_cm_commissioner/dynamic_field.rb +12 -1
- data/app/models/spree_cm_commissioner/dynamic_field_option.rb +11 -0
- data/app/models/spree_cm_commissioner/guest.rb +8 -0
- data/app/models/spree_cm_commissioner/mini_app_provider/acleda_bank.rb +28 -0
- data/app/models/spree_cm_commissioner/mini_app_provider/base.rb +219 -0
- data/app/models/spree_cm_commissioner/mini_app_provider/vattanac_bank.rb +6 -0
- data/app/models/spree_cm_commissioner/mini_app_provider/wing_bank.rb +30 -0
- data/app/models/spree_cm_commissioner/oauth_application_decorator.rb +7 -0
- data/app/models/spree_cm_commissioner/product_dynamic_field.rb +9 -0
- data/app/models/spree_cm_commissioner/user_identity_provider.rb +1 -1
- data/app/services/spree_cm_commissioner/mini_app_provider/api_key_history.rb +128 -0
- data/app/services/spree_cm_commissioner/mini_app_provider/ip_rules.rb +26 -0
- data/app/services/spree_cm_commissioner/mini_app_provider/session_gate.rb +79 -0
- data/app/services/spree_cm_commissioner/mini_app_provider/session_throttle.rb +66 -0
- data/app/services/spree_cm_commissioner/payment_method_type_mapper.rb +3 -1
- data/app/services/spree_cm_commissioner/rate_limiter/result.rb +20 -0
- data/app/services/spree_cm_commissioner/rate_limiter.rb +62 -0
- data/app/services/spree_cm_commissioner/seeds/user_usernames.rb +3 -1
- data/app/services/spree_cm_commissioner/ticket_transfers/eligibility_checker.rb +5 -6
- data/app/services/spree_cm_commissioner/ticket_transfers/preview_rule.rb +3 -5
- data/app/services/spree_cm_commissioner/user_authenticator.rb +12 -2
- data/app/services/spree_cm_commissioner/wing_mini_app/config.rb +44 -0
- data/app/services/spree_cm_commissioner/wing_mini_app/find_or_create_user.rb +89 -0
- data/app/services/spree_cm_commissioner/wing_mini_app/profile_verifier.rb +65 -0
- data/app/views/spree/admin/guests/_table.html.erb +4 -0
- data/app/views/spree/admin/oauth_applications/_api_key_minted_modal.html.erb +71 -0
- data/app/views/spree/admin/oauth_applications/_api_key_modal.html.erb +68 -0
- data/app/views/spree/admin/oauth_applications/_api_key_restore_list.html.erb +40 -0
- data/app/views/spree/admin/oauth_applications/_api_key_restore_modal.html.erb +72 -0
- data/app/views/spree/admin/oauth_applications/_api_key_revoke_modal.html.erb +54 -0
- data/app/views/spree/admin/oauth_applications/_form.html.erb +152 -0
- data/app/views/spree/admin/oauth_applications/edit.html.erb +36 -0
- data/app/views/spree/admin/shared/_ticket_transfer_details.html.erb +8 -2
- data/config/locales/en.yml +12 -0
- data/config/routes.rb +15 -113
- data/db/migrate/20260928070109_add_provider_and_api_key_to_spree_oauth_applications.rb +11 -0
- data/lib/spree_cm_commissioner/test_helper/factories/vendor_factory.rb +16 -2
- data/lib/spree_cm_commissioner/version.rb +1 -1
- data/lib/spree_cm_commissioner.rb +8 -1
- metadata +28 -229
- data/app/assets/javascripts/spree_cm_commissioner/address_hide_form.js +0 -17
- data/app/assets/javascripts/spree_cm_commissioner/location_checkbox_manager.js +0 -45
- data/app/assets/javascripts/spree_cm_commissioner/report_hide_filter.js +0 -37
- data/app/assets/javascripts/spree_cm_commissioner/role_form.js +0 -51
- data/app/assets/javascripts/spree_cm_commissioner/selected_field_filter_origin_destination.js +0 -37
- data/app/assets/javascripts/spree_cm_commissioner/trip_stop_selection.js +0 -41
- data/app/assets/javascripts/spree_cm_commissioner/update_popup_confirmation.js +0 -42
- data/app/assets/javascripts/spree_cm_commissioner/vehicle_seat.js +0 -291
- data/app/assets/stylesheets/spree_cm_commissioner/backend/invoice.scss +0 -92
- data/app/assets/stylesheets/spree_cm_commissioner/backend/vehicle_seat.scss +0 -28
- data/app/controllers/concerns/spree/billing/order_parents_concern.rb +0 -31
- data/app/controllers/concerns/spree/billing/payment_creatable.rb +0 -64
- data/app/controllers/concerns/spree/billing/payment_fireable.rb +0 -35
- data/app/controllers/concerns/spree_cm_commissioner/billing/role_authorization.rb +0 -53
- data/app/controllers/spree/billing/addresses_controller.rb +0 -32
- data/app/controllers/spree/billing/adjustments_controller.rb +0 -72
- data/app/controllers/spree/billing/base_controller.rb +0 -82
- data/app/controllers/spree/billing/businesses_controller.rb +0 -135
- data/app/controllers/spree/billing/customers_controller.rb +0 -107
- data/app/controllers/spree/billing/errors_controller.rb +0 -14
- data/app/controllers/spree/billing/invoice_controller.rb +0 -36
- data/app/controllers/spree/billing/option_types_controller.rb +0 -32
- data/app/controllers/spree/billing/orders_controller.rb +0 -90
- data/app/controllers/spree/billing/payment_qrcodes_controller.rb +0 -14
- data/app/controllers/spree/billing/payments_controller.rb +0 -148
- data/app/controllers/spree/billing/places_controller.rb +0 -35
- data/app/controllers/spree/billing/products_controller.rb +0 -56
- data/app/controllers/spree/billing/refunds_controller.rb +0 -9
- data/app/controllers/spree/billing/reports_controller.rb +0 -202
- data/app/controllers/spree/billing/roles_controller.rb +0 -107
- data/app/controllers/spree/billing/store_credits_controller.rb +0 -117
- data/app/controllers/spree/billing/subscriptions_controller.rb +0 -49
- data/app/controllers/spree/billing/users_controller.rb +0 -19
- data/app/controllers/spree/billing/variants_controller.rb +0 -36
- data/app/controllers/spree/billing/vendors_controller.rb +0 -83
- data/app/controllers/spree/transit/amenities_controller.rb +0 -79
- data/app/controllers/spree/transit/amenity_values_controller.rb +0 -16
- data/app/controllers/spree/transit/base_controller.rb +0 -53
- data/app/controllers/spree/transit/branches_controller.rb +0 -35
- data/app/controllers/spree/transit/locations_controller.rb +0 -38
- data/app/controllers/spree/transit/places_controller.rb +0 -151
- data/app/controllers/spree/transit/reservations_controller.rb +0 -15
- data/app/controllers/spree/transit/routes_controller.rb +0 -66
- data/app/controllers/spree/transit/service_calendars_controller.rb +0 -109
- data/app/controllers/spree/transit/stops_controller.rb +0 -32
- data/app/controllers/spree/transit/trip_stops_controller.rb +0 -35
- data/app/controllers/spree/transit/trips_controller.rb +0 -90
- data/app/controllers/spree/transit/vectors/amenity_values_controller.rb +0 -40
- data/app/controllers/spree/transit/vectors/icons_controller.rb +0 -26
- data/app/controllers/spree/transit/vehicle_photos_controller.rb +0 -52
- data/app/controllers/spree/transit/vehicle_seats_controller.rb +0 -29
- data/app/controllers/spree/transit/vehicle_types_controller.rb +0 -81
- data/app/controllers/spree/transit/vehicles_controller.rb +0 -54
- data/app/controllers/spree/transit/vendors_controller.rb +0 -83
- data/app/helpers/spree/billing/sortable_tree_helper.rb +0 -32
- data/app/helpers/spree/transit/navigation_helper_decorator.rb +0 -14
- data/app/helpers/spree/transit/sortable_tree_helper.rb +0 -32
- data/app/helpers/spree_cm_commissioner/billing/monthly_orders_helper.rb +0 -66
- data/app/helpers/spree_cm_commissioner/billing/qrcodes_helper.rb +0 -11
- data/app/helpers/spree_cm_commissioner/transit/service_calendars_helper.rb +0 -17
- data/app/views/spree/billing/addresses/_form.html.erb +0 -38
- data/app/views/spree/billing/addresses/index.html.erb +0 -19
- data/app/views/spree/billing/adjustments/_adjustment.html.erb +0 -34
- data/app/views/spree/billing/adjustments/_adjustments_table.html.erb +0 -32
- data/app/views/spree/billing/adjustments/_form.html.erb +0 -12
- data/app/views/spree/billing/adjustments/edit.html.erb +0 -13
- data/app/views/spree/billing/adjustments/index.html.erb +0 -32
- data/app/views/spree/billing/adjustments/new.html.erb +0 -9
- data/app/views/spree/billing/businesses/_assets_form.html.erb +0 -8
- data/app/views/spree/billing/businesses/_form.html.erb +0 -88
- data/app/views/spree/billing/businesses/edit.html.erb +0 -21
- data/app/views/spree/billing/businesses/index.html.erb +0 -32
- data/app/views/spree/billing/businesses/new.html.erb +0 -16
- data/app/views/spree/billing/customers/_form.html.erb +0 -44
- data/app/views/spree/billing/customers/edit.html.erb +0 -23
- data/app/views/spree/billing/customers/index.html.erb +0 -160
- data/app/views/spree/billing/customers/new.html.erb +0 -15
- data/app/views/spree/billing/errors/forbidden.html.erb +0 -7
- data/app/views/spree/billing/invoice/show.html.erb +0 -21
- data/app/views/spree/billing/option_types/_form.html.erb +0 -29
- data/app/views/spree/billing/option_types/_option_value_fields.html.erb +0 -13
- data/app/views/spree/billing/option_types/edit.html.erb +0 -56
- data/app/views/spree/billing/option_types/index.html.erb +0 -50
- data/app/views/spree/billing/option_types/new.html.erb +0 -13
- data/app/views/spree/billing/orders/_form.html.erb +0 -54
- data/app/views/spree/billing/orders/_line_item.html.erb +0 -57
- data/app/views/spree/billing/orders/_search.html.erb +0 -62
- data/app/views/spree/billing/orders/edit.html.erb +0 -16
- data/app/views/spree/billing/orders/index.html.erb +0 -84
- data/app/views/spree/billing/orders/show.html.erb +0 -1
- data/app/views/spree/billing/payments/_list.html.erb +0 -49
- data/app/views/spree/billing/payments/_state_changes.html.erb +0 -33
- data/app/views/spree/billing/payments/index.html.erb +0 -35
- data/app/views/spree/billing/payments/new.html.erb +0 -24
- data/app/views/spree/billing/payments/show.html.erb +0 -15
- data/app/views/spree/billing/places/_form.html.erb +0 -16
- data/app/views/spree/billing/places/edit.html.erb +0 -12
- data/app/views/spree/billing/places/index.html.erb +0 -72
- data/app/views/spree/billing/places/new.html.erb +0 -15
- data/app/views/spree/billing/products/_add_stock_form.html.erb +0 -40
- data/app/views/spree/billing/products/_filters.html.erb +0 -31
- data/app/views/spree/billing/products/_form.html.erb +0 -123
- data/app/views/spree/billing/products/edit.html.erb +0 -8
- data/app/views/spree/billing/products/index.html.erb +0 -72
- data/app/views/spree/billing/products/new.html.erb +0 -85
- data/app/views/spree/billing/products/stock.html.erb +0 -77
- data/app/views/spree/billing/reports/_date_range_picker.html.erb +0 -78
- data/app/views/spree/billing/reports/_overview.html.erb +0 -48
- data/app/views/spree/billing/reports/active_subscribers.html.erb +0 -97
- data/app/views/spree/billing/reports/balance_due.html.erb +0 -188
- data/app/views/spree/billing/reports/export.html.erb +0 -70
- data/app/views/spree/billing/reports/failed_orders.html.erb +0 -58
- data/app/views/spree/billing/reports/index.html.erb +0 -62
- data/app/views/spree/billing/reports/overdue.html.erb +0 -61
- data/app/views/spree/billing/reports/paid.html.erb +0 -60
- data/app/views/spree/billing/roles/_form.html.erb +0 -52
- data/app/views/spree/billing/roles/edit.html.erb +0 -13
- data/app/views/spree/billing/roles/index.html.erb +0 -44
- data/app/views/spree/billing/roles/new.html.erb +0 -13
- data/app/views/spree/billing/shared/_customer_credit_form.html.erb +0 -33
- data/app/views/spree/billing/shared/_customer_promotion_form.html.erb +0 -29
- data/app/views/spree/billing/shared/_customer_promotion_table.html.erb +0 -39
- data/app/views/spree/billing/shared/_customer_tabs.html.erb +0 -46
- data/app/views/spree/billing/shared/_header.html.erb +0 -21
- data/app/views/spree/billing/shared/_invoice.html.erb +0 -280
- data/app/views/spree/billing/shared/_language_switcher.html.erb +0 -5
- data/app/views/spree/billing/shared/_main_menu.html.erb +0 -73
- data/app/views/spree/billing/shared/_order_tabs.html.erb +0 -41
- data/app/views/spree/billing/shared/_pop_up_confirmation.html.erb +0 -103
- data/app/views/spree/billing/shared/_product_tabs.html.erb +0 -24
- data/app/views/spree/billing/shared/_role_tabs.html.erb +0 -13
- data/app/views/spree/billing/shared/_subscription_tabs.html.erb +0 -20
- data/app/views/spree/billing/shared/_user_tabs.html.erb +0 -13
- data/app/views/spree/billing/shared/_vendor_logo.html.erb +0 -1
- data/app/views/spree/billing/shared/_vendor_switcher.html.erb +0 -57
- data/app/views/spree/billing/shared/sortable_tree/_taxonomy.html.erb +0 -29
- data/app/views/spree/billing/shared/sub_menu/_product.html.erb +0 -8
- data/app/views/spree/billing/shared/sub_menu/_report.html.erb +0 -23
- data/app/views/spree/billing/shared/sub_menu/_setting.html.erb +0 -11
- data/app/views/spree/billing/store_credits/_form.html.erb +0 -11
- data/app/views/spree/billing/store_credits/index.html.erb +0 -44
- data/app/views/spree/billing/store_credits/new.html.erb +0 -17
- data/app/views/spree/billing/subscriptions/_form.html.erb +0 -27
- data/app/views/spree/billing/subscriptions/_subscribed_variants_table.html.erb +0 -24
- data/app/views/spree/billing/subscriptions/edit.html.erb +0 -12
- data/app/views/spree/billing/subscriptions/index.html.erb +0 -86
- data/app/views/spree/billing/subscriptions/new.html.erb +0 -21
- data/app/views/spree/billing/users/_form.html.erb +0 -64
- data/app/views/spree/billing/users/edit.html.erb +0 -25
- data/app/views/spree/billing/users/index.html.erb +0 -116
- data/app/views/spree/billing/users/new.html.erb +0 -17
- data/app/views/spree/billing/variants/_form.html.erb +0 -28
- data/app/views/spree/billing/variants/edit.html.erb +0 -15
- data/app/views/spree/billing/variants/index.html.erb +0 -66
- data/app/views/spree/billing/variants/new.html.erb +0 -15
- data/app/views/spree/billing/vendors/_form.html.erb +0 -75
- data/app/views/spree/billing/vendors/edit.html.erb +0 -9
- data/app/views/spree/layouts/billing.html.erb +0 -74
- data/app/views/spree/layouts/transit.html.erb +0 -74
- data/app/views/spree/transit/amenities/_form.html.erb +0 -50
- data/app/views/spree/transit/amenities/_option_value_fields.html.erb +0 -15
- data/app/views/spree/transit/amenities/edit.html.erb +0 -53
- data/app/views/spree/transit/branches/_filter_form.erb +0 -23
- data/app/views/spree/transit/branches/_form.html.erb +0 -36
- data/app/views/spree/transit/branches/edit.html.erb +0 -15
- data/app/views/spree/transit/branches/index.html.erb +0 -43
- data/app/views/spree/transit/branches/new.html.erb +0 -19
- data/app/views/spree/transit/locations/_filter_form.erb +0 -30
- data/app/views/spree/transit/locations/_form.html.erb +0 -19
- data/app/views/spree/transit/locations/edit.html.erb +0 -15
- data/app/views/spree/transit/locations/index.html.erb +0 -49
- data/app/views/spree/transit/locations/new.html.erb +0 -17
- data/app/views/spree/transit/places/_assets_form.html.erb +0 -8
- data/app/views/spree/transit/places/_form.html.erb +0 -128
- data/app/views/spree/transit/places/edit.html.erb +0 -21
- data/app/views/spree/transit/places/index.html.erb +0 -32
- data/app/views/spree/transit/places/new.html.erb +0 -16
- data/app/views/spree/transit/reservations/index.html.erb +0 -5
- data/app/views/spree/transit/routes/_filter_form.erb +0 -24
- data/app/views/spree/transit/routes/_form.html.erb +0 -74
- data/app/views/spree/transit/routes/edit.html.erb +0 -13
- data/app/views/spree/transit/routes/index.html.erb +0 -79
- data/app/views/spree/transit/routes/new.html.erb +0 -18
- data/app/views/spree/transit/service_calendars/_exception_rules.html.erb +0 -44
- data/app/views/spree/transit/service_calendars/_filters.html.erb +0 -17
- data/app/views/spree/transit/service_calendars/_form.html.erb +0 -107
- data/app/views/spree/transit/service_calendars/index.html.erb +0 -77
- data/app/views/spree/transit/service_calendars/new.html.erb +0 -13
- data/app/views/spree/transit/shared/_amenity_tabs.html.erb +0 -21
- data/app/views/spree/transit/shared/_header.html.erb +0 -20
- data/app/views/spree/transit/shared/_main_menu.html.erb +0 -34
- data/app/views/spree/transit/shared/_route_tabs.html.erb +0 -25
- data/app/views/spree/transit/shared/_vehicle_tabs.html.erb +0 -15
- data/app/views/spree/transit/shared/_vendor_switcher.html.erb +0 -40
- data/app/views/spree/transit/shared/sortable_tree/_taxonomy.html.erb +0 -29
- data/app/views/spree/transit/shared/sub_menu/_setting.html.erb +0 -3
- data/app/views/spree/transit/stops/_filters.html.erb +0 -33
- data/app/views/spree/transit/stops/_form.html.erb +0 -68
- data/app/views/spree/transit/stops/edit.html.erb +0 -16
- data/app/views/spree/transit/stops/index.html.erb +0 -58
- data/app/views/spree/transit/stops/new.html.erb +0 -16
- data/app/views/spree/transit/trip_stops/index.html.erb +0 -38
- data/app/views/spree/transit/trips/_form.html.erb +0 -82
- data/app/views/spree/transit/trips/edit.html.erb +0 -15
- data/app/views/spree/transit/trips/new.html.erb +0 -15
- data/app/views/spree/transit/vectors/amenity_values/index.html.erb +0 -111
- data/app/views/spree/transit/vehicle_photos/_form.html.erb +0 -22
- data/app/views/spree/transit/vehicle_photos/edit.html.erb +0 -9
- data/app/views/spree/transit/vehicle_photos/index.html.erb +0 -61
- data/app/views/spree/transit/vehicle_photos/new.html.erb +0 -9
- data/app/views/spree/transit/vehicle_seats/_form.html.erb +0 -48
- data/app/views/spree/transit/vehicle_seats/_seats.html.erb +0 -63
- data/app/views/spree/transit/vehicle_types/_filter_form.html.erb +0 -29
- data/app/views/spree/transit/vehicle_types/_form.html.erb +0 -65
- data/app/views/spree/transit/vehicle_types/_seat_view.erb +0 -83
- data/app/views/spree/transit/vehicle_types/edit.html.erb +0 -93
- data/app/views/spree/transit/vehicle_types/index.html.erb +0 -94
- data/app/views/spree/transit/vehicle_types/new.html.erb +0 -14
- data/app/views/spree/transit/vehicles/_filter_form.html.erb +0 -23
- data/app/views/spree/transit/vehicles/_form.html.erb +0 -19
- data/app/views/spree/transit/vehicles/edit.html.erb +0 -19
- data/app/views/spree/transit/vehicles/index.html.erb +0 -57
- data/app/views/spree/transit/vehicles/new.html.erb +0 -19
- data/app/views/spree/transit/vendors/_form.html.erb +0 -34
- data/app/views/spree/transit/vendors/_tabs.html.erb +0 -16
- data/app/views/spree/transit/vendors/edit.html.erb +0 -10
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
# Fixed-window counter in Redis: "at most `limit` hits on `key` per `window`".
|
|
3
|
+
#
|
|
4
|
+
# Owns the atomic increment and the fail-open policy so callers can't get either
|
|
5
|
+
# wrong. A non-positive `limit` disables the check without touching Redis, the
|
|
6
|
+
# same "0 = disabled" convention FraudCheck's thresholds use.
|
|
7
|
+
class RateLimiter
|
|
8
|
+
# KEYS[1] - counter key, ARGV[1] - window in seconds.
|
|
9
|
+
#
|
|
10
|
+
# INCR and EXPIRE issued as two commands race: two callers can both observe
|
|
11
|
+
# the first write and each re-apply EXPIRE, sliding the window forward so the
|
|
12
|
+
# counter never resets. Lua runs them as a single atomic unit in Redis.
|
|
13
|
+
INCREMENT_SCRIPT = <<~LUA.freeze
|
|
14
|
+
local count = redis.call('INCR', KEYS[1])
|
|
15
|
+
if count == 1 then
|
|
16
|
+
redis.call('EXPIRE', KEYS[1], ARGV[1])
|
|
17
|
+
end
|
|
18
|
+
return count
|
|
19
|
+
LUA
|
|
20
|
+
|
|
21
|
+
def self.call(key:, limit:, window:)
|
|
22
|
+
new(key: key, limit: limit, window: window).call
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(key:, limit:, window:)
|
|
26
|
+
@key = key
|
|
27
|
+
@limit = limit.to_i
|
|
28
|
+
@window = window.to_i
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def call
|
|
32
|
+
return within_limit unless enabled?
|
|
33
|
+
|
|
34
|
+
count = SpreeCmCommissioner.redis_pool.with do |redis|
|
|
35
|
+
redis.eval(INCREMENT_SCRIPT, keys: [key], argv: [window])
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
Result.new(count: count, limit: limit, exceeded: count > limit)
|
|
39
|
+
rescue Redis::BaseError, ConnectionPool::TimeoutError => e
|
|
40
|
+
# Fail open: a Redis outage must not lock users out of whatever the caller
|
|
41
|
+
# protects. Log it so the blind spot is visible in monitoring — while this
|
|
42
|
+
# is firing, the limit is not being enforced at all.
|
|
43
|
+
CmAppLogger.error(
|
|
44
|
+
label: 'RateLimiter Redis error — failing open',
|
|
45
|
+
data: { key: key, error_class: e.class.name, error_message: e.message }
|
|
46
|
+
)
|
|
47
|
+
within_limit
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
attr_reader :key, :limit, :window
|
|
53
|
+
|
|
54
|
+
def enabled?
|
|
55
|
+
limit.positive? && window.positive?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def within_limit
|
|
59
|
+
Result.new(count: 0, limit: limit, exceeded: false)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -19,7 +19,9 @@ module SpreeCmCommissioner
|
|
|
19
19
|
begin
|
|
20
20
|
new_login = SpreeCmCommissioner::Users::Username::Generate.call(user: user)
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
# normalized_login must be written too: Username::Generate checks collisions against it,
|
|
23
|
+
# so a stale value lets later users in this run reuse a login just assigned.
|
|
24
|
+
if user.update_columns(login: new_login, normalized_login: new_login.downcase) # rubocop:disable Rails/SkipsModelValidations
|
|
23
25
|
updated_count += 1
|
|
24
26
|
|
|
25
27
|
# Log progress every 100 users to avoid log spam
|
|
@@ -21,7 +21,7 @@ module SpreeCmCommissioner
|
|
|
21
21
|
return failure(nil, error)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
if (error = check_price_range(
|
|
24
|
+
if (error = check_price_range(rule, price, transfer_type)) # rubocop:disable Lint/AssignmentInCondition
|
|
25
25
|
return failure(nil, error)
|
|
26
26
|
end
|
|
27
27
|
|
|
@@ -75,17 +75,16 @@ module SpreeCmCommissioner
|
|
|
75
75
|
I18n.t('ticket_transfer.event_cutoff_reached', hours: rule.event_cutoff_hours)
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
-
# Check 4 — price range. Enforces min/max price set on the rule
|
|
79
|
-
#
|
|
78
|
+
# Check 4 — price range. Enforces min/max price set on the rule, and only those: a rule that
|
|
79
|
+
# names no minimum sets no floor, rather than inheriting what the ticket originally sold for.
|
|
80
80
|
# Skips for gift and manual_payment transfers (no platform pricing).
|
|
81
|
-
def check_price_range(
|
|
81
|
+
def check_price_range(rule, price, transfer_type)
|
|
82
82
|
return nil unless SpreeCmCommissioner::TicketTransfer.new(transfer_type: transfer_type).purchase?
|
|
83
83
|
return nil if price.blank?
|
|
84
84
|
|
|
85
85
|
price_decimal = price.to_d
|
|
86
|
-
|
|
86
|
+
min_allowed = rule.min_price
|
|
87
87
|
|
|
88
|
-
min_allowed = rule.min_price.presence || variant_price
|
|
89
88
|
return I18n.t('ticket_transfer.price_below_minimum', minimum: min_allowed) if min_allowed.present? && price_decimal < min_allowed.to_d
|
|
90
89
|
|
|
91
90
|
if rule.max_price.present? && price_decimal > rule.max_price.to_d
|
|
@@ -12,21 +12,19 @@ module SpreeCmCommissioner
|
|
|
12
12
|
|
|
13
13
|
success(
|
|
14
14
|
currency: currency,
|
|
15
|
-
rule: rule_summary(fee_result.value[:rule]
|
|
15
|
+
rule: rule_summary(fee_result.value[:rule])
|
|
16
16
|
)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
private
|
|
20
20
|
|
|
21
|
-
def rule_summary(rule
|
|
21
|
+
def rule_summary(rule)
|
|
22
22
|
return nil if rule.nil?
|
|
23
23
|
|
|
24
|
-
variant_price = from_guest.variant&.price
|
|
25
|
-
|
|
26
24
|
{
|
|
27
25
|
fee_percentage: rule.fee_percentage,
|
|
28
26
|
fee_flat_amount: rule.fee_flat_amount,
|
|
29
|
-
min_price: rule.min_price
|
|
27
|
+
min_price: rule.min_price,
|
|
30
28
|
max_price: rule.max_price,
|
|
31
29
|
cooldown_hours: rule.cooldown_hours,
|
|
32
30
|
max_transfers_per_ticket: rule.max_transfers_per_ticket,
|
|
@@ -9,6 +9,8 @@ module SpreeCmCommissioner
|
|
|
9
9
|
# :fb_access_token
|
|
10
10
|
# :telegram_init_data, :tg_bot
|
|
11
11
|
# :session_id
|
|
12
|
+
# :session_key (ACLEDA Mini App JWT, self-identifying, minted by Vpago::AcledaMiniApp::Session::Create)
|
|
13
|
+
# :wing_session_key (Wing Mini App JWT, self-identifying, minted by Vpago::WingMiniApp::Session::Create)
|
|
12
14
|
|
|
13
15
|
def self.call!(params)
|
|
14
16
|
context = auth_context(params)
|
|
@@ -31,7 +33,7 @@ module SpreeCmCommissioner
|
|
|
31
33
|
user
|
|
32
34
|
end
|
|
33
35
|
|
|
34
|
-
def self.auth_context(params)
|
|
36
|
+
def self.auth_context(params) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
35
37
|
oauth_application = find_oauth_application(params)
|
|
36
38
|
tenant_id = oauth_application&.tenant_id
|
|
37
39
|
|
|
@@ -62,6 +64,12 @@ module SpreeCmCommissioner
|
|
|
62
64
|
when 'vattanac_bank_web_app_auth'
|
|
63
65
|
options = { session_id: params[:session_id] }
|
|
64
66
|
SpreeCmCommissioner::UserVattanacBankWebAppAuthenticator.call(options)
|
|
67
|
+
when 'acleda_bank_web_app_auth'
|
|
68
|
+
options = { session_key: params[:session_key] }
|
|
69
|
+
SpreeCmCommissioner::UserAcledaBankWebAppAuthenticator.call(options)
|
|
70
|
+
when 'wing_bank_web_app_auth'
|
|
71
|
+
options = { session_key: params[:wing_session_key] }
|
|
72
|
+
SpreeCmCommissioner::UserWingBankWebAppAuthenticator.call(options)
|
|
65
73
|
end
|
|
66
74
|
end
|
|
67
75
|
|
|
@@ -76,7 +84,9 @@ module SpreeCmCommissioner
|
|
|
76
84
|
'social_auth' => -> (params) { params.key?(:id_token) },
|
|
77
85
|
'facebook_auth' => -> (params) { params.key?(:fb_access_token) },
|
|
78
86
|
'telegram_web_app_auth' => -> (params) { params.key?(:telegram_init_data) && params.key?(:tg_bot) },
|
|
79
|
-
'vattanac_bank_web_app_auth' => -> (params) { params.key?(:session_id) }
|
|
87
|
+
'vattanac_bank_web_app_auth' => -> (params) { params.key?(:session_id) },
|
|
88
|
+
'acleda_bank_web_app_auth' => -> (params) { params.key?(:session_key) },
|
|
89
|
+
'wing_bank_web_app_auth' => -> (params) { params.key?(:wing_session_key) }
|
|
80
90
|
}.freeze
|
|
81
91
|
|
|
82
92
|
def self.flow_type(params)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module WingMiniApp
|
|
3
|
+
# Partner credentials for the Wing Web JS Bridge SDK, read from Wing's own mini app row
|
|
4
|
+
# (MiniAppProvider::WingBank) so they can be rotated from the admin, and so clearing one
|
|
5
|
+
# revokes the mini app instead of falling through to another source.
|
|
6
|
+
#
|
|
7
|
+
# Both halves of the integration read here — login, and Spree::Gateway::WingMiniApp for
|
|
8
|
+
# doPayment and webhook signing. Wing issues one pair, so they must come from one place:
|
|
9
|
+
# two sources would let the halves act as different partners, a mismatch that only shows
|
|
10
|
+
# up at the webhook, once the customer has already paid.
|
|
11
|
+
class Config
|
|
12
|
+
PROVIDER = 'wing_bank'.freeze
|
|
13
|
+
|
|
14
|
+
# The lowest getProfile data level whose hash formula includes `phone`, which is
|
|
15
|
+
# how a Wing customer is matched to an existing BookMe+ account.
|
|
16
|
+
ACCEPTED_LEVELS_FLOOR = %w[level_1a level_1b level_2 level_3].freeze
|
|
17
|
+
|
|
18
|
+
# Wing sends no api key — its WebView is opened by the partner, so the customer's
|
|
19
|
+
# browser arrives. Nil until someone configures the row.
|
|
20
|
+
def self.mini_app
|
|
21
|
+
Spree::OauthApplication.keyless_mini_app_for(PROVIDER)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.app_id
|
|
25
|
+
mini_app&.preferred_app_id.presence
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.secret_api_key
|
|
29
|
+
mini_app&.preferred_secret_api_key.presence
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Blank (default) accepts level_1a or above. Set to pin verification to exactly
|
|
33
|
+
# one level (level_0..level_3, level_1a, level_1b), including a weaker one if
|
|
34
|
+
# Wing insists.
|
|
35
|
+
def self.data_level
|
|
36
|
+
mini_app&.preferred_data_level.presence
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.accepted_levels
|
|
40
|
+
data_level.present? ? [data_level] : ACCEPTED_LEVELS_FLOOR
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module WingMiniApp
|
|
3
|
+
# Finds or creates the BookMe+ customer for a verified Wing getProfile response.
|
|
4
|
+
#
|
|
5
|
+
# Unlike ACLEDA (server-to-server, phone only), Wing hands us a stable per-user
|
|
6
|
+
# `id`, so that — not phone — is what the wing_bank identity is keyed on, the
|
|
7
|
+
# same way the google/facebook/telegram identities key on the provider's own
|
|
8
|
+
# account id rather than a mutable field. Phone is still how a *new* Wing login
|
|
9
|
+
# gets matched to an existing BookMe+ account.
|
|
10
|
+
class FindOrCreateUser
|
|
11
|
+
prepend Spree::ServiceModule::Base
|
|
12
|
+
|
|
13
|
+
# `gate` is optional: without one nothing is configured to cap new accounts, which is
|
|
14
|
+
# how this behaved before mini apps had a row to configure.
|
|
15
|
+
def call(profile:, gate: nil)
|
|
16
|
+
@profile = profile.with_indifferent_access
|
|
17
|
+
@gate = gate
|
|
18
|
+
|
|
19
|
+
return failure(nil, 'id is required') if profile['id'].blank?
|
|
20
|
+
return failure({ status: :too_many_requests }, 'too many requests') if new_user_throttled?
|
|
21
|
+
|
|
22
|
+
user = find_or_create
|
|
23
|
+
return failure(user, 'user creation failed') if user.nil? || !user.persisted?
|
|
24
|
+
|
|
25
|
+
success(user: user)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
attr_reader :profile, :gate
|
|
31
|
+
|
|
32
|
+
# Only asked where this call would mint an account — the damage a leaked key does.
|
|
33
|
+
# Returning customers never spend that quota.
|
|
34
|
+
def new_user_throttled?
|
|
35
|
+
return false if gate.nil?
|
|
36
|
+
return false if wing_identity.persisted? || find_existing_user.present?
|
|
37
|
+
|
|
38
|
+
gate.new_user_exceeded?(phone: intel_phone)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def find_or_create
|
|
42
|
+
identity = wing_identity
|
|
43
|
+
|
|
44
|
+
# Returning customer: the Wing identity already points at a user.
|
|
45
|
+
return identity.user if identity.persisted?
|
|
46
|
+
|
|
47
|
+
# New to Wing: reuse a customer matched by phone, otherwise build one, then
|
|
48
|
+
# tag them with the wing_bank identity so the lookup above hits next time.
|
|
49
|
+
user = find_existing_user || build_user
|
|
50
|
+
identity.name = profile['fullName'].presence
|
|
51
|
+
user.user_identity_providers << identity
|
|
52
|
+
user.save
|
|
53
|
+
|
|
54
|
+
user
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Memoized: #new_user_throttled? asks the same questions, and this row is the one
|
|
58
|
+
# that gets tagged onto the user.
|
|
59
|
+
def wing_identity
|
|
60
|
+
@wing_identity ||= SpreeCmCommissioner::UserIdentityProvider.wing_bank.find_or_initialize_by(sub: profile['id'])
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def find_existing_user
|
|
64
|
+
return @find_existing_user if defined?(@find_existing_user)
|
|
65
|
+
return @find_existing_user = nil if intel_phone.blank?
|
|
66
|
+
|
|
67
|
+
@find_existing_user = Spree::User.by_non_tenant.find_by(intel_phone_number: intel_phone)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def build_user
|
|
71
|
+
Spree::User.new(
|
|
72
|
+
first_name: profile['firstName'].presence,
|
|
73
|
+
last_name: profile['lastName'].presence,
|
|
74
|
+
phone_number: profile['phone'].presence,
|
|
75
|
+
intel_phone_number: intel_phone,
|
|
76
|
+
password: SecureRandom.base64(16),
|
|
77
|
+
confirmed_at: Time.zone.now
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def intel_phone
|
|
82
|
+
return @intel_phone if defined?(@intel_phone)
|
|
83
|
+
return @intel_phone = nil if profile['phone'].blank?
|
|
84
|
+
|
|
85
|
+
@intel_phone = SpreeCmCommissioner::PhoneNumberParser.call(phone_number: profile['phone']).intel_phone_number
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module WingMiniApp
|
|
3
|
+
# Verifies the `secret_key` a Wing getProfile response carries (Wing Web JS
|
|
4
|
+
# Bridge SDK spec, Native API #1).
|
|
5
|
+
#
|
|
6
|
+
# Each data level has its own exact hash formula, and they are NOT simply "the
|
|
7
|
+
# response fields that came back non-blank" — `sex` is a case in point: Wing's
|
|
8
|
+
# spec returns it in the response starting at level_1a, but it only enters the
|
|
9
|
+
# *hash formula* one tier later, at level_1b. So which fields are present in the
|
|
10
|
+
# payload cannot be used to infer which formula to check; every level's formula
|
|
11
|
+
# has to be tried explicitly against LEVEL_FIELDS, which is transcribed directly
|
|
12
|
+
# from the spec rather than derived.
|
|
13
|
+
class ProfileVerifier
|
|
14
|
+
prepend Spree::ServiceModule::Base
|
|
15
|
+
|
|
16
|
+
# Field order per data level, transcribed from the spec's plainText formulas.
|
|
17
|
+
# Order matters. Every level's field list is a prefix of the next level's.
|
|
18
|
+
LEVEL_FIELDS = {
|
|
19
|
+
'level_0' => %w[id appId],
|
|
20
|
+
'level_1' => %w[id appId fullName],
|
|
21
|
+
'level_1a' => %w[id appId fullName phone],
|
|
22
|
+
'level_1b' => %w[id appId fullName phone sex age nationality],
|
|
23
|
+
'level_2' => %w[id appId fullName phone sex age nationality occupation],
|
|
24
|
+
'level_3' => %w[id appId fullName phone sex age nationality occupation dobFull nidNumber nidType]
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
def call(profile:)
|
|
28
|
+
@profile = profile.with_indifferent_access
|
|
29
|
+
|
|
30
|
+
return failure(nil, 'appId does not match configured partner') unless app_id_matches?
|
|
31
|
+
return failure(nil, 'secret_key is missing or invalid') unless matched_level
|
|
32
|
+
return failure(nil, "data level #{matched_level} is below the accepted floor") unless level_accepted?
|
|
33
|
+
|
|
34
|
+
success(profile: profile, level: matched_level)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
attr_reader :profile
|
|
40
|
+
|
|
41
|
+
def app_id_matches?
|
|
42
|
+
profile['appId'].present? && profile['appId'] == Config.app_id
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The one level (if any) whose exact formula produces the received secret_key.
|
|
46
|
+
# Cryptographically, at most one level can match unless there's a SHA-256
|
|
47
|
+
# collision, since every formula is salted with the same secret_api_key.
|
|
48
|
+
def matched_level
|
|
49
|
+
return @matched_level if defined?(@matched_level)
|
|
50
|
+
|
|
51
|
+
received = profile['secret_key']
|
|
52
|
+
@matched_level = received.present? && LEVEL_FIELDS.keys.find { |level| hash_for(level) == received.to_s.upcase }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def hash_for(level)
|
|
56
|
+
plaintext = LEVEL_FIELDS.fetch(level).map { |field| profile[field].to_s }.join + Config.secret_api_key.to_s
|
|
57
|
+
Digest::SHA256.hexdigest(plaintext.upcase).upcase
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def level_accepted?
|
|
61
|
+
Config.accepted_levels.include?(matched_level)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -114,6 +114,10 @@
|
|
|
114
114
|
|
|
115
115
|
<td class="text-left"><%= guest_state_badge(guest) %></td>
|
|
116
116
|
<td class="text-right">
|
|
117
|
+
<% buyer_order = guest.transferred_to_order %>
|
|
118
|
+
<% if buyer_order.present? %>
|
|
119
|
+
<%= link_to_with_icon 'box-arrow-up-right.svg', 'View order', spree.edit_admin_order_path(buyer_order.number), no_text: true, class: 'btn btn-light btn-sm' %>
|
|
120
|
+
<% end %>
|
|
117
121
|
<%= link_to_edit guest, url: edit_admin_order_guest_path(@order, guest), no_text: true %>
|
|
118
122
|
<% if guest&.id_card&.front_image.present? || guest&.id_card&.back_image.present? %>
|
|
119
123
|
<%= link_to_with_icon 'images.svg', 'Edit Id Card Images', edit_admin_order_guest_id_card_path(order_id: @order, guest_id: guest.id, id: guest.id_card.id), no_text: true, class: 'mx-2' %>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<%# Rendered already open, with a backdrop of its own and its own close handler, so it needs nothing
|
|
2
|
+
from Bootstrap's JS to appear. The other dialogs here are opened by a data-toggle button and can
|
|
3
|
+
afford to wait for the plugin; this one carries the only copy of a freshly minted key, and a dialog
|
|
4
|
+
that fails to open would lose it. Bootstrap's CSS still does the styling. %>
|
|
5
|
+
|
|
6
|
+
<div class="modal show" id="mini-app-api-key-minted-modal" tabindex="-1" role="dialog"
|
|
7
|
+
aria-labelledby="miniAppApiKeyMintedModalLabel" aria-modal="true" style="display: block;">
|
|
8
|
+
<div class="modal-dialog" role="document">
|
|
9
|
+
<div class="modal-content">
|
|
10
|
+
<div class="modal-header">
|
|
11
|
+
<h5 class="modal-title" id="miniAppApiKeyMintedModalLabel">Copy this API key</h5>
|
|
12
|
+
<button type="button" class="close" aria-label="Close" data-minted-key-close>
|
|
13
|
+
<span aria-hidden="true">×</span>
|
|
14
|
+
</button>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div class="modal-body">
|
|
18
|
+
<div class="alert alert-warning">
|
|
19
|
+
Send it to the partner now — <strong>closing this dialog is the last you see of it</strong>.
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div class="input-group">
|
|
23
|
+
<%= tag.input type: 'text', class: 'form-control', readonly: true,
|
|
24
|
+
value: api_key, id: 'mini-app-api-key-minted-value' %>
|
|
25
|
+
<div class="input-group-append">
|
|
26
|
+
<button type="button" class="btn btn-primary" id="mini-app-api-key-copy">Copy</button>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div class="modal-footer">
|
|
32
|
+
<button type="button" class="btn btn-secondary" data-minted-key-close>Done</button>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div class="modal-backdrop show" id="mini-app-api-key-minted-backdrop"></div>
|
|
39
|
+
|
|
40
|
+
<script>
|
|
41
|
+
(function () {
|
|
42
|
+
var modal = document.getElementById('mini-app-api-key-minted-modal');
|
|
43
|
+
var backdrop = document.getElementById('mini-app-api-key-minted-backdrop');
|
|
44
|
+
var field = document.getElementById('mini-app-api-key-minted-value');
|
|
45
|
+
var button = document.getElementById('mini-app-api-key-copy');
|
|
46
|
+
|
|
47
|
+
if (field && button) {
|
|
48
|
+
button.addEventListener('click', function () {
|
|
49
|
+
field.select();
|
|
50
|
+
|
|
51
|
+
// navigator.clipboard is absent outside a secure context. select() has still put the key on
|
|
52
|
+
// screen selected, so it can be copied by hand rather than lost.
|
|
53
|
+
if (navigator.clipboard) navigator.clipboard.writeText(field.value);
|
|
54
|
+
|
|
55
|
+
button.textContent = 'Copied';
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Our own close rather than data-dismiss: the dialog was never handed to Bootstrap, so Bootstrap
|
|
60
|
+
// has no state to hide. Removing both nodes is the whole of it.
|
|
61
|
+
function close() {
|
|
62
|
+
if (modal) modal.remove();
|
|
63
|
+
if (backdrop) backdrop.remove();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
Array.prototype.forEach.call(
|
|
67
|
+
document.querySelectorAll('[data-minted-key-close]'),
|
|
68
|
+
function (el) { el.addEventListener('click', close); }
|
|
69
|
+
);
|
|
70
|
+
})();
|
|
71
|
+
</script>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<%# Outside the application's form — forms cannot nest. Replacing a key cuts the mini app off
|
|
2
|
+
until they deploy the new one, so only that asks for the name typed back; a first key has
|
|
3
|
+
nothing to lose. The controller checks it again — a stale page walks straight past this. %>
|
|
4
|
+
|
|
5
|
+
<% replacing_key = mini_app.api_key.present? %>
|
|
6
|
+
|
|
7
|
+
<div class="modal fade" id="mini-app-api-key-modal" tabindex="-1" role="dialog" aria-labelledby="miniAppApiKeyModalLabel" aria-hidden="true">
|
|
8
|
+
<div class="modal-dialog" role="document">
|
|
9
|
+
<div class="modal-content">
|
|
10
|
+
<div class="modal-header">
|
|
11
|
+
<h5 class="modal-title" id="miniAppApiKeyModalLabel">
|
|
12
|
+
<%= replacing_key ? 'Regenerate this API key?' : 'Generate an API key?' %>
|
|
13
|
+
</h5>
|
|
14
|
+
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
|
15
|
+
<span aria-hidden="true">×</span>
|
|
16
|
+
</button>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<%= form_with url: spree.admin_oauth_application_api_key_path(application), method: :post, local: true do %>
|
|
20
|
+
<div class="modal-body">
|
|
21
|
+
<% if replacing_key %>
|
|
22
|
+
<div class="alert alert-warning mb-3">
|
|
23
|
+
The current key stops working the moment this is done.
|
|
24
|
+
<strong>The partner cannot reach us again until they deploy the new one.</strong>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div class="form-group mb-0">
|
|
28
|
+
<label>Type the application name <code class="text-danger"><%= application.name %></code> to confirm</label>
|
|
29
|
+
<%= text_field_tag :api_key_confirm, nil, class: 'form-control', autocomplete: 'off', id: 'mini-app-api-key-confirm' %>
|
|
30
|
+
</div>
|
|
31
|
+
<% else %>
|
|
32
|
+
<div class="alert alert-info mb-0">
|
|
33
|
+
This key names the application on its server-to-server calls.
|
|
34
|
+
<strong>Send it to the partner</strong> — they have to include it on every request,
|
|
35
|
+
and nothing they send reaches us until they do.
|
|
36
|
+
</div>
|
|
37
|
+
<% end %>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="modal-footer">
|
|
41
|
+
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
|
|
42
|
+
<%= submit_tag replacing_key ? 'Regenerate key' : 'Generate key',
|
|
43
|
+
class: "btn #{replacing_key ? 'btn-danger' : 'btn-primary'}",
|
|
44
|
+
id: 'mini-app-api-key-generate' %>
|
|
45
|
+
</div>
|
|
46
|
+
<% end %>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<% if replacing_key %>
|
|
52
|
+
<%# Left enabled in the markup so a script that never runs cannot strand an admin who needs to
|
|
53
|
+
rotate a leaked key; the controller is what actually refuses. %>
|
|
54
|
+
<script>
|
|
55
|
+
(function () {
|
|
56
|
+
var input = document.getElementById('mini-app-api-key-confirm');
|
|
57
|
+
var submit = document.getElementById('mini-app-api-key-generate');
|
|
58
|
+
if (!input || !submit) return;
|
|
59
|
+
|
|
60
|
+
var expected = '<%= j application.name %>';
|
|
61
|
+
|
|
62
|
+
function sync() { submit.disabled = input.value.trim() !== expected; }
|
|
63
|
+
|
|
64
|
+
sync();
|
|
65
|
+
input.addEventListener('input', sync);
|
|
66
|
+
})();
|
|
67
|
+
</script>
|
|
68
|
+
<% end %>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<%# Fetched when the dialog is opened, not rendered with the page: reading history costs a scan of the
|
|
2
|
+
application's versions, and this way the keys are never in the markup of a page nobody asked. %>
|
|
3
|
+
|
|
4
|
+
<% if entries.empty? %>
|
|
5
|
+
<div class="alert alert-info mb-0">
|
|
6
|
+
This application has held no other key — there is nothing to restore.
|
|
7
|
+
</div>
|
|
8
|
+
<% else %>
|
|
9
|
+
<p class="text-muted mb-3">
|
|
10
|
+
Pick the key the partner is still using — their calls start working again with no deploy on
|
|
11
|
+
their side.
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<div class="list-group mb-3">
|
|
15
|
+
<% entries.each do |entry| %>
|
|
16
|
+
<label class="list-group-item list-group-item-action d-flex align-items-start mb-0">
|
|
17
|
+
<%= radio_button_tag :version_id, entry.version_id, false, class: 'mt-1 mr-3' %>
|
|
18
|
+
|
|
19
|
+
<span class="text-break">
|
|
20
|
+
<code class="text-body"><%= entry.key %></code>
|
|
21
|
+
<% if entry.revoked %>
|
|
22
|
+
<span class="badge badge-danger ml-1" title="Cleared rather than replaced — usually a leak">revoked</span>
|
|
23
|
+
<% end %>
|
|
24
|
+
|
|
25
|
+
<small class="d-block text-muted">
|
|
26
|
+
<%= entry.source.to_s.titleize.presence || 'Unknown' %>
|
|
27
|
+
<%= entry.created_at&.strftime('%d %b %Y %H:%M') %>
|
|
28
|
+
<%= "· #{entry.created_by}" if entry.created_by.present? %>
|
|
29
|
+
</small>
|
|
30
|
+
</span>
|
|
31
|
+
</label>
|
|
32
|
+
<% end %>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div class="form-group mb-0">
|
|
36
|
+
<label>Type the application name <code class="text-danger"><%= application.name %></code> to confirm</label>
|
|
37
|
+
<%= text_field_tag :api_key_confirm, nil, class: 'form-control', autocomplete: 'off',
|
|
38
|
+
id: 'mini-app-api-key-restore-confirm' %>
|
|
39
|
+
</div>
|
|
40
|
+
<% end %>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<%# The form submits a version id, never a key — the plaintext is read out of history in the controller,
|
|
2
|
+
so nothing weak or foreign can be typed into the column. The body arrives from #history the first
|
|
3
|
+
time the dialog is opened. %>
|
|
4
|
+
|
|
5
|
+
<div class="modal fade" id="mini-app-api-key-restore-modal" tabindex="-1" role="dialog"
|
|
6
|
+
aria-labelledby="miniAppApiKeyRestoreModalLabel" aria-hidden="true">
|
|
7
|
+
<div class="modal-dialog modal-lg" role="document">
|
|
8
|
+
<div class="modal-content">
|
|
9
|
+
<div class="modal-header">
|
|
10
|
+
<h5 class="modal-title" id="miniAppApiKeyRestoreModalLabel">Restore a previous API key</h5>
|
|
11
|
+
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
|
12
|
+
<span aria-hidden="true">×</span>
|
|
13
|
+
</button>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<%= form_with url: spree.admin_oauth_application_api_key_path(application), method: :patch, local: true do %>
|
|
17
|
+
<div class="modal-body" id="mini-app-api-key-restore-body">
|
|
18
|
+
<p class="text-muted mb-0">Loading previous keys…</p>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div class="modal-footer">
|
|
22
|
+
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
|
|
23
|
+
<%= submit_tag 'Restore key', class: 'btn btn-primary', id: 'mini-app-api-key-restore-submit',
|
|
24
|
+
disabled: true %>
|
|
25
|
+
</div>
|
|
26
|
+
<% end %>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<script>
|
|
32
|
+
(function () {
|
|
33
|
+
var modal = document.getElementById('mini-app-api-key-restore-modal');
|
|
34
|
+
var opener = document.getElementById('mini-app-api-key-restore-open');
|
|
35
|
+
var body = document.getElementById('mini-app-api-key-restore-body');
|
|
36
|
+
var submit = document.getElementById('mini-app-api-key-restore-submit');
|
|
37
|
+
if (!modal || !opener || !body || !submit) return;
|
|
38
|
+
|
|
39
|
+
var url = '<%= j spree.history_admin_oauth_application_api_key_path(application) %>';
|
|
40
|
+
var expected = '<%= j application.name %>';
|
|
41
|
+
var requested = false;
|
|
42
|
+
|
|
43
|
+
// Delegated, because the radios and the confirmation field do not exist until the body arrives.
|
|
44
|
+
function sync() {
|
|
45
|
+
var chosen = modal.querySelector('input[name="version_id"]:checked');
|
|
46
|
+
var typed = modal.querySelector('#mini-app-api-key-restore-confirm');
|
|
47
|
+
|
|
48
|
+
submit.disabled = !chosen || !typed || typed.value.trim() !== expected;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function load() {
|
|
52
|
+
if (requested) return;
|
|
53
|
+
requested = true;
|
|
54
|
+
|
|
55
|
+
fetch(url, { credentials: 'same-origin', headers: { 'Accept': 'text/html' } })
|
|
56
|
+
.then(function (response) {
|
|
57
|
+
if (!response.ok) throw new Error(response.status);
|
|
58
|
+
return response.text();
|
|
59
|
+
})
|
|
60
|
+
.then(function (html) { body.innerHTML = html; sync(); })
|
|
61
|
+
.catch(function () {
|
|
62
|
+
// Left retryable: the next click asks again rather than stranding an admin mid-incident.
|
|
63
|
+
requested = false;
|
|
64
|
+
body.innerHTML = '<div class="alert alert-danger mb-0">Could not load previous keys. Close this and try again.</div>';
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
opener.addEventListener('click', load);
|
|
69
|
+
modal.addEventListener('change', sync);
|
|
70
|
+
modal.addEventListener('input', sync);
|
|
71
|
+
})();
|
|
72
|
+
</script>
|