From 671323b9ac3e02c450289ceef1d4a8626b11e90f Mon Sep 17 00:00:00 2001 From: BOHA Date: Sun, 7 Jun 2026 19:54:27 +0200 Subject: [PATCH] fix(auth): return expires_in on token responses so the client refreshes early MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The login / TOTP / refresh endpoints returned { access_token, user } with no expires_in. The client (AuthContext.setAccessTokenFn) therefore fell back to ttl=900s while the dev access token actually lives 60s (ACCESS_TOKEN_EXPIRY). So the client believed every token was valid for 15 min, kept handing it out (getAccessTokenFn only nulls 30s before the *believed* expiry) and scheduled its proactive refresh 14 min out. The token silently died at 60s, the next request (e.g. the offer lock heartbeat) hit a real 401, and apiFetch refreshed reactively — producing the steady "…heartbeats → 401 → refresh → …" cycle. Now every access-token response includes expires_in = accessTokenExpiry, so the client tracks the true lifetime and refreshes BEFORE expiry (proactive up-front refresh via the 30s-early null window). No more reactive 401s. In production (900s) behaviour is unchanged; it just makes the client honour the real TTL. Verified in Chrome: /api/admin/refresh now returns expires_in:60, and 11 consecutive heartbeats across >1 token cycle were all 200 with one proactive refresh and zero 401s. tsc -b --noEmit, npm run build, vitest 152/152 clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/routes/admin/auth.ts | 8 +++++++- src/routes/admin/totp.ts | 6 +++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/routes/admin/auth.ts b/src/routes/admin/auth.ts index 4f4dc2b..6f9b8ff 100644 --- a/src/routes/admin/auth.ts +++ b/src/routes/admin/auth.ts @@ -84,6 +84,7 @@ export default async function authRoutes( setRefreshCookie(reply, result.refreshToken, remember_me); return success(reply, { access_token: result.accessToken, + expires_in: config.jwt.accessTokenExpiry, user: result.user, }); }, @@ -249,7 +250,11 @@ export default async function authRoutes( description: `TOTP přihlášení uživatele ${user.username}`, }); - return success(reply, { access_token: accessToken, user: authData }); + return success(reply, { + access_token: accessToken, + expires_in: config.jwt.accessTokenExpiry, + user: authData, + }); }, ); @@ -287,6 +292,7 @@ export default async function authRoutes( setRefreshCookie(reply, result.refreshToken, result.rememberMe); return success(reply, { access_token: result.accessToken, + expires_in: config.jwt.accessTokenExpiry, user: result.user, }); }, diff --git a/src/routes/admin/totp.ts b/src/routes/admin/totp.ts index 81918f0..9f63f56 100644 --- a/src/routes/admin/totp.ts +++ b/src/routes/admin/totp.ts @@ -414,7 +414,11 @@ export default async function totpRoutes( description: `Backup code login for user ${user.username}`, }); - return success(reply, { access_token: accessToken, user: authData }); + return success(reply, { + access_token: accessToken, + expires_in: config.jwt.accessTokenExpiry, + user: authData, + }); }, ); }