fix(auth): return expires_in on token responses so the client refreshes early

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) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-07 19:54:27 +02:00
parent 2e4fe2a8fe
commit 671323b9ac
2 changed files with 12 additions and 2 deletions

View File

@@ -84,6 +84,7 @@ export default async function authRoutes(
setRefreshCookie(reply, result.refreshToken, remember_me); setRefreshCookie(reply, result.refreshToken, remember_me);
return success(reply, { return success(reply, {
access_token: result.accessToken, access_token: result.accessToken,
expires_in: config.jwt.accessTokenExpiry,
user: result.user, user: result.user,
}); });
}, },
@@ -249,7 +250,11 @@ export default async function authRoutes(
description: `TOTP přihlášení uživatele ${user.username}`, 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); setRefreshCookie(reply, result.refreshToken, result.rememberMe);
return success(reply, { return success(reply, {
access_token: result.accessToken, access_token: result.accessToken,
expires_in: config.jwt.accessTokenExpiry,
user: result.user, user: result.user,
}); });
}, },

View File

@@ -414,7 +414,11 @@ export default async function totpRoutes(
description: `Backup code login for user ${user.username}`, 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,
});
}, },
); );
} }