From ee2e1687df9ff9bb16a1cb9a5024dcdffe10edcd Mon Sep 17 00:00:00 2001 From: Peng Xiao Date: Thu, 9 Feb 2023 23:10:52 +0800 Subject: [PATCH 1/3] fix: store user info locally --- .../provider/affine/__tests__/mock-apis.ts | 2 +- .../data-center/src/provider/affine/affine.ts | 92 +++++++++---------- .../src/provider/affine/apis/index.ts | 19 +++- .../src/provider/affine/apis/request.ts | 1 + .../src/provider/affine/apis/token.ts | 79 +++++++++++----- .../src/provider/affine/storage.ts | 4 +- 6 files changed, 117 insertions(+), 80 deletions(-) diff --git a/packages/data-center/src/provider/affine/__tests__/mock-apis.ts b/packages/data-center/src/provider/affine/__tests__/mock-apis.ts index b05b9cd66..21a36c769 100644 --- a/packages/data-center/src/provider/affine/__tests__/mock-apis.ts +++ b/packages/data-center/src/provider/affine/__tests__/mock-apis.ts @@ -1,7 +1,7 @@ import type { Apis, AccessTokenMessage } from '../apis'; const user: AccessTokenMessage = { - create_at: Date.now(), + created_at: Date.now(), exp: 100000000, email: 'demo@demo.demo', id: '123', diff --git a/packages/data-center/src/provider/affine/affine.ts b/packages/data-center/src/provider/affine/affine.ts index e8e564e09..d3f110950 100644 --- a/packages/data-center/src/provider/affine/affine.ts +++ b/packages/data-center/src/provider/affine/affine.ts @@ -5,13 +5,11 @@ import type { } from '../base'; import type { User } from '../../types'; import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store'; -import { storage } from './storage.js'; import assert from 'assert'; import { WebsocketProvider } from './sync.js'; // import { IndexedDBProvider } from '../local/indexeddb'; import { getApis, Workspace } from './apis/index.js'; -import type { Apis, WorkspaceDetail, Callback } from './apis'; -import { token } from './apis/token.js'; +import type { Apis, WorkspaceDetail } from './apis'; import { WebsocketClient } from './channel'; import { loadWorkspaceUnit, @@ -40,10 +38,10 @@ const { export class AffineProvider extends BaseProvider { public id = 'affine'; - private _onTokenRefresh?: Callback = undefined; private _wsMap: Map = new Map(); private _apis: Apis; private _channel?: WebsocketClient; + private _refreshToken?: string; // private _idbMap: Map = new Map(); private _workspaceLoadingQueue: Set = new Set(); @@ -53,40 +51,25 @@ export class AffineProvider extends BaseProvider { } override async init() { - this._onTokenRefresh = () => { - if (this._apis.token.refresh) { - storage.setItem('token', this._apis.token.refresh); + this._apis.token.onChange(() => { + if (this._apis.token.isLogin) { + this._reconnectChannel(); + } else { + this._destroyChannel(); } - }; + }); - this._apis.token.onChange(this._onTokenRefresh); - - // initial login token - if (this._apis.token.isExpired) { - try { - const refreshToken = storage.getItem('token'); - if (!refreshToken) return; - await this._apis.token.refreshToken(refreshToken); - - if (this._apis.token.refresh) { - storage.set('token', this._apis.token.refresh); - } - - assert(this._apis.token.isLogin); - } catch (_) { - // this._logger('Authorization failed, fallback to local mode'); - } - } else { - storage.setItem('token', this._apis.token.refresh); - } - - if (token.isLogin) { - this._connectChannel(); + if (this._apis.token.isExpired && this._apis.token.refresh) { + // do we need to await the following? + this._apis.token.refreshToken(); } } - private _connectChannel() { - if (!this._channel) { + private _reconnectChannel() { + if (this._refreshToken !== this._apis.token.refresh) { + // need to reconnect + this._destroyChannel(); + this._channel = new WebsocketClient( `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${ window.location.host @@ -98,10 +81,21 @@ export class AffineProvider extends BaseProvider { }, } ); + + this._channel.on('message', (msg: ChannelMessage) => { + this._handlerAffineListMessage(msg); + }); + + this._refreshToken = this._apis.token.refresh; + } + } + + private _destroyChannel() { + if (this._channel) { + this._channel.disconnect(); + this._channel.destroy(); + this._channel = undefined; } - this._channel.on('message', (msg: ChannelMessage) => { - this._handlerAffineListMessage(msg); - }); } private async _handlerAffineListMessage({ @@ -111,7 +105,7 @@ export class AffineProvider extends BaseProvider { this._logger('receive server message'); const newlyCreatedWorkspaces: WorkspaceUnit[] = []; const currentWorkspaceIds = this._workspaces.list().map(w => w.id); - const newlyRemovedWorkspacecIds = currentWorkspaceIds; + const newlyRemovedWorkspaceIds = currentWorkspaceIds; for (const [id, detail] of Object.entries(ws_details)) { const { name, avatar } = metadata[id]; @@ -121,7 +115,7 @@ export class AffineProvider extends BaseProvider { const workspaceIndex = currentWorkspaceIds.indexOf(id); const ifWorkspaceExist = workspaceIndex !== -1; if (ifWorkspaceExist) { - newlyRemovedWorkspacecIds.splice(workspaceIndex, 1); + newlyRemovedWorkspaceIds.splice(workspaceIndex, 1); } /** @@ -163,7 +157,7 @@ export class AffineProvider extends BaseProvider { this._workspaces.add(newlyCreatedWorkspaces); // sync newlyRemoveWorkspaces to context - this._workspaces.remove(newlyRemovedWorkspacecIds); + this._workspaces.remove(newlyRemovedWorkspaceIds); } private _getWebsocketProvider(workspace: BlocksuiteWorkspace) { @@ -181,8 +175,8 @@ export class AffineProvider extends BaseProvider { awareness: workspace.awarenessStore.awareness, }); workspace.awarenessStore.awareness.setLocalStateField('user', { - name: token.user?.name ?? 'other', - id: Number(token.user?.id ?? -1), + name: this._apis.token.user?.name ?? 'other', + id: Number(this._apis.token.user?.id ?? -1), color: '#ffa500', }); @@ -261,23 +255,22 @@ export class AffineProvider extends BaseProvider { } override async auth() { - const refreshToken = await storage.getItem('token'); - if (refreshToken) { - await this._apis.token.refreshToken(refreshToken); + if (this._apis.token.isLogin) { + await this._apis.token.refreshToken(); if (this._apis.token.isLogin && !this._apis.token.isExpired) { // login success return; } } + const user = await this._apis.signInWithGoogle?.(); - if (!this._channel?.connected) { - this._connectChannel(); - } + if (!user) { this._sendMessage(MessageCenter.messageCode.loginError); } } + // TODO: may need to update related workspace attributes on user info change? public override async getUserInfo(): Promise { const user = this._apis.token.user; return user @@ -441,11 +434,10 @@ export class AffineProvider extends BaseProvider { } public override async logout(): Promise { - token.clear(); - this._channel?.disconnect(); + this._apis.token.clear(); + this._destroyChannel(); this._wsMap.forEach(ws => ws.disconnect()); this._workspaces.clear(false); - storage.removeItem('token'); } public override async getWorkspaceMembers(id: string) { diff --git a/packages/data-center/src/provider/affine/apis/index.ts b/packages/data-center/src/provider/affine/apis/index.ts index d3ac78088..27eebb513 100644 --- a/packages/data-center/src/provider/affine/apis/index.ts +++ b/packages/data-center/src/provider/affine/apis/index.ts @@ -6,11 +6,20 @@ import * as user from './user.js'; import * as workspace from './workspace.js'; import { token } from './token.js'; -export type Apis = typeof user & - Omit & { - signInWithGoogle: ReturnType[0]; - onAuthStateChanged: ReturnType[1]; - } & { token: typeof token }; +// See https://twitter.com/mattpocockuk/status/1622730173446557697 +// TODO: move to ts utils? +type Prettify = { + [K in keyof T]: T[K]; + // eslint-disable-next-line @typescript-eslint/ban-types +} & {}; + +export type Apis = Prettify< + typeof user & + Omit & { + signInWithGoogle: ReturnType[0]; + onAuthStateChanged: ReturnType[1]; + } & { token: typeof token } +>; export const getApis = (): Apis => { const [signInWithGoogle, onAuthStateChanged] = getAuthorizer(); diff --git a/packages/data-center/src/provider/affine/apis/request.ts b/packages/data-center/src/provider/affine/apis/request.ts index 306ac5b9e..29f90e19d 100644 --- a/packages/data-center/src/provider/affine/apis/request.ts +++ b/packages/data-center/src/provider/affine/apis/request.ts @@ -43,6 +43,7 @@ export const client: KyInstance = bareClient.extend({ beforeRetry: [ async ({ request }) => { + console.log('beforeRetry'); await token.refreshToken(); request.headers.set('Authorization', token.token); }, diff --git a/packages/data-center/src/provider/affine/apis/token.ts b/packages/data-center/src/provider/affine/apis/token.ts index 5b8e16592..60cc8deb3 100644 --- a/packages/data-center/src/provider/affine/apis/token.ts +++ b/packages/data-center/src/provider/affine/apis/token.ts @@ -5,9 +5,10 @@ import { decode } from 'js-base64'; import { getLogger } from '../../../logger.js'; import { bareClient } from './request.js'; +import { storage } from '../storage.js'; export interface AccessTokenMessage { - create_at: number; + created_at: number; exp: number; email: string; id: string; @@ -29,55 +30,85 @@ type LoginResponse = { refresh: string; }; -const login = (params: LoginParams): Promise => +// TODO: organize storage keys in a better way +const AFFINE_LOGIN_STORAGE_KEY = 'affine:login'; + +/** + * Use refresh token to get a new access token (JWT) + * The returned token also contains the user info payload. + */ +const doLogin = (params: LoginParams): Promise => bareClient.post('api/user/token', { json: params }).json(); export class Token { private readonly _logger; - private _accessToken!: string; - private _refreshToken!: string; + private _accessToken = ''; // idtoken (JWT) + private _refreshToken = ''; - private _user!: AccessTokenMessage | null; + private _user: AccessTokenMessage | null = null; private _padding?: Promise; constructor() { this._logger = getLogger('token'); this._logger.enabled = true; - this._setToken(); // fill with default value + this.restoreLogin(); } get user() { return this._user; } - private _setToken(login?: LoginResponse) { - this._accessToken = login?.token || ''; - this._refreshToken = login?.refresh || ''; - + setLogin(login: LoginResponse) { + this._accessToken = login.token; + this._refreshToken = login.refresh; this._user = Token.parse(this._accessToken); - if (login) { - this._logger('set login', login); - this.triggerChange(this._user); - } else { - this._logger('empty login'); + + this.triggerChange(this._user); + this.storeLogin(); + } + + private storeLogin() { + if (this.refresh) { + const { token, refresh } = this; + storage.setItem( + AFFINE_LOGIN_STORAGE_KEY, + JSON.stringify({ token, refresh }) + ); + } + } + + private restoreLogin() { + const loginStr = storage.getItem(AFFINE_LOGIN_STORAGE_KEY); + if (!loginStr) { + return; + } + try { + const login: LoginResponse = JSON.parse(loginStr); + this.setLogin(login); + } catch (err) { + this._logger('Failed to parse login info', err); } } async initToken(token: string) { - const tokens = await login({ token, type: 'Google' }); - this._setToken(tokens); + const res = await doLogin({ token, type: 'Google' }); + this.setLogin(res); return this._user; } - async refreshToken(token?: string) { + async refreshToken(refreshToken?: string) { if (!this._padding) { - this._padding = login({ + this._padding = doLogin({ type: 'Refresh', - token: this._refreshToken || token!, + token: refreshToken || this._refreshToken, }); + this._refreshToken = refreshToken || this._refreshToken; + } + const res = await this._padding; + if (!refreshToken || refreshToken !== this._refreshToken) { + this.setLogin(res); } - this._setToken(await this._padding); this._padding = undefined; } @@ -95,7 +126,7 @@ export class Token { get isExpired() { if (!this._user) return true; - return Date.now() - this._user.create_at > this._user.exp; + return Date.now() > this._user.exp; } static parse(token: string): AccessTokenMessage | null { @@ -128,7 +159,9 @@ export class Token { } clear() { - this._setToken(); + this._accessToken = ''; + this._refreshToken = ''; + storage.removeItem(AFFINE_LOGIN_STORAGE_KEY); } } diff --git a/packages/data-center/src/provider/affine/storage.ts b/packages/data-center/src/provider/affine/storage.ts index 4ac1b7469..95031de65 100644 --- a/packages/data-center/src/provider/affine/storage.ts +++ b/packages/data-center/src/provider/affine/storage.ts @@ -1 +1,3 @@ -export { varStorage as storage } from 'lib0/storage'; +import { varStorage } from 'lib0/storage'; + +export const storage = varStorage as Storage; From 385e9afba65112b21c9ce4bafb6d17efae06edf2 Mon Sep 17 00:00:00 2001 From: Peng Xiao Date: Thu, 9 Feb 2023 23:12:36 +0800 Subject: [PATCH 2/3] fix: token exp --- packages/data-center/src/provider/affine/apis/token.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/data-center/src/provider/affine/apis/token.ts b/packages/data-center/src/provider/affine/apis/token.ts index 60cc8deb3..5451ba1f9 100644 --- a/packages/data-center/src/provider/affine/apis/token.ts +++ b/packages/data-center/src/provider/affine/apis/token.ts @@ -126,7 +126,8 @@ export class Token { get isExpired() { if (!this._user) return true; - return Date.now() > this._user.exp; + // exp is in seconds + return Date.now() > this._user.exp * 1000; } static parse(token: string): AccessTokenMessage | null { From be27b30b015e11844a10754500e4ff4f3fbcb27c Mon Sep 17 00:00:00 2001 From: Peng Xiao Date: Fri, 10 Feb 2023 14:21:53 +0800 Subject: [PATCH 3/3] fix: rename token.ts to auth.ts to make it clear of usage --- .../data-center/src/provider/affine/affine.ts | 35 ++--- .../__tests__/{token.spec.ts => auth.spec.ts} | 8 +- .../affine/apis/{token.ts => auth.ts} | 134 +++++++++++------- .../src/provider/affine/apis/index.ts | 17 ++- .../src/provider/affine/apis/request.ts | 23 ++- .../src/provider/affine/apis/workspace.ts | 60 ++++---- .../src/provider/affine/channel.ts | 4 +- 7 files changed, 166 insertions(+), 115 deletions(-) rename packages/data-center/src/provider/affine/apis/__tests__/{token.spec.ts => auth.spec.ts} (79%) rename packages/data-center/src/provider/affine/apis/{token.ts => auth.ts} (58%) diff --git a/packages/data-center/src/provider/affine/affine.ts b/packages/data-center/src/provider/affine/affine.ts index d3f110950..0b28813ee 100644 --- a/packages/data-center/src/provider/affine/affine.ts +++ b/packages/data-center/src/provider/affine/affine.ts @@ -51,22 +51,22 @@ export class AffineProvider extends BaseProvider { } override async init() { - this._apis.token.onChange(() => { - if (this._apis.token.isLogin) { + this._apis.auth.onChange(() => { + if (this._apis.auth.isLogin) { this._reconnectChannel(); } else { this._destroyChannel(); } }); - if (this._apis.token.isExpired && this._apis.token.refresh) { + if (this._apis.auth.isExpired && this._apis.auth.refresh) { // do we need to await the following? - this._apis.token.refreshToken(); + this._apis.auth.refreshToken(); } } private _reconnectChannel() { - if (this._refreshToken !== this._apis.token.refresh) { + if (this._refreshToken !== this._apis.auth.refresh) { // need to reconnect this._destroyChannel(); @@ -77,7 +77,7 @@ export class AffineProvider extends BaseProvider { this._logger, { params: { - token: this._apis.token.refresh, + token: this._apis.auth.refresh, }, } ); @@ -86,7 +86,7 @@ export class AffineProvider extends BaseProvider { this._handlerAffineListMessage(msg); }); - this._refreshToken = this._apis.token.refresh; + this._refreshToken = this._apis.auth.refresh; } } @@ -170,13 +170,13 @@ export class AffineProvider extends BaseProvider { window.location.protocol === 'https:' ? 'wss' : 'ws' }://${window.location.host}/api/sync/`; ws = new WebsocketProvider(wsUrl, room, doc, { - params: { token: this._apis.token.refresh }, + params: { token: this._apis.auth.refresh }, // @ts-expect-error ignore the type awareness: workspace.awarenessStore.awareness, }); workspace.awarenessStore.awareness.setLocalStateField('user', { - name: this._apis.token.user?.name ?? 'other', - id: Number(this._apis.token.user?.id ?? -1), + name: this._apis.auth.user?.name ?? 'other', + id: Number(this._apis.auth.user?.id ?? -1), color: '#ffa500', }); @@ -226,7 +226,7 @@ export class AffineProvider extends BaseProvider { } override async loadWorkspaces() { - if (!this._apis.token.isLogin) { + if (!this._apis.auth.isLogin) { return []; } const workspacesList = await this._apis.getWorkspaces(); @@ -255,9 +255,9 @@ export class AffineProvider extends BaseProvider { } override async auth() { - if (this._apis.token.isLogin) { - await this._apis.token.refreshToken(); - if (this._apis.token.isLogin && !this._apis.token.isExpired) { + if (this._apis.auth.isLogin) { + await this._apis.auth.refreshToken(); + if (this._apis.auth.isLogin && !this._apis.auth.isExpired) { // login success return; } @@ -272,7 +272,7 @@ export class AffineProvider extends BaseProvider { // TODO: may need to update related workspace attributes on user info change? public override async getUserInfo(): Promise { - const user = this._apis.token.user; + const user = this._apis.auth.user; return user ? { id: user.id, @@ -382,7 +382,7 @@ export class AffineProvider extends BaseProvider { } public override getToken(): string { - return this._apis.token.token; + return this._apis.auth.token; } public override async getUserByEmail( @@ -434,10 +434,11 @@ export class AffineProvider extends BaseProvider { } public override async logout(): Promise { - this._apis.token.clear(); + this._apis.auth.clear(); this._destroyChannel(); this._wsMap.forEach(ws => ws.disconnect()); this._workspaces.clear(false); + await this._apis.signOutFirebase(); } public override async getWorkspaceMembers(id: string) { diff --git a/packages/data-center/src/provider/affine/apis/__tests__/token.spec.ts b/packages/data-center/src/provider/affine/apis/__tests__/auth.spec.ts similarity index 79% rename from packages/data-center/src/provider/affine/apis/__tests__/token.spec.ts rename to packages/data-center/src/provider/affine/apis/__tests__/auth.spec.ts index 739bae9e9..72a271c12 100644 --- a/packages/data-center/src/provider/affine/apis/__tests__/token.spec.ts +++ b/packages/data-center/src/provider/affine/apis/__tests__/auth.spec.ts @@ -1,10 +1,10 @@ import { test, expect } from '@playwright/test'; -import { Token } from '../token.js'; +import { Auth } from '../auth.js'; -test.describe('class Token', () => { +test.describe('class Auth', () => { test('parse tokens', () => { const tokenString = `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NzU2Nzk1MjAsImlkIjo2LCJuYW1lIjoidGVzdCIsImVtYWlsIjoidGVzdEBnbWFpbC5jb20iLCJhdmF0YXJfdXJsIjoiaHR0cHM6Ly90ZXN0LmNvbS9hdmF0YXIiLCJjcmVhdGVkX2F0IjoxNjc1Njc4OTIwMzU4fQ.R8GxrNhn3gNumtapthrP6_J5eQjXLV7i-LanSPqe7hw`; - expect(Token.parse(tokenString)).toEqual({ + expect(Auth.parseIdToken(tokenString)).toEqual({ avatar_url: 'https://test.com/avatar', created_at: 1675678920358, email: 'test@gmail.com', @@ -16,6 +16,6 @@ test.describe('class Token', () => { test('parse invalid tokens', () => { const tokenString = `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.aaa.R8GxrNhn3gNumtapthrP6_J5eQjXLV7i-LanSPqe7hw`; - expect(Token.parse(tokenString)).toEqual(null); + expect(Auth.parseIdToken(tokenString)).toEqual(null); }); }); diff --git a/packages/data-center/src/provider/affine/apis/token.ts b/packages/data-center/src/provider/affine/apis/auth.ts similarity index 58% rename from packages/data-center/src/provider/affine/apis/token.ts rename to packages/data-center/src/provider/affine/apis/auth.ts index 5451ba1f9..cd79513f2 100644 --- a/packages/data-center/src/provider/affine/apis/token.ts +++ b/packages/data-center/src/provider/affine/apis/auth.ts @@ -1,5 +1,11 @@ import { initializeApp } from 'firebase/app'; -import { getAuth, GoogleAuthProvider, signInWithPopup } from 'firebase/auth'; +import { + type Auth as FirebaseAuth, + getAuth as getFirebaseAuth, + GoogleAuthProvider, + signInWithPopup, + signOut, +} from 'firebase/auth'; import type { User } from 'firebase/auth'; import { decode } from 'js-base64'; @@ -40,7 +46,7 @@ const AFFINE_LOGIN_STORAGE_KEY = 'affine:login'; const doLogin = (params: LoginParams): Promise => bareClient.post('api/user/token', { json: params }).json(); -export class Token { +export class Auth { private readonly _logger; private _accessToken = ''; // idtoken (JWT) private _refreshToken = ''; @@ -55,14 +61,10 @@ export class Token { this.restoreLogin(); } - get user() { - return this._user; - } - setLogin(login: LoginResponse) { this._accessToken = login.token; this._refreshToken = login.refresh; - this._user = Token.parse(this._accessToken); + this._user = Auth.parseIdToken(this._accessToken); this.triggerChange(this._user); this.storeLogin(); @@ -103,13 +105,22 @@ export class Token { type: 'Refresh', token: refreshToken || this._refreshToken, }); + this._padding.finally(() => { + // clear on settled + this._padding = undefined; + }); this._refreshToken = refreshToken || this._refreshToken; } const res = await this._padding; if (!refreshToken || refreshToken !== this._refreshToken) { this.setLogin(res); } - this._padding = undefined; + return true; + } + + get user() { + // computed through access token + return this._user; } get token() { @@ -130,7 +141,7 @@ export class Token { return Date.now() > this._user.exp * 1000; } - static parse(token: string): AccessTokenMessage | null { + static parseIdToken(token: string): AccessTokenMessage | null { try { return JSON.parse(decode(token.split('.')[1])); } catch (error) { @@ -166,55 +177,78 @@ export class Token { } } -export const token = new Token(); +export const auth = new Auth(); export const getAuthorizer = () => { - const app = initializeApp({ - apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, - authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, - projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, - storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET, - messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, - appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, - measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID, - }); - try { - const firebaseAuth = getAuth(app); + let _firebaseAuth: FirebaseAuth | null = null; - const googleAuthProvider = new GoogleAuthProvider(); - - const getToken = async () => { - const currentUser = firebaseAuth.currentUser; - if (currentUser) { - await currentUser.getIdTokenResult(true); - if (!currentUser.isAnonymous) { - return currentUser.getIdToken(); - } + // getAuth will send requests on calling thus we can lazy init it + const getAuth = () => { + try { + if (!_firebaseAuth) { + const app = initializeApp({ + apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, + authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN, + projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, + storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET, + messagingSenderId: + process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, + appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID, + measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID, + }); + _firebaseAuth = getFirebaseAuth(app); } - return; - }; + return _firebaseAuth; + } catch (error) { + getLogger('getAuthorizer')(error); + console.error('getAuthorizer', error); + return null; + } + }; - const signInWithGoogle = async () => { - const idToken = await getToken(); - let loginUser: AccessTokenMessage | null = null; - if (idToken) { - loginUser = await token.initToken(idToken); - } else { + const getToken = async () => { + const currentUser = getAuth()?.currentUser; + if (currentUser) { + await currentUser.getIdTokenResult(true); + if (!currentUser.isAnonymous) { + return currentUser.getIdToken(); + } + } + return; + }; + + const signInWithGoogle = async () => { + const idToken = await getToken(); + let loginUser: AccessTokenMessage | null = null; + if (idToken) { + loginUser = await auth.initToken(idToken); + } else { + const firebaseAuth = getAuth(); + if (firebaseAuth) { + const googleAuthProvider = new GoogleAuthProvider(); + // make sure the user has a chance to select an account + // https://developers.google.com/identity/openid-connect/openid-connect#prompt + googleAuthProvider.setCustomParameters({ + prompt: 'select_account', + }); const user = await signInWithPopup(firebaseAuth, googleAuthProvider); const idToken = await user.user.getIdToken(); - loginUser = await token.initToken(idToken); + loginUser = await auth.initToken(idToken); } - return loginUser; - }; + } + return loginUser; + }; - const onAuthStateChanged = (callback: (user: User | null) => void) => { - firebaseAuth.onAuthStateChanged(callback); - }; + const onAuthStateChanged = (callback: (user: User | null) => void) => { + getAuth()?.onAuthStateChanged(callback); + }; - return [signInWithGoogle, onAuthStateChanged] as const; - } catch (e) { - getLogger('getAuthorizer')(e); - console.error('getAuthorizer', e); - return [] as const; - } + const signOutFirebase = async () => { + const firebaseAuth = getAuth(); + if (firebaseAuth?.currentUser) { + await signOut(firebaseAuth); + } + }; + + return [signInWithGoogle, onAuthStateChanged, signOutFirebase] as const; }; diff --git a/packages/data-center/src/provider/affine/apis/index.ts b/packages/data-center/src/provider/affine/apis/index.ts index 27eebb513..d96fc07c5 100644 --- a/packages/data-center/src/provider/affine/apis/index.ts +++ b/packages/data-center/src/provider/affine/apis/index.ts @@ -1,10 +1,10 @@ // export { token } from './token.js'; -export type { Callback } from './token.js'; +export type { Callback } from './auth.js'; -import { getAuthorizer } from './token.js'; +import { getAuthorizer } from './auth.js'; import * as user from './user.js'; import * as workspace from './workspace.js'; -import { token } from './token.js'; +import { auth } from './auth.js'; // See https://twitter.com/mattpocockuk/status/1622730173446557697 // TODO: move to ts utils? @@ -18,20 +18,23 @@ export type Apis = Prettify< Omit & { signInWithGoogle: ReturnType[0]; onAuthStateChanged: ReturnType[1]; - } & { token: typeof token } + signOutFirebase: ReturnType[2]; + } & { auth: typeof auth } >; export const getApis = (): Apis => { - const [signInWithGoogle, onAuthStateChanged] = getAuthorizer(); + const [signInWithGoogle, onAuthStateChanged, signOutFirebase] = + getAuthorizer(); return { ...user, ...workspace, signInWithGoogle, + signOutFirebase, onAuthStateChanged, - token, + auth, }; }; -export type { AccessTokenMessage } from './token'; +export type { AccessTokenMessage } from './auth'; export type { Member, Workspace, WorkspaceDetail } from './workspace'; export { WorkspaceType } from './workspace.js'; diff --git a/packages/data-center/src/provider/affine/apis/request.ts b/packages/data-center/src/provider/affine/apis/request.ts index 29f90e19d..b0a758468 100644 --- a/packages/data-center/src/provider/affine/apis/request.ts +++ b/packages/data-center/src/provider/affine/apis/request.ts @@ -1,6 +1,6 @@ import ky from 'ky-universal'; import { MessageCenter } from '../../../message/index.js'; -import { token } from './token.js'; +import { auth } from './auth.js'; type KyInstance = typeof ky; @@ -28,13 +28,23 @@ export const bareClient: KyInstance = ky.extend({ }, }); +const refreshTokenIfExpired = async () => { + if (auth.isLogin && auth.isExpired) { + try { + await auth.refreshToken(); + } catch (err) { + return new Response('Unauthorized', { status: 401 }); + } + } +}; + export const client: KyInstance = bareClient.extend({ hooks: { beforeRequest: [ async request => { - if (token.isLogin) { - if (token.isExpired) await token.refreshToken(); - request.headers.set('Authorization', token.token); + if (auth.isLogin) { + await refreshTokenIfExpired(); + request.headers.set('Authorization', auth.token); } else { return new Response('Unauthorized', { status: 401 }); } @@ -43,9 +53,8 @@ export const client: KyInstance = bareClient.extend({ beforeRetry: [ async ({ request }) => { - console.log('beforeRetry'); - await token.refreshToken(); - request.headers.set('Authorization', token.token); + await refreshTokenIfExpired(); + request.headers.set('Authorization', auth.token); }, ], diff --git a/packages/data-center/src/provider/affine/apis/workspace.ts b/packages/data-center/src/provider/affine/apis/workspace.ts index 584db159f..c12fed6e6 100644 --- a/packages/data-center/src/provider/affine/apis/workspace.ts +++ b/packages/data-center/src/provider/affine/apis/workspace.ts @@ -41,7 +41,7 @@ export interface Workspace { export async function getWorkspaces(): Promise { try { - return client + return await client .get('api/workspace', { headers: { 'Cache-Control': 'no-cache', @@ -63,8 +63,7 @@ export async function getWorkspaceDetail( params: GetWorkspaceDetailParams ): Promise { try { - const response = client.get(`api/workspace/${params.id}`); - return response.json(); + return await client.get(`api/workspace/${params.id}`).json(); } catch (error) { sendMessage(messageCode.getDetailFailed); throw new RequestError('get detail failed', error); @@ -100,7 +99,7 @@ export async function getWorkspaceMembers( params: GetWorkspaceDetailParams ): Promise { try { - return client.get(`api/workspace/${params.id}/permission`).json(); + return await client.get(`api/workspace/${params.id}/permission`).json(); } catch (error) { sendMessage(messageCode.getMembersFailed); throw new RequestError('get members failed', error); @@ -115,7 +114,7 @@ export async function createWorkspace( encodedYDoc: Blob ): Promise<{ id: string }> { try { - return client.post('api/workspace', { body: encodedYDoc }).json(); + return await client.post('api/workspace', { body: encodedYDoc }).json(); } catch (error) { sendMessage(messageCode.createWorkspaceFailed); throw new RequestError('create workspace failed', error); @@ -131,7 +130,7 @@ export async function updateWorkspace( params: UpdateWorkspaceParams ): Promise<{ public: boolean | null }> { try { - return client + return await client .post(`api/workspace/${params.id}`, { json: { public: params.public, @@ -151,10 +150,12 @@ export interface DeleteWorkspaceParams { export async function deleteWorkspace( params: DeleteWorkspaceParams ): Promise { - await client.delete(`api/workspace/${params.id}`).catch(error => { + try { + await client.delete(`api/workspace/${params.id}`); + } catch (error) { sendMessage(messageCode.deleteWorkspaceFailed); throw new RequestError('delete workspace failed', error); - }); + } } export interface InviteMemberParams { @@ -167,13 +168,11 @@ export interface InviteMemberParams { */ export async function inviteMember(params: InviteMemberParams): Promise { try { - return client - .post(`api/workspace/${params.id}/permission`, { - json: { - email: params.email, - }, - }) - .json(); + await client.post(`api/workspace/${params.id}/permission`, { + json: { + email: params.email, + }, + }); } catch (error) { sendMessage(messageCode.inviteMemberFailed); throw new RequestError('invite member failed', error); @@ -185,10 +184,12 @@ export interface RemoveMemberParams { } export async function removeMember(params: RemoveMemberParams): Promise { - await client.delete(`api/permission/${params.permissionId}`).catch(error => { + try { + await client.delete(`api/permission/${params.permissionId}`); + } catch (error) { sendMessage(messageCode.removeMemberFailed); throw new RequestError('remove member failed', error); - }); + } } export interface AcceptInvitingParams { @@ -199,7 +200,9 @@ export async function acceptInviting( params: AcceptInvitingParams ): Promise { try { - return bareClient.post(`api/invitation/${params.invitingCode}`).json(); + return await bareClient + .post(`api/invitation/${params.invitingCode}`) + .json(); } catch (error) { sendMessage(messageCode.acceptInvitingFailed); throw new RequestError('accept inviting failed', error); @@ -214,7 +217,7 @@ export async function getBlob(params: { blobId: string; }): Promise { try { - return client.get(`api/blob/${params.blobId}`).arrayBuffer(); + return await client.get(`api/blob/${params.blobId}`).arrayBuffer(); } catch (error) { sendMessage(messageCode.getBlobFailed); throw new RequestError('get blob failed', error); @@ -226,13 +229,12 @@ export interface LeaveWorkspaceParams { } export async function leaveWorkspace({ id }: LeaveWorkspaceParams) { - await client - .delete(`api/workspace/${id}/permission`) - .json() - .catch(error => { - sendMessage(messageCode.leaveWorkspaceFailed); - throw new RequestError('leave workspace failed', error); - }); + try { + await client.delete(`api/workspace/${id}/permission`); + } catch (error) { + sendMessage(messageCode.leaveWorkspaceFailed); + throw new RequestError('leave workspace failed', error); + } } export async function downloadWorkspace( @@ -241,9 +243,11 @@ export async function downloadWorkspace( ): Promise { try { if (published) { - return bareClient.get(`api/public/doc/${workspaceId}`).arrayBuffer(); + return await bareClient + .get(`api/public/doc/${workspaceId}`) + .arrayBuffer(); } - return client.get(`api/workspace/${workspaceId}/doc`).arrayBuffer(); + return await client.get(`api/workspace/${workspaceId}/doc`).arrayBuffer(); } catch (error) { sendMessage(messageCode.downloadWorkspaceFailed); throw new RequestError('download workspace failed', error); diff --git a/packages/data-center/src/provider/affine/channel.ts b/packages/data-center/src/provider/affine/channel.ts index da9e81c35..ce19a35ac 100644 --- a/packages/data-center/src/provider/affine/channel.ts +++ b/packages/data-center/src/provider/affine/channel.ts @@ -1,6 +1,6 @@ import * as websocket from 'lib0/websocket'; import { Logger } from 'src/types'; -import { token } from './apis/token'; +import { auth } from './apis/auth'; import * as url from 'lib0/url'; const RECONNECT_INTERVAL_TIME = 500; @@ -40,7 +40,7 @@ export class WebsocketClient extends websocket.WebsocketClient { this.on('disconnect', ({ error }: { error: Error }) => { if (error) { // Try reconnect if connect error has occurred - if (this.shouldReconnect && token.isLogin && !this.connected) { + if (this.shouldReconnect && auth.isLogin && !this.connected) { try { setTimeout(() => { if (this._retryTimes <= MAX_RECONNECT_TIMES) {