mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
refactor: init package @affine/workspace (#1661)
This commit is contained in:
22
packages/workspace/package.json
Normal file
22
packages/workspace/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@affine/workspace",
|
||||
"private": true,
|
||||
"exports": {
|
||||
"./utils": "./src/utils.ts",
|
||||
"./type": "./src/type.ts",
|
||||
"./affine/*": "./src/affine/*.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@affine/component": "workspace:*",
|
||||
"@affine/debug": "workspace:*",
|
||||
"@affine/env": "workspace:*",
|
||||
"@blocksuite/blocks": "0.5.0-20230323085636-3110abb",
|
||||
"@blocksuite/store": "0.5.0-20230323085636-3110abb",
|
||||
"firebase": "^9.18.0",
|
||||
"jotai": "^2.0.3",
|
||||
"js-base64": "^3.7.5",
|
||||
"ky": "^0.33.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
}
|
||||
}
|
||||
20
packages/workspace/src/affine/__tests__/atom.spec.ts
Normal file
20
packages/workspace/src/affine/__tests__/atom.spec.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { getDefaultStore } from 'jotai';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { currentAffineUserAtom } from '../atom';
|
||||
|
||||
describe('atom', () => {
|
||||
test('currentAffineUserAtom', () => {
|
||||
const store = getDefaultStore();
|
||||
const mock = {
|
||||
created_at: 0,
|
||||
exp: 0,
|
||||
email: '',
|
||||
id: '',
|
||||
name: '',
|
||||
avatar_url: '',
|
||||
};
|
||||
store.set(currentAffineUserAtom, mock);
|
||||
expect(store.get(currentAffineUserAtom)).toEqual(mock);
|
||||
});
|
||||
});
|
||||
42
packages/workspace/src/affine/__tests__/login.spec.ts
Normal file
42
packages/workspace/src/affine/__tests__/login.spec.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
import type { AccessTokenMessage } from '@affine/workspace/affine/login';
|
||||
import {
|
||||
getLoginStorage,
|
||||
isExpired,
|
||||
setLoginStorage,
|
||||
STORAGE_KEY,
|
||||
} from '@affine/workspace/affine/login';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
describe('storage', () => {
|
||||
test('should work', () => {
|
||||
setLoginStorage({
|
||||
token: '1',
|
||||
refresh: '2',
|
||||
});
|
||||
const data = localStorage.getItem(STORAGE_KEY);
|
||||
expect(data).toBe('{"token":"1","refresh":"2"}');
|
||||
const login = getLoginStorage();
|
||||
expect(login).toEqual({
|
||||
token: '1',
|
||||
refresh: '2',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('utils', () => {
|
||||
test('isExpired', async () => {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
expect(isExpired({ exp: now + 1 } as AccessTokenMessage)).toBeFalsy();
|
||||
const promise = new Promise<void>(resolve => {
|
||||
setTimeout(() => {
|
||||
expect(isExpired({ exp: now + 1 } as AccessTokenMessage)).toBeTruthy();
|
||||
resolve();
|
||||
}, 2000);
|
||||
});
|
||||
expect(isExpired({ exp: now - 1 } as AccessTokenMessage)).toBeTruthy();
|
||||
await promise;
|
||||
});
|
||||
});
|
||||
4
packages/workspace/src/affine/atom.ts
Normal file
4
packages/workspace/src/affine/atom.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import type { AccessTokenMessage } from '@affine/workspace/affine/login';
|
||||
import { atom } from 'jotai';
|
||||
|
||||
export const currentAffineUserAtom = atom<AccessTokenMessage | null>(null);
|
||||
175
packages/workspace/src/affine/login.ts
Normal file
175
packages/workspace/src/affine/login.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { initializeApp } from 'firebase/app';
|
||||
import type { AuthProvider } from 'firebase/auth';
|
||||
import {
|
||||
type Auth as FirebaseAuth,
|
||||
connectAuthEmulator,
|
||||
getAuth as getFirebaseAuth,
|
||||
GithubAuthProvider,
|
||||
GoogleAuthProvider,
|
||||
signInWithPopup,
|
||||
} from 'firebase/auth';
|
||||
import { decode } from 'js-base64';
|
||||
// Connect emulators based on env vars
|
||||
const envConnectEmulators = process.env.REACT_APP_FIREBASE_EMULATORS === 'true';
|
||||
|
||||
export type AccessTokenMessage = {
|
||||
created_at: number;
|
||||
exp: number;
|
||||
email: string;
|
||||
id: string;
|
||||
name: string;
|
||||
avatar_url: string;
|
||||
};
|
||||
|
||||
export type LoginParams = {
|
||||
type: 'Google' | 'Refresh';
|
||||
token: string;
|
||||
};
|
||||
|
||||
export type LoginResponse = {
|
||||
// access token, expires in a very short time
|
||||
token: string;
|
||||
// Refresh token
|
||||
refresh: string;
|
||||
};
|
||||
|
||||
const logger = new DebugLogger('token');
|
||||
|
||||
export const STORAGE_KEY = 'affine-login-v2';
|
||||
|
||||
export function parseIdToken(token: string): AccessTokenMessage {
|
||||
return JSON.parse(decode(token.split('.')[1]));
|
||||
}
|
||||
|
||||
export const isExpired = (token: AccessTokenMessage): boolean => {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
return token.exp < now;
|
||||
};
|
||||
|
||||
export const setLoginStorage = (login: LoginResponse) => {
|
||||
localStorage.setItem(
|
||||
STORAGE_KEY,
|
||||
JSON.stringify({
|
||||
token: login.token,
|
||||
refresh: login.refresh,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const clearLoginStorage = () => {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
};
|
||||
|
||||
export const getLoginStorage = (): LoginResponse | null => {
|
||||
const login = localStorage.getItem(STORAGE_KEY);
|
||||
if (login) {
|
||||
try {
|
||||
return JSON.parse(login);
|
||||
} catch (error) {
|
||||
logger.error('Failed to parse login', error);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const enum SignMethod {
|
||||
Google = 'Google',
|
||||
GitHub = 'GitHub',
|
||||
// Twitter = 'Twitter',
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var firebaseAuthEmulatorStarted: boolean | undefined;
|
||||
}
|
||||
|
||||
export function createAffineAuth() {
|
||||
let _firebaseAuth: FirebaseAuth | null = null;
|
||||
const getAuth = (): FirebaseAuth | null => {
|
||||
try {
|
||||
if (!_firebaseAuth) {
|
||||
const app = initializeApp({
|
||||
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
|
||||
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
|
||||
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
|
||||
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
|
||||
messagingSenderId:
|
||||
process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
|
||||
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
|
||||
measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
|
||||
});
|
||||
_firebaseAuth = getFirebaseAuth(app);
|
||||
}
|
||||
if (envConnectEmulators && !globalThis.firebaseAuthEmulatorStarted) {
|
||||
connectAuthEmulator(_firebaseAuth, 'http://localhost:9099', {
|
||||
disableWarnings: true,
|
||||
});
|
||||
globalThis.firebaseAuthEmulatorStarted = true;
|
||||
}
|
||||
return _firebaseAuth;
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize firebase', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
generateToken: async (
|
||||
method: SignMethod
|
||||
): Promise<LoginResponse | null> => {
|
||||
const auth = getAuth();
|
||||
if (!auth) {
|
||||
throw new Error('Failed to initialize firebase');
|
||||
}
|
||||
let provider: AuthProvider;
|
||||
switch (method) {
|
||||
case SignMethod.Google:
|
||||
provider = new GoogleAuthProvider();
|
||||
break;
|
||||
case SignMethod.GitHub:
|
||||
provider = new GithubAuthProvider();
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unsupported sign method');
|
||||
}
|
||||
try {
|
||||
const response = await signInWithPopup(auth, provider);
|
||||
const idToken = await response.user.getIdToken();
|
||||
logger.debug(idToken);
|
||||
return fetch('/api/user/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type: 'Google',
|
||||
token: idToken,
|
||||
}),
|
||||
}).then(r => r.json()) as Promise<LoginResponse>;
|
||||
} catch (error) {
|
||||
if (error instanceof Error && 'code' in error) {
|
||||
if (error.code === 'auth/popup-closed-by-user') {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
logger.error('Failed to sign in', error);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
refreshToken: async (
|
||||
loginResponse: LoginResponse
|
||||
): Promise<LoginResponse | null> => {
|
||||
return fetch('/api/user/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type: 'Refresh',
|
||||
token: loginResponse.refresh,
|
||||
}),
|
||||
}).then(r => r.json()) as Promise<LoginResponse>;
|
||||
},
|
||||
} as const;
|
||||
}
|
||||
74
packages/workspace/src/type.ts
Normal file
74
packages/workspace/src/type.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
||||
import type { FC } from 'react';
|
||||
|
||||
export const enum LoadPriority {
|
||||
HIGH = 1,
|
||||
MEDIUM = 2,
|
||||
LOW = 3,
|
||||
}
|
||||
|
||||
export const enum WorkspaceFlavour {
|
||||
AFFINE = 'affine',
|
||||
LOCAL = 'local',
|
||||
}
|
||||
export const settingPanel = {
|
||||
General: 'general',
|
||||
Collaboration: 'collaboration',
|
||||
Publish: 'publish',
|
||||
Export: 'export',
|
||||
Sync: 'sync',
|
||||
} as const;
|
||||
export const settingPanelValues = [...Object.values(settingPanel)] as const;
|
||||
export type SettingPanel = (typeof settingPanel)[keyof typeof settingPanel];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface WorkspaceRegistry {}
|
||||
|
||||
export interface WorkspaceCRUD<Flavour extends keyof WorkspaceRegistry> {
|
||||
create: (blockSuiteWorkspace: BlockSuiteWorkspace) => Promise<string>;
|
||||
delete: (workspace: WorkspaceRegistry[Flavour]) => Promise<void>;
|
||||
get: (workspaceId: string) => Promise<WorkspaceRegistry[Flavour] | null>;
|
||||
// not supported yet
|
||||
// update: (workspace: FlavourToWorkspace[Flavour]) => Promise<void>;
|
||||
list: () => Promise<WorkspaceRegistry[Flavour][]>;
|
||||
}
|
||||
|
||||
type UIBaseProps<Flavour extends keyof WorkspaceRegistry> = {
|
||||
currentWorkspace: WorkspaceRegistry[Flavour];
|
||||
};
|
||||
|
||||
type SettingProps<Flavour extends keyof WorkspaceRegistry> =
|
||||
UIBaseProps<Flavour> & {
|
||||
currentTab: SettingPanel;
|
||||
onChangeTab: (tab: SettingPanel) => void;
|
||||
onDeleteWorkspace: () => void;
|
||||
onTransformWorkspace: <
|
||||
From extends keyof WorkspaceRegistry,
|
||||
To extends keyof WorkspaceRegistry
|
||||
>(
|
||||
from: From,
|
||||
to: To,
|
||||
workspace: WorkspaceRegistry[From]
|
||||
) => void;
|
||||
};
|
||||
|
||||
type PageDetailProps<Flavour extends keyof WorkspaceRegistry> =
|
||||
UIBaseProps<Flavour> & {
|
||||
currentPageId: string;
|
||||
};
|
||||
|
||||
type PageListProps<Flavour extends keyof WorkspaceRegistry> = {
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||
onOpenPage: (pageId: string, newTab?: boolean) => void;
|
||||
};
|
||||
|
||||
type SideBarMenuProps<Flavour extends keyof WorkspaceRegistry> =
|
||||
UIBaseProps<Flavour> & {
|
||||
setSideBarOpen: (open: boolean) => void;
|
||||
};
|
||||
|
||||
export interface WorkspaceUISchema<Flavour extends keyof WorkspaceRegistry> {
|
||||
PageDetail: FC<PageDetailProps<Flavour>>;
|
||||
PageList: FC<PageListProps<Flavour>>;
|
||||
SettingsDetail: FC<SettingProps<Flavour>>;
|
||||
}
|
||||
24
packages/workspace/src/utils.ts
Normal file
24
packages/workspace/src/utils.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
|
||||
import type { BlobOptionsGetter, Generator } from '@blocksuite/store';
|
||||
import { Workspace } from '@blocksuite/store';
|
||||
|
||||
const hashMap = new Map<string, Workspace>();
|
||||
export const createEmptyBlockSuiteWorkspace = (
|
||||
id: string,
|
||||
blobOptionsGetter?: BlobOptionsGetter,
|
||||
idGenerator?: Generator
|
||||
): Workspace => {
|
||||
if (hashMap.has(id)) {
|
||||
return hashMap.get(id) as Workspace;
|
||||
}
|
||||
const workspace = new Workspace({
|
||||
id,
|
||||
isSSR: typeof window === 'undefined',
|
||||
blobOptionsGetter,
|
||||
idGenerator,
|
||||
})
|
||||
.register(AffineSchemas)
|
||||
.register(__unstableSchemas);
|
||||
hashMap.set(id, workspace);
|
||||
return workspace;
|
||||
};
|
||||
4
packages/workspace/tsconfig.json
Normal file
4
packages/workspace/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["./src"]
|
||||
}
|
||||
Reference in New Issue
Block a user