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,219 @@
|
|
|
1
|
+
require 'ipaddr'
|
|
2
|
+
|
|
3
|
+
module SpreeCmCommissioner
|
|
4
|
+
module MiniAppProvider
|
|
5
|
+
# One class per mini app, the way a payment gateway is one class per provider: the
|
|
6
|
+
# class declares what that mini app is configured with, and the application's
|
|
7
|
+
# `provider` names it the way a payment method's `type` names its gateway.
|
|
8
|
+
#
|
|
9
|
+
# Preferences are stored in the application's `settings`, under a key of the
|
|
10
|
+
# mini app's own, so two mini apps can never collide and one mini app's whole
|
|
11
|
+
# configuration can be read or dropped as a single value.
|
|
12
|
+
class Base
|
|
13
|
+
include Spree::Preferences::Preferable
|
|
14
|
+
|
|
15
|
+
# Every mini app is throttled the same way, by MiniAppProvider::SessionThrottle; a
|
|
16
|
+
# mini app shows them by naming this in #preference_groups. Set one to 0 to turn that
|
|
17
|
+
# cap off.
|
|
18
|
+
RATE_LIMITS = %i[max_sessions_per_ip max_sessions_per_phone max_new_users].freeze
|
|
19
|
+
|
|
20
|
+
preference :max_sessions_per_ip, :integer, default: 600
|
|
21
|
+
preference :max_sessions_per_phone, :integer, default: 10
|
|
22
|
+
preference :max_new_users, :integer, default: 100
|
|
23
|
+
|
|
24
|
+
# Which addresses a mini app's own servers may call from, for the ones that
|
|
25
|
+
# call us server to server — see MiniAppProvider::IpRules for what the lists
|
|
26
|
+
# accept. A mini app shows them by naming this in #preference_groups.
|
|
27
|
+
IP_RULES = %i[allowed_ips blocked_ips].freeze
|
|
28
|
+
|
|
29
|
+
preference :allowed_ips, :string, default: ''
|
|
30
|
+
preference :blocked_ips, :string, default: ''
|
|
31
|
+
|
|
32
|
+
# Who put the current key in place, when, and whether they typed it or we minted it.
|
|
33
|
+
# Replacing the key overwrites all three; superseded keys survive in the application's
|
|
34
|
+
# PaperTrail versions.
|
|
35
|
+
preference :api_key_created_by, :string, default: ''
|
|
36
|
+
preference :api_key_created_at, :string, default: ''
|
|
37
|
+
preference :api_key_source, :string, default: ''
|
|
38
|
+
|
|
39
|
+
# SecureRandom.hex renders one byte as two characters. Every key is minted here, so any other
|
|
40
|
+
# length is a truncated paste — it would look right in the field and fail every call after.
|
|
41
|
+
API_KEY_BYTES = 32
|
|
42
|
+
API_KEY_LENGTH = 64
|
|
43
|
+
|
|
44
|
+
class << self
|
|
45
|
+
# Registered mini apps, the equivalent of Spree::PaymentMethod.providers.
|
|
46
|
+
def providers
|
|
47
|
+
[AcledaBank, WingBank, VattanacBank]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Matches UserIdentityProvider#identity_type, so the mini app that authenticates a
|
|
51
|
+
# customer and the identity row it writes name the same one. Those enum values
|
|
52
|
+
# end in `_bank`, which is why these class names do — not every mini app has to
|
|
53
|
+
# be one.
|
|
54
|
+
def provider
|
|
55
|
+
name.demodulize.underscore
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def for(application)
|
|
59
|
+
klass = providers.find { |provider_class| provider_class.provider == application.provider.to_s }
|
|
60
|
+
|
|
61
|
+
klass&.new(application)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Preferences grouped for the admin form. A mini app with none is configured from
|
|
65
|
+
# the environment rather than from a row.
|
|
66
|
+
def preference_groups
|
|
67
|
+
{}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The preference this mini app cannot be used without. Clearing it revokes the
|
|
71
|
+
# mini app, and one with none is not run from an application at all.
|
|
72
|
+
def credential
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Whether this mini app names itself with an API key on the way in. Only one we take
|
|
77
|
+
# server-to-server calls from has a request of its own to put a header on — a mini app
|
|
78
|
+
# whose WebView the partner merely opens arrives as the customer's browser, and the admin
|
|
79
|
+
# is offered no key it would have nowhere to send.
|
|
80
|
+
def supports_api_key?
|
|
81
|
+
false
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
attr_reader :application
|
|
86
|
+
|
|
87
|
+
delegate :id, :uid, to: :application
|
|
88
|
+
delegate :provider, :preference_groups, :credential, :supports_api_key?, to: :class
|
|
89
|
+
|
|
90
|
+
def initialize(application)
|
|
91
|
+
@application = application
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# `ip` is Rails' request.remote_ip, so this is only as trustworthy as the proxy
|
|
95
|
+
# configuration in front of it.
|
|
96
|
+
#
|
|
97
|
+
# An address we cannot read is denied whenever an allow list is set: a mini app that named
|
|
98
|
+
# its addresses is asking us to refuse anything it cannot vouch for. Blocked wins over
|
|
99
|
+
# allowed.
|
|
100
|
+
def ip_allowed?(ip)
|
|
101
|
+
address = to_address(ip)
|
|
102
|
+
|
|
103
|
+
return false if address.nil? && allowed_ip_rules.any?
|
|
104
|
+
return true if address.nil?
|
|
105
|
+
return false if blocked_ip_rules.any? { |range| range.include?(address) }
|
|
106
|
+
|
|
107
|
+
allowed_ip_rules.empty? || allowed_ip_rules.any? { |range| range.include?(address) }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Which address lists do not parse, for the validation that refuses to save them.
|
|
111
|
+
def invalid_ip_rules
|
|
112
|
+
IP_RULES.reject { |name| IpRules.valid?(public_send("preferred_#{name}")) }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# The header a mini app names itself with, and what an inbound call is looked up by — hence a
|
|
116
|
+
# column on the application rather than a key in `settings`, where nothing could index it or
|
|
117
|
+
# keep it unique. A handle, not a credential: #credential is what proves the call.
|
|
118
|
+
delegate :api_key, to: :application
|
|
119
|
+
|
|
120
|
+
# Mints a key, or takes `value` as the key when one is given — the way back from a Generate
|
|
121
|
+
# pressed by mistake, which no amount of history can undo on its own: the versions say which
|
|
122
|
+
# keys existed, never which one the partner is holding. Writes in memory like any other
|
|
123
|
+
# setter, so persisting is the caller's job.
|
|
124
|
+
def replace_api_key!(value = nil, actor: nil, source: nil)
|
|
125
|
+
typed = value.to_s.strip
|
|
126
|
+
|
|
127
|
+
application.api_key = typed.presence || SecureRandom.hex(API_KEY_BYTES)
|
|
128
|
+
self.preferred_api_key_source = source || 'generated'
|
|
129
|
+
self.preferred_api_key_created_by = actor&.id.to_s
|
|
130
|
+
self.preferred_api_key_created_at = Time.current.iso8601
|
|
131
|
+
|
|
132
|
+
api_key
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Leaves the mini app with no way in until a new key is generated — the answer to a leak,
|
|
136
|
+
# where replacing the key would hand the partner a working one they never asked for. The stamp
|
|
137
|
+
# goes with it: provenance for a key that is gone reads as though one were still in place. The
|
|
138
|
+
# revoked key survives in the application's PaperTrail versions.
|
|
139
|
+
def revoke_api_key!
|
|
140
|
+
application.api_key = nil
|
|
141
|
+
self.preferred_api_key_source = ''
|
|
142
|
+
self.preferred_api_key_created_by = ''
|
|
143
|
+
self.preferred_api_key_created_at = ''
|
|
144
|
+
|
|
145
|
+
nil
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# What the form shows in place of the key, which it never renders after the one dialog that
|
|
149
|
+
# follows minting. The last four characters are what the partner can read back over the phone, and
|
|
150
|
+
# four of sixty-four hex characters give away nothing that matters.
|
|
151
|
+
def masked_api_key
|
|
152
|
+
return nil if api_key.blank?
|
|
153
|
+
|
|
154
|
+
"#{'•' * 20}#{api_key.last(4)}"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# The caption the admin form renders under the key — one sentence, so one method rather
|
|
158
|
+
# than three readers the view has to reassemble. Each half degrades on its own: the admin
|
|
159
|
+
# who generated the key may be gone, and `settings` is hand-editable JSON, so the stamp may
|
|
160
|
+
# not parse. Neither may cost us the fact that someone did put this key in place.
|
|
161
|
+
def api_key_provenance
|
|
162
|
+
return nil if api_key.blank?
|
|
163
|
+
|
|
164
|
+
action = preferred_api_key_source == 'restored' ? 'Restored' : 'Generated'
|
|
165
|
+
|
|
166
|
+
"#{action} #{api_key_created_at_phrase} by #{api_key_author}"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Preferable reads and writes this in place, so it must be one hash for the life of
|
|
170
|
+
# the instance; #preferences_will_change! is what puts it back on the record. The
|
|
171
|
+
# defaults are merged in so the admin form can render every field of a mini app
|
|
172
|
+
# that has never been saved, the way Spree::Base seeds a payment method's column.
|
|
173
|
+
def preferences
|
|
174
|
+
@preferences ||= default_preferences.with_indifferent_access.merge(stored_preferences)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def preferences_will_change!
|
|
178
|
+
application.settings = application.settings.merge(provider => preferences.to_h)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
private
|
|
182
|
+
|
|
183
|
+
def allowed_ip_rules
|
|
184
|
+
@allowed_ip_rules ||= IpRules.parse(preferred_allowed_ips)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def blocked_ip_rules
|
|
188
|
+
@blocked_ip_rules ||= IpRules.parse(preferred_blocked_ips)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def to_address(ip)
|
|
192
|
+
IPAddr.new(ip.to_s)
|
|
193
|
+
rescue IPAddr::Error
|
|
194
|
+
nil
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def api_key_created_at_phrase
|
|
198
|
+
time = Time.zone.parse(preferred_api_key_created_at.to_s)
|
|
199
|
+
|
|
200
|
+
time ? time.strftime('%d %b %Y %H:%M') : 'at an unknown time'
|
|
201
|
+
rescue ArgumentError
|
|
202
|
+
'at an unknown time'
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Falls back to the stored id once that admin is gone, rather than losing the fact that
|
|
206
|
+
# someone did generate it.
|
|
207
|
+
def api_key_author
|
|
208
|
+
stored_id = preferred_api_key_created_by
|
|
209
|
+
return 'unknown' if stored_id.blank?
|
|
210
|
+
|
|
211
|
+
Spree.user_class.find_by(id: stored_id)&.email || stored_id
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def stored_preferences
|
|
215
|
+
(application.settings[provider] || {}).with_indifferent_access
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module MiniAppProvider
|
|
3
|
+
class WingBank < Base
|
|
4
|
+
# The mini app pair Wing issues. `app_id` is echoed back in every getProfile
|
|
5
|
+
# response and `secret_api_key` salts the hash that signs it — see
|
|
6
|
+
# SpreeCmCommissioner::WingMiniApp::ProfileVerifier for both.
|
|
7
|
+
preference :app_id, :string, default: ''
|
|
8
|
+
preference :secret_api_key, :string, default: ''
|
|
9
|
+
|
|
10
|
+
# Blank accepts level_1a or above — the lowest level whose hash formula includes
|
|
11
|
+
# `phone`, which is how a Wing customer is matched to an existing account. Set it
|
|
12
|
+
# to pin verification to exactly one level: level_0, level_1, level_1a, level_1b,
|
|
13
|
+
# level_2 or level_3.
|
|
14
|
+
preference :data_level, :string, default: ''
|
|
15
|
+
|
|
16
|
+
def self.credential
|
|
17
|
+
:secret_api_key
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.preference_groups
|
|
21
|
+
{
|
|
22
|
+
credentials: %i[app_id secret_api_key],
|
|
23
|
+
verification: %i[data_level],
|
|
24
|
+
ip_rules: IP_RULES,
|
|
25
|
+
rate_limits: RATE_LIMITS
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -2,6 +2,13 @@ module SpreeCmCommissioner
|
|
|
2
2
|
module OauthApplicationDecorator
|
|
3
3
|
def self.prepended(base)
|
|
4
4
|
base.belongs_to :tenant, class_name: 'SpreeCmCommissioner::Tenant'
|
|
5
|
+
base.include SpreeCmCommissioner::OauthApplication::MiniApp
|
|
6
|
+
|
|
7
|
+
# The only record of a mini app's past API keys. MiniAppProvider::Base#replace_api_key!
|
|
8
|
+
# overwrites the key and its stamp in place, so without this a rotation leaves nothing behind to
|
|
9
|
+
# answer what the old key was, who replaced it, or when. `versions.object` carries the whole
|
|
10
|
+
# previous row, the mini app's `settings` with it.
|
|
11
|
+
base.has_paper_trail
|
|
5
12
|
|
|
6
13
|
# treat a key inside a JSONB column like a normal Rails attribute,
|
|
7
14
|
# with getter/setter methods
|
|
@@ -6,9 +6,18 @@ module SpreeCmCommissioner
|
|
|
6
6
|
validates :dynamic_field_id, uniqueness: { scope: :product_id }
|
|
7
7
|
|
|
8
8
|
after_commit :enqueue_data_fill_stage_maintenance, on: %i[create]
|
|
9
|
+
after_commit :enqueue_product_cache_invalidation
|
|
9
10
|
|
|
10
11
|
private
|
|
11
12
|
|
|
13
|
+
# Purges the CDN-cached storefront dynamic_fields response for this product. Also covers
|
|
14
|
+
# DynamicField destroy, which cascades here via dependent: :destroy.
|
|
15
|
+
def enqueue_product_cache_invalidation
|
|
16
|
+
SpreeCmCommissioner::MaintenanceTasks::CacheInvalidation
|
|
17
|
+
.pending
|
|
18
|
+
.create_or_find_by(maintainable_type: 'Spree::Product', maintainable_id: product_id)
|
|
19
|
+
end
|
|
20
|
+
|
|
12
21
|
# Only newly-added fields need active recompute: adding a field can make an
|
|
13
22
|
# already-complete guest newly incomplete, which needs a persisted-column fix and a
|
|
14
23
|
# catch-up notification. Removing a field can only shrink the required set, which
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module SpreeCmCommissioner
|
|
2
2
|
class UserIdentityProvider < Base
|
|
3
|
-
enum identity_type: { :google => 0, :apple => 1, :facebook => 2, :telegram => 3, :vattanac_bank => 4 }
|
|
3
|
+
enum identity_type: { :google => 0, :apple => 1, :facebook => 2, :telegram => 3, :vattanac_bank => 4, :acleda_bank => 5, :wing_bank => 6 }
|
|
4
4
|
|
|
5
5
|
belongs_to :user, class_name: Spree.user_class.to_s, optional: false
|
|
6
6
|
has_one :tenant, through: :user
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module MiniAppProvider
|
|
3
|
+
# The keys an application used to hold, newest first, read out of its PaperTrail versions.
|
|
4
|
+
#
|
|
5
|
+
# A version's `object` is the row *before* that change, so its `api_key` is a key that was current
|
|
6
|
+
# until then and its `settings` carry that key's own stamp.
|
|
7
|
+
class ApiKeyHistory
|
|
8
|
+
Entry = Struct.new(:version_id, :key, :created_at, :created_by, :source, :revoked, keyword_init: true)
|
|
9
|
+
|
|
10
|
+
# How many versions to read, not how many keys come back: most versions record a settings change
|
|
11
|
+
# and carry the key unchanged, so a window of 50 is what it takes to reach the last few keys. It
|
|
12
|
+
# has to be bounded at all because this runs on every render of the edit page and versions are
|
|
13
|
+
# never pruned.
|
|
14
|
+
LIMIT = 50
|
|
15
|
+
|
|
16
|
+
def initialize(application)
|
|
17
|
+
@application = application
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Deduplicated, because saving without touching the key writes the same one into another version,
|
|
21
|
+
# and the key in use now is left out — restoring it would do nothing.
|
|
22
|
+
def entries
|
|
23
|
+
seen = [current_ciphertext].compact
|
|
24
|
+
|
|
25
|
+
versions.filter_map do |version|
|
|
26
|
+
ciphertext = ciphertext_in(version)
|
|
27
|
+
next if ciphertext.blank? || seen.include?(ciphertext)
|
|
28
|
+
|
|
29
|
+
seen << ciphertext
|
|
30
|
+
entry_for(version, ciphertext)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The plaintext a version recorded, or nil if that version is gone or no longer decryptable — an
|
|
35
|
+
# encryption key rotation leaves the ciphertext unreadable, and a caller must not take nil for a
|
|
36
|
+
# key worth saving.
|
|
37
|
+
def key_for(version_id)
|
|
38
|
+
version = versions.find { |candidate| candidate.id.to_s == version_id.to_s }
|
|
39
|
+
return nil if version.nil?
|
|
40
|
+
|
|
41
|
+
decrypt(ciphertext_in(version))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
attr_reader :application
|
|
47
|
+
|
|
48
|
+
def versions
|
|
49
|
+
@versions ||= application.versions.reorder(id: :desc).limit(LIMIT).to_a
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def entry_for(version, ciphertext)
|
|
53
|
+
return nil if decrypt(ciphertext).blank?
|
|
54
|
+
|
|
55
|
+
stamp = stamp_in(version)
|
|
56
|
+
|
|
57
|
+
Entry.new(
|
|
58
|
+
version_id: version.id,
|
|
59
|
+
key: decrypt(ciphertext),
|
|
60
|
+
created_at: parse_time(stamp['api_key_created_at']),
|
|
61
|
+
created_by: author(stamp['api_key_created_by']),
|
|
62
|
+
source: stamp['api_key_source'],
|
|
63
|
+
revoked: revoked?(version, ciphertext)
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The key this one gave way to. nil there means it was cleared rather than replaced — a revoke,
|
|
68
|
+
# which is what a leak is answered with, so the list says so rather than offering it silently.
|
|
69
|
+
def revoked?(version, ciphertext)
|
|
70
|
+
index = versions.index(version)
|
|
71
|
+
successor = index.zero? ? current_ciphertext : ciphertext_in(versions[index - 1])
|
|
72
|
+
|
|
73
|
+
successor.blank? && ciphertext.present?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def attributes_in(version)
|
|
77
|
+
@attributes_in ||= {}
|
|
78
|
+
@attributes_in[version.id] ||= YAML.unsafe_load(version.object.to_s) || {}
|
|
79
|
+
rescue Psych::Exception, ArgumentError
|
|
80
|
+
{}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def ciphertext_in(version)
|
|
84
|
+
attributes_in(version)['api_key']
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def stamp_in(version)
|
|
88
|
+
settings = attributes_in(version)['settings']
|
|
89
|
+
settings = JSON.parse(settings) if settings.is_a?(String)
|
|
90
|
+
|
|
91
|
+
(settings || {})[application.provider.to_s] || {}
|
|
92
|
+
rescue JSON::ParserError
|
|
93
|
+
{}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Raw SQL, because reading the attribute would hand back the decrypted key and there would be
|
|
97
|
+
# nothing left to compare the versions' ciphertext against.
|
|
98
|
+
def current_ciphertext
|
|
99
|
+
@current_ciphertext ||= application.class.connection.select_value(
|
|
100
|
+
application.class.where(id: application.id).select(:api_key).to_sql
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def decrypt(ciphertext)
|
|
105
|
+
return nil if ciphertext.blank?
|
|
106
|
+
|
|
107
|
+
@decrypted ||= {}
|
|
108
|
+
@decrypted[ciphertext] ||= application.class.type_for_attribute(:api_key).deserialize(ciphertext)
|
|
109
|
+
rescue StandardError
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# `settings` is hand-editable JSON, so the stamp may not parse. A row with no date still names a
|
|
114
|
+
# key worth restoring.
|
|
115
|
+
def parse_time(value)
|
|
116
|
+
Time.zone.parse(value.to_s)
|
|
117
|
+
rescue ArgumentError
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def author(id)
|
|
122
|
+
return nil if id.blank?
|
|
123
|
+
|
|
124
|
+
Spree.user_class.find_by(id: id)&.email || id
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require 'ipaddr'
|
|
2
|
+
|
|
3
|
+
module SpreeCmCommissioner
|
|
4
|
+
module MiniAppProvider
|
|
5
|
+
# Reads a mini app's address list — a free-form set of addresses or CIDR ranges,
|
|
6
|
+
# `1.2.3.4`, `203.0.113.0/24`, IPv6 alike, separated by commas, spaces or newlines.
|
|
7
|
+
# MiniAppProvider::Base#ip_allowed? is what matches an address against them.
|
|
8
|
+
class IpRules
|
|
9
|
+
def self.parse(value)
|
|
10
|
+
value.to_s.split(/[,\s]+/).filter_map do |entry|
|
|
11
|
+
IPAddr.new(entry)
|
|
12
|
+
rescue IPAddr::Error
|
|
13
|
+
nil
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# True when every entry is a usable address or range, so a typo is caught on save
|
|
18
|
+
# rather than by locking the mini app out on its next request.
|
|
19
|
+
def self.valid?(value)
|
|
20
|
+
entries = value.to_s.split(/[,\s]+/)
|
|
21
|
+
|
|
22
|
+
entries.size == parse(value).size
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module MiniAppProvider
|
|
3
|
+
# Who is calling, from where, and how often — the three questions every mini app session
|
|
4
|
+
# asks, answered once so each new mini app inherits them.
|
|
5
|
+
#
|
|
6
|
+
# It answers them, it does not sequence them: SessionThrottle has to run after the mini
|
|
7
|
+
# app's own signature check, so each service keeps its own order. With no application row
|
|
8
|
+
# nothing is configured, so everything is allowed — the signature is what guards the
|
|
9
|
+
# endpoint, not this.
|
|
10
|
+
class SessionGate
|
|
11
|
+
def initialize(provider:, api_key: nil, ip: nil)
|
|
12
|
+
@provider = provider.to_s
|
|
13
|
+
@api_key = api_key
|
|
14
|
+
@ip = ip
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# A mini app we take server-to-server calls from must always name itself, so only a key
|
|
18
|
+
# that finds its row passes. One whose WebView the partner merely opens has no request to
|
|
19
|
+
# put a header on — the customer's browser arrives — so it sends no key and passes on
|
|
20
|
+
# provider alone, even before anyone has configured a row for it.
|
|
21
|
+
def api_key_accepted?
|
|
22
|
+
return mini_app.present? if key_required?
|
|
23
|
+
|
|
24
|
+
true
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def address_allowed?
|
|
28
|
+
mini_app.nil? || ip.blank? || mini_app.ip_allowed?(ip)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# `phone` must be the normalized intel_phone_number — see SessionThrottle.
|
|
32
|
+
def session_exceeded?(phone: nil)
|
|
33
|
+
throttle(phone)&.session_exceeded? || false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def new_user_exceeded?(phone: nil)
|
|
37
|
+
throttle(phone)&.new_user_exceeded? || false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The configured mini app, or nil when none is set up. Carries the credential each
|
|
41
|
+
# mini app checks its own callers with.
|
|
42
|
+
#
|
|
43
|
+
# .mini_app_for takes the key as a required keyword so it can never answer as though a
|
|
44
|
+
# missing one had been checked, which is why the caller picks the lookup rather than
|
|
45
|
+
# passing a key that may be nil.
|
|
46
|
+
def mini_app
|
|
47
|
+
return @mini_app if defined?(@mini_app)
|
|
48
|
+
|
|
49
|
+
@mini_app = if api_key.present?
|
|
50
|
+
Spree::OauthApplication.mini_app_for(provider, api_key: api_key)
|
|
51
|
+
else
|
|
52
|
+
Spree::OauthApplication.keyless_mini_app_for(provider)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
attr_reader :provider, :api_key, :ip
|
|
59
|
+
|
|
60
|
+
# Asked of the registry rather than of a row: a call with no key may have no row to ask,
|
|
61
|
+
# and that is exactly the case this has to answer for.
|
|
62
|
+
#
|
|
63
|
+
# Fully qualified — a bare `Base` resolves up the nesting to SpreeCmCommissioner::Base.
|
|
64
|
+
def key_required?
|
|
65
|
+
provider_class = SpreeCmCommissioner::MiniAppProvider::Base.providers.find do |klass|
|
|
66
|
+
klass.provider == provider
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
provider_class&.supports_api_key? || false
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def throttle(phone)
|
|
73
|
+
return nil if mini_app.nil?
|
|
74
|
+
|
|
75
|
+
SpreeCmCommissioner::MiniAppProvider::SessionThrottle.new(mini_app: mini_app, ip: ip, phone: phone)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module MiniAppProvider
|
|
3
|
+
# Runs *after* the signature is verified, so a trip here means correctly signed traffic is
|
|
4
|
+
# flooding us — the signal that a mini app's key leaked — not unsigned internet noise.
|
|
5
|
+
# Rack::Attack sits in front of Rails and cannot tell those apart, hence not middleware.
|
|
6
|
+
#
|
|
7
|
+
# #new_user_exceeded? is the cap that survives IP rotation: minting accounts is the damage
|
|
8
|
+
# a leaked key actually does, and returning customers never consume that quota.
|
|
9
|
+
class SessionThrottle
|
|
10
|
+
WINDOW = 60 # seconds
|
|
11
|
+
|
|
12
|
+
# `phone` must be the normalized intel_phone_number, not what the caller sent: keying on
|
|
13
|
+
# the raw string multiplies an allowance by alternating 012345678 and +85512345678.
|
|
14
|
+
def initialize(mini_app:, ip:, phone:)
|
|
15
|
+
@mini_app = mini_app
|
|
16
|
+
@ip = ip
|
|
17
|
+
@phone = phone
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def session_exceeded?
|
|
21
|
+
(ip.present? && exceeded?(scope: 'ip', key: "ip:#{ip}", limit: mini_app.preferred_max_sessions_per_ip)) ||
|
|
22
|
+
(phone.present? && exceeded?(scope: 'phone', key: "phone:#{phone}", limit: mini_app.preferred_max_sessions_per_phone))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def new_user_exceeded?
|
|
26
|
+
exceeded?(scope: 'new_user', key: 'new_users', limit: mini_app.preferred_max_new_users)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
attr_reader :mini_app, :ip, :phone
|
|
32
|
+
|
|
33
|
+
def exceeded?(scope:, key:, limit:)
|
|
34
|
+
namespaced_key = "throttle:mini_app:#{mini_app.provider}:#{mini_app.id}:#{key}"
|
|
35
|
+
|
|
36
|
+
result = SpreeCmCommissioner::RequestGuard.with_request_guard do
|
|
37
|
+
SpreeCmCommissioner::RateLimiter.call(key: namespaced_key, limit: limit, window: WINDOW)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# nil means the guard skipped the block for an exempt (load-test) request.
|
|
41
|
+
return false if result.nil? || !result.exceeded?
|
|
42
|
+
|
|
43
|
+
log_throttled(scope, namespaced_key, result)
|
|
44
|
+
true
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def log_throttled(scope, key, result)
|
|
48
|
+
CmAppLogger.error(
|
|
49
|
+
label: 'Mini app session throttled',
|
|
50
|
+
data: {
|
|
51
|
+
scope: scope,
|
|
52
|
+
key: key,
|
|
53
|
+
count: result.count,
|
|
54
|
+
limit: result.limit,
|
|
55
|
+
window: WINDOW,
|
|
56
|
+
provider: mini_app.provider,
|
|
57
|
+
mini_app_id: mini_app.id,
|
|
58
|
+
client_id: mini_app.uid,
|
|
59
|
+
ip: ip,
|
|
60
|
+
phone: phone
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
module SpreeCmCommissioner
|
|
2
2
|
class PaymentMethodTypeMapper
|
|
3
3
|
TYPES = {
|
|
4
|
-
'
|
|
4
|
+
'vattanac' => 'Spree::Gateway::VattanacMiniApp',
|
|
5
|
+
'acleda' => 'Spree::Gateway::AcledaMiniApp',
|
|
6
|
+
'wing' => 'Spree::Gateway::WingMiniApp'
|
|
5
7
|
}.freeze
|
|
6
8
|
|
|
7
9
|
def self.call(type)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
class RateLimiter
|
|
3
|
+
# Outcome of one increment: hits seen in the current window, the cap that
|
|
4
|
+
# applied, and whether this call went over it. A plain class rather than a
|
|
5
|
+
# Struct so `count` stays a hit count and not Enumerable#count.
|
|
6
|
+
class Result
|
|
7
|
+
attr_reader :count, :limit
|
|
8
|
+
|
|
9
|
+
def initialize(count:, limit:, exceeded:)
|
|
10
|
+
@count = count
|
|
11
|
+
@limit = limit
|
|
12
|
+
@exceeded = exceeded
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def exceeded?
|
|
16
|
+
@exceeded
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|