mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-23 08:53:27 +03:00
feat: login support
This commit is contained in:
parent
3aca098bee
commit
7b34ea010c
@ -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",
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { AffineProvider } from './provider.js';
|
@ -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);
|
||||
}
|
||||
}
|
@ -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 };
|
117
packages/data-center/src/datacenter/provider/affine/token.ts
Normal file
117
packages/data-center/src/datacenter/provider/affine/token.ts
Normal 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();
|
@ -16,6 +16,10 @@ export class BaseProvider {
|
||||
this._workspace = workspace;
|
||||
}
|
||||
|
||||
async destroy() {
|
||||
// Nothing to do here
|
||||
}
|
||||
|
||||
async initData() {
|
||||
throw Error('Not implemented: initData');
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user