Fix frontend settings auth wiring#32
Conversation
Co-authored-by: up2itnow0822 <up2itnow0822@gmail.com>
Co-authored-by: up2itnow0822 <up2itnow0822@gmail.com>
| export function persistSessionAuthToken(token: string): void { | ||
| const normalizedToken = token.trim(); | ||
| if (!normalizedToken) return; | ||
|
|
||
| browserSessionStorage()?.setItem(AUTH_TOKEN_KEY, normalizedToken); | ||
| } |
There was a problem hiding this comment.
No path to clear a session auth token once set
persistSessionAuthToken treats an empty string as a no-op and leaves the old token in sessionStorage untouched. The corresponding UI in both the settings page and modal also clears the input on open, so a user who enters a wrong or revoked token and then opens Settings again will find the field empty. Leaving it blank and saving does nothing — the broken token remains active for the rest of the browser session. The only recovery is closing the browser tab. A clearSessionAuthToken() export (e.g., browserSessionStorage()?.removeItem(AUTH_TOKEN_KEY)) plus a "Clear token" control in the UI would let users recover without losing their entire session.
| export function persistBackendBaseUrl(url: string): void { | ||
| const storage = browserLocalStorage(); | ||
| if (!storage) return; | ||
|
|
||
| const normalizedUrl = url.trim(); | ||
| storage.setItem(BACKEND_BASE_URL_KEY, normalizedUrl); | ||
| for (const legacyKey of LEGACY_BACKEND_BASE_URL_KEYS) { | ||
| storage.removeItem(legacyKey); | ||
| } | ||
| } |
There was a problem hiding this comment.
Legacy
auth_token in localStorage left behind after migration
The URL migration explicitly removes old localStorage keys (api_url, agentnexus_backend_url) so they don't shadow the new canonical key. The auth token migration has no equivalent cleanup: any auth_token entry in localStorage from a prior session (manually set or from an older code path) is never removed. It won't be read by the new sessionStorage-based getter, but it does sit indefinitely in localStorage as a stale credential. Removing it alongside the URL cleanup would be consistent.
| export function persistBackendBaseUrl(url: string): void { | |
| const storage = browserLocalStorage(); | |
| if (!storage) return; | |
| const normalizedUrl = url.trim(); | |
| storage.setItem(BACKEND_BASE_URL_KEY, normalizedUrl); | |
| for (const legacyKey of LEGACY_BACKEND_BASE_URL_KEYS) { | |
| storage.removeItem(legacyKey); | |
| } | |
| } | |
| export function persistBackendBaseUrl(url: string): void { | |
| const storage = browserLocalStorage(); | |
| if (!storage) return; | |
| const normalizedUrl = url.trim(); | |
| storage.setItem(BACKEND_BASE_URL_KEY, normalizedUrl); | |
| for (const legacyKey of LEGACY_BACKEND_BASE_URL_KEYS) { | |
| storage.removeItem(legacyKey); | |
| } | |
| // Remove the legacy localStorage auth-token key to avoid stale credential leakage | |
| storage.removeItem(AUTH_TOKEN_KEY); | |
| } |
| const normalizedUrl = url.trim(); | ||
| storage.setItem(BACKEND_BASE_URL_KEY, normalizedUrl); |
There was a problem hiding this comment.
persistBackendBaseUrl('') stores an empty string that is silently ignored on read
When the user clears the API Endpoint field and saves, normalizedUrl becomes '' and storage.setItem(BACKEND_BASE_URL_KEY, '') is called. getBackendBaseUrl then evaluates Boolean('') as false and falls back to the env-var default, meaning the stored empty string has no effect but the legacy keys have already been removed. A user who intended to "reset to default" loses their legacy-stored URL permanently and silently gets the env default instead. Adding an early return (matching the pattern in persistSessionAuthToken) avoids the ambiguous write.
| const normalizedUrl = url.trim(); | |
| storage.setItem(BACKEND_BASE_URL_KEY, normalizedUrl); | |
| const normalizedUrl = url.trim(); | |
| if (!normalizedUrl) return; | |
| storage.setItem(BACKEND_BASE_URL_KEY, normalizedUrl); |
| it('forwards the session token to the configured backend URL on API requests', async () => { | ||
| let receivedRequest; | ||
| const server = http.createServer((req, res) => { | ||
| res.setHeader('Access-Control-Allow-Origin', 'http://localhost'); | ||
| res.setHeader('Access-Control-Allow-Headers', 'authorization,content-type'); | ||
| if (req.method === 'OPTIONS') { | ||
| res.writeHead(204); | ||
| res.end(); | ||
| return; | ||
| } | ||
|
|
||
| receivedRequest = req; | ||
| res.writeHead(200, { 'Content-Type': 'application/json' }); | ||
| res.end('[]'); | ||
| }); | ||
|
|
||
| const address = await listen(server); | ||
| persistBackendBaseUrl(`http://127.0.0.1:${address.port}`); | ||
| persistSessionAuthToken(' protected-token '); | ||
|
|
||
| try { | ||
| await api.get('/agents'); | ||
| } finally { | ||
| await close(server); | ||
| } | ||
|
|
||
| expect(receivedRequest.url).toBe('/api/agents'); | ||
| expect(receivedRequest.headers.authorization).toBe('Bearer protected-token'); | ||
| }); |
There was a problem hiding this comment.
Integration test relies on jsdom's XHR reaching a real 127.0.0.1 socket
The test environment is jest-environment-jsdom, which means axios picks the XHR adapter (because window.XMLHttpRequest is present). jsdom's XHR implementation does not make real network calls in all versions — whether this works depends on the exact jsdom/jest-environment-jsdom version. If the XHR silently fails, api.get('/agents') throws before receivedRequest is set; the finally closes the server and the error propagates, which is correct test failure behaviour. But the test is more fragile than it looks. Forcing the Node.js http adapter (e.g., axios.defaults.adapter = 'http') or switching to a node test environment for this file would make it deterministically reliable.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Co-authored-by: up2itnow0822 <up2itnow0822@gmail.com>


