feat: login support

This commit is contained in:
DarkSky 2022-12-30 16:52:06 +08:00 committed by DarkSky
parent 3aca098bee
commit 7b34ea010c
12 changed files with 347 additions and 7 deletions

View File

@ -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",

View File

@ -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<DataCenter> {
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;

View File

@ -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);
}
}
}

View File

@ -0,0 +1 @@
export { AffineProvider } from './provider.js';

View File

@ -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);
}
}

View File

@ -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 };

View File

@ -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<LoginResponse> =>
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<LoginResponse>;
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();

View File

@ -16,6 +16,10 @@ export class BaseProvider {
this._workspace = workspace;
}
async destroy() {
// Nothing to do here
}
async initData() {
throw Error('Not implemented: initData');
}

View File

@ -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';

View File

@ -24,7 +24,7 @@ const scopedIndexedDB = () => {
return <T = any>(scope: string): Readonly<ConfigStore<T>> => {
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

View File

@ -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();
});

View File

@ -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