fix(loyalty): make device tokens authenticate via require_module_access too
Some checks failed
CI / ruff (push) Successful in 15s
CI / docs (push) Has been cancelled
CI / pytest (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled

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) <noreply@anthropic.com>
This commit is contained in:
2026-05-05 20:39:58 +02:00
parent 6276e9e3ac
commit cdacc8bc0d
2 changed files with 15 additions and 3 deletions

View File

@@ -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)

View File

@@ -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);
}