mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-11-22 09:13:18 +03:00
fix: reload the page when login token expired (#1839)
This commit is contained in:
parent
5ac36b6f0a
commit
efe5444816
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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 */}
|
||||||
|
49
apps/web/src/plugins/affine/__tests__/index.spec.tsx
Normal file
49
apps/web/src/plugins/affine/__tests__/index.spec.tsx
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
@ -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 }) => {
|
||||||
|
@ -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));
|
||||||
|
@ -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;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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
3
scripts/setup/search.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.location.search = '?prefixUrl=http://127.0.0.1:3000/';
|
||||||
|
}
|
@ -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,
|
||||||
|
@ -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',
|
||||||
|
Loading…
Reference in New Issue
Block a user