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
@@ -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 };
@@ -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';
+1 -1
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