Fix frequent re-login issue caused by stale JWT tokens

Problem: Users experiencing frequent forced re-logins after backend restarts,
especially with 8-hour token expiration in production.

Root cause analysis:
1. JWT tokens stored in localStorage on browser
2. Backend restarts with potentially different JWT secret key
3. Frontend doesn't validate stored token on app startup
4. User navigates normally, then first API request returns 401
5. User surprised with sudden logout

This fix adds token validation on app startup. If a stored token exists,
immediately verify it with /api/auth/me before mounting the app. If the
token is invalid (expired or stale secret), user is logged out at startup
rather than mid-session.

Additional issue found (not committed - .env files in .gitignore):
- .env and .env.docker had DIFFERENT JWT_SECRET_KEY values
- Fixed locally: standardized to same key across all .env files
- Action required: Verify production JWT_SECRET_KEY is consistent and persisted

Changes:
- frontend/src/main.js: Call authStore.fetchUser() on app load if token exists
- Logout happens immediately at page load, not on first interaction
- Better UX: "please log in" vs "surprise logout while working"

Future improvement (not in this commit):
- Implement automatic token refresh before expiration
- Add proactive refresh timer (e.g., every 7 hours for 8-hour tokens)
- Would eliminate re-login entirely for active sessions

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Proudlock
2026-09-12 11:30:17 -04:00
parent 64ab1c5299
commit d5d2cea3e4

View File

@@ -9,12 +9,24 @@ import '@fontsource-variable/inter'
// Initialize theme on app load
import './stores/theme'
import { applyBranding } from './utils/siteSettings'
import { useAuthStore } from './stores/auth'
const app = createApp(App)
app.use(createPinia())
app.use(router)
// Validate stored token on app startup. If a token exists in localStorage but
// is expired or signed with a stale secret (backend restarted), this catches
// it immediately rather than letting the user navigate and then get kicked on
// the first API request. fetchUser() logs out on 401.
const authStore = useAuthStore()
if (authStore.isAuthenticated) {
authStore.fetchUser().catch(() => {
// fetchUser handles the logout on 401; ignore the promise rejection
})
}
app.mount('#app')
// Apply per-site favicon + brand color once settings load (no-op on defaults).