Skip to content

fix: make QR-code login and endpoint switch reliable (PROD-8200) - #247

Open
deaflynx wants to merge 5 commits into
thingsboard:develop/1.9.0from
deaflynx:fix/prod-8200-qr-host-switch
Open

fix: make QR-code login and endpoint switch reliable (PROD-8200)#247
deaflynx wants to merge 5 commits into
thingsboard:develop/1.9.0from
deaflynx:fix/prod-8200-qr-host-switch

Conversation

@deaflynx

Copy link
Copy Markdown
Contributor

Fixes the QR-code login / endpoint switch flow. Related: PROD-8200, thingsboard/flutter_thingsboard_pe_app#303.

Root causes

  • Login and NotificationService captured the ThingsboardClient once at construction. After a QR switch re-creates the client (ITbClientService.reInit), they kept calling the old host with the new host's tokens → 401 Token is outdated, and the failed refresh wiped the fresh tokens from shared storage, undoing the switch. A full app restart "fixed" it because the client was re-captured.
  • switchEndpoint loaded the new tokens into the old client before switching and rolled back to the compiled default endpoint (not the previous one) on failure, losing the user's session.
  • reInit's onAuthError re-threw from inside the error callback → unhandled zone errors, and the noauth view rendered ThingsboardError.toString(), which embeds the stacktrace shown on screen.
  • A QR link without a secret (the mobile-app QR on the web login page) rendered a spinner forever.
  • The v2 router never subscribes to app links (TbContext.init is no longer called), so camera-scanned links were silently dropped.
  • Re-scanning the same QR after a logout: the server returns the same, already-revoked JWT pair (bound to the secret), sending the app into a silent login/refresh loop.
  • Best-effort calls right after login (getLoginMobileInfo, notification mobile-session sync, the client's internal version check) surfaced their expected 401/403 answers as "You don't have permission" toasts.

Changes

  • Read the live client via getter in Login and NotificationService.
  • Rework NoauthProvider.switchEndpoint: stage the exchanged pair in storage before reInit (the new client can only start with exactly these tokens), verify the pair with GET /api/auth/user on the target host before committing (a revoked pair now shows the server's message within seconds and rolls back), roll back to the previous endpoint on failure keeping the previous session, surface server error messages instead of raw Dio text.
  • Handle no-secret QR links as a host-only switch that lands on the new host's login page; SwitchEndpointArgs.secret is now optional.
  • Never render stacktraces in the noauth view; navigate on completion (login page for host-only switches, home for already-authenticated duplicates) with a fallback so the user is never stranded on the spinner.
  • Restore app-link handling in the app root (stream + cold-start link, platform duplicate deliveries dropped).
  • Pop the QR scanner once per scan (repeated MLKit detections crashed with GoError: There is nothing to pop).
  • Mark best-effort requests (getLoginMobileInfo, mobile-session calls, unread count) with ignoreErrors/ignoreLoading extras and suppress client-internal init errors so expected 401/403 answers no longer toast; guard fire-and-forget handleUserLoaded against unhandled async errors.

Testing

Verified on emulator (Android 16) against a local TB instance and demo.thingsboard.io, driving the same navigateByAppLink path the in-app scanner uses, plus on-device scanner runs:

  • form login, QR switch to another host with auto-login, same-host QR relogin, re-scan of the same QR within TTL (logs in again), re-scan after logout (clean server message + rollback, session-preserving), expired/invalid secret (server message + rollback), no-secret QR cross-host (lands on the new host's login page) and same-host (returns home), logout/login regression
  • zero unhandled exceptions across all runs, flutter analyze at the develop/1.9.0 baseline

- Read the live ThingsboardClient via getter in Login and
  NotificationService: after a QR endpoint switch re-creates the client,
  captured references kept hitting the old host with new tokens (401
  'Token is outdated'), and the failed refresh wiped the fresh session.
- Rework NoauthProvider.switchEndpoint: stage the exchanged JWT pair in
  storage before reInit so the new client can only start with exactly
  these tokens (a stale session left in storage can no longer win the
  race), roll back to the previous endpoint (not the compiled default)
  on failure, and surface the server's error message instead of raw
  Dio text.
- Handle QR links without a secret (the login-page app QR): switch the
  host and land on the new host's login page instead of spinning
  forever; SwitchEndpointArgs.secret is now optional.
- Never render ThingsboardError.toString() in the noauth view (it
  embeds the stacktrace); navigate on isDone and add a fallback so the
  user is never stranded on the spinner.
- Restore app-link handling in the v2 router (the TbContext listener is
  no longer initialized): listen in the app root, consume the cold-start
  link, and drop platform duplicate deliveries.
- Pop the QR scanner once per scan: repeated MLKit detections popped the
  route twice ('There is nothing to pop').
- Guard fire-and-forget handleUserLoaded calls against unhandled async
  errors.
…8200)

The dart client is autogenerated and must stay unmodified, so:

- TbClientService suppresses error toasts while client.init() runs
  (init and reInit): the client's internal best-effort version check
  hits /api/admin/updates, which answers 403 for non-SYS_ADMIN users
  and otherwise surfaced as an error toast on every (re)init. Real
  init failures still propagate and are handled by the callers.
- switchEndpoint re-applies the exchanged JWT pair to the new client
  if it comes out of reInit unauthenticated: a failing background
  refresh of the previous session may clear the shared token storage
  after the pair was staged but before init read it.
getLoginMobileInfo already has a graceful fallback (QR-only button
list), but its failure still surfaced through the interceptor's global
error channel as a 'You don't have permission' toast right after a
successful QR switch: some servers answer 403 for an unknown mobile
package. Pass ignoreErrors/ignoreLoading via the request extras so the
interceptor stays quiet; the fallback behavior is unchanged.
…-8200)

Two more sources of the 'You don't have permission' toast right after a
successful QR switch:

- NotificationService mobile-session calls (get/save/removeMobileSession,
  unread count) run against servers that may not know the mobile package
  and answer 403. Mark them ignoreErrors/ignoreLoading and guard the
  session sync so notification setup failures never surface to the user
  or abort init.
- The generated client delivers error callbacks via Future(() => cb()),
  so the internal version-check 403 raised during client.init() reaches
  onClientError one event-loop turn AFTER init() returns - just outside
  the suppression window. Release the suppression flag after a short
  grace period instead of synchronously.
The JWT pair returned for a QR secret is bound to the secret, so
re-scanning the same code after a logout hands the app tokens issued
before the logout watermark: the exchange succeeds but every
authenticated call answers 401 'Token is outdated', which sent the
switch into a silent ~10s login/refresh loop that ended on the login
page with no explanation.

Verify the pair with GET /api/auth/user on the target host before
committing the switch: a rejected pair now shows the server's message
within seconds and rolls back, same as an expired QR code.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant