fix: reload the page when login token expired (#1839)

This commit is contained in:
Himself65 2023-04-06 18:26:53 -05:00 committed by GitHub
parent 5ac36b6f0a
commit efe5444816
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 80 additions and 16 deletions

View File

@ -21,14 +21,22 @@ const revalidate = async () => {
const response = await affineAuth.refreshToken(storage); const response = await affineAuth.refreshToken(storage);
if (response) { if (response) {
setLoginStorage(response); setLoginStorage(response);
// todo: need to notify the app that the token has been refreshed
// this is a hack to force a reload
window.location.reload();
} }
} }
} }
return true; return true;
}; };
export function useAffineRefreshAuthToken() { export function useAffineRefreshAuthToken(
// every 30 seconds, check if the token is expired
refreshInterval = 30 * 1000
) {
useSWR('autoRefreshToken', { useSWR('autoRefreshToken', {
fetcher: revalidate, fetcher: revalidate,
refreshInterval,
}); });
} }

View File

@ -25,7 +25,6 @@ import {
import { HelpIsland } from '../components/pure/help-island'; import { HelpIsland } from '../components/pure/help-island';
import { PageLoading } from '../components/pure/loading'; import { PageLoading } from '../components/pure/loading';
import WorkSpaceSliderBar from '../components/pure/workspace-slider-bar'; import WorkSpaceSliderBar from '../components/pure/workspace-slider-bar';
import { useAffineRefreshAuthToken } from '../hooks/affine/use-affine-refresh-auth-token';
import { import {
useSidebarFloating, useSidebarFloating,
useSidebarResizing, useSidebarResizing,
@ -187,11 +186,6 @@ export const WorkspaceLayout: FC<PropsWithChildren> =
); );
}; };
function AffineWorkspaceEffect() {
useAffineRefreshAuthToken();
return null;
}
export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => { export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
const [currentWorkspace] = useCurrentWorkspace(); const [currentWorkspace] = useCurrentWorkspace();
const [currentPageId] = useCurrentPageId(); const [currentPageId] = useCurrentPageId();
@ -348,7 +342,6 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
</StyledSpacer> </StyledSpacer>
<MainContainerWrapper resizing={resizing} style={{ width: mainWidth }}> <MainContainerWrapper resizing={resizing} style={{ width: mainWidth }}>
<MainContainer className="main-container"> <MainContainer className="main-container">
<AffineWorkspaceEffect />
{children} {children}
<StyledToolWrapper> <StyledToolWrapper>
{/* fixme(himself65): remove this */} {/* fixme(himself65): remove this */}

View File

@ -0,0 +1,49 @@
/**
* @vitest-environment happy-dom
*/
import {
getLoginStorage,
isExpired,
loginResponseSchema,
parseIdToken,
setLoginStorage,
} from '@affine/workspace/affine/login';
import user1 from '@affine-test/fixtures/built-in-user1.json';
import { renderHook } from '@testing-library/react';
import { afterEach, describe, expect, test } from 'vitest';
import { useAffineRefreshAuthToken } from '../../../hooks/affine/use-affine-refresh-auth-token';
afterEach(() => {
localStorage.clear();
});
describe('AFFiNE workspace', () => {
test('Provider', async () => {
expect(getLoginStorage()).toBeNull();
const data = await fetch('http://127.0.0.1:3000/api/user/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'DebugLoginUser',
email: user1.email,
password: user1.password,
}),
}).then(r => r.json());
loginResponseSchema.parse(data);
setLoginStorage({
// expired token that already expired
token:
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2ODA4MjE0OTQsImlkIjoiaFd0dkFoM1E3SGhiWVlNeGxyX1I0IiwibmFtZSI6ImRlYnVnMSIsImVtYWlsIjoiZGVidWcxQHRvZXZlcnl0aGluZy5pbmZvIiwiYXZhdGFyX3VybCI6bnVsbCwiY3JlYXRlZF9hdCI6MTY4MDgxNTcxMTAwMH0.fDSkbM-ovmGD21sKYSTuiqC1dTiceOfcgIUfI2dLsBk',
// but refresh is still valid
refresh: data.refresh,
});
renderHook(() => useAffineRefreshAuthToken(1));
await new Promise(resolve => setTimeout(resolve, 3000));
const userData = parseIdToken(getLoginStorage()?.token as string);
expect(userData).not.toBeNull();
expect(isExpired(userData)).toBe(false);
});
});

View File

@ -21,9 +21,10 @@ import { PageNotFoundError } from '../../components/affine/affine-error-eoundary
import { WorkspaceSettingDetail } from '../../components/affine/workspace-setting-detail'; import { WorkspaceSettingDetail } from '../../components/affine/workspace-setting-detail';
import { BlockSuitePageList } from '../../components/blocksuite/block-suite-page-list'; import { BlockSuitePageList } from '../../components/blocksuite/block-suite-page-list';
import { PageDetailEditor } from '../../components/page-detail-editor'; import { PageDetailEditor } from '../../components/page-detail-editor';
import { useAffineRefreshAuthToken } from '../../hooks/affine/use-affine-refresh-auth-token';
import { AffineSWRConfigProvider } from '../../providers/AffineSWRConfigProvider'; import { AffineSWRConfigProvider } from '../../providers/AffineSWRConfigProvider';
import { BlockSuiteWorkspace } from '../../shared'; import { BlockSuiteWorkspace } from '../../shared';
import { affineApis } from '../../shared/apis'; import { affineApis, prefixUrl } from '../../shared/apis';
import { initPage, toast } from '../../utils'; import { initPage, toast } from '../../utils';
import type { WorkspacePlugin } from '..'; import type { WorkspacePlugin } from '..';
import { QueryKey } from './fetcher'; import { QueryKey } from './fetcher';
@ -65,7 +66,7 @@ const getPersistenceAllWorkspace = () => {
return allWorkspaces; return allWorkspaces;
}; };
export const affineAuth = createAffineAuth(); export const affineAuth = createAffineAuth(prefixUrl);
export const AffinePlugin: WorkspacePlugin<WorkspaceFlavour.AFFINE> = { export const AffinePlugin: WorkspacePlugin<WorkspaceFlavour.AFFINE> = {
flavour: WorkspaceFlavour.AFFINE, flavour: WorkspaceFlavour.AFFINE,
@ -231,6 +232,7 @@ export const AffinePlugin: WorkspacePlugin<WorkspaceFlavour.AFFINE> = {
}, },
UI: { UI: {
Provider: ({ children }) => { Provider: ({ children }) => {
useAffineRefreshAuthToken();
return <AffineSWRConfigProvider>{children}</AffineSWRConfigProvider>; return <AffineSWRConfigProvider>{children}</AffineSWRConfigProvider>;
}, },
PageDetail: ({ currentWorkspace, currentPageId }) => { PageDetail: ({ currentWorkspace, currentPageId }) => {

View File

@ -26,6 +26,8 @@ if (typeof window === 'undefined') {
params.get('prefixUrl') && (prefixUrl = params.get('prefixUrl') as string); params.get('prefixUrl') && (prefixUrl = params.get('prefixUrl') as string);
} }
export { prefixUrl };
const affineApis = {} as ReturnType<typeof createUserApis> & const affineApis = {} as ReturnType<typeof createUserApis> &
ReturnType<typeof createWorkspaceApis>; ReturnType<typeof createWorkspaceApis>;
Object.assign(affineApis, createUserApis(prefixUrl)); Object.assign(affineApis, createUserApis(prefixUrl));

View File

@ -29,14 +29,16 @@ describe('storage', () => {
describe('utils', () => { describe('utils', () => {
test('isExpired', async () => { test('isExpired', async () => {
const now = Math.floor(Date.now() / 1000); const now = Math.floor(Date.now() / 1000);
expect(isExpired({ exp: now + 1 } as AccessTokenMessage)).toBeFalsy(); expect(isExpired({ exp: now + 1 } as AccessTokenMessage, 0)).toBeFalsy();
const promise = new Promise<void>(resolve => { const promise = new Promise<void>(resolve => {
setTimeout(() => { setTimeout(() => {
expect(isExpired({ exp: now + 1 } as AccessTokenMessage)).toBeTruthy(); expect(
isExpired({ exp: now + 1 } as AccessTokenMessage, 0)
).toBeTruthy();
resolve(); resolve();
}, 2000); }, 2000);
}); });
expect(isExpired({ exp: now - 1 } as AccessTokenMessage)).toBeTruthy(); expect(isExpired({ exp: now - 1 } as AccessTokenMessage, 0)).toBeTruthy();
await promise; await promise;
}); });
}); });

View File

@ -43,9 +43,13 @@ export function parseIdToken(token: string): AccessTokenMessage {
return JSON.parse(decode(token.split('.')[1])); return JSON.parse(decode(token.split('.')[1]));
} }
export const isExpired = (token: AccessTokenMessage): boolean => { export const isExpired = (
token: AccessTokenMessage,
// earlier than `before`, consider it expired
before = 60 // 1 minute
): boolean => {
const now = Math.floor(Date.now() / 1000); const now = Math.floor(Date.now() / 1000);
return token.exp < now; return token.exp < now - before;
}; };
export const setLoginStorage = (login: LoginResponse) => { export const setLoginStorage = (login: LoginResponse) => {

3
scripts/setup/search.ts Normal file
View File

@ -0,0 +1,3 @@
if (typeof window !== 'undefined') {
window.location.search = '?prefixUrl=http://127.0.0.1:3000/';
}

View File

@ -6,7 +6,7 @@ export default function getConfig() {
gitVersion: 'UNKNOWN', gitVersion: 'UNKNOWN',
hash: 'UNKNOWN', hash: 'UNKNOWN',
editorVersion: 'UNKNOWN', editorVersion: 'UNKNOWN',
serverAPI: 'http://localhost:3000/', serverAPI: 'http://127.0.0.1:3000/',
enableBroadCastChannelProvider: true, enableBroadCastChannelProvider: true,
enableIndexedDBProvider: true, enableIndexedDBProvider: true,
enableDebugPage: true, enableDebugPage: true,

View File

@ -14,6 +14,7 @@ export default defineConfig({
}, },
}, },
test: { test: {
setupFiles: ['./scripts/setup/search.ts'],
include: [ include: [
'packages/**/*.spec.ts', 'packages/**/*.spec.ts',
'packages/**/*.spec.tsx', 'packages/**/*.spec.tsx',