From cdacc8bc0da905e6150f4edde266728f27c42044 Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Tue, 5 May 2026 20:39:58 +0200 Subject: [PATCH] fix(loyalty): make device tokens authenticate via require_module_access too Two follow-ups from the live smoke test: 1. The store router has two auth gates: its own get_current_store_api (already taught about device tokens) and router-level require_module_access("loyalty", FrontendType.STORE), which goes through get_current_store_from_cookie_or_header. That cookie-or-header variant didn't know about device tokens, so live curl with a paired device JWT was rejected with 401 "Authentication required". Tests passed only because dependency overrides bypass the module-access dep. Add the same _try_authenticate_terminal_device branch there. 2. Normalize the /merchants/loyalty/locations response in the devices Alpine factory: the endpoint returns {id, name, code} but the templates bind to loc.store_id/loc.store_name. Map both shapes so the pair-tablet store dropdown populates. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/api/deps.py | 6 ++++++ .../loyalty/static/shared/js/loyalty-devices-list.js | 12 +++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/app/api/deps.py b/app/api/deps.py index 011e2a47..c831e786 100644 --- a/app/api/deps.py +++ b/app/api/deps.py @@ -728,6 +728,12 @@ def get_current_store_from_cookie_or_header( logger.warning(f"Store auth failed: No token for {request.url.path}") raise InvalidTokenException("Store authentication required") + # Paired terminal-device tokens authenticate to the store API too. + # Cookie path won't carry a device JWT; this only matters for header. + device_ctx = _try_authenticate_terminal_device(token, db, request) + if device_ctx is not None: + return device_ctx + # Validate token and get user user = _validate_user_token(token, db) diff --git a/app/modules/loyalty/static/shared/js/loyalty-devices-list.js b/app/modules/loyalty/static/shared/js/loyalty-devices-list.js index 2c331ba0..8de43ea8 100644 --- a/app/modules/loyalty/static/shared/js/loyalty-devices-list.js +++ b/app/modules/loyalty/static/shared/js/loyalty-devices-list.js @@ -95,9 +95,15 @@ function loyaltyDevicesList(config) { async loadLocations() { try { const response = await apiClient.get(locationsPrefix + '/locations'); - if (response) { - this.locations = Array.isArray(response) ? response : (response.locations || []); - } + if (!response) return; + const raw = Array.isArray(response) ? response : (response.locations || []); + // Endpoint returns {id, name, code}; templates bind to store_id/store_name. + // Normalize so callers don't have to care about either shape. + this.locations = raw.map(loc => ({ + store_id: loc.store_id ?? loc.id, + store_name: loc.store_name ?? loc.name, + store_code: loc.store_code ?? loc.code, + })); } catch (error) { loyaltyDevicesListLog.warn('Failed to load locations:', error.message); }