fix: revalidate user token with no refresh page (#1842)

This commit is contained in:
Himself65
2023-04-07 17:51:51 -05:00
committed by GitHub
parent e50bf9fbfe
commit 20e56cc474
6 changed files with 83 additions and 41 deletions
@@ -4,6 +4,7 @@ import {
isExpired, isExpired,
parseIdToken, parseIdToken,
setLoginStorage, setLoginStorage,
storageChangeSlot,
} from '@affine/workspace/affine/login'; } from '@affine/workspace/affine/login';
import useSWR from 'swr'; import useSWR from 'swr';
@@ -21,10 +22,7 @@ const revalidate = async () => {
const response = await affineAuth.refreshToken(storage); const response = await affineAuth.refreshToken(storage);
if (response) { if (response) {
setLoginStorage(response); setLoginStorage(response);
storageChangeSlot.emit();
// todo: need to notify the app that the token has been refreshed
// this is a hack to force a reload
window.location.reload();
} }
} }
} }
@@ -38,5 +36,8 @@ export function useAffineRefreshAuthToken(
useSWR('autoRefreshToken', { useSWR('autoRefreshToken', {
fetcher: revalidate, fetcher: revalidate,
refreshInterval, refreshInterval,
revalidateOnFocus: true,
revalidateOnReconnect: true,
revalidateOnMount: true,
}); });
} }
@@ -40,10 +40,11 @@ describe('AFFiNE workspace', () => {
// but refresh is still valid // but refresh is still valid
refresh: data.refresh, refresh: data.refresh,
}); });
renderHook(() => useAffineRefreshAuthToken(1)); const hook = renderHook(() => useAffineRefreshAuthToken(1));
await new Promise(resolve => setTimeout(resolve, 3000)); await new Promise(resolve => setTimeout(resolve, 3000));
const userData = parseIdToken(getLoginStorage()?.token as string); const userData = parseIdToken(getLoginStorage()?.token as string);
expect(userData).not.toBeNull(); expect(userData).not.toBeNull();
expect(isExpired(userData)).toBe(false); expect(isExpired(userData)).toBe(false);
hook.unmount();
}); });
}); });
+15 -30
View File
@@ -1,8 +1,7 @@
import { MessageCode, Messages } from '@affine/env'; import { MessageCode, Messages } from '@affine/env';
import { assertExists } from '@blocksuite/global/utils';
import { z } from 'zod'; import { z } from 'zod';
import { getLoginStorage } from '../login'; import { checkLoginStorage } from '../login';
export class RequestError extends Error { export class RequestError extends Error {
public readonly code: MessageCode; public readonly code: MessageCode;
@@ -51,8 +50,7 @@ export type UsageResponse = z.infer<typeof usageResponseSchema>;
export function createUserApis(prefixUrl = '/') { export function createUserApis(prefixUrl = '/') {
return { return {
getUsage: async (): Promise<UsageResponse> => { getUsage: async (): Promise<UsageResponse> => {
const auth = getLoginStorage(); const auth = await checkLoginStorage(prefixUrl);
assertExists(auth);
return fetch(prefixUrl + 'api/resource/usage', { return fetch(prefixUrl + 'api/resource/usage', {
method: 'GET', method: 'GET',
headers: { headers: {
@@ -63,8 +61,7 @@ export function createUserApis(prefixUrl = '/') {
getUserByEmail: async ( getUserByEmail: async (
params: GetUserByEmailParams params: GetUserByEmailParams
): Promise<User[] | null> => { ): Promise<User[] | null> => {
const auth = getLoginStorage(); const auth = await checkLoginStorage(prefixUrl);
assertExists(auth);
const target = new URL(prefixUrl + 'api/user', window.location.origin); const target = new URL(prefixUrl + 'api/user', window.location.origin);
target.searchParams.append('email', params.email); target.searchParams.append('email', params.email);
target.searchParams.append('workspace_id', params.workspace_id); target.searchParams.append('workspace_id', params.workspace_id);
@@ -187,8 +184,7 @@ export const createWorkspaceResponseSchema = z.object({
export function createWorkspaceApis(prefixUrl = '/') { export function createWorkspaceApis(prefixUrl = '/') {
return { return {
getWorkspaces: async (): Promise<Workspace[]> => { getWorkspaces: async (): Promise<Workspace[]> => {
const auth = getLoginStorage(); const auth = await checkLoginStorage(prefixUrl);
assertExists(auth);
return fetch(prefixUrl + 'api/workspace', { return fetch(prefixUrl + 'api/workspace', {
method: 'GET', method: 'GET',
headers: { headers: {
@@ -204,8 +200,7 @@ export function createWorkspaceApis(prefixUrl = '/') {
getWorkspaceDetail: async ( getWorkspaceDetail: async (
params: GetWorkspaceDetailParams params: GetWorkspaceDetailParams
): Promise<WorkspaceDetail | null> => { ): Promise<WorkspaceDetail | null> => {
const auth = getLoginStorage(); const auth = await checkLoginStorage(prefixUrl);
assertExists(auth);
return fetch(prefixUrl + `api/workspace/${params.id}`, { return fetch(prefixUrl + `api/workspace/${params.id}`, {
method: 'GET', method: 'GET',
headers: { headers: {
@@ -220,8 +215,7 @@ export function createWorkspaceApis(prefixUrl = '/') {
getWorkspaceMembers: async ( getWorkspaceMembers: async (
params: GetWorkspaceDetailParams params: GetWorkspaceDetailParams
): Promise<Member[]> => { ): Promise<Member[]> => {
const auth = getLoginStorage(); const auth = await checkLoginStorage(prefixUrl);
assertExists(auth);
return fetch(prefixUrl + `api/workspace/${params.id}/permission`, { return fetch(prefixUrl + `api/workspace/${params.id}/permission`, {
method: 'GET', method: 'GET',
headers: { headers: {
@@ -236,8 +230,7 @@ export function createWorkspaceApis(prefixUrl = '/') {
createWorkspace: async ( createWorkspace: async (
encodedYDoc: ArrayBuffer encodedYDoc: ArrayBuffer
): Promise<{ id: string }> => { ): Promise<{ id: string }> => {
const auth = getLoginStorage(); const auth = await checkLoginStorage();
assertExists(auth);
return fetch(prefixUrl + 'api/workspace', { return fetch(prefixUrl + 'api/workspace', {
method: 'POST', method: 'POST',
body: encodedYDoc, body: encodedYDoc,
@@ -254,8 +247,7 @@ export function createWorkspaceApis(prefixUrl = '/') {
updateWorkspace: async ( updateWorkspace: async (
params: UpdateWorkspaceParams params: UpdateWorkspaceParams
): Promise<{ public: boolean | null }> => { ): Promise<{ public: boolean | null }> => {
const auth = getLoginStorage(); const auth = await checkLoginStorage(prefixUrl);
assertExists(auth);
return fetch(prefixUrl + `api/workspace/${params.id}`, { return fetch(prefixUrl + `api/workspace/${params.id}`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
@@ -274,8 +266,7 @@ export function createWorkspaceApis(prefixUrl = '/') {
deleteWorkspace: async ( deleteWorkspace: async (
params: DeleteWorkspaceParams params: DeleteWorkspaceParams
): Promise<boolean> => { ): Promise<boolean> => {
const auth = getLoginStorage(); const auth = await checkLoginStorage(prefixUrl);
assertExists(auth);
return fetch(prefixUrl + `api/workspace/${params.id}`, { return fetch(prefixUrl + `api/workspace/${params.id}`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
@@ -292,8 +283,7 @@ export function createWorkspaceApis(prefixUrl = '/') {
* Notice: Only support normal(contrast to private) workspace. * Notice: Only support normal(contrast to private) workspace.
*/ */
inviteMember: async (params: InviteMemberParams): Promise<void> => { inviteMember: async (params: InviteMemberParams): Promise<void> => {
const auth = getLoginStorage(); const auth = await checkLoginStorage(prefixUrl);
assertExists(auth);
return fetch(prefixUrl + `api/workspace/${params.id}/permission`, { return fetch(prefixUrl + `api/workspace/${params.id}/permission`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
@@ -310,8 +300,7 @@ export function createWorkspaceApis(prefixUrl = '/') {
}); });
}, },
removeMember: async (params: RemoveMemberParams): Promise<void> => { removeMember: async (params: RemoveMemberParams): Promise<void> => {
const auth = getLoginStorage(); const auth = await checkLoginStorage(prefixUrl);
assertExists(auth);
return fetch(prefixUrl + `api/permission/${params.permissionId}`, { return fetch(prefixUrl + `api/permission/${params.permissionId}`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
@@ -339,8 +328,7 @@ export function createWorkspaceApis(prefixUrl = '/') {
arrayBuffer: ArrayBuffer, arrayBuffer: ArrayBuffer,
type: string type: string
): Promise<string> => { ): Promise<string> => {
const auth = getLoginStorage(); const auth = await checkLoginStorage(prefixUrl);
assertExists(auth);
const mb = arrayBuffer.byteLength / 1048576; const mb = arrayBuffer.byteLength / 1048576;
if (mb > 10) { if (mb > 10) {
throw new RequestError(MessageCode.blobTooLarge); throw new RequestError(MessageCode.blobTooLarge);
@@ -358,8 +346,7 @@ export function createWorkspaceApis(prefixUrl = '/') {
workspaceId: string, workspaceId: string,
blobId: string blobId: string
): Promise<ArrayBuffer> => { ): Promise<ArrayBuffer> => {
const auth = getLoginStorage(); const auth = await checkLoginStorage(prefixUrl);
assertExists(auth);
return fetch(prefixUrl + `api/workspace/${workspaceId}/blob/${blobId}`, { return fetch(prefixUrl + `api/workspace/${workspaceId}/blob/${blobId}`, {
method: 'GET', method: 'GET',
headers: { headers: {
@@ -372,8 +359,7 @@ export function createWorkspaceApis(prefixUrl = '/') {
}); });
}, },
leaveWorkspace: async ({ id }: LeaveWorkspaceParams) => { leaveWorkspace: async ({ id }: LeaveWorkspaceParams) => {
const auth = getLoginStorage(); const auth = await checkLoginStorage(prefixUrl);
assertExists(auth);
return fetch(prefixUrl + `api/workspace/${id}/permission`, { return fetch(prefixUrl + `api/workspace/${id}/permission`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
@@ -405,8 +391,7 @@ export function createWorkspaceApis(prefixUrl = '/') {
method: 'GET', method: 'GET',
}).then(r => r.arrayBuffer()); }).then(r => r.arrayBuffer());
} else { } else {
const auth = getLoginStorage(); const auth = await checkLoginStorage(prefixUrl);
assertExists(auth);
return fetch(prefixUrl + `api/workspace/${workspaceId}/doc`, { return fetch(prefixUrl + `api/workspace/${workspaceId}/doc`, {
method: 'GET', method: 'GET',
headers: { headers: {
+28
View File
@@ -1,4 +1,6 @@
import { DebugLogger } from '@affine/debug'; import { DebugLogger } from '@affine/debug';
import { assertExists } from '@blocksuite/global/utils';
import { Slot } from '@blocksuite/store';
import { initializeApp } from 'firebase/app'; import { initializeApp } from 'firebase/app';
import type { AuthProvider } from 'firebase/auth'; import type { AuthProvider } from 'firebase/auth';
import { import {
@@ -78,6 +80,32 @@ export const getLoginStorage = (): LoginResponse | null => {
return null; return null;
}; };
export const storageChangeSlot = new Slot();
export const checkLoginStorage = async (
prefixUrl = '/'
): Promise<LoginResponse> => {
const storage = getLoginStorage();
assertExists(storage, 'Login token is not set');
if (isExpired(parseIdToken(storage.token), 0)) {
logger.debug('refresh token needed');
const response: LoginResponse = await fetch(prefixUrl + 'api/user/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'Refresh',
token: storage.refresh,
}),
}).then(r => r.json());
setLoginStorage(response);
logger.debug('refresh token emit');
storageChangeSlot.emit();
}
return getLoginStorage() as LoginResponse;
};
export const enum SignMethod { export const enum SignMethod {
Google = 'Google', Google = 'Google',
GitHub = 'GitHub', GitHub = 'GitHub',
+15 -3
View File
@@ -1,14 +1,19 @@
import { DebugLogger } from '@affine/debug';
import { import {
workspaceDetailSchema, workspaceDetailSchema,
workspaceSchema, workspaceSchema,
} from '@affine/workspace/affine/api'; } from '@affine/workspace/affine/api';
import { WebsocketClient } from '@affine/workspace/affine/channel'; import { WebsocketClient } from '@affine/workspace/affine/channel';
import { storageChangeSlot } from '@affine/workspace/affine/login';
import { jotaiStore, jotaiWorkspacesAtom } from '@affine/workspace/atom'; import { jotaiStore, jotaiWorkspacesAtom } from '@affine/workspace/atom';
import type { WorkspaceCRUD } from '@affine/workspace/type'; import type { WorkspaceCRUD } from '@affine/workspace/type';
import type { WorkspaceFlavour } from '@affine/workspace/type'; import type { WorkspaceFlavour } from '@affine/workspace/type';
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
import type { Disposable } from '@blocksuite/store';
import { z } from 'zod'; import { z } from 'zod';
const logger = new DebugLogger('affine-sync');
const channelMessageSchema = z.object({ const channelMessageSchema = z.object({
ws_list: z.array(workspaceSchema), ws_list: z.array(workspaceSchema),
ws_details: z.record(workspaceDetailSchema), ws_details: z.record(workspaceDetailSchema),
@@ -28,7 +33,7 @@ export function createAffineGlobalChannel(
let client: WebsocketClient | null; let client: WebsocketClient | null;
async function handleMessage(channelMessage: ChannelMessage) { async function handleMessage(channelMessage: ChannelMessage) {
console.log('channelMessage', channelMessage); logger.debug('channelMessage', channelMessage);
const parseResult = channelMessageSchema.safeParse(channelMessage); const parseResult = channelMessageSchema.safeParse(channelMessage);
if (!parseResult.success) { if (!parseResult.success) {
console.error( console.error(
@@ -53,8 +58,8 @@ export function createAffineGlobalChannel(
} }
} }
} }
let dispose: Disposable | undefined = undefined;
return { const apis = {
connect: () => { connect: () => {
client = new WebsocketClient( client = new WebsocketClient(
`${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${ `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${
@@ -62,11 +67,18 @@ export function createAffineGlobalChannel(
}/api/global/sync` }/api/global/sync`
); );
client.connect(handleMessage); client.connect(handleMessage);
dispose = storageChangeSlot.on(() => {
apis.disconnect();
apis.connect();
});
}, },
disconnect: () => { disconnect: () => {
assertExists(client, 'client is null'); assertExists(client, 'client is null');
client.disconnect(); client.disconnect();
dispose?.dispose();
client = null; client = null;
}, },
}; };
return apis;
} }
+18 -3
View File
@@ -1,12 +1,18 @@
import { config } from '@affine/env'; import { config } from '@affine/env';
import { KeckProvider } from '@affine/workspace/affine/keck'; import { KeckProvider } from '@affine/workspace/affine/keck';
import { getLoginStorage } from '@affine/workspace/affine/login'; import {
getLoginStorage,
storageChangeSlot,
} from '@affine/workspace/affine/login';
import type { Provider } from '@affine/workspace/type'; import type { Provider } from '@affine/workspace/type';
import type { import type {
AffineWebSocketProvider, AffineWebSocketProvider,
LocalIndexedDBProvider, LocalIndexedDBProvider,
} from '@affine/workspace/type'; } from '@affine/workspace/type';
import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store'; import type {
Disposable,
Workspace as BlockSuiteWorkspace,
} from '@blocksuite/store';
import { assertExists } from '@blocksuite/store'; import { assertExists } from '@blocksuite/store';
import { import {
createIndexedDBProvider as create, createIndexedDBProvider as create,
@@ -20,15 +26,21 @@ const createAffineWebSocketProvider = (
blockSuiteWorkspace: BlockSuiteWorkspace blockSuiteWorkspace: BlockSuiteWorkspace
): AffineWebSocketProvider => { ): AffineWebSocketProvider => {
let webSocketProvider: KeckProvider | null = null; let webSocketProvider: KeckProvider | null = null;
return { let dispose: Disposable | undefined = undefined;
const apis: AffineWebSocketProvider = {
flavour: 'affine-websocket', flavour: 'affine-websocket',
background: false, background: false,
cleanup: () => { cleanup: () => {
assertExists(webSocketProvider); assertExists(webSocketProvider);
webSocketProvider.destroy(); webSocketProvider.destroy();
webSocketProvider = null; webSocketProvider = null;
dispose?.dispose();
}, },
connect: () => { connect: () => {
dispose = storageChangeSlot.on(() => {
apis.disconnect();
apis.connect();
});
const wsUrl = `${ const wsUrl = `${
window.location.protocol === 'https:' ? 'wss' : 'ws' window.location.protocol === 'https:' ? 'wss' : 'ws'
}://${window.location.host}/api/sync/`; }://${window.location.host}/api/sync/`;
@@ -53,8 +65,11 @@ const createAffineWebSocketProvider = (
localProviderLogger.info('disconnect', webSocketProvider.url); localProviderLogger.info('disconnect', webSocketProvider.url);
webSocketProvider.destroy(); webSocketProvider.destroy();
webSocketProvider = null; webSocketProvider = null;
dispose?.dispose();
}, },
}; };
return apis;
}; };
const createIndexedDBProvider = ( const createIndexedDBProvider = (