mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-24 22:09:08 +08:00
feat: login support
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user