diff --git a/packages/data-center/package.json b/packages/data-center/package.json index eab7ce011f..e530ece8ce 100644 --- a/packages/data-center/package.json +++ b/packages/data-center/package.json @@ -30,6 +30,7 @@ "firebase": "^9.15.0", "idb-keyval": "^6.2.0", "ky": "^0.33.0", + "ky-universal": "^0.11.0", "lib0": "^0.2.58", "swr": "^2.0.0", "yjs": "^13.5.43", diff --git a/packages/data-center/src/datacenter/datacenter.ts b/packages/data-center/src/datacenter/datacenter.ts index e0e0a8d174..9085bdf490 100644 --- a/packages/data-center/src/datacenter/datacenter.ts +++ b/packages/data-center/src/datacenter/datacenter.ts @@ -1,7 +1,7 @@ import { Workspace } from '@blocksuite/store'; import assert from 'assert'; -import type { BaseProvider } from './provider/index.js'; +import { AffineProvider, BaseProvider } from './provider/index.js'; import { MemoryProvider } from './provider/index.js'; import { getKVConfigure } from './store.js'; @@ -12,6 +12,7 @@ export class DataCenter { static async init(): Promise { const dc = new DataCenter(); + dc.addProvider(AffineProvider); dc.addProvider(MemoryProvider); return dc; @@ -31,10 +32,10 @@ export class DataCenter { const Provider = this._providers.get(providerId); assert(Provider); const provider = new Provider(); + console.log(`Loading workspace ${id} with provider ${Provider.id}`); await provider.init(getKVConfigure(id), workspace); await provider.initData(); - console.log(`Workspace ${id} loaded`); return provider; diff --git a/packages/data-center/src/datacenter/provider/affine/events.ts b/packages/data-center/src/datacenter/provider/affine/events.ts new file mode 100644 index 0000000000..ae90c6756d --- /dev/null +++ b/packages/data-center/src/datacenter/provider/affine/events.ts @@ -0,0 +1,28 @@ +import { AccessTokenMessage } from './token'; + +export type Callback = (user: AccessTokenMessage | null) => void; + +export class AuthorizationEvent { + private callbacks: Callback[] = []; + private lastState: AccessTokenMessage | null = null; + + /** + * Callback will execute when call this function. + */ + onChange(callback: Callback) { + this.callbacks.push(callback); + callback(this.lastState); + } + + triggerChange(user: AccessTokenMessage | null) { + this.lastState = user; + this.callbacks.forEach(callback => callback(user)); + } + + removeCallback(callback: Callback) { + const index = this.callbacks.indexOf(callback); + if (index > -1) { + this.callbacks.splice(index, 1); + } + } +} diff --git a/packages/data-center/src/datacenter/provider/affine/index.ts b/packages/data-center/src/datacenter/provider/affine/index.ts new file mode 100644 index 0000000000..cae85d878d --- /dev/null +++ b/packages/data-center/src/datacenter/provider/affine/index.ts @@ -0,0 +1 @@ +export { AffineProvider } from './provider.js'; diff --git a/packages/data-center/src/datacenter/provider/affine/provider.ts b/packages/data-center/src/datacenter/provider/affine/provider.ts new file mode 100644 index 0000000000..1f7c3f9c55 --- /dev/null +++ b/packages/data-center/src/datacenter/provider/affine/provider.ts @@ -0,0 +1,42 @@ +import assert from 'assert'; +import { Workspace } from '@blocksuite/store'; +import { BaseProvider } from '../base.js'; +import { ConfigStore } from '../index.js'; +import { token } from './token.js'; +import { Callback } from './events.js'; + +export class AffineProvider extends BaseProvider { + static id = 'affine'; + private _onTokenRefresh?: Callback = undefined; + + constructor() { + super(); + } + + async init(config: ConfigStore, workspace: Workspace) { + super.init(config, workspace); + + this._onTokenRefresh = () => { + if (token.refresh) { + this._config.set('token', token.refresh); + } + }; + assert(this._onTokenRefresh); + + token.onChange(this._onTokenRefresh); + if (token.isExpired) { + const refreshToken = await this._config.get('token'); + await token.refreshToken(refreshToken); + } + } + + async destroy() { + if (this._onTokenRefresh) { + token.offChange(this._onTokenRefresh); + } + } + + async initData() { + console.log('initData', token.isLogin); + } +} diff --git a/packages/data-center/src/datacenter/provider/affine/request.ts b/packages/data-center/src/datacenter/provider/affine/request.ts new file mode 100644 index 0000000000..77db147452 --- /dev/null +++ b/packages/data-center/src/datacenter/provider/affine/request.ts @@ -0,0 +1,46 @@ +import ky from 'ky-universal'; +import { token } from './token.js'; + +export const bareClient = ky.extend({ + prefixUrl: 'http://localhost:8080', + retry: 1, + hooks: { + // afterResponse: [ + // async (_request, _options, response) => { + // if (response.status === 200) { + // const data = await response.json(); + // if (data.error) { + // return new Response(data.error.message, { + // status: data.error.code, + // }); + // } + // } + // return response; + // }, + // ], + }, +}); +export const client = bareClient.extend({ + hooks: { + beforeRequest: [ + async request => { + if (token.isLogin) { + if (token.isExpired) await token.refreshToken(); + request.headers.set('Authorization', token.token); + } else { + return new Response('Unauthorized', { status: 401 }); + } + }, + ], + + beforeRetry: [ + async ({ request }) => { + await token.refreshToken(); + request.headers.set('Authorization', token.token); + }, + ], + }, +}); + +export type { AccessTokenMessage } from './token'; +export { token }; diff --git a/packages/data-center/src/datacenter/provider/affine/token.ts b/packages/data-center/src/datacenter/provider/affine/token.ts new file mode 100644 index 0000000000..1ce9539dd1 --- /dev/null +++ b/packages/data-center/src/datacenter/provider/affine/token.ts @@ -0,0 +1,117 @@ +import { bareClient } from './request.js'; +import { AuthorizationEvent, Callback } from './events.js'; + +export interface AccessTokenMessage { + create_at: number; + exp: number; + email: string; + id: string; + name: string; + avatar_url: string; +} + +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 => + bareClient.post('api/user/token', { json: params }).json(); + +function b64DecodeUnicode(str: string) { + // Going backwards: from byte stream, to percent-encoding, to original string. + return decodeURIComponent( + window + .atob(str) + .split('') + .map(function (c) { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }) + .join('') + ); +} + +class Token { + private readonly _event: AuthorizationEvent; + private _accessToken!: string; + private _refreshToken!: string; + + private _user!: AccessTokenMessage | null; + private _padding?: Promise; + + constructor() { + this._event = new AuthorizationEvent(); + this._setToken(); // fill with default value + } + + private _setToken(login?: LoginResponse) { + this._accessToken = login?.token || ''; + this._refreshToken = login?.refresh || ''; + + this._user = Token.parse(this._accessToken); + this._event.triggerChange(this._user); + } + + async initToken(token: string) { + this._setToken(await login({ token, type: 'Google' })); + } + + async refreshToken(token?: string) { + if (!this._refreshToken && !token) { + throw new Error('No authorization token.'); + } + 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 { + const message: AccessTokenMessage = JSON.parse( + b64DecodeUnicode(token.split('.')[1]) + ); + return message; + } catch (error) { + return null; + } + } + + onChange(callback: Callback) { + this._event.onChange(callback); + } + + offChange(callback: Callback) { + this._event.removeCallback(callback); + } +} + +export const token = new Token(); diff --git a/packages/data-center/src/datacenter/provider/base.ts b/packages/data-center/src/datacenter/provider/base.ts index a3d7202171..3f9d8e4642 100644 --- a/packages/data-center/src/datacenter/provider/base.ts +++ b/packages/data-center/src/datacenter/provider/base.ts @@ -16,6 +16,10 @@ export class BaseProvider { this._workspace = workspace; } + async destroy() { + // Nothing to do here + } + async initData() { throw Error('Not implemented: initData'); } diff --git a/packages/data-center/src/datacenter/provider/index.ts b/packages/data-center/src/datacenter/provider/index.ts index dcec48039b..50c70023a3 100644 --- a/packages/data-center/src/datacenter/provider/index.ts +++ b/packages/data-center/src/datacenter/provider/index.ts @@ -1,2 +1,5 @@ +export type { ConfigStore } from '../store'; + export { BaseProvider } from './base.js'; +export { AffineProvider } from './affine/index.js'; export { MemoryProvider } from './memory.js'; diff --git a/packages/data-center/src/datacenter/store.ts b/packages/data-center/src/datacenter/store.ts index 68cbebae6a..2b8e5473e9 100644 --- a/packages/data-center/src/datacenter/store.ts +++ b/packages/data-center/src/datacenter/store.ts @@ -24,7 +24,7 @@ const scopedIndexedDB = () => { return (scope: string): Readonly> => { if (!storeCache.has(scope)) { const store = { - get: (key: string) => idb.get(`${scope}:${key}`), + get: async (key: string) => idb.get(`${scope}:${key}`), set: (key: string, value: T) => idb.set(`${scope}:${key}`, value), keys: () => idb diff --git a/packages/data-center/tests/datacenter.spec.ts b/packages/data-center/tests/datacenter.spec.ts index 3b0602c1f9..9c755b68a7 100644 --- a/packages/data-center/tests/datacenter.spec.ts +++ b/packages/data-center/tests/datacenter.spec.ts @@ -8,10 +8,7 @@ test('can init data center', async () => { const dataCenter = await getDataCenter(); expect(dataCenter).toBeTruthy(); - dataCenter.setWorkspaceConfig('test', 'key', 'value'); - const workspace = await dataCenter.initWorkspace('test'); - expect(workspace).toBeTruthy(); }); @@ -21,3 +18,14 @@ test('should init error', async () => { test.fail(); await dataCenter.initWorkspace('test', 'not exist provider'); }); + +test('can init affine provider', async () => { + const dataCenter = await getDataCenter(); + + // TODO: set constant token for testing + await dataCenter.setWorkspaceConfig('test', 'token', ''); + + const workspace = await dataCenter.initWorkspace('test', 'affine'); + + expect(workspace).toBeTruthy(); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ce22115325..33fa612f95 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -138,17 +138,19 @@ importers: firebase: ^9.15.0 idb-keyval: ^6.2.0 ky: ^0.33.0 + ky-universal: ^0.11.0 lib0: ^0.2.58 swr: ^2.0.0 typescript: ^4.8.4 y-protocols: ^1.0.5 yjs: ^13.5.43 dependencies: - '@blocksuite/store': 0.3.0-20221228162706-9576a3a_yjs@13.5.43 + '@blocksuite/store': 0.3.0-20221228162706-9576a3a_yjs@13.5.44 encoding: 0.1.13 firebase: 9.15.0_encoding@0.1.13 idb-keyval: 6.2.0 ky: 0.33.1 + ky-universal: 0.11.0_ky@0.33.1 lib0: 0.2.58 swr: 2.0.0 y-protocols: 1.0.5 @@ -1532,6 +1534,27 @@ packages: react: 18.2.0 dev: false + /@blocksuite/store/0.3.0-20221228162706-9576a3a_yjs@13.5.44: + resolution: {integrity: sha512-uMbLD+zfhHasDwCmE1ZTtFtK9GNwN37Iaugbj/rtdwhQbdE6qfKHidSY0UzwKtNrQaUdAmp2IpnBpQ0vgk0ArQ==} + peerDependencies: + yjs: ^13 + dependencies: + '@types/flexsearch': 0.7.3 + '@types/quill': 1.3.10 + buffer: 6.0.3 + flexsearch: 0.7.21 + idb-keyval: 6.2.0 + ky: 0.33.1 + lib0: 0.2.58 + y-protocols: 1.0.5 + y-webrtc: 10.2.3 + yjs: 13.5.44 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + /@blocksuite/store/0.3.1_yjs@13.5.44: resolution: {integrity: sha512-kynVTDfNCSChz2JI2rtGHxRIV2YrLzvAgVajcbfDVCuXKG0siBoEjLasG1a0kvevbvW/FabrNAj+xaIplklioA==} peerDependencies: @@ -3927,6 +3950,13 @@ packages: eslint-visitor-keys: 3.3.0 dev: true + /abort-controller/3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + dependencies: + event-target-shim: 5.0.1 + dev: false + /acorn-jsx/5.3.2_acorn@8.8.0: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -4710,6 +4740,11 @@ packages: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} dev: true + /data-uri-to-buffer/4.0.0: + resolution: {integrity: sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==} + engines: {node: '>= 12'} + dev: false + /dayjs/1.11.7: resolution: {integrity: sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==} dev: false @@ -5567,6 +5602,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /event-target-shim/5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + dev: false + /eventemitter3/2.0.3: resolution: {integrity: sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==} dev: false @@ -5690,6 +5730,14 @@ packages: bser: 2.1.1 dev: true + /fetch-blob/3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.2.1 + dev: false + /file-entry-cache/6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -5810,6 +5858,13 @@ packages: engines: {node: '>= 14.17'} dev: true + /formdata-polyfill/4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + dependencies: + fetch-blob: 3.2.0 + dev: false + /fs-extra/7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -7037,6 +7092,21 @@ packages: engines: {node: '>=6'} dev: true + /ky-universal/0.11.0_ky@0.33.1: + resolution: {integrity: sha512-65KyweaWvk+uKKkCrfAf+xqN2/epw1IJDtlyCPxYffFCMR8u1sp2U65NtWpnozYfZxQ6IUzIlvUcw+hQ82U2Xw==} + engines: {node: '>=14.16'} + peerDependencies: + ky: '>=0.31.4' + web-streams-polyfill: '>=3.2.1' + peerDependenciesMeta: + web-streams-polyfill: + optional: true + dependencies: + abort-controller: 3.0.0 + ky: 0.33.1 + node-fetch: 3.3.0 + dev: false + /ky/0.33.1: resolution: {integrity: sha512-zZ9OlhgM4UEunvgJBH1bBl7+a7vas1HnCLSezu2CJawc4Ka+yJculRAVKbakUece4gW7kC5Dz+UGvbXIlpDt1w==} engines: {node: '>=14.16'} @@ -7545,6 +7615,11 @@ packages: - '@babel/core' - babel-plugin-macros + /node-domexception/1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + dev: false + /node-fetch/2.6.7_encoding@0.1.13: resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} engines: {node: 4.x || >=6.0.0} @@ -7558,6 +7633,15 @@ packages: whatwg-url: 5.0.0 dev: false + /node-fetch/3.3.0: + resolution: {integrity: sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + data-uri-to-buffer: 4.0.0 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + dev: false + /node-int64/0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} dev: true @@ -9307,6 +9391,11 @@ packages: defaults: 1.0.4 dev: true + /web-streams-polyfill/3.2.1: + resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==} + engines: {node: '>= 8'} + dev: false + /webidl-conversions/3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} dev: false