mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-25 06:18:45 +08:00
fix: revalidate user token with no refresh page (#1842)
This commit is contained in:
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = (
|
||||||
|
|||||||
Reference in New Issue
Block a user