Skip to content

Commit 8a3c6dc

Browse files
fix: autoRefresh not working due to stale closure and missing in client config (#14612)
### What? Fixes `autoRefresh` functionality that was causing "Stay logged in" modal to appear instead of automatically refreshing the token. ### Why? Two issues were preventing `autoRefresh` from working: 1. `autoRefresh` was not exposed in the client config, causing it to be `undefined` in the browser 2. The Auth provider's reminder timeout callback was capturing a stale `autoRefresh` value due to closure timing during initial component render ### How? - Added `autoRefresh` to client config creation to expose it to the client - Used `useEffectEvent` in Auth provider to ensure the reminder timeout handler always captures the latest `autoRefresh` value ### Fixes #14613 --------- Co-authored-by: Patrik Kozak <35232443+PatrikKozak@users.noreply.github.com>
1 parent 9b6e1a3 commit 8a3c6dc

File tree

5 files changed

+74
-9
lines changed

5 files changed

+74
-9
lines changed

packages/payload/src/config/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import type { DeepPartial } from 'ts-essentials'
44
import type { ImportMap } from '../bin/generateImportMap/index.js'
55
import type { ClientBlock } from '../fields/config/types.js'
66
import type { BlockSlug, TypedUser } from '../index.js'
7-
import type { PayloadRequest } from '../types/index.js'
87
import type {
98
RootLivePreviewConfig,
109
SanitizedConfig,
@@ -164,6 +163,7 @@ export const createClientConfig = ({
164163
case 'admin':
165164
clientConfig.admin = {
166165
autoLogin: config.admin.autoLogin,
166+
autoRefresh: config.admin.autoRefresh,
167167
avatar: config.admin.avatar,
168168
custom: config.admin.custom,
169169
dateFormat: config.admin.dateFormat,

packages/ui/src/providers/Auth/index.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,15 @@ export function AuthProvider({
139139
clearTimeout(refreshTokenTimeoutRef.current)
140140
}, [])
141141

142+
// Handler for reminder timeout - uses useEffectEvent to capture latest autoRefresh value
143+
const handleReminderTimeout = useEffectEvent(() => {
144+
if (autoRefresh) {
145+
refreshCookieEvent()
146+
} else {
147+
openModal(stayLoggedInModalSlug)
148+
}
149+
})
150+
142151
const setNewUser = useCallback(
143152
(userResponse: null | UserWithToken) => {
144153
clearTimeout(reminderTimeoutRef.current)
@@ -159,13 +168,7 @@ export function AuthProvider({
159168
setForceLogoutBufferMs(nextForceLogoutBufferMs)
160169

161170
reminderTimeoutRef.current = setTimeout(
162-
() => {
163-
if (autoRefresh) {
164-
refreshCookieEvent()
165-
} else {
166-
openModal(stayLoggedInModalSlug)
167-
}
168-
},
171+
handleReminderTimeout,
169172
Math.max(expiresInMs - nextForceLogoutBufferMs, 0),
170173
)
171174

@@ -178,7 +181,7 @@ export function AuthProvider({
178181
revokeTokenAndExpire()
179182
}
180183
},
181-
[autoRefresh, redirectToInactivityRoute, revokeTokenAndExpire, openModal],
184+
[redirectToInactivityRoute, revokeTokenAndExpire],
182185
)
183186

184187
const refreshCookie = useCallback(

test/auth/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export default buildConfigWithDefaults({
2121
password: devUser.password,
2222
prefillOnly: true,
2323
},
24+
autoRefresh: true,
2425
components: {
2526
beforeDashboard: ['./BeforeDashboard.js#BeforeDashboard'],
2627
beforeLogin: ['./BeforeLogin.js#BeforeLogin'],

test/auth/e2e.spec.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,4 +497,37 @@ describe('Auth', () => {
497497
})
498498
})
499499
})
500+
501+
describe('autoRefresh', () => {
502+
beforeAll(async () => {
503+
await reInitializeDB({
504+
serverURL,
505+
snapshotKey: 'auth',
506+
deleteOnly: false,
507+
})
508+
509+
await ensureCompilationIsDone({ page, serverURL, noAutoLogin: true })
510+
511+
url = new AdminUrlUtil(serverURL, slug)
512+
513+
// Install clock before login so token expiration and clock are in sync
514+
await page.clock.install({ time: Date.now() })
515+
516+
await login({ page, serverURL })
517+
})
518+
519+
test('should automatically refresh token without showing modal', async () => {
520+
await expect(page.locator('.nav')).toBeVisible()
521+
522+
// Fast forward time to just past the reminder timeout
523+
await page.clock.fastForward(7141000) // 1 hour 59 minutes + 1 second
524+
525+
// Resume clock so timers can execute
526+
await page.clock.resume()
527+
528+
await expect(page.locator('.confirmation-modal')).toBeHidden()
529+
530+
await expect(page.locator('.nav')).toBeVisible()
531+
})
532+
})
500533
})

test/auth/payload-types.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export interface Config {
7979
'public-users': PublicUser;
8080
relationsCollection: RelationsCollection;
8181
'api-keys-with-field-read-access': ApiKeysWithFieldReadAccess;
82+
'payload-kv': PayloadKv;
8283
'payload-locked-documents': PayloadLockedDocument;
8384
'payload-preferences': PayloadPreference;
8485
'payload-migrations': PayloadMigration;
@@ -92,13 +93,15 @@ export interface Config {
9293
'public-users': PublicUsersSelect<false> | PublicUsersSelect<true>;
9394
relationsCollection: RelationsCollectionSelect<false> | RelationsCollectionSelect<true>;
9495
'api-keys-with-field-read-access': ApiKeysWithFieldReadAccessSelect<false> | ApiKeysWithFieldReadAccessSelect<true>;
96+
'payload-kv': PayloadKvSelect<false> | PayloadKvSelect<true>;
9597
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
9698
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
9799
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
98100
};
99101
db: {
100102
defaultIDType: string;
101103
};
104+
fallbackLocale: null;
102105
globals: {};
103106
globalsSelect: {};
104107
locale: null;
@@ -392,6 +395,23 @@ export interface ApiKeysWithFieldReadAccess {
392395
apiKey?: string | null;
393396
apiKeyIndex?: string | null;
394397
}
398+
/**
399+
* This interface was referenced by `Config`'s JSON-Schema
400+
* via the `definition` "payload-kv".
401+
*/
402+
export interface PayloadKv {
403+
id: string;
404+
key: string;
405+
data:
406+
| {
407+
[k: string]: unknown;
408+
}
409+
| unknown[]
410+
| string
411+
| number
412+
| boolean
413+
| null;
414+
}
395415
/**
396416
* This interface was referenced by `Config`'s JSON-Schema
397417
* via the `definition` "payload-locked-documents".
@@ -653,6 +673,14 @@ export interface ApiKeysWithFieldReadAccessSelect<T extends boolean = true> {
653673
apiKey?: T;
654674
apiKeyIndex?: T;
655675
}
676+
/**
677+
* This interface was referenced by `Config`'s JSON-Schema
678+
* via the `definition` "payload-kv_select".
679+
*/
680+
export interface PayloadKvSelect<T extends boolean = true> {
681+
key?: T;
682+
data?: T;
683+
}
656684
/**
657685
* This interface was referenced by `Config`'s JSON-Schema
658686
* via the `definition` "payload-locked-documents_select".

0 commit comments

Comments
 (0)