diff --git a/apps/web/src/hooks/affine/use-affine-refresh-auth-token.ts b/apps/web/src/hooks/affine/use-affine-refresh-auth-token.ts index 5b362b8cd2..80a0674c06 100644 --- a/apps/web/src/hooks/affine/use-affine-refresh-auth-token.ts +++ b/apps/web/src/hooks/affine/use-affine-refresh-auth-token.ts @@ -21,14 +21,22 @@ const revalidate = async () => { const response = await affineAuth.refreshToken(storage); if (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; }; -export function useAffineRefreshAuthToken() { +export function useAffineRefreshAuthToken( + // every 30 seconds, check if the token is expired + refreshInterval = 30 * 1000 +) { useSWR('autoRefreshToken', { fetcher: revalidate, + refreshInterval, }); } diff --git a/apps/web/src/layouts/index.tsx b/apps/web/src/layouts/index.tsx index f46a6ae9a6..7ffacf5992 100644 --- a/apps/web/src/layouts/index.tsx +++ b/apps/web/src/layouts/index.tsx @@ -25,7 +25,6 @@ import { import { HelpIsland } from '../components/pure/help-island'; import { PageLoading } from '../components/pure/loading'; import WorkSpaceSliderBar from '../components/pure/workspace-slider-bar'; -import { useAffineRefreshAuthToken } from '../hooks/affine/use-affine-refresh-auth-token'; import { useSidebarFloating, useSidebarResizing, @@ -187,11 +186,6 @@ export const WorkspaceLayout: FC = ); }; -function AffineWorkspaceEffect() { - useAffineRefreshAuthToken(); - return null; -} - export const WorkspaceLayoutInner: FC = ({ children }) => { const [currentWorkspace] = useCurrentWorkspace(); const [currentPageId] = useCurrentPageId(); @@ -348,7 +342,6 @@ export const WorkspaceLayoutInner: FC = ({ children }) => { - {children} {/* fixme(himself65): remove this */} diff --git a/apps/web/src/plugins/affine/__tests__/index.spec.tsx b/apps/web/src/plugins/affine/__tests__/index.spec.tsx new file mode 100644 index 0000000000..52e224b5be --- /dev/null +++ b/apps/web/src/plugins/affine/__tests__/index.spec.tsx @@ -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); + }); +}); diff --git a/apps/web/src/plugins/affine/index.tsx b/apps/web/src/plugins/affine/index.tsx index 56b932d195..9e40129476 100644 --- a/apps/web/src/plugins/affine/index.tsx +++ b/apps/web/src/plugins/affine/index.tsx @@ -21,9 +21,10 @@ import { PageNotFoundError } from '../../components/affine/affine-error-eoundary import { WorkspaceSettingDetail } from '../../components/affine/workspace-setting-detail'; import { BlockSuitePageList } from '../../components/blocksuite/block-suite-page-list'; import { PageDetailEditor } from '../../components/page-detail-editor'; +import { useAffineRefreshAuthToken } from '../../hooks/affine/use-affine-refresh-auth-token'; import { AffineSWRConfigProvider } from '../../providers/AffineSWRConfigProvider'; import { BlockSuiteWorkspace } from '../../shared'; -import { affineApis } from '../../shared/apis'; +import { affineApis, prefixUrl } from '../../shared/apis'; import { initPage, toast } from '../../utils'; import type { WorkspacePlugin } from '..'; import { QueryKey } from './fetcher'; @@ -65,7 +66,7 @@ const getPersistenceAllWorkspace = () => { return allWorkspaces; }; -export const affineAuth = createAffineAuth(); +export const affineAuth = createAffineAuth(prefixUrl); export const AffinePlugin: WorkspacePlugin = { flavour: WorkspaceFlavour.AFFINE, @@ -231,6 +232,7 @@ export const AffinePlugin: WorkspacePlugin = { }, UI: { Provider: ({ children }) => { + useAffineRefreshAuthToken(); return {children}; }, PageDetail: ({ currentWorkspace, currentPageId }) => { diff --git a/apps/web/src/shared/apis.ts b/apps/web/src/shared/apis.ts index 0d8c05cbf6..08b8947a6c 100644 --- a/apps/web/src/shared/apis.ts +++ b/apps/web/src/shared/apis.ts @@ -26,6 +26,8 @@ if (typeof window === 'undefined') { params.get('prefixUrl') && (prefixUrl = params.get('prefixUrl') as string); } +export { prefixUrl }; + const affineApis = {} as ReturnType & ReturnType; Object.assign(affineApis, createUserApis(prefixUrl)); diff --git a/packages/workspace/src/affine/__tests__/login.spec.ts b/packages/workspace/src/affine/__tests__/login.spec.ts index 8065b77da3..ad0f361687 100644 --- a/packages/workspace/src/affine/__tests__/login.spec.ts +++ b/packages/workspace/src/affine/__tests__/login.spec.ts @@ -29,14 +29,16 @@ describe('storage', () => { describe('utils', () => { test('isExpired', async () => { 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(resolve => { setTimeout(() => { - expect(isExpired({ exp: now + 1 } as AccessTokenMessage)).toBeTruthy(); + expect( + isExpired({ exp: now + 1 } as AccessTokenMessage, 0) + ).toBeTruthy(); resolve(); }, 2000); }); - expect(isExpired({ exp: now - 1 } as AccessTokenMessage)).toBeTruthy(); + expect(isExpired({ exp: now - 1 } as AccessTokenMessage, 0)).toBeTruthy(); await promise; }); }); diff --git a/packages/workspace/src/affine/login.ts b/packages/workspace/src/affine/login.ts index 9018e82cd2..855b9b000f 100644 --- a/packages/workspace/src/affine/login.ts +++ b/packages/workspace/src/affine/login.ts @@ -43,9 +43,13 @@ export function parseIdToken(token: string): AccessTokenMessage { 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); - return token.exp < now; + return token.exp < now - before; }; export const setLoginStorage = (login: LoginResponse) => { diff --git a/scripts/setup/search.ts b/scripts/setup/search.ts new file mode 100644 index 0000000000..c5fda34bf3 --- /dev/null +++ b/scripts/setup/search.ts @@ -0,0 +1,3 @@ +if (typeof window !== 'undefined') { + window.location.search = '?prefixUrl=http://127.0.0.1:3000/'; +} diff --git a/scripts/vitest/next-config-mock.ts b/scripts/vitest/next-config-mock.ts index 4f60e7aeba..6559d20a10 100644 --- a/scripts/vitest/next-config-mock.ts +++ b/scripts/vitest/next-config-mock.ts @@ -6,7 +6,7 @@ export default function getConfig() { gitVersion: 'UNKNOWN', hash: 'UNKNOWN', editorVersion: 'UNKNOWN', - serverAPI: 'http://localhost:3000/', + serverAPI: 'http://127.0.0.1:3000/', enableBroadCastChannelProvider: true, enableIndexedDBProvider: true, enableDebugPage: true, diff --git a/vitest.config.ts b/vitest.config.ts index 6a12c2a4c2..708ef9e6a7 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -14,6 +14,7 @@ export default defineConfig({ }, }, test: { + setupFiles: ['./scripts/setup/search.ts'], include: [ 'packages/**/*.spec.ts', 'packages/**/*.spec.tsx',