mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-17 01:56:27 +08:00
feat!: affine cloud support (#3813)
Co-authored-by: Hongtao Lye <codert.sn@gmail.com> Co-authored-by: liuyi <forehalo@gmail.com> Co-authored-by: LongYinan <lynweklm@gmail.com> Co-authored-by: X1a0t <405028157@qq.com> Co-authored-by: JimmFly <yangjinfei001@gmail.com> Co-authored-by: Peng Xiao <pengxiao@outlook.com> Co-authored-by: xiaodong zuo <53252747+zuoxiaodong0815@users.noreply.github.com> Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com> Co-authored-by: Qi <474021214@qq.com> Co-authored-by: danielchim <kahungchim@gmail.com>
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
import {
|
||||
ChangeEmailPage,
|
||||
ChangePasswordPage,
|
||||
SetPasswordPage,
|
||||
SignInSuccessPage,
|
||||
SignUpPage,
|
||||
} from '@affine/component/auth-components';
|
||||
import { isDesktop } from '@affine/env/constant';
|
||||
import { changeEmailMutation, changePasswordMutation } from '@affine/graphql';
|
||||
import { useMutation } from '@affine/workspace/affine/gql';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { type LoaderFunction, redirect, useParams } from 'react-router-dom';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useCurrenLoginStatus } from '../hooks/affine/use-curren-login-status';
|
||||
import { useCurrentUser } from '../hooks/affine/use-current-user';
|
||||
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||
|
||||
const authTypeSchema = z.enum([
|
||||
'setPassword',
|
||||
'signIn',
|
||||
'changePassword',
|
||||
'signUp',
|
||||
'changeEmail',
|
||||
]);
|
||||
|
||||
export const AuthPage = (): ReactElement | null => {
|
||||
const user = useCurrentUser();
|
||||
const { authType } = useParams();
|
||||
const { trigger: changePassword } = useMutation({
|
||||
mutation: changePasswordMutation,
|
||||
});
|
||||
|
||||
const { trigger: changeEmail } = useMutation({
|
||||
mutation: changeEmailMutation,
|
||||
});
|
||||
const { jumpToIndex } = useNavigateHelper();
|
||||
|
||||
const onChangeEmail = useCallback(
|
||||
async (email: string) => {
|
||||
const res = await changeEmail({
|
||||
id: user.id,
|
||||
newEmail: email,
|
||||
});
|
||||
return !!res?.changeEmail;
|
||||
},
|
||||
[changeEmail, user.id]
|
||||
);
|
||||
|
||||
const onSetPassword = useCallback(
|
||||
(password: string) => {
|
||||
changePassword({
|
||||
id: user.id,
|
||||
newPassword: password,
|
||||
}).catch(console.error);
|
||||
},
|
||||
[changePassword, user.id]
|
||||
);
|
||||
const onOpenAffine = useCallback(() => {
|
||||
if (isDesktop) {
|
||||
window.apis.ui.handleFinishLogin();
|
||||
} else {
|
||||
jumpToIndex(RouteLogic.REPLACE);
|
||||
}
|
||||
}, [jumpToIndex]);
|
||||
|
||||
switch (authType) {
|
||||
case 'signUp': {
|
||||
return (
|
||||
<SignUpPage
|
||||
user={user}
|
||||
onSetPassword={onSetPassword}
|
||||
onOpenAffine={onOpenAffine}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'signIn': {
|
||||
return <SignInSuccessPage onOpenAffine={onOpenAffine} />;
|
||||
}
|
||||
case 'changePassword': {
|
||||
return (
|
||||
<ChangePasswordPage
|
||||
user={user}
|
||||
onSetPassword={onSetPassword}
|
||||
onOpenAffine={onOpenAffine}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'setPassword': {
|
||||
return (
|
||||
<SetPasswordPage
|
||||
user={user}
|
||||
onSetPassword={onSetPassword}
|
||||
onOpenAffine={onOpenAffine}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'changeEmail': {
|
||||
return (
|
||||
<ChangeEmailPage
|
||||
user={user}
|
||||
onChangeEmail={onChangeEmail}
|
||||
onOpenAffine={onOpenAffine}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const loader: LoaderFunction = async args => {
|
||||
if (!args.params.authType) {
|
||||
return redirect('/404');
|
||||
}
|
||||
if (!authTypeSchema.safeParse(args.params.authType).success) {
|
||||
return redirect('/404');
|
||||
}
|
||||
return null;
|
||||
};
|
||||
export const Component = () => {
|
||||
const loginStatus = useCurrenLoginStatus();
|
||||
const { jumpToExpired } = useNavigateHelper();
|
||||
|
||||
if (loginStatus === 'unauthenticated') {
|
||||
jumpToExpired(RouteLogic.REPLACE);
|
||||
}
|
||||
|
||||
if (loginStatus === 'authenticated') {
|
||||
return <AuthPage />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { AuthPageContainer } from '@affine/component/auth-components';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||
|
||||
export const Component = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const { jumpToIndex } = useNavigateHelper();
|
||||
const onOpenAffine = useCallback(() => {
|
||||
jumpToIndex(RouteLogic.REPLACE);
|
||||
}, [jumpToIndex]);
|
||||
|
||||
return (
|
||||
<AuthPageContainer
|
||||
title={t['com.affine.expired.page.title']()}
|
||||
subtitle={t['com.affine.expired.page.subtitle']()}
|
||||
>
|
||||
<Button type="primary" size="large" onClick={onOpenAffine}>
|
||||
{t['com.affine.auth.open.affine']()}
|
||||
</Button>
|
||||
</AuthPageContainer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
import { AcceptInvitePage } from '@affine/component/member-components';
|
||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import {
|
||||
acceptInviteByInviteIdMutation,
|
||||
type GetInviteInfoQuery,
|
||||
getInviteInfoQuery,
|
||||
} from '@affine/graphql';
|
||||
import { fetcher } from '@affine/workspace/affine/gql';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { type LoaderFunction, redirect, useLoaderData } from 'react-router-dom';
|
||||
|
||||
import { authAtom } from '../atoms';
|
||||
import { useCurrenLoginStatus } from '../hooks/affine/use-curren-login-status';
|
||||
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||
import { useAppHelper } from '../hooks/use-workspaces';
|
||||
|
||||
export const loader: LoaderFunction = async args => {
|
||||
const inviteId = args.params.inviteId || '';
|
||||
const res = await fetcher({
|
||||
query: getInviteInfoQuery,
|
||||
variables: {
|
||||
inviteId,
|
||||
},
|
||||
}).catch(console.error);
|
||||
|
||||
// If the inviteId is invalid, redirect to 404 page
|
||||
if (!res || !res?.getInviteInfo) {
|
||||
return redirect('/404');
|
||||
}
|
||||
|
||||
// No mater sign in or not, we need to accept the invite
|
||||
await fetcher({
|
||||
query: acceptInviteByInviteIdMutation,
|
||||
variables: {
|
||||
workspaceId: res.getInviteInfo.workspace.id,
|
||||
inviteId,
|
||||
},
|
||||
}).catch(console.error);
|
||||
|
||||
return {
|
||||
inviteId,
|
||||
inviteInfo: res.getInviteInfo,
|
||||
};
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
const loginStatus = useCurrenLoginStatus();
|
||||
const { jumpToSignIn } = useNavigateHelper();
|
||||
const { addCloudWorkspace } = useAppHelper();
|
||||
const { jumpToSubPath } = useNavigateHelper();
|
||||
|
||||
const [, setAuthAtom] = useAtom(authAtom);
|
||||
const { inviteInfo } = useLoaderData() as {
|
||||
inviteId: string;
|
||||
inviteInfo: GetInviteInfoQuery['getInviteInfo'];
|
||||
};
|
||||
|
||||
const loadWorkspaceAfterSignIn = useCallback(() => {
|
||||
addCloudWorkspace(inviteInfo.workspace.id);
|
||||
}, [addCloudWorkspace, inviteInfo.workspace.id]);
|
||||
|
||||
const openWorkspace = useCallback(() => {
|
||||
addCloudWorkspace(inviteInfo.workspace.id);
|
||||
jumpToSubPath(
|
||||
inviteInfo.workspace.id,
|
||||
WorkspaceSubPath.ALL,
|
||||
RouteLogic.REPLACE
|
||||
);
|
||||
}, [addCloudWorkspace, inviteInfo.workspace.id, jumpToSubPath]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loginStatus === 'unauthenticated') {
|
||||
// We can not pass function to navigate state, so we need to save it in atom
|
||||
setAuthAtom(prev => ({
|
||||
...prev,
|
||||
onceSignedIn: loadWorkspaceAfterSignIn,
|
||||
}));
|
||||
jumpToSignIn(RouteLogic.REPLACE, {
|
||||
state: {
|
||||
callbackURL: `/workspace/${inviteInfo.workspace.id}/all`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [
|
||||
inviteInfo.workspace.id,
|
||||
jumpToSignIn,
|
||||
loadWorkspaceAfterSignIn,
|
||||
loginStatus,
|
||||
setAuthAtom,
|
||||
]);
|
||||
|
||||
if (loginStatus === 'authenticated') {
|
||||
return (
|
||||
<AcceptInvitePage
|
||||
inviteInfo={inviteInfo}
|
||||
onOpenWorkspace={openWorkspace}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import { MainContainer } from '@affine/component/workspace';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { getOrCreateWorkspace } from '@affine/workspace/manager';
|
||||
import { downloadBinaryFromCloud } from '@affine/workspace/providers';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { noop } from 'foxact/noop';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import type { LoaderFunction } from 'react-router-dom';
|
||||
import { redirect, useLoaderData } from 'react-router-dom';
|
||||
import { applyUpdate } from 'yjs';
|
||||
|
||||
import { PageDetailEditor } from '../../adapters/shared';
|
||||
import { AppContainer } from '../../components/affine/app-container';
|
||||
|
||||
function assertArrayBuffer(value: unknown): asserts value is ArrayBuffer {
|
||||
if (!(value instanceof ArrayBuffer)) {
|
||||
throw new Error('value is not ArrayBuffer');
|
||||
}
|
||||
}
|
||||
|
||||
const logger = new DebugLogger('public:share-page');
|
||||
|
||||
export const loader: LoaderFunction = async ({ params }) => {
|
||||
const workspaceId = params?.workspaceId;
|
||||
const pageId = params?.pageId;
|
||||
if (!workspaceId || !pageId) {
|
||||
return redirect('/404');
|
||||
}
|
||||
const workspace = getOrCreateWorkspace(
|
||||
workspaceId,
|
||||
WorkspaceFlavour.AFFINE_PUBLIC
|
||||
);
|
||||
// download root workspace
|
||||
{
|
||||
const buffer = await downloadBinaryFromCloud(workspaceId, workspaceId);
|
||||
assertArrayBuffer(buffer);
|
||||
applyUpdate(workspace.doc, new Uint8Array(buffer));
|
||||
}
|
||||
const page = workspace.getPage(pageId);
|
||||
assertExists(page, 'cannot find page');
|
||||
// download page
|
||||
{
|
||||
const buffer = await downloadBinaryFromCloud(
|
||||
workspaceId,
|
||||
page.spaceDoc.guid
|
||||
);
|
||||
assertArrayBuffer(buffer);
|
||||
applyUpdate(page.spaceDoc, new Uint8Array(buffer));
|
||||
}
|
||||
logger.info('workspace', workspace);
|
||||
workspace.awarenessStore.setReadonly(page, true);
|
||||
return page;
|
||||
};
|
||||
|
||||
export const Component = (): ReactElement => {
|
||||
const page = useLoaderData() as Page;
|
||||
return (
|
||||
<AppContainer>
|
||||
<MainContainer>
|
||||
<PageDetailEditor
|
||||
isPublic
|
||||
workspace={page.workspace}
|
||||
pageId={page.id}
|
||||
onInit={useCallback(() => noop, [])}
|
||||
onLoad={useCallback(() => noop, [])}
|
||||
/>
|
||||
</MainContainer>
|
||||
</AppContainer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
import { SignInPageContainer } from '@affine/component/auth-components';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { authAtom } from '../atoms';
|
||||
import { AuthPanel } from '../components/affine/auth';
|
||||
import { useCurrenLoginStatus } from '../hooks/affine/use-curren-login-status';
|
||||
|
||||
interface LocationState {
|
||||
state: {
|
||||
callbackURL?: string;
|
||||
};
|
||||
}
|
||||
export const Component = () => {
|
||||
const [
|
||||
{ state, email = '', emailType = 'changePassword', onceSignedIn },
|
||||
setAuthAtom,
|
||||
] = useAtom(authAtom);
|
||||
const loginStatus = useCurrenLoginStatus();
|
||||
const location = useLocation() as LocationState;
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const afterSignedIn = async () => {
|
||||
if (loginStatus === 'authenticated') {
|
||||
if (onceSignedIn) {
|
||||
await onceSignedIn();
|
||||
setAuthAtom(prev => ({ ...prev, onceSignedIn: undefined }));
|
||||
}
|
||||
if (location.state.callbackURL) {
|
||||
navigate(location.state.callbackURL, {
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
afterSignedIn();
|
||||
}, [
|
||||
location.state.callbackURL,
|
||||
loginStatus,
|
||||
navigate,
|
||||
onceSignedIn,
|
||||
setAuthAtom,
|
||||
]);
|
||||
|
||||
return (
|
||||
<SignInPageContainer>
|
||||
<AuthPanel
|
||||
state={state}
|
||||
email={email}
|
||||
emailType={emailType}
|
||||
setEmailType={useCallback(
|
||||
emailType => {
|
||||
setAuthAtom(prev => ({ ...prev, emailType }));
|
||||
},
|
||||
[setAuthAtom]
|
||||
)}
|
||||
setAuthState={useCallback(
|
||||
state => {
|
||||
setAuthAtom(prev => ({ ...prev, state }));
|
||||
},
|
||||
[setAuthAtom]
|
||||
)}
|
||||
setAuthEmail={useCallback(
|
||||
email => {
|
||||
setAuthAtom(prev => ({ ...prev, email }));
|
||||
},
|
||||
[setAuthAtom]
|
||||
)}
|
||||
/>
|
||||
</SignInPageContainer>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user