mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-22 19:11:32 +03:00
refactor: login method (#1676)
This commit is contained in:
parent
a415e4aa5c
commit
56acb2bdeb
@ -44,6 +44,7 @@
|
||||
"devDependencies": {
|
||||
"@perfsee/webpack": "^1.5.0",
|
||||
"@redux-devtools/extension": "^3.2.5",
|
||||
"@rich-data/viewer": "^2.2.4",
|
||||
"@swc-jotai/debug-label": "^0.0.9",
|
||||
"@swc-jotai/react-refresh": "^0.0.7",
|
||||
"@types/react": "^18.0.28",
|
||||
|
@ -4,7 +4,7 @@ import { messages } from '@affine/datacenter';
|
||||
import type React from 'react';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
|
||||
import { useOnGoogleLogout } from '../../../hooks/use-on-google-logout';
|
||||
import { useAffineLogOut } from '../../../hooks/affine/use-affine-log-out';
|
||||
import { apis } from '../../../shared/apis';
|
||||
|
||||
declare global {
|
||||
@ -17,7 +17,7 @@ declare global {
|
||||
|
||||
export const MessageCenter: React.FC = memo(function MessageCenter() {
|
||||
const [popup, setPopup] = useState(false);
|
||||
const onLogout = useOnGoogleLogout();
|
||||
const onLogout = useAffineLogOut();
|
||||
useEffect(() => {
|
||||
const listener = (
|
||||
event: CustomEvent<{
|
||||
|
26
apps/web/src/hooks/affine/use-affine-log-in.ts
Normal file
26
apps/web/src/hooks/affine/use-affine-log-in.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { toast } from '@affine/component';
|
||||
import {
|
||||
createAffineAuth,
|
||||
setLoginStorage,
|
||||
SignMethod,
|
||||
} from '@affine/workspace/affine/login';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { apis } from '../../shared/apis';
|
||||
|
||||
export const affineAuth = createAffineAuth();
|
||||
|
||||
export function useAffineLogIn() {
|
||||
const router = useRouter();
|
||||
return useCallback(async () => {
|
||||
const response = await affineAuth.generateToken(SignMethod.Google);
|
||||
if (response) {
|
||||
setLoginStorage(response);
|
||||
apis.auth.setLogin(response);
|
||||
router.reload();
|
||||
} else {
|
||||
toast('Login failed');
|
||||
}
|
||||
}, [router]);
|
||||
}
|
@ -1,13 +1,14 @@
|
||||
import { clearLoginStorage } from '@affine/workspace/affine/login';
|
||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { jotaiWorkspacesAtom } from '../atoms';
|
||||
import { WorkspacePlugins } from '../plugins';
|
||||
import { apis } from '../shared/apis';
|
||||
import { jotaiWorkspacesAtom } from '../../atoms';
|
||||
import { WorkspacePlugins } from '../../plugins';
|
||||
import { apis } from '../../shared/apis';
|
||||
|
||||
export function useOnGoogleLogout() {
|
||||
export function useAffineLogOut() {
|
||||
const set = useSetAtom(jotaiWorkspacesAtom);
|
||||
const router = useRouter();
|
||||
return useCallback(() => {
|
||||
@ -18,6 +19,7 @@ export function useOnGoogleLogout() {
|
||||
)
|
||||
);
|
||||
WorkspacePlugins[WorkspaceFlavour.AFFINE].cleanup?.();
|
||||
clearLoginStorage();
|
||||
router.reload();
|
||||
}, [router, set]);
|
||||
}
|
36
apps/web/src/hooks/affine/use-affine-refresh-auth-token.ts
Normal file
36
apps/web/src/hooks/affine/use-affine-refresh-auth-token.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import {
|
||||
getLoginStorage,
|
||||
isExpired,
|
||||
parseIdToken,
|
||||
setLoginStorage,
|
||||
} from '@affine/workspace/affine/login';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { apis } from '../../shared/apis';
|
||||
import { affineAuth } from './use-affine-log-in';
|
||||
|
||||
const logger = new DebugLogger('auth-token');
|
||||
|
||||
const revalidate = async () => {
|
||||
const storage = getLoginStorage();
|
||||
if (storage) {
|
||||
const tokenMessage = parseIdToken(storage.token);
|
||||
logger.debug('revalidate affine user');
|
||||
if (isExpired(tokenMessage)) {
|
||||
logger.debug('need to refresh token');
|
||||
const response = await affineAuth.refreshToken(storage);
|
||||
if (response) {
|
||||
setLoginStorage(response);
|
||||
apis.auth.setLogin(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export function useAffineRefreshAuthToken() {
|
||||
useSWR('autoRefreshToken', {
|
||||
fetcher: revalidate,
|
||||
});
|
||||
}
|
@ -19,6 +19,7 @@ import {
|
||||
import { HelpIsland } from '../components/pure/help-island';
|
||||
import { PageLoading } from '../components/pure/loading';
|
||||
import WorkSpaceSliderBar from '../components/pure/workspace-slider-bar';
|
||||
import { useAffineRefreshAuthToken } from '../hooks/affine/use-affine-refresh-auth-token';
|
||||
import { useCurrentPageId } from '../hooks/current/use-current-page-id';
|
||||
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
|
||||
import { useBlockSuiteWorkspaceHelper } from '../hooks/use-blocksuite-workspace-helper';
|
||||
@ -92,6 +93,11 @@ export const WorkspaceLayout: React.FC<React.PropsWithChildren> =
|
||||
);
|
||||
};
|
||||
|
||||
function AffineWorkspaceEffect() {
|
||||
useAffineRefreshAuthToken();
|
||||
return null;
|
||||
}
|
||||
|
||||
export const WorkspaceLayoutInner: React.FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
@ -196,6 +202,7 @@ export const WorkspaceLayoutInner: React.FC<React.PropsWithChildren> = ({
|
||||
paths={isPublicWorkspace ? publicPathGenerator : pathGenerator}
|
||||
/>
|
||||
<StyledWrapper>
|
||||
<AffineWorkspaceEffect />
|
||||
{children}
|
||||
<StyledToolWrapper>
|
||||
{/* fixme(himself65): remove this */}
|
||||
|
@ -67,39 +67,6 @@ const App = function App({
|
||||
>
|
||||
<Head>
|
||||
<title>AFFiNE</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="initial-scale=1, width=device-width"
|
||||
/>
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:url" content="https://app.affine.pro/" />
|
||||
<meta
|
||||
name="twitter:title"
|
||||
content="AFFiNE:There can be more than Notion and Miro."
|
||||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="There can be more than Notion and Miro. AFFiNE is a next-gen knowledge base that brings planning, sorting and creating all together."
|
||||
/>
|
||||
<meta name="twitter:site" content="@AffineOfficial" />
|
||||
<meta
|
||||
name="twitter:image"
|
||||
content="https://affine.pro/og.jpeg"
|
||||
/>
|
||||
<meta
|
||||
property="og:title"
|
||||
content="AFFiNE:There can be more than Notion and Miro."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="There can be more than Notion and Miro. AFFiNE is a next-gen knowledge base that brings planning, sorting and creating all together."
|
||||
/>
|
||||
<meta property="og:url" content="https://app.affine.pro/" />
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://affine.pro/og.jpeg"
|
||||
/>
|
||||
</Head>
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
</ProviderComposer>
|
||||
|
@ -11,8 +11,16 @@ import {
|
||||
} from '@affine/workspace/affine/login';
|
||||
import { useAtom } from 'jotai';
|
||||
import type { NextPage } from 'next';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
const Viewer = dynamic(
|
||||
() => import('@rich-data/viewer').then(m => ({ default: m.JsonViewer })),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
import { useTheme } from 'next-themes';
|
||||
|
||||
import { StyledPage, StyledWrapper } from '../../layouts/styles';
|
||||
|
||||
const LoginDevPage: NextPage = () => {
|
||||
@ -69,7 +77,24 @@ const LoginDevPage: NextPage = () => {
|
||||
>
|
||||
Reset Storage
|
||||
</Button>
|
||||
{user && JSON.stringify(user)}
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const status = await fetch('/api/workspace', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
Authorization: getLoginStorage()?.token ?? '',
|
||||
},
|
||||
}).then(r => r.status);
|
||||
toast(`Response Status: ${status}`);
|
||||
}}
|
||||
>
|
||||
Check Permission
|
||||
</Button>
|
||||
<Viewer
|
||||
theme={useTheme().resolvedTheme === 'light' ? 'light' : 'dark'}
|
||||
value={user}
|
||||
/>
|
||||
</StyledWrapper>
|
||||
</StyledPage>
|
||||
);
|
||||
|
@ -51,6 +51,30 @@ export default class AppDocument extends Document<{
|
||||
/>
|
||||
<link rel="icon" sizes="192x192" href="/chrome-192x192.png" />
|
||||
<meta name="emotion-insertion-point" content="" />
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:url" content="https://app.affine.pro/" />
|
||||
<meta
|
||||
name="twitter:title"
|
||||
content="AFFiNE:There can be more than Notion and Miro."
|
||||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="There can be more than Notion and Miro. AFFiNE is a next-gen knowledge base that brings planning, sorting and creating all together."
|
||||
/>
|
||||
<meta name="twitter:site" content="@AffineOfficial" />
|
||||
<meta name="twitter:image" content="https://affine.pro/og.jpeg" />
|
||||
<meta
|
||||
property="og:title"
|
||||
content="AFFiNE:There can be more than Notion and Miro."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="There can be more than Notion and Miro. AFFiNE is a next-gen knowledge base that brings planning, sorting and creating all together."
|
||||
/>
|
||||
<meta property="og:url" content="https://app.affine.pro/" />
|
||||
<meta property="og:image" content="https://affine.pro/og.jpeg" />
|
||||
{this.props.emotionStyleTags}
|
||||
</Head>
|
||||
<body>
|
||||
|
@ -9,13 +9,13 @@ import {
|
||||
openCreateWorkspaceModalAtom,
|
||||
openWorkspacesModalAtom,
|
||||
} from '../atoms';
|
||||
import { useAffineLogIn } from '../hooks/affine/use-affine-log-in';
|
||||
import { useAffineLogOut } from '../hooks/affine/use-affine-log-out';
|
||||
import { useCurrentUser } from '../hooks/current/use-current-user';
|
||||
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
|
||||
import { useOnGoogleLogout } from '../hooks/use-on-google-logout';
|
||||
import { useRouterHelper } from '../hooks/use-router-helper';
|
||||
import { useWorkspaces, useWorkspacesHelper } from '../hooks/use-workspaces';
|
||||
import { WorkspaceSubPath } from '../shared';
|
||||
import { apis } from '../shared/apis';
|
||||
|
||||
const WorkspaceListModal = dynamic(
|
||||
async () =>
|
||||
@ -69,12 +69,8 @@ export function Modals() {
|
||||
},
|
||||
[jumpToSubPath, setCurrentWorkspace, setOpenWorkspacesModal]
|
||||
)}
|
||||
onClickLogin={useCallback(() => {
|
||||
apis.signInWithGoogle().then(() => {
|
||||
router.reload();
|
||||
});
|
||||
}, [router])}
|
||||
onClickLogout={useOnGoogleLogout()}
|
||||
onClickLogin={useAffineLogIn()}
|
||||
onClickLogout={useAffineLogOut()}
|
||||
onCreateWorkspace={useCallback(() => {
|
||||
setOpenCreateWorkspaceModal(true);
|
||||
}, [setOpenCreateWorkspaceModal])}
|
||||
|
@ -5,6 +5,10 @@ import {
|
||||
GoogleAuth,
|
||||
} from '@affine/datacenter';
|
||||
import { config } from '@affine/env';
|
||||
import {
|
||||
createUserApis,
|
||||
createWorkspaceApis,
|
||||
} from '@affine/workspace/affine/api';
|
||||
|
||||
import { isValidIPAddress } from '../utils/is-valid-ip-address';
|
||||
|
||||
@ -27,6 +31,22 @@ if (typeof window === 'undefined') {
|
||||
params.get('prefixUrl') && (prefixUrl = params.get('prefixUrl') as string);
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var affineApis:
|
||||
| undefined
|
||||
| (ReturnType<typeof createUserApis> &
|
||||
ReturnType<typeof createWorkspaceApis>);
|
||||
}
|
||||
|
||||
const affineApis = {} as ReturnType<typeof createUserApis> &
|
||||
ReturnType<typeof createWorkspaceApis>;
|
||||
Object.assign(affineApis, createUserApis(prefixUrl));
|
||||
Object.assign(affineApis, createWorkspaceApis(prefixUrl));
|
||||
if (!globalThis.affineApis) {
|
||||
globalThis.affineApis = affineApis;
|
||||
}
|
||||
|
||||
const bareAuth = createBareClient(prefixUrl);
|
||||
const googleAuth = new GoogleAuth(bareAuth);
|
||||
export const clientAuth = createAuthClient(bareAuth, googleAuth);
|
||||
|
@ -6,7 +6,8 @@
|
||||
"license": "MPL-2.0",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
"packages/*",
|
||||
"tests/fixtures"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "dev-web",
|
||||
|
@ -15,6 +15,7 @@
|
||||
"dependencies": {
|
||||
"@affine/debug": "workspace:*",
|
||||
"@blocksuite/blocks": "0.5.0-20230323085636-3110abb",
|
||||
"@blocksuite/global": "0.5.0-20230323085636-3110abb",
|
||||
"@blocksuite/store": "0.5.0-20230323085636-3110abb",
|
||||
"@tauri-apps/api": "^1.2.0",
|
||||
"encoding": "^0.1.13",
|
||||
|
@ -4,9 +4,11 @@
|
||||
"exports": {
|
||||
"./utils": "./src/utils.ts",
|
||||
"./type": "./src/type.ts",
|
||||
"./affine/*": "./src/affine/*.ts"
|
||||
"./affine/*": "./src/affine/*.ts",
|
||||
"./affine/api": "./src/affine/api/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@affine-test/fixtures": "workspace:^",
|
||||
"@affine/component": "workspace:*",
|
||||
"@affine/debug": "workspace:*",
|
||||
"@affine/env": "workspace:*",
|
||||
@ -17,6 +19,7 @@
|
||||
"js-base64": "^3.7.5",
|
||||
"ky": "^0.33.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"zod": "^3.21.4"
|
||||
}
|
||||
}
|
||||
|
86
packages/workspace/src/affine/__tests__/api.spec.ts
Normal file
86
packages/workspace/src/affine/__tests__/api.spec.ts
Normal file
@ -0,0 +1,86 @@
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
import 'fake-indexeddb/auto';
|
||||
|
||||
import userA from '@affine-test/fixtures/userA.json';
|
||||
import { Workspace } from '@blocksuite/store';
|
||||
import { beforeAll, beforeEach, describe, expect, test } from 'vitest';
|
||||
|
||||
import { createWorkspaceApis, createWorkspaceResponseSchema } from '../api';
|
||||
import { loginResponseSchema, setLoginStorage } from '../login';
|
||||
|
||||
let workspaceApis: ReturnType<typeof createWorkspaceApis>;
|
||||
|
||||
beforeAll(() => {
|
||||
workspaceApis = createWorkspaceApis('http://localhost:3000/');
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
let data;
|
||||
// first step: try to log in
|
||||
const response = await fetch('http://localhost:3000/api/user/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type: 'DebugLoginUser',
|
||||
email: userA.email,
|
||||
password: userA.password,
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
data = await fetch('http://localhost:3000/api/user/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type: 'DebugCreateUser',
|
||||
...userA,
|
||||
}),
|
||||
}).then(r => r.json());
|
||||
setLoginStorage(data);
|
||||
} else {
|
||||
setLoginStorage((data = await response.json()));
|
||||
}
|
||||
loginResponseSchema.parse(data);
|
||||
});
|
||||
|
||||
describe('api', () => {
|
||||
test(
|
||||
'create workspace',
|
||||
async () => {
|
||||
const workspace = new Workspace({
|
||||
id: 'test',
|
||||
});
|
||||
const binary = Workspace.Y.encodeStateAsUpdate(workspace.doc);
|
||||
const data = await workspaceApis.createWorkspace(new Blob([binary]));
|
||||
createWorkspaceResponseSchema.parse(data);
|
||||
},
|
||||
{
|
||||
timeout: 30000,
|
||||
}
|
||||
);
|
||||
|
||||
test(
|
||||
'delete workspace',
|
||||
async () => {
|
||||
const workspace = new Workspace({
|
||||
id: 'test',
|
||||
});
|
||||
const binary = Workspace.Y.encodeStateAsUpdate(workspace.doc);
|
||||
const data = await workspaceApis.createWorkspace(new Blob([binary]));
|
||||
createWorkspaceResponseSchema.parse(data);
|
||||
const id = data.id;
|
||||
const response = await workspaceApis.deleteWorkspace({
|
||||
id,
|
||||
});
|
||||
expect(response).toBe(true);
|
||||
},
|
||||
{
|
||||
timeout: 30000,
|
||||
}
|
||||
);
|
||||
});
|
293
packages/workspace/src/affine/api/index.ts
Normal file
293
packages/workspace/src/affine/api/index.ts
Normal file
@ -0,0 +1,293 @@
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getLoginStorage } from '../login';
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
avatar_url: string;
|
||||
create_at: string;
|
||||
}
|
||||
|
||||
export interface GetUserByEmailParams {
|
||||
email: string;
|
||||
workspace_id: string;
|
||||
}
|
||||
|
||||
export function createUserApis(prefixUrl = '/') {
|
||||
return {
|
||||
getUserByEmail: async (
|
||||
params: GetUserByEmailParams
|
||||
): Promise<User[] | null> => {
|
||||
const auth = getLoginStorage();
|
||||
assertExists(auth);
|
||||
const target = new URL(prefixUrl + 'api/user', window.location.origin);
|
||||
target.searchParams.append('email', params.email);
|
||||
target.searchParams.append('workspace_id', params.workspace_id);
|
||||
return fetch(target, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
}).then(r => r.json());
|
||||
},
|
||||
} as const;
|
||||
}
|
||||
|
||||
export interface GetWorkspaceDetailParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export enum WorkspaceType {
|
||||
Private = 0,
|
||||
Normal = 1,
|
||||
}
|
||||
|
||||
export enum PermissionType {
|
||||
Read = 0,
|
||||
Write = 1,
|
||||
Admin = 10,
|
||||
Owner = 99,
|
||||
}
|
||||
|
||||
export interface Workspace {
|
||||
id: string;
|
||||
type: WorkspaceType;
|
||||
public: boolean;
|
||||
permission: PermissionType;
|
||||
}
|
||||
|
||||
export interface WorkspaceDetail extends Workspace {
|
||||
owner: User;
|
||||
member_count: number;
|
||||
}
|
||||
|
||||
export interface Permission {
|
||||
id: string;
|
||||
type: PermissionType;
|
||||
workspace_id: string;
|
||||
user_id: string;
|
||||
user_email: string;
|
||||
accepted: boolean;
|
||||
create_at: number;
|
||||
}
|
||||
|
||||
export interface RegisteredUser extends User {
|
||||
type: 'Registered';
|
||||
}
|
||||
|
||||
export interface UnregisteredUser {
|
||||
type: 'Unregistered';
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface Member extends Permission {
|
||||
user: RegisteredUser | UnregisteredUser;
|
||||
}
|
||||
|
||||
export interface GetWorkspaceMembersParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface CreateWorkspaceParams {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface UpdateWorkspaceParams {
|
||||
id: string;
|
||||
public: boolean;
|
||||
}
|
||||
|
||||
export interface DeleteWorkspaceParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface InviteMemberParams {
|
||||
id: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface RemoveMemberParams {
|
||||
permissionId: number;
|
||||
}
|
||||
|
||||
export interface AcceptInvitingParams {
|
||||
invitingCode: string;
|
||||
}
|
||||
|
||||
export interface LeaveWorkspaceParams {
|
||||
id: number | string;
|
||||
}
|
||||
|
||||
export const createWorkspaceResponseSchema = z.object({
|
||||
id: z.string(),
|
||||
public: z.boolean(),
|
||||
type: z.nativeEnum(WorkspaceType),
|
||||
created_at: z.number(),
|
||||
});
|
||||
|
||||
export function createWorkspaceApis(prefixUrl = '/') {
|
||||
return {
|
||||
getWorkspaces: async (): Promise<Workspace[]> => {
|
||||
const auth = getLoginStorage();
|
||||
assertExists(auth);
|
||||
return fetch(prefixUrl + 'api/workspace', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
'Cache-Control': 'no-cache',
|
||||
},
|
||||
}).then(r => r.json());
|
||||
},
|
||||
getWorkspaceDetail: async (
|
||||
params: GetWorkspaceDetailParams
|
||||
): Promise<WorkspaceDetail | null> => {
|
||||
const auth = getLoginStorage();
|
||||
assertExists(auth);
|
||||
return fetch(prefixUrl + `api/workspace/${params.id}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
}).then(r => r.json());
|
||||
},
|
||||
getWorkspaceMembers: async (
|
||||
params: GetWorkspaceDetailParams
|
||||
): Promise<Member[]> => {
|
||||
const auth = getLoginStorage();
|
||||
assertExists(auth);
|
||||
return fetch(prefixUrl + `api/workspace/${params.id}/permission`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
}).then(r => r.json());
|
||||
},
|
||||
createWorkspace: async (encodedYDoc: Blob): Promise<{ id: string }> => {
|
||||
const auth = getLoginStorage();
|
||||
assertExists(auth);
|
||||
return fetch(prefixUrl + 'api/workspace', {
|
||||
method: 'POST',
|
||||
body: encodedYDoc,
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
}).then(r => r.json());
|
||||
},
|
||||
updateWorkspace: async (
|
||||
params: UpdateWorkspaceParams
|
||||
): Promise<{ public: boolean | null }> => {
|
||||
const auth = getLoginStorage();
|
||||
assertExists(auth);
|
||||
return fetch(prefixUrl + `api/workspace/${params.id}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
public: params.public,
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: auth.token,
|
||||
},
|
||||
}).then(r => r.json());
|
||||
},
|
||||
deleteWorkspace: async (
|
||||
params: DeleteWorkspaceParams
|
||||
): Promise<boolean> => {
|
||||
const auth = getLoginStorage();
|
||||
assertExists(auth);
|
||||
return fetch(prefixUrl + `api/workspace/${params.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
}).then(r => r.ok);
|
||||
},
|
||||
|
||||
/**
|
||||
* Notice: Only support normal(contrast to private) workspace.
|
||||
*/
|
||||
inviteMember: async (params: InviteMemberParams): Promise<void> => {
|
||||
const auth = getLoginStorage();
|
||||
assertExists(auth);
|
||||
return fetch(prefixUrl + `api/workspace/${params.id}/permission`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
email: params.email,
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: auth.token,
|
||||
},
|
||||
}).then(r => r.json());
|
||||
},
|
||||
removeMember: async (params: RemoveMemberParams): Promise<void> => {
|
||||
const auth = getLoginStorage();
|
||||
assertExists(auth);
|
||||
return fetch(prefixUrl + `api/permission/${params.permissionId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
}).then(r => r.json());
|
||||
},
|
||||
acceptInviting: async (
|
||||
params: AcceptInvitingParams
|
||||
): Promise<Permission> => {
|
||||
return fetch(prefixUrl + `api/invitation/${params.invitingCode}`, {
|
||||
method: 'POST',
|
||||
}).then(r => r.json());
|
||||
},
|
||||
uploadBlob: async (params: { blob: Blob }): Promise<string> => {
|
||||
const auth = getLoginStorage();
|
||||
assertExists(auth);
|
||||
return fetch(prefixUrl + 'api/blob', {
|
||||
method: 'PUT',
|
||||
body: params.blob,
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
}).then(r => r.text());
|
||||
},
|
||||
getBlob: async (params: { blobId: string }): Promise<ArrayBuffer> => {
|
||||
const auth = getLoginStorage();
|
||||
assertExists(auth);
|
||||
return fetch(prefixUrl + `api/blob/${params.blobId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
}).then(r => r.arrayBuffer());
|
||||
},
|
||||
leaveWorkspace: async ({ id }: LeaveWorkspaceParams) => {
|
||||
const auth = getLoginStorage();
|
||||
assertExists(auth);
|
||||
return fetch(prefixUrl + `api/workspace/${id}/permission`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
}).then(r => r.json());
|
||||
},
|
||||
downloadWorkspace: async (
|
||||
workspaceId: string,
|
||||
published = false
|
||||
): Promise<ArrayBuffer> => {
|
||||
if (published) {
|
||||
return fetch(prefixUrl + `api/public/doc/${workspaceId}`, {
|
||||
method: 'GET',
|
||||
}).then(r => r.arrayBuffer());
|
||||
} else {
|
||||
const auth = getLoginStorage();
|
||||
assertExists(auth);
|
||||
return fetch(prefixUrl + `api/workspace/${workspaceId}/doc`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: auth.token,
|
||||
},
|
||||
}).then(r => r.arrayBuffer());
|
||||
}
|
||||
},
|
||||
} as const;
|
||||
}
|
@ -1,4 +1,7 @@
|
||||
import type { AccessTokenMessage } from '@affine/workspace/affine/login';
|
||||
import { atom } from 'jotai';
|
||||
import { atomWithStorage } from 'jotai/utils';
|
||||
|
||||
export const currentAffineUserAtom = atom<AccessTokenMessage | null>(null);
|
||||
export const currentAffineUserAtom = atomWithStorage<AccessTokenMessage | null>(
|
||||
'affine-user-atom',
|
||||
null
|
||||
);
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
signInWithPopup,
|
||||
} from 'firebase/auth';
|
||||
import { decode } from 'js-base64';
|
||||
import { z } from 'zod';
|
||||
// Connect emulators based on env vars
|
||||
const envConnectEmulators = process.env.REACT_APP_FIREBASE_EMULATORS === 'true';
|
||||
|
||||
@ -27,12 +28,12 @@ export type LoginParams = {
|
||||
token: string;
|
||||
};
|
||||
|
||||
export type LoginResponse = {
|
||||
// access token, expires in a very short time
|
||||
token: string;
|
||||
// Refresh token
|
||||
refresh: string;
|
||||
};
|
||||
export const loginResponseSchema = z.object({
|
||||
token: z.string(),
|
||||
refresh: z.string(),
|
||||
});
|
||||
|
||||
export type LoginResponse = z.infer<typeof loginResponseSchema>;
|
||||
|
||||
const logger = new DebugLogger('token');
|
||||
|
||||
|
6
tests/fixtures/package.json
vendored
Normal file
6
tests/fixtures/package.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@affine-test/fixtures",
|
||||
"exports": {
|
||||
"./*": "./*"
|
||||
}
|
||||
}
|
4
tests/fixtures/tsconfig.json
vendored
Normal file
4
tests/fixtures/tsconfig.json
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["./src"]
|
||||
}
|
@ -28,7 +28,8 @@
|
||||
"@affine/env": ["./packages/env"],
|
||||
"@affine/env/*": ["./packages/env/src/*"],
|
||||
"@affine/utils": ["./packages/utils"],
|
||||
"@affine/workspace/*": ["./packages/workspace/src/*"]
|
||||
"@affine/workspace/*": ["./packages/workspace/src/*"],
|
||||
"@affine-test/fixtures/*": ["./tests/fixtures/*"]
|
||||
}
|
||||
},
|
||||
"references": [
|
||||
@ -58,6 +59,9 @@
|
||||
},
|
||||
{
|
||||
"path": "./packages/workspace"
|
||||
},
|
||||
{
|
||||
"path": "./tests/fixtures"
|
||||
}
|
||||
],
|
||||
"files": [],
|
||||
|
61
yarn.lock
61
yarn.lock
@ -12,6 +12,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@affine-test/fixtures@workspace:^, @affine-test/fixtures@workspace:tests/fixtures":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@affine-test/fixtures@workspace:tests/fixtures"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@affine/app@workspace:apps/web":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@affine/app@workspace:apps/web"
|
||||
@ -34,6 +40,7 @@ __metadata:
|
||||
"@mui/material": ^5.11.13
|
||||
"@perfsee/webpack": ^1.5.0
|
||||
"@redux-devtools/extension": ^3.2.5
|
||||
"@rich-data/viewer": ^2.2.4
|
||||
"@swc-jotai/debug-label": ^0.0.9
|
||||
"@swc-jotai/react-refresh": ^0.0.7
|
||||
"@types/react": ^18.0.28
|
||||
@ -166,6 +173,7 @@ __metadata:
|
||||
dependencies:
|
||||
"@affine/debug": "workspace:*"
|
||||
"@blocksuite/blocks": 0.5.0-20230323085636-3110abb
|
||||
"@blocksuite/global": 0.5.0-20230323085636-3110abb
|
||||
"@blocksuite/store": 0.5.0-20230323085636-3110abb
|
||||
"@tauri-apps/api": ^1.2.0
|
||||
encoding: ^0.1.13
|
||||
@ -256,6 +264,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@affine/workspace@workspace:packages/workspace"
|
||||
dependencies:
|
||||
"@affine-test/fixtures": "workspace:^"
|
||||
"@affine/component": "workspace:*"
|
||||
"@affine/debug": "workspace:*"
|
||||
"@affine/env": "workspace:*"
|
||||
@ -267,6 +276,7 @@ __metadata:
|
||||
ky: ^0.33.3
|
||||
react: ^18.2.0
|
||||
react-dom: ^18.2.0
|
||||
zod: ^3.21.4
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
@ -4685,6 +4695,22 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rich-data/viewer@npm:^2.2.4":
|
||||
version: 2.14.1
|
||||
resolution: "@rich-data/viewer@npm:2.14.1"
|
||||
dependencies:
|
||||
"@mui/material": ^5.11.13
|
||||
copy-to-clipboard: ^3.3.3
|
||||
zustand: ^4.3.6
|
||||
peerDependencies:
|
||||
"@emotion/react": ^11.10
|
||||
"@emotion/styled": ^11.10
|
||||
react: ^17 || ^18
|
||||
react-dom: ^17 || ^18
|
||||
checksum: a3fa1748eff116d5377ed2a3ecd24db3f6b0147d0663df66791ad9f065126ee77d2cd8b1158570be546b8735ae6db64f39081ad8f6dacda6c47455230aaefe54
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rollup/pluginutils@npm:^4.2.0":
|
||||
version: 4.2.1
|
||||
resolution: "@rollup/pluginutils@npm:4.2.1"
|
||||
@ -9340,6 +9366,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"copy-to-clipboard@npm:^3.3.3":
|
||||
version: 3.3.3
|
||||
resolution: "copy-to-clipboard@npm:3.3.3"
|
||||
dependencies:
|
||||
toggle-selection: ^1.0.6
|
||||
checksum: e0a325e39b7615108e6c1c8ac110ae7b829cdc4ee3278b1df6a0e4228c490442cc86444cd643e2da344fbc424b3aab8909e2fec82f8bc75e7e5b190b7c24eecf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"core-js-compat@npm:^3.25.1":
|
||||
version: 3.29.1
|
||||
resolution: "core-js-compat@npm:3.29.1"
|
||||
@ -19499,6 +19534,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"toggle-selection@npm:^1.0.6":
|
||||
version: 1.0.6
|
||||
resolution: "toggle-selection@npm:1.0.6"
|
||||
checksum: a90dc80ed1e7b18db8f4e16e86a5574f87632dc729cfc07d9ea3ced50021ad42bb4e08f22c0913e0b98e3837b0b717e0a51613c65f30418e21eb99da6556a74c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"toidentifier@npm:1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "toidentifier@npm:1.0.1"
|
||||
@ -20077,7 +20119,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"use-sync-external-store@npm:^1.2.0":
|
||||
"use-sync-external-store@npm:1.2.0, use-sync-external-store@npm:^1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "use-sync-external-store@npm:1.2.0"
|
||||
peerDependencies:
|
||||
@ -21016,6 +21058,23 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"zustand@npm:^4.3.6":
|
||||
version: 4.3.6
|
||||
resolution: "zustand@npm:4.3.6"
|
||||
dependencies:
|
||||
use-sync-external-store: 1.2.0
|
||||
peerDependencies:
|
||||
immer: ">=9.0"
|
||||
react: ">=16.8"
|
||||
peerDependenciesMeta:
|
||||
immer:
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
checksum: 4d3cec03526f04ff3de6dc45b6f038c47f091836af9660fbf5f682cae1628221102882df20e4048dfe699a43f67424e5d6afc1116f3838a80eea5dd4f95ddaed
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"zx@npm:^7.2.1":
|
||||
version: 7.2.1
|
||||
resolution: "zx@npm:7.2.1"
|
||||
|
Loading…
Reference in New Issue
Block a user