Bug and impact
The frontend settings UI saved backend endpoint values under keys that the API client did not read, and the API token input was never persisted to the request path. After a user saved Settings and triggered protected actions such as agent execution or builder calls, requests were sent to the wrong/default backend and without
Authorization, causing backend 401 failures.Root cause
frontend/src/lib/api.tsreadapi_urlandauth_tokenfrom localStorage, while the settings page/modal wrote different endpoint keys and never connected the token input to the interceptor.Fix
/apibase URL and bearer token are used.Validation
Passed:
pnpm --filter @agentnexus/frontend test -- settings-storage.test.js --runInBandpnpm --filter @agentnexus/frontend test -- --runInBandpnpm --filter @agentnexus/frontend type-checkpython3 -m pytest agents/paper-trader/test_price_feed.pyPre-existing blockers observed outside this fix:
pnpm --filter @agentnexus/frontend lintfails becausenext lintis incompatible with the installed Next CLI.pnpm --filter @agentnexus/frontend buildfails in the existing wagmi/viem dependency graph (sendCallsSync/sendTransactionSyncmissing fromviem/actions).python3 -m ruff check ...,python3 -m bandit ..., andpython3 -m mdformat --check ...report pre-existing issues unrelated to the frontend settings/auth changes.Greptile Summary
This PR fixes a long-standing settings/auth wiring bug where the settings UI wrote keys that the API client never read, and the auth token was never forwarded to requests. A shared
settings-storage.tshelper is introduced so both the settings page and modal write to the same canonicalbackend_base_urlandauth_tokenkeys, which the axios client now reads per-request via interceptors.settings-storage.tscentralises URL persistence (with legacy-key migration) and session-only token storage; both settings entry points are updated to use it.api.tsgains a request interceptor that re-reads the backend URL and session token before each request, replacing the old stale module-levellocalStorage.getItem('api_url')lookup.Confidence Score: 4/5
The core auth wiring fix is correct and well-structured; the token-clearing limitation means a user with a revoked session token has no recovery path short of closing the browser tab.
The key alignment and per-request interceptor pattern cleanly solves the stated bug. However, persistSessionAuthToken treats an empty input as a no-op, so a user who saves Settings after clearing the API key field will silently retain whatever broken token was already stored in sessionStorage — there is no way to revoke it within the same browser session without closing the tab.
frontend/src/lib/settings-storage.ts deserves a second look for the token-clearing path before merging.
Important Files Changed
Sequence Diagram
sequenceDiagram participant UI as Settings Page / Modal participant SS as settings-storage.ts participant LS as localStorage participant SES as sessionStorage participant API as api.ts (axios) participant BE as Backend UI->>SS: persistBackendBaseUrl(url) SS->>LS: setItem('backend_base_url', url) SS->>LS: removeItem('api_url') [legacy] SS->>LS: removeItem('agentnexus_backend_url') [legacy] UI->>SS: persistSessionAuthToken(token) SS->>SES: setItem('auth_token', token) Note over API: Request interceptor #1 (per-request) API->>SS: getBackendBaseUrl(fallback) SS->>LS: getItem('backend_base_url') LS-->>SS: stored URL SS-->>API: resolved base URL API->>API: "config.baseURL = resolvedUrl + '/api'" Note over API: Request interceptor #2 (per-request) API->>SS: getSessionAuthToken() SS->>SES: getItem('auth_token') SES-->>SS: token SS-->>API: token API->>API: "config.headers.Authorization = 'Bearer ' + token" API->>BE: HTTP request with correct baseURL + Bearer tokenReviews (2): Last reviewed commit: "fix: align viem with wagmi runtime" | Re-trigger Greptile