Merge pull request #936 from toeverything/token-refactor
refactor: token handling
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import type { Apis, AccessTokenMessage } from '../apis';
|
import type { Apis, AccessTokenMessage } from '../apis';
|
||||||
|
|
||||||
const user: AccessTokenMessage = {
|
const user: AccessTokenMessage = {
|
||||||
create_at: Date.now(),
|
created_at: Date.now(),
|
||||||
exp: 100000000,
|
exp: 100000000,
|
||||||
email: 'demo@demo.demo',
|
email: 'demo@demo.demo',
|
||||||
id: '123',
|
id: '123',
|
||||||
|
|||||||
@@ -5,13 +5,11 @@ import type {
|
|||||||
} from '../base';
|
} from '../base';
|
||||||
import type { User } from '../../types';
|
import type { User } from '../../types';
|
||||||
import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store';
|
import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store';
|
||||||
import { storage } from './storage.js';
|
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
import { WebsocketProvider } from './sync.js';
|
import { WebsocketProvider } from './sync.js';
|
||||||
// import { IndexedDBProvider } from '../local/indexeddb';
|
// import { IndexedDBProvider } from '../local/indexeddb';
|
||||||
import { getApis, Workspace } from './apis/index.js';
|
import { getApis, Workspace } from './apis/index.js';
|
||||||
import type { Apis, WorkspaceDetail, Callback } from './apis';
|
import type { Apis, WorkspaceDetail } from './apis';
|
||||||
import { token } from './apis/token.js';
|
|
||||||
import { WebsocketClient } from './channel';
|
import { WebsocketClient } from './channel';
|
||||||
import {
|
import {
|
||||||
loadWorkspaceUnit,
|
loadWorkspaceUnit,
|
||||||
@@ -40,10 +38,10 @@ const {
|
|||||||
|
|
||||||
export class AffineProvider extends BaseProvider {
|
export class AffineProvider extends BaseProvider {
|
||||||
public id = 'affine';
|
public id = 'affine';
|
||||||
private _onTokenRefresh?: Callback = undefined;
|
|
||||||
private _wsMap: Map<BlocksuiteWorkspace, WebsocketProvider> = new Map();
|
private _wsMap: Map<BlocksuiteWorkspace, WebsocketProvider> = new Map();
|
||||||
private _apis: Apis;
|
private _apis: Apis;
|
||||||
private _channel?: WebsocketClient;
|
private _channel?: WebsocketClient;
|
||||||
|
private _refreshToken?: string;
|
||||||
// private _idbMap: Map<string, IndexedDBProvider> = new Map();
|
// private _idbMap: Map<string, IndexedDBProvider> = new Map();
|
||||||
private _workspaceLoadingQueue: Set<string> = new Set();
|
private _workspaceLoadingQueue: Set<string> = new Set();
|
||||||
|
|
||||||
@@ -53,40 +51,25 @@ export class AffineProvider extends BaseProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override async init() {
|
override async init() {
|
||||||
this._onTokenRefresh = () => {
|
this._apis.auth.onChange(() => {
|
||||||
if (this._apis.token.refresh) {
|
if (this._apis.auth.isLogin) {
|
||||||
storage.setItem('token', this._apis.token.refresh);
|
this._reconnectChannel();
|
||||||
|
} else {
|
||||||
|
this._destroyChannel();
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
this._apis.token.onChange(this._onTokenRefresh);
|
if (this._apis.auth.isExpired && this._apis.auth.refresh) {
|
||||||
|
// do we need to await the following?
|
||||||
// initial login token
|
this._apis.auth.refreshToken();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _connectChannel() {
|
private _reconnectChannel() {
|
||||||
if (!this._channel) {
|
if (this._refreshToken !== this._apis.auth.refresh) {
|
||||||
|
// need to reconnect
|
||||||
|
this._destroyChannel();
|
||||||
|
|
||||||
this._channel = new WebsocketClient(
|
this._channel = new WebsocketClient(
|
||||||
`${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${
|
`${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${
|
||||||
window.location.host
|
window.location.host
|
||||||
@@ -94,14 +77,25 @@ export class AffineProvider extends BaseProvider {
|
|||||||
this._logger,
|
this._logger,
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
token: this._apis.token.refresh,
|
token: this._apis.auth.refresh,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this._channel.on('message', (msg: ChannelMessage) => {
|
||||||
|
this._handlerAffineListMessage(msg);
|
||||||
|
});
|
||||||
|
|
||||||
|
this._refreshToken = this._apis.auth.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({
|
private async _handlerAffineListMessage({
|
||||||
@@ -111,7 +105,7 @@ export class AffineProvider extends BaseProvider {
|
|||||||
this._logger('receive server message');
|
this._logger('receive server message');
|
||||||
const newlyCreatedWorkspaces: WorkspaceUnit[] = [];
|
const newlyCreatedWorkspaces: WorkspaceUnit[] = [];
|
||||||
const currentWorkspaceIds = this._workspaces.list().map(w => w.id);
|
const currentWorkspaceIds = this._workspaces.list().map(w => w.id);
|
||||||
const newlyRemovedWorkspacecIds = currentWorkspaceIds;
|
const newlyRemovedWorkspaceIds = currentWorkspaceIds;
|
||||||
for (const [id, detail] of Object.entries(ws_details)) {
|
for (const [id, detail] of Object.entries(ws_details)) {
|
||||||
const { name, avatar } = metadata[id];
|
const { name, avatar } = metadata[id];
|
||||||
|
|
||||||
@@ -121,7 +115,7 @@ export class AffineProvider extends BaseProvider {
|
|||||||
const workspaceIndex = currentWorkspaceIds.indexOf(id);
|
const workspaceIndex = currentWorkspaceIds.indexOf(id);
|
||||||
const ifWorkspaceExist = workspaceIndex !== -1;
|
const ifWorkspaceExist = workspaceIndex !== -1;
|
||||||
if (ifWorkspaceExist) {
|
if (ifWorkspaceExist) {
|
||||||
newlyRemovedWorkspacecIds.splice(workspaceIndex, 1);
|
newlyRemovedWorkspaceIds.splice(workspaceIndex, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -163,7 +157,7 @@ export class AffineProvider extends BaseProvider {
|
|||||||
this._workspaces.add(newlyCreatedWorkspaces);
|
this._workspaces.add(newlyCreatedWorkspaces);
|
||||||
|
|
||||||
// sync newlyRemoveWorkspaces to context
|
// sync newlyRemoveWorkspaces to context
|
||||||
this._workspaces.remove(newlyRemovedWorkspacecIds);
|
this._workspaces.remove(newlyRemovedWorkspaceIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getWebsocketProvider(workspace: BlocksuiteWorkspace) {
|
private _getWebsocketProvider(workspace: BlocksuiteWorkspace) {
|
||||||
@@ -176,13 +170,13 @@ export class AffineProvider extends BaseProvider {
|
|||||||
window.location.protocol === 'https:' ? 'wss' : 'ws'
|
window.location.protocol === 'https:' ? 'wss' : 'ws'
|
||||||
}://${window.location.host}/api/sync/`;
|
}://${window.location.host}/api/sync/`;
|
||||||
ws = new WebsocketProvider(wsUrl, room, doc, {
|
ws = new WebsocketProvider(wsUrl, room, doc, {
|
||||||
params: { token: this._apis.token.refresh },
|
params: { token: this._apis.auth.refresh },
|
||||||
// @ts-expect-error ignore the type
|
// @ts-expect-error ignore the type
|
||||||
awareness: workspace.awarenessStore.awareness,
|
awareness: workspace.awarenessStore.awareness,
|
||||||
});
|
});
|
||||||
workspace.awarenessStore.awareness.setLocalStateField('user', {
|
workspace.awarenessStore.awareness.setLocalStateField('user', {
|
||||||
name: token.user?.name ?? 'other',
|
name: this._apis.auth.user?.name ?? 'other',
|
||||||
id: Number(token.user?.id ?? -1),
|
id: Number(this._apis.auth.user?.id ?? -1),
|
||||||
color: '#ffa500',
|
color: '#ffa500',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -232,7 +226,7 @@ export class AffineProvider extends BaseProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override async loadWorkspaces() {
|
override async loadWorkspaces() {
|
||||||
if (!this._apis.token.isLogin) {
|
if (!this._apis.auth.isLogin) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const workspacesList = await this._apis.getWorkspaces();
|
const workspacesList = await this._apis.getWorkspaces();
|
||||||
@@ -261,25 +255,24 @@ export class AffineProvider extends BaseProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override async auth() {
|
override async auth() {
|
||||||
const refreshToken = await storage.getItem('token');
|
if (this._apis.auth.isLogin) {
|
||||||
if (refreshToken) {
|
await this._apis.auth.refreshToken();
|
||||||
await this._apis.token.refreshToken(refreshToken);
|
if (this._apis.auth.isLogin && !this._apis.auth.isExpired) {
|
||||||
if (this._apis.token.isLogin && !this._apis.token.isExpired) {
|
|
||||||
// login success
|
// login success
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await this._apis.signInWithGoogle?.();
|
const user = await this._apis.signInWithGoogle?.();
|
||||||
if (!this._channel?.connected) {
|
|
||||||
this._connectChannel();
|
|
||||||
}
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
this._sendMessage(MessageCenter.messageCode.loginError);
|
this._sendMessage(MessageCenter.messageCode.loginError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: may need to update related workspace attributes on user info change?
|
||||||
public override async getUserInfo(): Promise<User | undefined> {
|
public override async getUserInfo(): Promise<User | undefined> {
|
||||||
const user = this._apis.token.user;
|
const user = this._apis.auth.user;
|
||||||
return user
|
return user
|
||||||
? {
|
? {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
@@ -389,7 +382,7 @@ export class AffineProvider extends BaseProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public override getToken(): string {
|
public override getToken(): string {
|
||||||
return this._apis.token.token;
|
return this._apis.auth.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async getUserByEmail(
|
public override async getUserByEmail(
|
||||||
@@ -441,11 +434,11 @@ export class AffineProvider extends BaseProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public override async logout(): Promise<void> {
|
public override async logout(): Promise<void> {
|
||||||
token.clear();
|
this._apis.auth.clear();
|
||||||
this._channel?.disconnect();
|
this._destroyChannel();
|
||||||
this._wsMap.forEach(ws => ws.disconnect());
|
this._wsMap.forEach(ws => ws.disconnect());
|
||||||
this._workspaces.clear(false);
|
this._workspaces.clear(false);
|
||||||
storage.removeItem('token');
|
await this._apis.signOutFirebase();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async getWorkspaceMembers(id: string) {
|
public override async getWorkspaceMembers(id: string) {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
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', () => {
|
test('parse tokens', () => {
|
||||||
const tokenString = `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NzU2Nzk1MjAsImlkIjo2LCJuYW1lIjoidGVzdCIsImVtYWlsIjoidGVzdEBnbWFpbC5jb20iLCJhdmF0YXJfdXJsIjoiaHR0cHM6Ly90ZXN0LmNvbS9hdmF0YXIiLCJjcmVhdGVkX2F0IjoxNjc1Njc4OTIwMzU4fQ.R8GxrNhn3gNumtapthrP6_J5eQjXLV7i-LanSPqe7hw`;
|
const tokenString = `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NzU2Nzk1MjAsImlkIjo2LCJuYW1lIjoidGVzdCIsImVtYWlsIjoidGVzdEBnbWFpbC5jb20iLCJhdmF0YXJfdXJsIjoiaHR0cHM6Ly90ZXN0LmNvbS9hdmF0YXIiLCJjcmVhdGVkX2F0IjoxNjc1Njc4OTIwMzU4fQ.R8GxrNhn3gNumtapthrP6_J5eQjXLV7i-LanSPqe7hw`;
|
||||||
expect(Token.parse(tokenString)).toEqual({
|
expect(Auth.parseIdToken(tokenString)).toEqual({
|
||||||
avatar_url: 'https://test.com/avatar',
|
avatar_url: 'https://test.com/avatar',
|
||||||
created_at: 1675678920358,
|
created_at: 1675678920358,
|
||||||
email: 'test@gmail.com',
|
email: 'test@gmail.com',
|
||||||
@@ -16,6 +16,6 @@ test.describe('class Token', () => {
|
|||||||
|
|
||||||
test('parse invalid tokens', () => {
|
test('parse invalid tokens', () => {
|
||||||
const tokenString = `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.aaa.R8GxrNhn3gNumtapthrP6_J5eQjXLV7i-LanSPqe7hw`;
|
const tokenString = `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.aaa.R8GxrNhn3gNumtapthrP6_J5eQjXLV7i-LanSPqe7hw`;
|
||||||
expect(Token.parse(tokenString)).toEqual(null);
|
expect(Auth.parseIdToken(tokenString)).toEqual(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
254
packages/data-center/src/provider/affine/apis/auth.ts
Normal file
254
packages/data-center/src/provider/affine/apis/auth.ts
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
import { initializeApp } from 'firebase/app';
|
||||||
|
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';
|
||||||
|
|
||||||
|
import { getLogger } from '../../../logger.js';
|
||||||
|
import { bareClient } from './request.js';
|
||||||
|
import { storage } from '../storage.js';
|
||||||
|
|
||||||
|
export interface AccessTokenMessage {
|
||||||
|
created_at: number;
|
||||||
|
exp: number;
|
||||||
|
email: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
avatar_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Callback = (user: AccessTokenMessage | null) => void;
|
||||||
|
|
||||||
|
type LoginParams = {
|
||||||
|
type: 'Google' | 'Refresh';
|
||||||
|
token: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LoginResponse = {
|
||||||
|
// access token, expires in a very short time
|
||||||
|
token: string;
|
||||||
|
// Refresh token
|
||||||
|
refresh: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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<LoginResponse> =>
|
||||||
|
bareClient.post('api/user/token', { json: params }).json();
|
||||||
|
|
||||||
|
export class Auth {
|
||||||
|
private readonly _logger;
|
||||||
|
private _accessToken = ''; // idtoken (JWT)
|
||||||
|
private _refreshToken = '';
|
||||||
|
|
||||||
|
private _user: AccessTokenMessage | null = null;
|
||||||
|
private _padding?: Promise<LoginResponse>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this._logger = getLogger('token');
|
||||||
|
this._logger.enabled = true;
|
||||||
|
|
||||||
|
this.restoreLogin();
|
||||||
|
}
|
||||||
|
|
||||||
|
setLogin(login: LoginResponse) {
|
||||||
|
this._accessToken = login.token;
|
||||||
|
this._refreshToken = login.refresh;
|
||||||
|
this._user = Auth.parseIdToken(this._accessToken);
|
||||||
|
|
||||||
|
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 res = await doLogin({ token, type: 'Google' });
|
||||||
|
this.setLogin(res);
|
||||||
|
return this._user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshToken(refreshToken?: string) {
|
||||||
|
if (!this._padding) {
|
||||||
|
this._padding = doLogin({
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
get user() {
|
||||||
|
// computed through access token
|
||||||
|
return this._user;
|
||||||
|
}
|
||||||
|
|
||||||
|
get token() {
|
||||||
|
return this._accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
get refresh() {
|
||||||
|
return this._refreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isLogin() {
|
||||||
|
return !!this._refreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isExpired() {
|
||||||
|
if (!this._user) return true;
|
||||||
|
// exp is in seconds
|
||||||
|
return Date.now() > this._user.exp * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
static parseIdToken(token: string): AccessTokenMessage | null {
|
||||||
|
try {
|
||||||
|
return JSON.parse(decode(token.split('.')[1]));
|
||||||
|
} catch (error) {
|
||||||
|
// todo: log errors?
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private callbacks: Callback[] = [];
|
||||||
|
private lastState: AccessTokenMessage | null = null;
|
||||||
|
|
||||||
|
triggerChange(user: AccessTokenMessage | null) {
|
||||||
|
this.lastState = user;
|
||||||
|
this.callbacks.forEach(callback => callback(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(callback: Callback) {
|
||||||
|
this.callbacks.push(callback);
|
||||||
|
callback(this.lastState);
|
||||||
|
}
|
||||||
|
|
||||||
|
offChange(callback: Callback) {
|
||||||
|
const index = this.callbacks.indexOf(callback);
|
||||||
|
if (index > -1) {
|
||||||
|
this.callbacks.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this._accessToken = '';
|
||||||
|
this._refreshToken = '';
|
||||||
|
storage.removeItem(AFFINE_LOGIN_STORAGE_KEY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const auth = new Auth();
|
||||||
|
|
||||||
|
export const getAuthorizer = () => {
|
||||||
|
let _firebaseAuth: FirebaseAuth | null = null;
|
||||||
|
|
||||||
|
// 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 _firebaseAuth;
|
||||||
|
} catch (error) {
|
||||||
|
getLogger('getAuthorizer')(error);
|
||||||
|
console.error('getAuthorizer', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 auth.initToken(idToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return loginUser;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAuthStateChanged = (callback: (user: User | null) => void) => {
|
||||||
|
getAuth()?.onAuthStateChanged(callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
const signOutFirebase = async () => {
|
||||||
|
const firebaseAuth = getAuth();
|
||||||
|
if (firebaseAuth?.currentUser) {
|
||||||
|
await signOut(firebaseAuth);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return [signInWithGoogle, onAuthStateChanged, signOutFirebase] as const;
|
||||||
|
};
|
||||||
@@ -1,28 +1,40 @@
|
|||||||
// export { token } from './token.js';
|
// 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 user from './user.js';
|
||||||
import * as workspace from './workspace.js';
|
import * as workspace from './workspace.js';
|
||||||
import { token } from './token.js';
|
import { auth } from './auth.js';
|
||||||
|
|
||||||
export type Apis = typeof user &
|
// See https://twitter.com/mattpocockuk/status/1622730173446557697
|
||||||
Omit<typeof workspace, 'WorkspaceType' | 'PermissionType'> & {
|
// TODO: move to ts utils?
|
||||||
signInWithGoogle: ReturnType<typeof getAuthorizer>[0];
|
type Prettify<T> = {
|
||||||
onAuthStateChanged: ReturnType<typeof getAuthorizer>[1];
|
[K in keyof T]: T[K];
|
||||||
} & { token: typeof token };
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
|
} & {};
|
||||||
|
|
||||||
|
export type Apis = Prettify<
|
||||||
|
typeof user &
|
||||||
|
Omit<typeof workspace, 'WorkspaceType' | 'PermissionType'> & {
|
||||||
|
signInWithGoogle: ReturnType<typeof getAuthorizer>[0];
|
||||||
|
onAuthStateChanged: ReturnType<typeof getAuthorizer>[1];
|
||||||
|
signOutFirebase: ReturnType<typeof getAuthorizer>[2];
|
||||||
|
} & { auth: typeof auth }
|
||||||
|
>;
|
||||||
|
|
||||||
export const getApis = (): Apis => {
|
export const getApis = (): Apis => {
|
||||||
const [signInWithGoogle, onAuthStateChanged] = getAuthorizer();
|
const [signInWithGoogle, onAuthStateChanged, signOutFirebase] =
|
||||||
|
getAuthorizer();
|
||||||
return {
|
return {
|
||||||
...user,
|
...user,
|
||||||
...workspace,
|
...workspace,
|
||||||
signInWithGoogle,
|
signInWithGoogle,
|
||||||
|
signOutFirebase,
|
||||||
onAuthStateChanged,
|
onAuthStateChanged,
|
||||||
token,
|
auth,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type { AccessTokenMessage } from './token';
|
export type { AccessTokenMessage } from './auth';
|
||||||
export type { Member, Workspace, WorkspaceDetail } from './workspace';
|
export type { Member, Workspace, WorkspaceDetail } from './workspace';
|
||||||
export { WorkspaceType } from './workspace.js';
|
export { WorkspaceType } from './workspace.js';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import ky from 'ky-universal';
|
import ky from 'ky-universal';
|
||||||
import { MessageCenter } from '../../../message/index.js';
|
import { MessageCenter } from '../../../message/index.js';
|
||||||
import { token } from './token.js';
|
import { auth } from './auth.js';
|
||||||
|
|
||||||
type KyInstance = typeof ky;
|
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({
|
export const client: KyInstance = bareClient.extend({
|
||||||
hooks: {
|
hooks: {
|
||||||
beforeRequest: [
|
beforeRequest: [
|
||||||
async request => {
|
async request => {
|
||||||
if (token.isLogin) {
|
if (auth.isLogin) {
|
||||||
if (token.isExpired) await token.refreshToken();
|
await refreshTokenIfExpired();
|
||||||
request.headers.set('Authorization', token.token);
|
request.headers.set('Authorization', auth.token);
|
||||||
} else {
|
} else {
|
||||||
return new Response('Unauthorized', { status: 401 });
|
return new Response('Unauthorized', { status: 401 });
|
||||||
}
|
}
|
||||||
@@ -43,8 +53,8 @@ export const client: KyInstance = bareClient.extend({
|
|||||||
|
|
||||||
beforeRetry: [
|
beforeRetry: [
|
||||||
async ({ request }) => {
|
async ({ request }) => {
|
||||||
await token.refreshToken();
|
await refreshTokenIfExpired();
|
||||||
request.headers.set('Authorization', token.token);
|
request.headers.set('Authorization', auth.token);
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
@@ -1,186 +0,0 @@
|
|||||||
import { initializeApp } from 'firebase/app';
|
|
||||||
import { getAuth, GoogleAuthProvider, signInWithPopup } from 'firebase/auth';
|
|
||||||
import type { User } from 'firebase/auth';
|
|
||||||
import { decode } from 'js-base64';
|
|
||||||
|
|
||||||
import { getLogger } from '../../../logger.js';
|
|
||||||
import { bareClient } from './request.js';
|
|
||||||
|
|
||||||
export interface AccessTokenMessage {
|
|
||||||
create_at: number;
|
|
||||||
exp: number;
|
|
||||||
email: string;
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
avatar_url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Callback = (user: AccessTokenMessage | null) => void;
|
|
||||||
|
|
||||||
type LoginParams = {
|
|
||||||
type: 'Google' | 'Refresh';
|
|
||||||
token: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type LoginResponse = {
|
|
||||||
// access token, expires in a very short time
|
|
||||||
token: string;
|
|
||||||
// Refresh token
|
|
||||||
refresh: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const login = (params: LoginParams): Promise<LoginResponse> =>
|
|
||||||
bareClient.post('api/user/token', { json: params }).json();
|
|
||||||
|
|
||||||
export class Token {
|
|
||||||
private readonly _logger;
|
|
||||||
private _accessToken!: string;
|
|
||||||
private _refreshToken!: string;
|
|
||||||
|
|
||||||
private _user!: AccessTokenMessage | null;
|
|
||||||
private _padding?: Promise<LoginResponse>;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this._logger = getLogger('token');
|
|
||||||
this._logger.enabled = true;
|
|
||||||
|
|
||||||
this._setToken(); // fill with default value
|
|
||||||
}
|
|
||||||
|
|
||||||
get user() {
|
|
||||||
return this._user;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _setToken(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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async initToken(token: string) {
|
|
||||||
const tokens = await login({ token, type: 'Google' });
|
|
||||||
this._setToken(tokens);
|
|
||||||
return this._user;
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshToken(token?: string) {
|
|
||||||
if (!this._padding) {
|
|
||||||
this._padding = login({
|
|
||||||
type: 'Refresh',
|
|
||||||
token: this._refreshToken || token!,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this._setToken(await this._padding);
|
|
||||||
this._padding = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
get token() {
|
|
||||||
return this._accessToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
get refresh() {
|
|
||||||
return this._refreshToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
get isLogin() {
|
|
||||||
return !!this._refreshToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
get isExpired() {
|
|
||||||
if (!this._user) return true;
|
|
||||||
return Date.now() - this._user.create_at > this._user.exp;
|
|
||||||
}
|
|
||||||
|
|
||||||
static parse(token: string): AccessTokenMessage | null {
|
|
||||||
try {
|
|
||||||
return JSON.parse(decode(token.split('.')[1]));
|
|
||||||
} catch (error) {
|
|
||||||
// todo: log errors?
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private callbacks: Callback[] = [];
|
|
||||||
private lastState: AccessTokenMessage | null = null;
|
|
||||||
|
|
||||||
triggerChange(user: AccessTokenMessage | null) {
|
|
||||||
this.lastState = user;
|
|
||||||
this.callbacks.forEach(callback => callback(user));
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(callback: Callback) {
|
|
||||||
this.callbacks.push(callback);
|
|
||||||
callback(this.lastState);
|
|
||||||
}
|
|
||||||
|
|
||||||
offChange(callback: Callback) {
|
|
||||||
const index = this.callbacks.indexOf(callback);
|
|
||||||
if (index > -1) {
|
|
||||||
this.callbacks.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clear() {
|
|
||||||
this._setToken();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const token = new Token();
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
const googleAuthProvider = new GoogleAuthProvider();
|
|
||||||
|
|
||||||
const getToken = async () => {
|
|
||||||
const currentUser = firebaseAuth.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 token.initToken(idToken);
|
|
||||||
} else {
|
|
||||||
const user = await signInWithPopup(firebaseAuth, googleAuthProvider);
|
|
||||||
const idToken = await user.user.getIdToken();
|
|
||||||
loginUser = await token.initToken(idToken);
|
|
||||||
}
|
|
||||||
return loginUser;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onAuthStateChanged = (callback: (user: User | null) => void) => {
|
|
||||||
firebaseAuth.onAuthStateChanged(callback);
|
|
||||||
};
|
|
||||||
|
|
||||||
return [signInWithGoogle, onAuthStateChanged] as const;
|
|
||||||
} catch (e) {
|
|
||||||
getLogger('getAuthorizer')(e);
|
|
||||||
console.error('getAuthorizer', e);
|
|
||||||
return [] as const;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -41,7 +41,7 @@ export interface Workspace {
|
|||||||
|
|
||||||
export async function getWorkspaces(): Promise<Workspace[]> {
|
export async function getWorkspaces(): Promise<Workspace[]> {
|
||||||
try {
|
try {
|
||||||
return client
|
return await client
|
||||||
.get('api/workspace', {
|
.get('api/workspace', {
|
||||||
headers: {
|
headers: {
|
||||||
'Cache-Control': 'no-cache',
|
'Cache-Control': 'no-cache',
|
||||||
@@ -63,8 +63,7 @@ export async function getWorkspaceDetail(
|
|||||||
params: GetWorkspaceDetailParams
|
params: GetWorkspaceDetailParams
|
||||||
): Promise<WorkspaceDetail | null> {
|
): Promise<WorkspaceDetail | null> {
|
||||||
try {
|
try {
|
||||||
const response = client.get(`api/workspace/${params.id}`);
|
return await client.get(`api/workspace/${params.id}`).json();
|
||||||
return response.json();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
sendMessage(messageCode.getDetailFailed);
|
sendMessage(messageCode.getDetailFailed);
|
||||||
throw new RequestError('get detail failed', error);
|
throw new RequestError('get detail failed', error);
|
||||||
@@ -100,7 +99,7 @@ export async function getWorkspaceMembers(
|
|||||||
params: GetWorkspaceDetailParams
|
params: GetWorkspaceDetailParams
|
||||||
): Promise<Member[]> {
|
): Promise<Member[]> {
|
||||||
try {
|
try {
|
||||||
return client.get(`api/workspace/${params.id}/permission`).json();
|
return await client.get(`api/workspace/${params.id}/permission`).json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
sendMessage(messageCode.getMembersFailed);
|
sendMessage(messageCode.getMembersFailed);
|
||||||
throw new RequestError('get members failed', error);
|
throw new RequestError('get members failed', error);
|
||||||
@@ -115,7 +114,7 @@ export async function createWorkspace(
|
|||||||
encodedYDoc: Blob
|
encodedYDoc: Blob
|
||||||
): Promise<{ id: string }> {
|
): Promise<{ id: string }> {
|
||||||
try {
|
try {
|
||||||
return client.post('api/workspace', { body: encodedYDoc }).json();
|
return await client.post('api/workspace', { body: encodedYDoc }).json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
sendMessage(messageCode.createWorkspaceFailed);
|
sendMessage(messageCode.createWorkspaceFailed);
|
||||||
throw new RequestError('create workspace failed', error);
|
throw new RequestError('create workspace failed', error);
|
||||||
@@ -131,7 +130,7 @@ export async function updateWorkspace(
|
|||||||
params: UpdateWorkspaceParams
|
params: UpdateWorkspaceParams
|
||||||
): Promise<{ public: boolean | null }> {
|
): Promise<{ public: boolean | null }> {
|
||||||
try {
|
try {
|
||||||
return client
|
return await client
|
||||||
.post(`api/workspace/${params.id}`, {
|
.post(`api/workspace/${params.id}`, {
|
||||||
json: {
|
json: {
|
||||||
public: params.public,
|
public: params.public,
|
||||||
@@ -151,10 +150,12 @@ export interface DeleteWorkspaceParams {
|
|||||||
export async function deleteWorkspace(
|
export async function deleteWorkspace(
|
||||||
params: DeleteWorkspaceParams
|
params: DeleteWorkspaceParams
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await client.delete(`api/workspace/${params.id}`).catch(error => {
|
try {
|
||||||
|
await client.delete(`api/workspace/${params.id}`);
|
||||||
|
} catch (error) {
|
||||||
sendMessage(messageCode.deleteWorkspaceFailed);
|
sendMessage(messageCode.deleteWorkspaceFailed);
|
||||||
throw new RequestError('delete workspace failed', error);
|
throw new RequestError('delete workspace failed', error);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InviteMemberParams {
|
export interface InviteMemberParams {
|
||||||
@@ -167,13 +168,11 @@ export interface InviteMemberParams {
|
|||||||
*/
|
*/
|
||||||
export async function inviteMember(params: InviteMemberParams): Promise<void> {
|
export async function inviteMember(params: InviteMemberParams): Promise<void> {
|
||||||
try {
|
try {
|
||||||
return client
|
await client.post(`api/workspace/${params.id}/permission`, {
|
||||||
.post(`api/workspace/${params.id}/permission`, {
|
json: {
|
||||||
json: {
|
email: params.email,
|
||||||
email: params.email,
|
},
|
||||||
},
|
});
|
||||||
})
|
|
||||||
.json();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
sendMessage(messageCode.inviteMemberFailed);
|
sendMessage(messageCode.inviteMemberFailed);
|
||||||
throw new RequestError('invite member failed', error);
|
throw new RequestError('invite member failed', error);
|
||||||
@@ -185,10 +184,12 @@ export interface RemoveMemberParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function removeMember(params: RemoveMemberParams): Promise<void> {
|
export async function removeMember(params: RemoveMemberParams): Promise<void> {
|
||||||
await client.delete(`api/permission/${params.permissionId}`).catch(error => {
|
try {
|
||||||
|
await client.delete(`api/permission/${params.permissionId}`);
|
||||||
|
} catch (error) {
|
||||||
sendMessage(messageCode.removeMemberFailed);
|
sendMessage(messageCode.removeMemberFailed);
|
||||||
throw new RequestError('remove member failed', error);
|
throw new RequestError('remove member failed', error);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AcceptInvitingParams {
|
export interface AcceptInvitingParams {
|
||||||
@@ -199,7 +200,9 @@ export async function acceptInviting(
|
|||||||
params: AcceptInvitingParams
|
params: AcceptInvitingParams
|
||||||
): Promise<Permission> {
|
): Promise<Permission> {
|
||||||
try {
|
try {
|
||||||
return bareClient.post(`api/invitation/${params.invitingCode}`).json();
|
return await bareClient
|
||||||
|
.post(`api/invitation/${params.invitingCode}`)
|
||||||
|
.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
sendMessage(messageCode.acceptInvitingFailed);
|
sendMessage(messageCode.acceptInvitingFailed);
|
||||||
throw new RequestError('accept inviting failed', error);
|
throw new RequestError('accept inviting failed', error);
|
||||||
@@ -214,7 +217,7 @@ export async function getBlob(params: {
|
|||||||
blobId: string;
|
blobId: string;
|
||||||
}): Promise<ArrayBuffer> {
|
}): Promise<ArrayBuffer> {
|
||||||
try {
|
try {
|
||||||
return client.get(`api/blob/${params.blobId}`).arrayBuffer();
|
return await client.get(`api/blob/${params.blobId}`).arrayBuffer();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
sendMessage(messageCode.getBlobFailed);
|
sendMessage(messageCode.getBlobFailed);
|
||||||
throw new RequestError('get blob failed', error);
|
throw new RequestError('get blob failed', error);
|
||||||
@@ -226,13 +229,12 @@ export interface LeaveWorkspaceParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function leaveWorkspace({ id }: LeaveWorkspaceParams) {
|
export async function leaveWorkspace({ id }: LeaveWorkspaceParams) {
|
||||||
await client
|
try {
|
||||||
.delete(`api/workspace/${id}/permission`)
|
await client.delete(`api/workspace/${id}/permission`);
|
||||||
.json()
|
} catch (error) {
|
||||||
.catch(error => {
|
sendMessage(messageCode.leaveWorkspaceFailed);
|
||||||
sendMessage(messageCode.leaveWorkspaceFailed);
|
throw new RequestError('leave workspace failed', error);
|
||||||
throw new RequestError('leave workspace failed', error);
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function downloadWorkspace(
|
export async function downloadWorkspace(
|
||||||
@@ -241,9 +243,11 @@ export async function downloadWorkspace(
|
|||||||
): Promise<ArrayBuffer> {
|
): Promise<ArrayBuffer> {
|
||||||
try {
|
try {
|
||||||
if (published) {
|
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) {
|
} catch (error) {
|
||||||
sendMessage(messageCode.downloadWorkspaceFailed);
|
sendMessage(messageCode.downloadWorkspaceFailed);
|
||||||
throw new RequestError('download workspace failed', error);
|
throw new RequestError('download workspace failed', error);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as websocket from 'lib0/websocket';
|
import * as websocket from 'lib0/websocket';
|
||||||
import { Logger } from 'src/types';
|
import { Logger } from 'src/types';
|
||||||
import { token } from './apis/token';
|
import { auth } from './apis/auth';
|
||||||
import * as url from 'lib0/url';
|
import * as url from 'lib0/url';
|
||||||
|
|
||||||
const RECONNECT_INTERVAL_TIME = 500;
|
const RECONNECT_INTERVAL_TIME = 500;
|
||||||
@@ -40,7 +40,7 @@ export class WebsocketClient extends websocket.WebsocketClient {
|
|||||||
this.on('disconnect', ({ error }: { error: Error }) => {
|
this.on('disconnect', ({ error }: { error: Error }) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
// Try reconnect if connect error has occurred
|
// Try reconnect if connect error has occurred
|
||||||
if (this.shouldReconnect && token.isLogin && !this.connected) {
|
if (this.shouldReconnect && auth.isLogin && !this.connected) {
|
||||||
try {
|
try {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (this._retryTimes <= MAX_RECONNECT_TIMES) {
|
if (this._retryTimes <= MAX_RECONNECT_TIMES) {
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
export { varStorage as storage } from 'lib0/storage';
|
import { varStorage } from 'lib0/storage';
|
||||||
|
|
||||||
|
export const storage = varStorage as Storage;
|
||||||
|
|||||||
Reference in New Issue
Block a user