Settings page overhaul, auth resilience, and automated testing #38
Reference in New Issue
Block a user
No description provided.
Delete Branch "staging"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Work Item
Summary
Changes
SettingsPageClient.tsxwith sidebar navigation covering profile, security activity (SecurityActivityCard.tsx), trusted devices (TrustedDevicesCard.tsx), and notification preferences with granular security alertslib/auth/context.tsxnow handles refresh token flow;lib/api/client.tspropagates Request ID headers; local storage management updatedlib/api/activity.tsandlib/api/devices.tsfor security activity and trusted device endpointslib/utils/errorHandler.tsandlib/utils/requestId.tsutilities for structured error handling and request tracingvitest.config.ts,vitest.setup.ts), middleware tests, and unit tests foruseOnlineStatus,useVisibilityPolling,errorHandler,logger, andrequestId.github/workflows/test.ymlruns tests on push withPKG_READ_TOKENfor private dependency access@ciphera-net/uito 0.0.74, added Vitest and testing-library dev dependenciesTest Plan
npm testlocally and verify all unit tests passGreptile Summary
This PR introduces a comprehensive settings page redesign, auth resilience improvements, and automated testing infrastructure. The settings page features an expandable sidebar with profile management, security activity logs, trusted devices, and granular notification preferences. Auth improvements include automatic refresh token handling, session expiry warnings, and cross-tab session synchronization using BroadcastChannel.
Key Changes:
SettingsPageClient.tsxwith expandable sidebar navigation,SecurityActivityCard.tsxfor audit logs,TrustedDevicesCard.tsxfor device management, and granular security alert toggles (login, password, 2FA alerts)lib/auth/context.tsxnow handles automatic token refresh on 401, displays session expiry warnings, and syncs logout/login events across browser tabslib/api/client.tsadds request ID propagation for debugging, automatic retry after token refresh, and GET request deduplication/caching (2s TTL)PKG_READ_TOKENfor private package access@ciphera-net/uifrom 0.0.69 to 0.0.78 (addsSessionExpiryWarninganduseSessionSync)Issues Found:
lib/api/user.tscontains dead code attempting to readrefreshTokenfrom localStorage, but refresh tokens are stored exclusively in httpOnly cookies. TheX-Current-Session-Hashheader will never be sent.Confidence Score: 4/5
getUserSessionswhere dead code tries to read a refresh token from localStorage that doesn't exist (tokens are in httpOnly cookies). This should be cleaned up but won't cause runtime errors since the code handles null gracefully.lib/api/user.ts- thegetUserSessionsfunction contains dead code that should be removed or fixedImportant Files Changed
Sequence Diagram
Last reviewed commit:
29e84e325 files reviewed, 5 comments
Edit Code Review Agent Settings | Greptile
@@ -0,0 +1,130 @@'use client'Duplicated
formatRelativeTime/formatFullDatehelpersformatRelativeTimeandformatFullDateare defined identically in bothTrustedDevicesCard.tsxandSecurityActivityCard.tsx. Consider extracting them to a shared utility (e.g.,lib/utils/formatDate.ts) to avoid the two implementations drifting out of sync.Prompt To Fix With AI
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!
Incomplete localStorage cleanup on cross-tab logout
The
onLogoutcallback inuseSessionSynconly removesuserfrom localStorage, whereas the mainlogout()function also clearsciphera_token_refreshed_atandciphera_last_activity. While the page redirect (window.location.href = '/') prevents these stale entries from causing authentication issues on reload, it's inconsistent and leaves orphaned keys for any logic that might read them before the redirect completes.Prompt To Fix With AI
@@ -0,0 +39,4 @@export function getLastRequestId(): string | null {return lastRequestId}Module-level singleton is unsafe for concurrent server-side use
lastRequestIdis a module-level mutable variable. In a Node.js server (SSR/server actions), module state is shared across all concurrent requests. IfapiRequestis called simultaneously from two server-side contexts, request A setslastRequestId = "REQ_A", then request B sets it to"REQ_B", and request A's error handler ends up reporting"REQ_B"— attributing the wrong request ID to the error.Today
apiRequestis primarily called from'use client'components (single-threaded browser), so the practical risk is low. However, to be safe, consider scoping the request ID to each call rather than storing it globally, e.g. by returning it fromgenerateRequestIdand passing it explicitly through error paths, or by using anAsyncLocalStoragecontext for server-side scenarios.Prompt To Fix With AI
Additional Comments (1)
app/settings/SettingsPageClient.tsxParent section header not highlighted for child selections
When the user navigates to "Trusted Devices" (
active.section === 'devices') or "Security Activity" (active.section === 'activity'), both of which live under the "Manage Account" parent in the sidebar, theSectionHeaderfor "Manage Account" showsactive={false}because neither'devices'nor'activity'equals'account'. The childSubItemis correctly highlighted, but the parent heading is not — leaving no visual breadcrumb to indicate which top-level group the user is in.Prompt To Fix With AI
Issue: The
onLogoutcallback inuseSessionSynconly removeduserfrom localStorage, while the mainlogout()function also clearsciphera_token_refreshed_atandciphera_last_activity.Fix: Added the two missing
localStorage.removeItemcalls toonLogoutso it matches the mainlogout()cleanup.Why: Orphaned keys could be read by session-related logic in the window between the removeItem call and the redirect completing.
@@ -0,0 +39,4 @@export function getLastRequestId(): string | null {return lastRequestId}Issue:
lastRequestIdis a module-level mutable variable. In a Node.js SSR context, concurrent requests could overwrite each other's request IDs.Fix: Added documentation explicitly scoping this module to client-side use only.
apiRequest(the sole caller ofsetLastRequestId) runs exclusively in the browser where JS is single-threaded, so no concurrent mutation can occur.Why: The practical risk is zero today, but the intent constraint should be documented so future server-side callers know to use a different pattern (e.g.
AsyncLocalStorage).@@ -0,0 +1,130 @@'use client'Issue:
formatRelativeTimeandformatFullDatewere defined identically in bothTrustedDevicesCard.tsxandSecurityActivityCard.tsx.Fix: Extracted both functions to a shared
lib/utils/formatDate.tsmodule and replaced the inline definitions with imports.Why: Two copies can drift out of sync silently, and the functions are generic enough to be reused elsewhere.
Additional Comments (1)
lib/api/user.ts, line 32dead code - refresh tokens are stored in httpOnly cookies, not localStorage
This code tries to read
refreshTokenfrom localStorage and hash it, but the auth implementation (seelib/auth/context.tsxandmiddleware.ts) stores refresh tokens exclusively in httpOnly cookies.localStorage.getItem('refreshToken')will always return null, socurrentTokenHashwill always be empty and theX-Current-Session-Hashheader will never be sent.Either remove this dead code if the backend doesn't need this header anymore, or find an alternative way to identify the current session (backend could use the
refresh_tokencookie directly).