mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-07 01:09:54 +08:00
refactor(auth): authenticate user in main window (#8032)
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
import { atom } from 'jotai';
|
import { atom } from 'jotai';
|
||||||
|
|
||||||
import type { AuthProps } from '../components/affine/auth';
|
|
||||||
import type { SettingProps } from '../components/affine/setting-modal';
|
import type { SettingProps } from '../components/affine/setting-modal';
|
||||||
import type { ActiveTab } from '../components/affine/setting-modal/types';
|
import type { ActiveTab } from '../components/affine/setting-modal/types';
|
||||||
// modal atoms
|
// modal atoms
|
||||||
@@ -37,18 +36,37 @@ export const openSettingModalAtom = atom<SettingAtom>({
|
|||||||
open: false,
|
open: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type AuthAtom = {
|
export type AuthAtomData =
|
||||||
openModal: boolean;
|
| { state: 'signIn' }
|
||||||
state: AuthProps['state'];
|
| {
|
||||||
email?: string;
|
state: 'afterSignUpSendEmail';
|
||||||
emailType?: AuthProps['emailType'];
|
email: string;
|
||||||
};
|
}
|
||||||
|
| {
|
||||||
|
state: 'afterSignInSendEmail';
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
state: 'signInWithPassword';
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
state: 'sendEmail';
|
||||||
|
email: string;
|
||||||
|
emailType:
|
||||||
|
| 'setPassword'
|
||||||
|
| 'changePassword'
|
||||||
|
| 'changeEmail'
|
||||||
|
| 'verifyEmail';
|
||||||
|
};
|
||||||
|
|
||||||
export const authAtom = atom<AuthAtom>({
|
export const authAtom = atom<
|
||||||
|
AuthAtomData & {
|
||||||
|
openModal: boolean;
|
||||||
|
}
|
||||||
|
>({
|
||||||
openModal: false,
|
openModal: false,
|
||||||
state: 'signIn',
|
state: 'signIn',
|
||||||
email: '',
|
|
||||||
emailType: 'changeEmail',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type AllPageFilterOption = 'docs' | 'collections' | 'tags';
|
export type AllPageFilterOption = 'docs' | 'collections' | 'tags';
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Button } from '@affine/component/ui/button';
|
|||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import { AuthService } from '@affine/core/modules/cloud';
|
import { AuthService } from '@affine/core/modules/cloud';
|
||||||
import { Trans, useI18n } from '@affine/i18n';
|
import { Trans, useI18n } from '@affine/i18n';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useService } from '@toeverything/infra';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import type { AuthPanelProps } from './index';
|
import type { AuthPanelProps } from './index';
|
||||||
@@ -17,10 +17,9 @@ import * as style from './style.css';
|
|||||||
import { Captcha, useCaptcha } from './use-captcha';
|
import { Captcha, useCaptcha } from './use-captcha';
|
||||||
|
|
||||||
export const AfterSignInSendEmail = ({
|
export const AfterSignInSendEmail = ({
|
||||||
setAuthState,
|
setAuthData: setAuth,
|
||||||
email,
|
email,
|
||||||
onSignedIn,
|
}: AuthPanelProps<'afterSignInSendEmail'>) => {
|
||||||
}: AuthPanelProps) => {
|
|
||||||
const [resendCountDown, setResendCountDown] = useState(60);
|
const [resendCountDown, setResendCountDown] = useState(60);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -37,22 +36,9 @@ export const AfterSignInSendEmail = ({
|
|||||||
|
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const authService = useService(AuthService);
|
const authService = useService(AuthService);
|
||||||
useEffect(() => {
|
|
||||||
const timer = setInterval(() => {
|
|
||||||
authService.session.revalidate();
|
|
||||||
}, 3000);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearInterval(timer);
|
|
||||||
};
|
|
||||||
}, [authService]);
|
|
||||||
const loginStatus = useLiveData(authService.session.status$);
|
|
||||||
const [verifyToken, challenge] = useCaptcha();
|
const [verifyToken, challenge] = useCaptcha();
|
||||||
|
|
||||||
if (loginStatus === 'authenticated') {
|
|
||||||
onSignedIn?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
const onResendClick = useAsyncCallback(async () => {
|
const onResendClick = useAsyncCallback(async () => {
|
||||||
setIsSending(true);
|
setIsSending(true);
|
||||||
try {
|
try {
|
||||||
@@ -70,12 +56,12 @@ export const AfterSignInSendEmail = ({
|
|||||||
}, [authService, challenge, email, verifyToken]);
|
}, [authService, challenge, email, verifyToken]);
|
||||||
|
|
||||||
const onSignInWithPasswordClick = useCallback(() => {
|
const onSignInWithPasswordClick = useCallback(() => {
|
||||||
setAuthState('signInWithPassword');
|
setAuth({ state: 'signInWithPassword' });
|
||||||
}, [setAuthState]);
|
}, [setAuth]);
|
||||||
|
|
||||||
const onBackBottomClick = useCallback(() => {
|
const onBackBottomClick = useCallback(() => {
|
||||||
setAuthState('signIn');
|
setAuth({ state: 'signIn' });
|
||||||
}, [setAuthState]);
|
}, [setAuth]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
import { Button } from '@affine/component/ui/button';
|
import { Button } from '@affine/component/ui/button';
|
||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import { Trans, useI18n } from '@affine/i18n';
|
import { Trans, useI18n } from '@affine/i18n';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useService } from '@toeverything/infra';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
@@ -17,11 +17,9 @@ import type { AuthPanelProps } from './index';
|
|||||||
import * as style from './style.css';
|
import * as style from './style.css';
|
||||||
import { Captcha, useCaptcha } from './use-captcha';
|
import { Captcha, useCaptcha } from './use-captcha';
|
||||||
|
|
||||||
export const AfterSignUpSendEmail: FC<AuthPanelProps> = ({
|
export const AfterSignUpSendEmail: FC<
|
||||||
setAuthState,
|
AuthPanelProps<'afterSignUpSendEmail'>
|
||||||
email,
|
> = ({ setAuthData, email }) => {
|
||||||
onSignedIn,
|
|
||||||
}) => {
|
|
||||||
const [resendCountDown, setResendCountDown] = useState(60);
|
const [resendCountDown, setResendCountDown] = useState(60);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -37,19 +35,6 @@ export const AfterSignUpSendEmail: FC<AuthPanelProps> = ({
|
|||||||
const [isSending, setIsSending] = useState(false);
|
const [isSending, setIsSending] = useState(false);
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const authService = useService(AuthService);
|
const authService = useService(AuthService);
|
||||||
const loginStatus = useLiveData(authService.session.status$);
|
|
||||||
useEffect(() => {
|
|
||||||
const timeout = setInterval(() => {
|
|
||||||
// revalidate session to get the latest status
|
|
||||||
authService.session.revalidate();
|
|
||||||
}, 3000);
|
|
||||||
return () => {
|
|
||||||
clearInterval(timeout);
|
|
||||||
};
|
|
||||||
}, [authService]);
|
|
||||||
if (loginStatus === 'authenticated') {
|
|
||||||
onSignedIn?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
const [verifyToken, challenge] = useCaptcha();
|
const [verifyToken, challenge] = useCaptcha();
|
||||||
|
|
||||||
@@ -117,8 +102,8 @@ export const AfterSignUpSendEmail: FC<AuthPanelProps> = ({
|
|||||||
|
|
||||||
<BackButton
|
<BackButton
|
||||||
onClick={useCallback(() => {
|
onClick={useCallback(() => {
|
||||||
setAuthState('signIn');
|
setAuthData({ state: 'signIn' });
|
||||||
}, [setAuthState])}
|
}, [setAuthData])}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import type { AuthModalProps as AuthModalBaseProps } from '@affine/component/auth-components';
|
import { notify } from '@affine/component';
|
||||||
import { AuthModal as AuthModalBase } from '@affine/component/auth-components';
|
import { AuthModal as AuthModalBase } from '@affine/component/auth-components';
|
||||||
|
import { authAtom, type AuthAtomData } from '@affine/core/atoms';
|
||||||
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
|
import { AuthService } from '@affine/core/modules/cloud';
|
||||||
|
import { apis, events } from '@affine/electron-api';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
import { useAtom } from 'jotai/react';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
import { AfterSignInSendEmail } from './after-sign-in-send-email';
|
import { AfterSignInSendEmail } from './after-sign-in-send-email';
|
||||||
import { AfterSignUpSendEmail } from './after-sign-up-send-email';
|
import { AfterSignUpSendEmail } from './after-sign-up-send-email';
|
||||||
@@ -9,33 +16,25 @@ import { SendEmail } from './send-email';
|
|||||||
import { SignIn } from './sign-in';
|
import { SignIn } from './sign-in';
|
||||||
import { SignInWithPassword } from './sign-in-with-password';
|
import { SignInWithPassword } from './sign-in-with-password';
|
||||||
|
|
||||||
export type AuthProps = {
|
type AuthAtomType<T extends AuthAtomData['state']> = Extract<
|
||||||
state:
|
AuthAtomData,
|
||||||
| 'signIn'
|
{ state: T }
|
||||||
| 'afterSignUpSendEmail'
|
>;
|
||||||
| 'afterSignInSendEmail'
|
|
||||||
// throw away
|
|
||||||
| 'signInWithPassword'
|
|
||||||
| 'sendEmail';
|
|
||||||
setAuthState: (state: AuthProps['state']) => void;
|
|
||||||
setAuthEmail: (state: AuthProps['email']) => void;
|
|
||||||
setEmailType: (state: AuthProps['emailType']) => void;
|
|
||||||
email: string;
|
|
||||||
emailType: 'setPassword' | 'changePassword' | 'changeEmail' | 'verifyEmail';
|
|
||||||
onSignedIn?: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AuthPanelProps = {
|
// return field in B that is not in A
|
||||||
email: string;
|
type Difference<
|
||||||
setAuthState: AuthProps['setAuthState'];
|
A extends Record<string, any>,
|
||||||
setAuthEmail: AuthProps['setAuthEmail'];
|
B extends Record<string, any>,
|
||||||
setEmailType: AuthProps['setEmailType'];
|
> = Pick<B, Exclude<keyof B, keyof A>>;
|
||||||
emailType: AuthProps['emailType'];
|
|
||||||
onSignedIn?: () => void;
|
export type AuthPanelProps<State extends AuthAtomData['state']> = {
|
||||||
};
|
setAuthData: <T extends AuthAtomData['state']>(
|
||||||
|
updates: { state: T } & Difference<AuthAtomType<State>, AuthAtomType<T>>
|
||||||
|
) => void;
|
||||||
|
} & Extract<AuthAtomData, { state: State }>;
|
||||||
|
|
||||||
const config: {
|
const config: {
|
||||||
[k in AuthProps['state']]: FC<AuthPanelProps>;
|
[k in AuthAtomData['state']]: FC<AuthPanelProps<k>>;
|
||||||
} = {
|
} = {
|
||||||
signIn: SignIn,
|
signIn: SignIn,
|
||||||
afterSignUpSendEmail: AfterSignUpSendEmail,
|
afterSignUpSendEmail: AfterSignUpSendEmail,
|
||||||
@@ -44,58 +43,100 @@ const config: {
|
|||||||
sendEmail: SendEmail,
|
sendEmail: SendEmail,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AuthModal: FC<AuthModalBaseProps & AuthProps> = ({
|
export function AuthModal() {
|
||||||
open,
|
const t = useI18n();
|
||||||
state,
|
const [authAtomValue, setAuthAtom] = useAtom(authAtom);
|
||||||
setOpen,
|
const authService = useService(AuthService);
|
||||||
email,
|
const setOpen = useCallback(
|
||||||
setAuthEmail,
|
(open: boolean) => {
|
||||||
setAuthState,
|
setAuthAtom(prev => ({ ...prev, openModal: open }));
|
||||||
setEmailType,
|
},
|
||||||
emailType,
|
[setAuthAtom]
|
||||||
}) => {
|
);
|
||||||
const onSignedIn = useCallback(() => {
|
|
||||||
setAuthState('signIn');
|
const signIn = useAsyncCallback(
|
||||||
setAuthEmail('');
|
async ({
|
||||||
setOpen(false);
|
method,
|
||||||
}, [setAuthState, setAuthEmail, setOpen]);
|
payload,
|
||||||
|
}: {
|
||||||
|
method: 'magic-link' | 'oauth';
|
||||||
|
payload: any;
|
||||||
|
}) => {
|
||||||
|
if (!(await apis?.ui.isActiveTab())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
switch (method) {
|
||||||
|
case 'magic-link': {
|
||||||
|
const { email, token } = payload;
|
||||||
|
await authService.signInMagicLink(email, token);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'oauth': {
|
||||||
|
const { code, state } = payload;
|
||||||
|
await authService.signInOauth(code, state);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
authService.session.revalidate();
|
||||||
|
} catch (e) {
|
||||||
|
notify.error({
|
||||||
|
title: t['com.affine.auth.toast.title.failed'](),
|
||||||
|
message: (e as any).message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[authService, t]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return events?.ui.onAuthenticationRequest(signIn);
|
||||||
|
}, [signIn]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthModalBase open={open} setOpen={setOpen}>
|
<AuthModalBase open={authAtomValue.openModal} setOpen={setOpen}>
|
||||||
<AuthPanel
|
<AuthPanel />
|
||||||
state={state}
|
|
||||||
email={email}
|
|
||||||
setAuthEmail={setAuthEmail}
|
|
||||||
setAuthState={setAuthState}
|
|
||||||
setEmailType={setEmailType}
|
|
||||||
emailType={emailType}
|
|
||||||
onSignedIn={onSignedIn}
|
|
||||||
/>
|
|
||||||
</AuthModalBase>
|
</AuthModalBase>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export const AuthPanel: FC<AuthProps> = ({
|
export function AuthPanel() {
|
||||||
state,
|
const t = useI18n();
|
||||||
email,
|
const [authAtomValue, setAuthAtom] = useAtom(authAtom);
|
||||||
setAuthEmail,
|
const authService = useService(AuthService);
|
||||||
setAuthState,
|
const loginStatus = useLiveData(authService.session.status$);
|
||||||
setEmailType,
|
const previousLoginStatus = useRef(loginStatus);
|
||||||
emailType,
|
|
||||||
onSignedIn,
|
|
||||||
}) => {
|
|
||||||
const CurrentPanel = useMemo(() => {
|
|
||||||
return config[state];
|
|
||||||
}, [state]);
|
|
||||||
|
|
||||||
return (
|
const setAuthData = useCallback(
|
||||||
<CurrentPanel
|
(updates: Partial<AuthAtomData>) => {
|
||||||
email={email}
|
// @ts-expect-error checked in impls
|
||||||
setAuthState={setAuthState}
|
setAuthAtom(prev => ({
|
||||||
setAuthEmail={setAuthEmail}
|
...prev,
|
||||||
setEmailType={setEmailType}
|
...updates,
|
||||||
emailType={emailType}
|
}));
|
||||||
onSignedIn={onSignedIn}
|
},
|
||||||
/>
|
[setAuthAtom]
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
loginStatus === 'authenticated' &&
|
||||||
|
previousLoginStatus.current === 'unauthenticated'
|
||||||
|
) {
|
||||||
|
setAuthAtom({
|
||||||
|
openModal: false,
|
||||||
|
state: 'signIn',
|
||||||
|
});
|
||||||
|
notify.success({
|
||||||
|
title: t['com.affine.auth.toast.title.signed-in'](),
|
||||||
|
message: t['com.affine.auth.toast.message.signed-in'](),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
previousLoginStatus.current = loginStatus;
|
||||||
|
}, [loginStatus, setAuthAtom, t]);
|
||||||
|
|
||||||
|
const CurrentPanel = config[authAtomValue.state];
|
||||||
|
|
||||||
|
// @ts-expect-error checked in impls
|
||||||
|
return <CurrentPanel {...authAtomValue} setAuthData={setAuthData} />;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { notify, Skeleton } from '@affine/component';
|
|||||||
import { Button } from '@affine/component/ui/button';
|
import { Button } from '@affine/component/ui/button';
|
||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import { track } from '@affine/core/mixpanel';
|
import { track } from '@affine/core/mixpanel';
|
||||||
|
import { popupWindow } from '@affine/core/utils';
|
||||||
|
import { apis } from '@affine/electron-api';
|
||||||
import { OAuthProviderType } from '@affine/graphql';
|
import { OAuthProviderType } from '@affine/graphql';
|
||||||
import { GithubIcon, GoogleDuotoneIcon } from '@blocksuite/icons/rc';
|
import { GithubIcon, GoogleDuotoneIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
@@ -59,7 +61,12 @@ function OAuthProvider({ provider }: { provider: OAuthProviderType }) {
|
|||||||
const onClick = useAsyncCallback(async () => {
|
const onClick = useAsyncCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setIsConnecting(true);
|
setIsConnecting(true);
|
||||||
await authService.signInOauth(provider);
|
const url = await authService.oauthPreflight(provider);
|
||||||
|
if (environment.isDesktop) {
|
||||||
|
await apis?.ui.openExternal(url);
|
||||||
|
} else {
|
||||||
|
popupWindow(url);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
notify.error({ title: 'Failed to sign in, please try again.' });
|
notify.error({ title: 'Failed to sign in, please try again.' });
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { useMutation } from '../../../hooks/use-mutation';
|
|||||||
import { ServerConfigService } from '../../../modules/cloud';
|
import { ServerConfigService } from '../../../modules/cloud';
|
||||||
import type { AuthPanelProps } from './index';
|
import type { AuthPanelProps } from './index';
|
||||||
|
|
||||||
const useEmailTitle = (emailType: AuthPanelProps['emailType']) => {
|
const useEmailTitle = (emailType: AuthPanelProps<'sendEmail'>['emailType']) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
switch (emailType) {
|
switch (emailType) {
|
||||||
@@ -36,7 +36,9 @@ const useEmailTitle = (emailType: AuthPanelProps['emailType']) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const useNotificationHint = (emailType: AuthPanelProps['emailType']) => {
|
const useNotificationHint = (
|
||||||
|
emailType: AuthPanelProps<'sendEmail'>['emailType']
|
||||||
|
) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
switch (emailType) {
|
switch (emailType) {
|
||||||
@@ -49,7 +51,9 @@ const useNotificationHint = (emailType: AuthPanelProps['emailType']) => {
|
|||||||
return t['com.affine.auth.sent.verify.email.hint']();
|
return t['com.affine.auth.sent.verify.email.hint']();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const useButtonContent = (emailType: AuthPanelProps['emailType']) => {
|
const useButtonContent = (
|
||||||
|
emailType: AuthPanelProps<'sendEmail'>['emailType']
|
||||||
|
) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
switch (emailType) {
|
switch (emailType) {
|
||||||
@@ -63,7 +67,7 @@ const useButtonContent = (emailType: AuthPanelProps['emailType']) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const useSendEmail = (emailType: AuthPanelProps['emailType']) => {
|
const useSendEmail = (emailType: AuthPanelProps<'sendEmail'>['emailType']) => {
|
||||||
const {
|
const {
|
||||||
trigger: sendChangePasswordEmail,
|
trigger: sendChangePasswordEmail,
|
||||||
isMutating: isChangePasswordMutating,
|
isMutating: isChangePasswordMutating,
|
||||||
@@ -134,10 +138,10 @@ const useSendEmail = (emailType: AuthPanelProps['emailType']) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SendEmail = ({
|
export const SendEmail = ({
|
||||||
setAuthState,
|
setAuthData,
|
||||||
email,
|
email,
|
||||||
emailType,
|
emailType,
|
||||||
}: AuthPanelProps) => {
|
}: AuthPanelProps<'sendEmail'>) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const serverConfig = useService(ServerConfigService).serverConfig;
|
const serverConfig = useService(ServerConfigService).serverConfig;
|
||||||
|
|
||||||
@@ -160,8 +164,8 @@ export const SendEmail = ({
|
|||||||
}, [email, hint, sendEmail]);
|
}, [email, hint, sendEmail]);
|
||||||
|
|
||||||
const onBack = useCallback(() => {
|
const onBack = useCallback(() => {
|
||||||
setAuthState('signIn');
|
setAuthData({ state: 'signIn' });
|
||||||
}, [setAuthState]);
|
}, [setAuthData]);
|
||||||
|
|
||||||
if (!passwordLimits) {
|
if (!passwordLimits) {
|
||||||
// TODO(@eyhn): loading & error UI
|
// TODO(@eyhn): loading & error UI
|
||||||
|
|||||||
@@ -16,11 +16,9 @@ import type { AuthPanelProps } from './index';
|
|||||||
import * as styles from './style.css';
|
import * as styles from './style.css';
|
||||||
import { useCaptcha } from './use-captcha';
|
import { useCaptcha } from './use-captcha';
|
||||||
|
|
||||||
export const SignInWithPassword: FC<AuthPanelProps> = ({
|
export const SignInWithPassword: FC<AuthPanelProps<'signInWithPassword'>> = ({
|
||||||
setAuthState,
|
setAuthData,
|
||||||
setEmailType,
|
|
||||||
email,
|
email,
|
||||||
onSignedIn,
|
|
||||||
}) => {
|
}) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const authService = useService(AuthService);
|
const authService = useService(AuthService);
|
||||||
@@ -40,14 +38,13 @@ export const SignInWithPassword: FC<AuthPanelProps> = ({
|
|||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
onSignedIn?.();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
setPasswordError(true);
|
setPasswordError(true);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [isLoading, authService, email, password, onSignedIn]);
|
}, [isLoading, authService, email, password]);
|
||||||
|
|
||||||
const sendMagicLink = useAsyncCallback(async () => {
|
const sendMagicLink = useAsyncCallback(async () => {
|
||||||
if (sendingEmail) return;
|
if (sendingEmail) return;
|
||||||
@@ -55,7 +52,7 @@ export const SignInWithPassword: FC<AuthPanelProps> = ({
|
|||||||
try {
|
try {
|
||||||
if (verifyToken) {
|
if (verifyToken) {
|
||||||
await authService.sendEmailMagicLink(email, verifyToken, challenge);
|
await authService.sendEmailMagicLink(email, verifyToken, challenge);
|
||||||
setAuthState('afterSignInSendEmail');
|
setAuthData({ state: 'afterSignInSendEmail' });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -65,12 +62,11 @@ export const SignInWithPassword: FC<AuthPanelProps> = ({
|
|||||||
// TODO(@eyhn): handle error better
|
// TODO(@eyhn): handle error better
|
||||||
}
|
}
|
||||||
setSendingEmail(false);
|
setSendingEmail(false);
|
||||||
}, [sendingEmail, verifyToken, authService, email, challenge, setAuthState]);
|
}, [sendingEmail, verifyToken, authService, email, challenge, setAuthData]);
|
||||||
|
|
||||||
const sendChangePasswordEmail = useCallback(() => {
|
const sendChangePasswordEmail = useCallback(() => {
|
||||||
setEmailType('changePassword');
|
setAuthData({ state: 'sendEmail', emailType: 'changePassword' });
|
||||||
setAuthState('sendEmail');
|
}, [setAuthData]);
|
||||||
}, [setAuthState, setEmailType]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -140,8 +136,8 @@ export const SignInWithPassword: FC<AuthPanelProps> = ({
|
|||||||
</Wrapper>
|
</Wrapper>
|
||||||
<BackButton
|
<BackButton
|
||||||
onClick={useCallback(() => {
|
onClick={useCallback(() => {
|
||||||
setAuthState('signIn');
|
setAuthData({ state: 'signIn' });
|
||||||
}, [setAuthState])}
|
}, [setAuthData])}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import { notify } from '@affine/component';
|
import { notify } from '@affine/component';
|
||||||
import { AuthInput, ModalHeader } from '@affine/component/auth-components';
|
import { AuthInput, ModalHeader } from '@affine/component/auth-components';
|
||||||
import { Button } from '@affine/component/ui/button';
|
import { Button } from '@affine/component/ui/button';
|
||||||
import { authAtom } from '@affine/core/atoms';
|
|
||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import { track } from '@affine/core/mixpanel';
|
import { track } from '@affine/core/mixpanel';
|
||||||
import { Trans, useI18n } from '@affine/i18n';
|
import { Trans, useI18n } from '@affine/i18n';
|
||||||
import { ArrowRightBigIcon } from '@blocksuite/icons/rc';
|
import { ArrowRightBigIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useService } from '@toeverything/infra';
|
||||||
import { cssVar } from '@toeverything/theme';
|
import { cssVar } from '@toeverything/theme';
|
||||||
import { useAtomValue } from 'jotai';
|
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { AuthService } from '../../../modules/cloud';
|
import { AuthService } from '../../../modules/cloud';
|
||||||
@@ -24,36 +22,19 @@ function validateEmail(email: string) {
|
|||||||
return emailRegex.test(email);
|
return emailRegex.test(email);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SignIn: FC<AuthPanelProps> = ({
|
export const SignIn: FC<AuthPanelProps<'signIn'>> = ({
|
||||||
setAuthState,
|
setAuthData: setAuthState,
|
||||||
setAuthEmail,
|
|
||||||
email,
|
|
||||||
onSignedIn,
|
|
||||||
}) => {
|
}) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const authService = useService(AuthService);
|
const authService = useService(AuthService);
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const [isMutating, setIsMutating] = useState(false);
|
const [isMutating, setIsMutating] = useState(false);
|
||||||
const [verifyToken, challenge] = useCaptcha();
|
const [verifyToken, challenge] = useCaptcha();
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
|
||||||
const [isValidEmail, setIsValidEmail] = useState(true);
|
const [isValidEmail, setIsValidEmail] = useState(true);
|
||||||
const { openModal } = useAtomValue(authAtom);
|
|
||||||
const errorMsg = searchParams.get('error');
|
const errorMsg = searchParams.get('error');
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const timeout = setInterval(() => {
|
|
||||||
// revalidate session to get the latest status
|
|
||||||
authService.session.revalidate();
|
|
||||||
}, 3000);
|
|
||||||
return () => {
|
|
||||||
clearInterval(timeout);
|
|
||||||
};
|
|
||||||
}, [authService]);
|
|
||||||
const loginStatus = useLiveData(authService.session.status$);
|
|
||||||
if (loginStatus === 'authenticated' && openModal) {
|
|
||||||
onSignedIn?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
const onContinue = useAsyncCallback(async () => {
|
const onContinue = useAsyncCallback(async () => {
|
||||||
if (!validateEmail(email)) {
|
if (!validateEmail(email)) {
|
||||||
setIsValidEmail(false);
|
setIsValidEmail(false);
|
||||||
@@ -61,10 +42,8 @@ export const SignIn: FC<AuthPanelProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setIsValidEmail(true);
|
setIsValidEmail(true);
|
||||||
|
|
||||||
setIsMutating(true);
|
setIsMutating(true);
|
||||||
|
|
||||||
setAuthEmail(email);
|
|
||||||
try {
|
try {
|
||||||
const { hasPassword, registered } =
|
const { hasPassword, registered } =
|
||||||
await authService.checkUserByEmail(email);
|
await authService.checkUserByEmail(email);
|
||||||
@@ -74,16 +53,25 @@ export const SignIn: FC<AuthPanelProps> = ({
|
|||||||
// provider password sign-in if user has by default
|
// provider password sign-in if user has by default
|
||||||
// If with payment, onl support email sign in to avoid redirect to affine app
|
// If with payment, onl support email sign in to avoid redirect to affine app
|
||||||
if (hasPassword) {
|
if (hasPassword) {
|
||||||
setAuthState('signInWithPassword');
|
setAuthState({
|
||||||
|
state: 'signInWithPassword',
|
||||||
|
email,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
track.$.$.auth.signIn();
|
track.$.$.auth.signIn();
|
||||||
await authService.sendEmailMagicLink(email, verifyToken, challenge);
|
await authService.sendEmailMagicLink(email, verifyToken, challenge);
|
||||||
setAuthState('afterSignInSendEmail');
|
setAuthState({
|
||||||
|
state: 'afterSignInSendEmail',
|
||||||
|
email,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await authService.sendEmailMagicLink(email, verifyToken, challenge);
|
await authService.sendEmailMagicLink(email, verifyToken, challenge);
|
||||||
track.$.$.auth.signUp();
|
track.$.$.auth.signUp();
|
||||||
setAuthState('afterSignUpSendEmail');
|
setAuthState({
|
||||||
|
state: 'afterSignUpSendEmail',
|
||||||
|
email,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -96,7 +84,7 @@ export const SignIn: FC<AuthPanelProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setIsMutating(false);
|
setIsMutating(false);
|
||||||
}, [authService, challenge, email, setAuthEmail, setAuthState, verifyToken]);
|
}, [authService, challenge, email, setAuthState, verifyToken]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -111,13 +99,7 @@ export const SignIn: FC<AuthPanelProps> = ({
|
|||||||
<AuthInput
|
<AuthInput
|
||||||
label={t['com.affine.settings.email']()}
|
label={t['com.affine.settings.email']()}
|
||||||
placeholder={t['com.affine.auth.sign.email.placeholder']()}
|
placeholder={t['com.affine.auth.sign.email.placeholder']()}
|
||||||
value={email}
|
onChange={setEmail}
|
||||||
onChange={useCallback(
|
|
||||||
(value: string) => {
|
|
||||||
setAuthEmail(value);
|
|
||||||
},
|
|
||||||
[setAuthEmail]
|
|
||||||
)}
|
|
||||||
error={!isValidEmail}
|
error={!isValidEmail}
|
||||||
errorHint={
|
errorHint={
|
||||||
isValidEmail ? '' : t['com.affine.auth.sign.email.error']()
|
isValidEmail ? '' : t['com.affine.auth.sign.email.error']()
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ export const AccountSetting: FC = () => {
|
|||||||
setAuthModal({
|
setAuthModal({
|
||||||
openModal: true,
|
openModal: true,
|
||||||
state: 'sendEmail',
|
state: 'sendEmail',
|
||||||
|
// @ts-expect-error accont email is always defined
|
||||||
email: account.email,
|
email: account.email,
|
||||||
emailType: account.info?.emailVerified ? 'changeEmail' : 'verifyEmail',
|
emailType: account.info?.emailVerified ? 'changeEmail' : 'verifyEmail',
|
||||||
});
|
});
|
||||||
@@ -204,6 +205,7 @@ export const AccountSetting: FC = () => {
|
|||||||
setAuthModal({
|
setAuthModal({
|
||||||
openModal: true,
|
openModal: true,
|
||||||
state: 'sendEmail',
|
state: 'sendEmail',
|
||||||
|
// @ts-expect-error accont email is always defined
|
||||||
email: account.email,
|
email: account.email,
|
||||||
emailType: account.info?.hasPassword ? 'changePassword' : 'setPassword',
|
emailType: account.info?.hasPassword ? 'changePassword' : 'setPassword',
|
||||||
});
|
});
|
||||||
|
|||||||
+2
-2
@@ -21,7 +21,7 @@ import {
|
|||||||
WorkspacesService,
|
WorkspacesService,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useAtom } from 'jotai/react';
|
import { useSetAtom } from 'jotai/react';
|
||||||
import {
|
import {
|
||||||
type MouseEvent,
|
type MouseEvent,
|
||||||
Suspense,
|
Suspense,
|
||||||
@@ -81,7 +81,7 @@ export const UserInfo = ({ onAccountSettingClick, active }: UserInfoProps) => {
|
|||||||
|
|
||||||
export const SignInButton = () => {
|
export const SignInButton = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const [, setAuthModal] = useAtom(authAtom);
|
const setAuthModal = useSetAtom(authAtom);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { AIProvider } from '@affine/core/blocksuite/presets/ai';
|
import { AIProvider } from '@affine/core/blocksuite/presets/ai';
|
||||||
import { buildAppUrl, popupWindow } from '@affine/core/utils';
|
import { appInfo } from '@affine/electron-api';
|
||||||
import { apis, appInfo } from '@affine/electron-api';
|
|
||||||
import type { OAuthProviderType } from '@affine/graphql';
|
import type { OAuthProviderType } from '@affine/graphql';
|
||||||
import {
|
import {
|
||||||
ApplicationFocused,
|
ApplicationFocused,
|
||||||
@@ -97,11 +96,7 @@ export class AuthService extends Service {
|
|||||||
email,
|
email,
|
||||||
// we call it [callbackUrl] instead of [redirect_uri]
|
// we call it [callbackUrl] instead of [redirect_uri]
|
||||||
// to make it clear the url is used to finish the sign-in process instead of redirect after signed-in
|
// to make it clear the url is used to finish the sign-in process instead of redirect after signed-in
|
||||||
callbackUrl: buildAppUrl('/magic-link', {
|
callbackUrl: `/magic-link?client=${environment.isDesktop ? appInfo?.schema : 'web'}`,
|
||||||
desktop: environment.isDesktop,
|
|
||||||
openInHiddenWindow: true,
|
|
||||||
redirectFromWeb: true,
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
headers: {
|
headers: {
|
||||||
'content-type': 'application/json',
|
'content-type': 'application/json',
|
||||||
@@ -113,19 +108,28 @@ export class AuthService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async signInOauth(provider: OAuthProviderType) {
|
async signInMagicLink(email: string, token: string) {
|
||||||
|
await this.fetchService.fetch('/api/auth/magic-link', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email, token }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async oauthPreflight(
|
||||||
|
provider: OAuthProviderType,
|
||||||
|
/** @deprecated*/ redirectUrl?: string
|
||||||
|
) {
|
||||||
const res = await this.fetchService.fetch('/api/oauth/preflight', {
|
const res = await this.fetchService.fetch('/api/oauth/preflight', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ provider }),
|
body: JSON.stringify({ provider, redirect_uri: redirectUrl }),
|
||||||
headers: {
|
headers: {
|
||||||
'content-type': 'application/json',
|
'content-type': 'application/json',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(`Failed to sign in with ${provider}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
let { url } = await res.json();
|
let { url } = await res.json();
|
||||||
|
|
||||||
// change `state=xxx` to `state={state:xxx,native:true}`
|
// change `state=xxx` to `state={state:xxx,native:true}`
|
||||||
@@ -140,13 +144,19 @@ export class AuthService extends Service {
|
|||||||
);
|
);
|
||||||
url = oauthUrl.toString();
|
url = oauthUrl.toString();
|
||||||
|
|
||||||
if (environment.isDesktop) {
|
return url;
|
||||||
await apis?.ui.openExternal(url);
|
}
|
||||||
} else {
|
|
||||||
popupWindow(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
async signInOauth(code: string, state: string) {
|
||||||
|
const res = await this.fetchService.fetch('/api/oauth/callback', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ code, state }),
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return await res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
async signInPassword(credential: { email: string; password: string }) {
|
async signInPassword(credential: { email: string; password: string }) {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { useCallback, useEffect, useState } from 'react';
|
|||||||
import { SignOutModal } from '../components/affine/sign-out-modal';
|
import { SignOutModal } from '../components/affine/sign-out-modal';
|
||||||
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||||
import { AuthService } from '../modules/cloud';
|
import { AuthService } from '../modules/cloud';
|
||||||
import { SignIn } from './sign-in';
|
import { SignIn } from './auth/sign-in';
|
||||||
|
|
||||||
export const PageNotFound = ({
|
export const PageNotFound = ({
|
||||||
noPermission,
|
noPermission,
|
||||||
|
|||||||
+3
-3
@@ -22,9 +22,9 @@ import type { LoaderFunction } from 'react-router-dom';
|
|||||||
import { redirect, useParams, useSearchParams } from 'react-router-dom';
|
import { redirect, useParams, useSearchParams } from 'react-router-dom';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { useMutation } from '../hooks/use-mutation';
|
import { useMutation } from '../../hooks/use-mutation';
|
||||||
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
import { RouteLogic, useNavigateHelper } from '../../hooks/use-navigate-helper';
|
||||||
import { AuthService, ServerConfigService } from '../modules/cloud';
|
import { AuthService, ServerConfigService } from '../../modules/cloud';
|
||||||
|
|
||||||
const authTypeSchema = z.enum([
|
const authTypeSchema = z.enum([
|
||||||
'onboarding',
|
'onboarding',
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { AuthService } from '@affine/core/modules/cloud';
|
||||||
|
import { OAuthProviderType } from '@affine/graphql';
|
||||||
|
import { useService } from '@toeverything/infra';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
type LoaderFunction,
|
||||||
|
redirect,
|
||||||
|
useLoaderData,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||||
|
useNavigate,
|
||||||
|
} from 'react-router-dom';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const supportedProvider = z.nativeEnum(OAuthProviderType);
|
||||||
|
|
||||||
|
interface LoaderData {
|
||||||
|
provider: OAuthProviderType;
|
||||||
|
redirectUri: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loader: LoaderFunction = async ({ request }) => {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const searchParams = url.searchParams;
|
||||||
|
const provider = searchParams.get('provider');
|
||||||
|
const redirectUri = searchParams.get('redirect_uri');
|
||||||
|
|
||||||
|
// sign out first
|
||||||
|
await fetch('/api/auth/sign-out');
|
||||||
|
|
||||||
|
const maybeProvider = supportedProvider.safeParse(provider);
|
||||||
|
if (maybeProvider.success) {
|
||||||
|
return {
|
||||||
|
provider,
|
||||||
|
redirectUri,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect(
|
||||||
|
`/signIn?error=${encodeURIComponent(`Invalid oauth provider ${provider}`)}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Component = () => {
|
||||||
|
const auth = useService(AuthService);
|
||||||
|
const data = useLoaderData() as LoaderData;
|
||||||
|
|
||||||
|
const nav = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
auth
|
||||||
|
.oauthPreflight(data.provider, data.redirectUri)
|
||||||
|
.then(url => {
|
||||||
|
// this is the url of oauth provider auth page, can't navigate with react-router
|
||||||
|
location.href = url;
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
nav(`/signIn?error=${encodeURIComponent(e.message)}`);
|
||||||
|
});
|
||||||
|
}, [data, auth, nav]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { useService } from '@toeverything/infra';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
type LoaderFunction,
|
||||||
|
redirect,
|
||||||
|
useLoaderData,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||||
|
useNavigate,
|
||||||
|
} from 'react-router-dom';
|
||||||
|
|
||||||
|
import { AuthService } from '../../modules/cloud';
|
||||||
|
|
||||||
|
interface LoaderData {
|
||||||
|
token: string;
|
||||||
|
email: string;
|
||||||
|
redirectUri: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loader: LoaderFunction = ({ request }) => {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const params = url.searchParams;
|
||||||
|
const client = params.get('client');
|
||||||
|
const email = params.get('email');
|
||||||
|
const token = params.get('token');
|
||||||
|
const redirectUri = params.get('redirect_uri');
|
||||||
|
|
||||||
|
if (!email || !token) {
|
||||||
|
return redirect('/signIn?error=Invalid magic link');
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: LoaderData = {
|
||||||
|
email,
|
||||||
|
token,
|
||||||
|
redirectUri,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!client || client === 'web') {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authParams = new URLSearchParams();
|
||||||
|
authParams.set('method', 'magic-link');
|
||||||
|
authParams.set('payload', JSON.stringify(payload));
|
||||||
|
|
||||||
|
return redirect(
|
||||||
|
`/open-app/url?url=${encodeURIComponent(`${client}://authentication?${authParams.toString()}`)}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Component = () => {
|
||||||
|
// TODO(@eyhn): loading ui
|
||||||
|
const auth = useService(AuthService);
|
||||||
|
const data = useLoaderData() as LoaderData;
|
||||||
|
|
||||||
|
const nav = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
auth
|
||||||
|
.signInMagicLink(data.email, data.token)
|
||||||
|
.then(() => {
|
||||||
|
// compatible with old client
|
||||||
|
if (data.redirectUri) {
|
||||||
|
nav(data.redirectUri);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
nav(`/signIn?error=${encodeURIComponent(e.message)}`);
|
||||||
|
});
|
||||||
|
}, [data, auth, nav]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { useService } from '@toeverything/infra';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
type LoaderFunction,
|
||||||
|
redirect,
|
||||||
|
useLoaderData,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||||
|
useNavigate,
|
||||||
|
} from 'react-router-dom';
|
||||||
|
|
||||||
|
import { AuthService } from '../../modules/cloud';
|
||||||
|
|
||||||
|
interface LoaderData {
|
||||||
|
state: string;
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loader: LoaderFunction = async ({ request }) => {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const queries = url.searchParams;
|
||||||
|
const code = queries.get('code');
|
||||||
|
let stateStr = queries.get('state') ?? '{}';
|
||||||
|
|
||||||
|
if (!code || !stateStr) {
|
||||||
|
return redirect('/signIn?error=Invalid oauth callback parameters');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { state, client } = JSON.parse(stateStr);
|
||||||
|
stateStr = state;
|
||||||
|
|
||||||
|
const payload: LoaderData = {
|
||||||
|
state,
|
||||||
|
code,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!client || client === 'web') {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authParams = new URLSearchParams();
|
||||||
|
authParams.set('method', 'oauth');
|
||||||
|
authParams.set('payload', JSON.stringify(payload));
|
||||||
|
|
||||||
|
return redirect(
|
||||||
|
`/open-app/url?url=${encodeURIComponent(`${client}://authentication?${authParams.toString()}`)}`
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return redirect('/signIn?error=Invalid oauth callback parameters');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Component = () => {
|
||||||
|
const auth = useService(AuthService);
|
||||||
|
const data = useLoaderData() as LoaderData;
|
||||||
|
|
||||||
|
const nav = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
auth
|
||||||
|
.signInOauth(data.code, data.state)
|
||||||
|
.then(({ redirectUri }) => {
|
||||||
|
if (redirectUri) {
|
||||||
|
nav(redirectUri);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
nav(`/signIn?error=${encodeURIComponent(e.message)}`);
|
||||||
|
});
|
||||||
|
}, [data, auth, nav]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
+10
-38
@@ -1,20 +1,16 @@
|
|||||||
import { AffineOtherPageLayout } from '@affine/component/affine-other-page-layout';
|
import { AffineOtherPageLayout } from '@affine/component/affine-other-page-layout';
|
||||||
import { SignInPageContainer } from '@affine/component/auth-components';
|
import { SignInPageContainer } from '@affine/component/auth-components';
|
||||||
import { AuthService } from '@affine/core/modules/cloud';
|
import { AuthService } from '@affine/core/modules/cloud';
|
||||||
|
import { appInfo } from '@affine/electron-api';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useAtom } from 'jotai';
|
import { useEffect } from 'react';
|
||||||
import { useCallback, useEffect } from 'react';
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { authAtom } from '../atoms';
|
import { AuthPanel } from '../../components/affine/auth';
|
||||||
import type { AuthProps } from '../components/affine/auth';
|
import { RouteLogic, useNavigateHelper } from '../../hooks/use-navigate-helper';
|
||||||
import { AuthPanel } from '../components/affine/auth';
|
|
||||||
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
|
||||||
|
|
||||||
export const SignIn = () => {
|
export const SignIn = () => {
|
||||||
const [{ state, email = '', emailType = 'changePassword' }, setAuthAtom] =
|
|
||||||
useAtom(authAtom);
|
|
||||||
const session = useService(AuthService).session;
|
const session = useService(AuthService).session;
|
||||||
const status = useLiveData(session.status$);
|
const status = useLiveData(session.status$);
|
||||||
const isRevalidating = useLiveData(session.isRevalidating$);
|
const isRevalidating = useLiveData(session.isRevalidating$);
|
||||||
@@ -24,6 +20,10 @@ export const SignIn = () => {
|
|||||||
const isLoggedIn = status === 'authenticated' && !isRevalidating;
|
const isLoggedIn = status === 'authenticated' && !isRevalidating;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (environment.isDesktop && appInfo?.windowName === 'hidden-window') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
const redirectUri = searchParams.get('redirect_uri');
|
const redirectUri = searchParams.get('redirect_uri');
|
||||||
if (redirectUri) {
|
if (redirectUri) {
|
||||||
@@ -36,40 +36,12 @@ export const SignIn = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [jumpToIndex, navigate, setAuthAtom, isLoggedIn, searchParams]);
|
}, [jumpToIndex, navigate, isLoggedIn, searchParams]);
|
||||||
|
|
||||||
const onSetEmailType = useCallback(
|
|
||||||
(emailType: AuthProps['emailType']) => {
|
|
||||||
setAuthAtom(prev => ({ ...prev, emailType }));
|
|
||||||
},
|
|
||||||
[setAuthAtom]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onSetAuthState = useCallback(
|
|
||||||
(state: AuthProps['state']) => {
|
|
||||||
setAuthAtom(prev => ({ ...prev, state }));
|
|
||||||
},
|
|
||||||
[setAuthAtom]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onSetAuthEmail = useCallback(
|
|
||||||
(email: AuthProps['email']) => {
|
|
||||||
setAuthAtom(prev => ({ ...prev, email }));
|
|
||||||
},
|
|
||||||
[setAuthAtom]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SignInPageContainer>
|
<SignInPageContainer>
|
||||||
<div style={{ maxWidth: '400px', width: '100%' }}>
|
<div style={{ maxWidth: '400px', width: '100%' }}>
|
||||||
<AuthPanel
|
<AuthPanel />
|
||||||
state={state}
|
|
||||||
email={email}
|
|
||||||
emailType={emailType}
|
|
||||||
setEmailType={onSetEmailType}
|
|
||||||
setAuthState={onSetAuthState}
|
|
||||||
setAuthEmail={onSetAuthEmail}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</SignInPageContainer>
|
</SignInPageContainer>
|
||||||
);
|
);
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { OAuthProviderType } from '@affine/graphql';
|
|
||||||
import type { LoaderFunction } from 'react-router-dom';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
const supportedProvider = z.enum([
|
|
||||||
'google',
|
|
||||||
...Object.values(OAuthProviderType),
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const loader: LoaderFunction = async ({ request }) => {
|
|
||||||
const url = new URL(request.url);
|
|
||||||
const searchParams = url.searchParams;
|
|
||||||
const provider = searchParams.get('provider');
|
|
||||||
const redirectUri =
|
|
||||||
searchParams.get('redirect_uri') ??
|
|
||||||
/* backward compatibility */ searchParams.get('callback_url');
|
|
||||||
|
|
||||||
if (!redirectUri) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// sign out first
|
|
||||||
await fetch('/api/auth/sign-out');
|
|
||||||
|
|
||||||
const maybeProvider = supportedProvider.safeParse(provider);
|
|
||||||
if (maybeProvider.success) {
|
|
||||||
let provider = maybeProvider.data;
|
|
||||||
// BACKWARD COMPATIBILITY
|
|
||||||
if (provider === 'google') {
|
|
||||||
provider = OAuthProviderType.Google;
|
|
||||||
}
|
|
||||||
location.href = `${
|
|
||||||
runtimeConfig.serverUrlPrefix
|
|
||||||
}/oauth/login?provider=${provider}&redirect_uri=${encodeURIComponent(
|
|
||||||
redirectUri
|
|
||||||
)}`;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Component = () => {
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import { useLiveData, useService } from '@toeverything/infra';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { type LoaderFunction, redirect } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { AuthService } from '../modules/cloud';
|
|
||||||
|
|
||||||
export const loader: LoaderFunction = async ({ request }) => {
|
|
||||||
const url = new URL(request.url);
|
|
||||||
const queries = url.searchParams;
|
|
||||||
const email = queries.get('email');
|
|
||||||
const token = queries.get('token');
|
|
||||||
const redirectUri = queries.get('redirect_uri');
|
|
||||||
|
|
||||||
if (!email || !token) {
|
|
||||||
return redirect('/404');
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await fetch('/api/auth/magic-link', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ email, token }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
let error: string;
|
|
||||||
try {
|
|
||||||
const { message } = await res.json();
|
|
||||||
error = message;
|
|
||||||
} catch {
|
|
||||||
error = 'failed to verify sign-in token';
|
|
||||||
}
|
|
||||||
return redirect(`/signIn?error=${encodeURIComponent(error)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
location.href = redirectUri || '/';
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Component = () => {
|
|
||||||
const service = useService(AuthService);
|
|
||||||
const user = useLiveData(service.session.account$);
|
|
||||||
useEffect(() => {
|
|
||||||
service.session.revalidate();
|
|
||||||
}, [service]);
|
|
||||||
|
|
||||||
// TODO(@pengx17): window.close() in electron hidden window will close main window as well
|
|
||||||
if (!environment.isDesktop && user) {
|
|
||||||
window.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(@eyhn): loading ui
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
import { useLiveData, useService } from '@toeverything/infra';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { type LoaderFunction, redirect } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { AuthService } from '../modules/cloud';
|
|
||||||
|
|
||||||
export const loader: LoaderFunction = async ({ request }) => {
|
|
||||||
const url = new URL(request.url);
|
|
||||||
const queries = url.searchParams;
|
|
||||||
const code = queries.get('code');
|
|
||||||
let stateStr = queries.get('state') ?? '{}';
|
|
||||||
|
|
||||||
let error: string | undefined;
|
|
||||||
try {
|
|
||||||
const { state, client } = JSON.parse(stateStr);
|
|
||||||
stateStr = state;
|
|
||||||
|
|
||||||
// bypass code & state to redirect_uri
|
|
||||||
if (!environment.isDesktop && client && client !== 'web') {
|
|
||||||
url.searchParams.set('state', JSON.stringify({ state }));
|
|
||||||
return redirect(
|
|
||||||
`/open-app/url?url=${encodeURIComponent(`${client}://${url.pathname}${url.search}`)}&hidden=true`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
error = 'Invalid oauth callback parameters';
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await fetch('/api/oauth/callback', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ code, state: stateStr }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
try {
|
|
||||||
const { message } = await res.json();
|
|
||||||
error = message;
|
|
||||||
} catch {
|
|
||||||
error = 'failed to verify sign-in token';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
// TODO(@pengx17): in desktop app, the callback page will be opened in a hidden window
|
|
||||||
// how could we tell the main window to show the error message?
|
|
||||||
return redirect(`/signIn?error=${encodeURIComponent(error)}`);
|
|
||||||
} else {
|
|
||||||
const body = await res.json();
|
|
||||||
/* @deprecated handle for old client */
|
|
||||||
if (body.redirect_uri) {
|
|
||||||
return redirect(body.redirect_uri);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Component = () => {
|
|
||||||
const service = useService(AuthService);
|
|
||||||
const user = useLiveData(service.session.account$);
|
|
||||||
useEffect(() => {
|
|
||||||
service.session.revalidate();
|
|
||||||
}, [service]);
|
|
||||||
|
|
||||||
// TODO(@pengx17): window.close() in electron hidden window will close main window as well
|
|
||||||
if (!environment.isDesktop && user) {
|
|
||||||
window.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import { AuthModal } from '@affine/core/components/affine/auth';
|
||||||
import { BlocksuiteHeaderTitle } from '@affine/core/components/blocksuite/block-suite-header/title';
|
import { BlocksuiteHeaderTitle } from '@affine/core/components/blocksuite/block-suite-header/title';
|
||||||
import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch';
|
import { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch';
|
||||||
import ShareHeaderRightItem from '@affine/core/components/cloud/share-header-right-item';
|
import ShareHeaderRightItem from '@affine/core/components/cloud/share-header-right-item';
|
||||||
import { AuthModal } from '@affine/core/providers/modal-provider';
|
|
||||||
import type { DocMode } from '@blocksuite/blocks';
|
import type { DocMode } from '@blocksuite/blocks';
|
||||||
import type { DocCollection } from '@blocksuite/store';
|
import type { DocCollection } from '@blocksuite/store';
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import type { ReactElement } from 'react';
|
|||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
import type { SettingAtom } from '../atoms';
|
import type { SettingAtom } from '../atoms';
|
||||||
import { authAtom, openSettingModalAtom, openSignOutModalAtom } from '../atoms';
|
import { openSettingModalAtom, openSignOutModalAtom } from '../atoms';
|
||||||
import { AuthModal as Auth } from '../components/affine/auth';
|
import { AuthModal } from '../components/affine/auth';
|
||||||
import { AiLoginRequiredModal } from '../components/affine/auth/ai-login-required';
|
import { AiLoginRequiredModal } from '../components/affine/auth/ai-login-required';
|
||||||
import { HistoryTipsModal } from '../components/affine/history-tips-modal';
|
import { HistoryTipsModal } from '../components/affine/history-tips-modal';
|
||||||
import { IssueFeedbackModal } from '../components/affine/issue-feedback-modal';
|
import { IssueFeedbackModal } from '../components/affine/issue-feedback-modal';
|
||||||
@@ -88,46 +88,6 @@ export const Setting = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AuthModal = (): ReactElement => {
|
|
||||||
const [
|
|
||||||
{ openModal, state, email = '', emailType = 'changePassword' },
|
|
||||||
setAuthAtom,
|
|
||||||
] = useAtom(authAtom);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Auth
|
|
||||||
open={openModal}
|
|
||||||
state={state}
|
|
||||||
email={email}
|
|
||||||
emailType={emailType}
|
|
||||||
setEmailType={useCallback(
|
|
||||||
emailType => {
|
|
||||||
setAuthAtom(prev => ({ ...prev, emailType }));
|
|
||||||
},
|
|
||||||
[setAuthAtom]
|
|
||||||
)}
|
|
||||||
setOpen={useCallback(
|
|
||||||
open => {
|
|
||||||
setAuthAtom(prev => ({ ...prev, openModal: open }));
|
|
||||||
},
|
|
||||||
[setAuthAtom]
|
|
||||||
)}
|
|
||||||
setAuthState={useCallback(
|
|
||||||
state => {
|
|
||||||
setAuthAtom(prev => ({ ...prev, state }));
|
|
||||||
},
|
|
||||||
[setAuthAtom]
|
|
||||||
)}
|
|
||||||
setAuthEmail={useCallback(
|
|
||||||
email => {
|
|
||||||
setAuthAtom(prev => ({ ...prev, email }));
|
|
||||||
},
|
|
||||||
[setAuthAtom]
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function CurrentWorkspaceModals() {
|
export function CurrentWorkspaceModals() {
|
||||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||||
|
|
||||||
|
|||||||
@@ -54,10 +54,6 @@ export const topLevelRoutes = [
|
|||||||
path: '/admin-panel',
|
path: '/admin-panel',
|
||||||
lazy: () => import('./pages/admin-panel'),
|
lazy: () => import('./pages/admin-panel'),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/auth/:authType',
|
|
||||||
lazy: () => import('./pages/auth'),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/expired',
|
path: '/expired',
|
||||||
lazy: () => import('./pages/expired'),
|
lazy: () => import('./pages/expired'),
|
||||||
@@ -66,14 +62,6 @@ export const topLevelRoutes = [
|
|||||||
path: '/invite/:inviteId',
|
path: '/invite/:inviteId',
|
||||||
lazy: () => import('./pages/invite'),
|
lazy: () => import('./pages/invite'),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/signIn',
|
|
||||||
lazy: () => import('./pages/sign-in'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/magic-link',
|
|
||||||
lazy: () => import('./pages/magic-link'),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/upgrade-success',
|
path: '/upgrade-success',
|
||||||
lazy: () => import('./pages/upgrade-success'),
|
lazy: () => import('./pages/upgrade-success'),
|
||||||
@@ -111,18 +99,33 @@ export const topLevelRoutes = [
|
|||||||
lazy: () => import('./pages/import-template'),
|
lazy: () => import('./pages/import-template'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/oauth/callback',
|
path: '/auth/:authType',
|
||||||
lazy: () => import('./pages/oauth-callback'),
|
lazy: () => import(/* webpackChunkName: "auth" */ './pages/auth/auth'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/open-app/:action',
|
path: '/signIn',
|
||||||
lazy: () => import('./pages/open-app'),
|
lazy: () =>
|
||||||
|
import(/* webpackChunkName: "auth" */ './pages/auth/sign-in'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/magic-link',
|
||||||
|
lazy: () =>
|
||||||
|
import(/* webpackChunkName: "auth" */ './pages/auth/magic-link'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/oauth/callback',
|
||||||
|
lazy: () =>
|
||||||
|
import(/* webpackChunkName: "auth" */ './pages/auth/oauth-callback'),
|
||||||
},
|
},
|
||||||
// deprecated, keep for old client compatibility
|
// deprecated, keep for old client compatibility
|
||||||
// TODO(@forehalo): remove
|
// TODO(@forehalo): remove
|
||||||
{
|
{
|
||||||
path: '/desktop-signin',
|
path: '/desktop-signin',
|
||||||
lazy: () => import('./pages/desktop-signin'),
|
lazy: () => import('./pages/auth/desktop-signin'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/open-app/:action',
|
||||||
|
lazy: () => import('./pages/open-app'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '*',
|
path: '*',
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ import { lazy, Suspense } from 'react';
|
|||||||
import { RouterProvider } from 'react-router-dom';
|
import { RouterProvider } from 'react-router-dom';
|
||||||
|
|
||||||
const desktopWhiteList = [
|
const desktopWhiteList = [
|
||||||
'/desktop-signin',
|
|
||||||
'/open-app/signin-redirect',
|
'/open-app/signin-redirect',
|
||||||
'/open-app/url',
|
'/open-app/url',
|
||||||
'/upgrade-success',
|
'/upgrade-success',
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ import path from 'node:path';
|
|||||||
import type { App } from 'electron';
|
import type { App } from 'electron';
|
||||||
|
|
||||||
import { buildType, isDev } from './config';
|
import { buildType, isDev } from './config';
|
||||||
import { mainWindowOrigin } from './constants';
|
|
||||||
import { logger } from './logger';
|
import { logger } from './logger';
|
||||||
|
import { uiSubjects } from './ui';
|
||||||
import {
|
import {
|
||||||
getMainWindow,
|
getMainWindow,
|
||||||
openUrlInHiddenWindow,
|
openUrlInHiddenWindow,
|
||||||
openUrlInMainWindow,
|
openUrlInMainWindow,
|
||||||
|
showMainWindow,
|
||||||
} from './windows-manager';
|
} from './windows-manager';
|
||||||
|
|
||||||
let protocol = buildType === 'stable' ? 'affine' : `affine-${buildType}`;
|
let protocol = buildType === 'stable' ? 'affine' : `affine-${buildType}`;
|
||||||
@@ -58,31 +59,39 @@ export function setupDeepLink(app: App) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleAffineUrl(url: string) {
|
async function handleAffineUrl(url: string) {
|
||||||
|
await showMainWindow();
|
||||||
|
|
||||||
logger.info('open affine url', url);
|
logger.info('open affine url', url);
|
||||||
const urlObj = new URL(url);
|
const urlObj = new URL(url);
|
||||||
logger.info('handle affine schema action', urlObj.hostname);
|
|
||||||
|
|
||||||
if (urlObj.hostname === 'bring-to-front') {
|
if (urlObj.hostname === 'authentication') {
|
||||||
const mainWindow = await getMainWindow();
|
const method = urlObj.searchParams.get('method');
|
||||||
if (mainWindow) {
|
const payload = JSON.parse(urlObj.searchParams.get('payload') ?? 'false');
|
||||||
mainWindow.show();
|
|
||||||
|
if (
|
||||||
|
!method ||
|
||||||
|
(method !== 'magic-link' && method !== 'oauth') ||
|
||||||
|
!payload
|
||||||
|
) {
|
||||||
|
logger.error('Invalid authentication url', url);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uiSubjects.authenticationRequest$.next({
|
||||||
|
method,
|
||||||
|
payload,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
await openUrl(urlObj);
|
const hiddenWindow = urlObj.searchParams.get('hidden')
|
||||||
}
|
? await openUrlInHiddenWindow(urlObj)
|
||||||
}
|
: await openUrlInMainWindow(urlObj);
|
||||||
|
|
||||||
async function openUrl(urlObj: URL) {
|
const main = await getMainWindow();
|
||||||
const params = urlObj.searchParams;
|
if (main && hiddenWindow) {
|
||||||
|
// when hidden window closed, the main window will be hidden somehow
|
||||||
const openInHiddenWindow = params.get('hidden');
|
hiddenWindow.on('close', () => {
|
||||||
params.delete('hidden');
|
main.show();
|
||||||
|
});
|
||||||
const url = mainWindowOrigin + urlObj.pathname + '?' + params.toString();
|
}
|
||||||
if (!openInHiddenWindow) {
|
|
||||||
await openUrlInHiddenWindow(url);
|
|
||||||
} else {
|
|
||||||
// TODO(@pengx17): somehow the page won't load the url passed, help needed
|
|
||||||
await openUrlInMainWindow(url);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { MainEventRegister } from '../type';
|
import type { MainEventRegister } from '../type';
|
||||||
import {
|
import {
|
||||||
|
type AuthenticationRequest,
|
||||||
onActiveTabChanged,
|
onActiveTabChanged,
|
||||||
onTabAction,
|
onTabAction,
|
||||||
onTabShellViewActiveChange,
|
onTabShellViewActiveChange,
|
||||||
@@ -35,4 +36,10 @@ export const uiEvents = {
|
|||||||
onTabsStatusChange,
|
onTabsStatusChange,
|
||||||
onActiveTabChanged,
|
onActiveTabChanged,
|
||||||
onTabShellViewActiveChange,
|
onTabShellViewActiveChange,
|
||||||
|
onAuthenticationRequest: (fn: (state: AuthenticationRequest) => void) => {
|
||||||
|
const sub = uiSubjects.authenticationRequest$.subscribe(fn);
|
||||||
|
return () => {
|
||||||
|
sub.unsubscribe();
|
||||||
|
};
|
||||||
|
},
|
||||||
} satisfies Record<string, MainEventRegister>;
|
} satisfies Record<string, MainEventRegister>;
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
|
import type { AuthenticationRequest } from '../windows-manager';
|
||||||
|
|
||||||
export const uiSubjects = {
|
export const uiSubjects = {
|
||||||
onMaximized$: new Subject<boolean>(),
|
onMaximized$: new Subject<boolean>(),
|
||||||
onFullScreen$: new Subject<boolean>(),
|
onFullScreen$: new Subject<boolean>(),
|
||||||
onToggleRightSidebar$: new Subject<string>(),
|
onToggleRightSidebar$: new Subject<string>(),
|
||||||
|
authenticationRequest$: new Subject<AuthenticationRequest>(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface AuthenticationRequest {
|
||||||
|
method: 'magic-link' | 'oauth';
|
||||||
|
payload: Record<string, any>;
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export * from './authentication';
|
||||||
export * from './launcher';
|
export * from './launcher';
|
||||||
export * from './main-window';
|
export * from './main-window';
|
||||||
export * from './onboarding';
|
export * from './onboarding';
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { BehaviorSubject } from 'rxjs';
|
|||||||
|
|
||||||
import { isLinux, isMacOS, isWindows } from '../../shared/utils';
|
import { isLinux, isMacOS, isWindows } from '../../shared/utils';
|
||||||
import { buildType } from '../config';
|
import { buildType } from '../config';
|
||||||
|
import { mainWindowOrigin } from '../constants';
|
||||||
import { ensureHelperProcess } from '../helper-process';
|
import { ensureHelperProcess } from '../helper-process';
|
||||||
import { logger } from '../logger';
|
import { logger } from '../logger';
|
||||||
import { uiSubjects } from '../ui/subject';
|
import { uiSubjects } from '../ui/subject';
|
||||||
@@ -227,33 +228,60 @@ export async function showMainWindow() {
|
|||||||
window.focus();
|
window.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getWindowAdditionalArguments = async () => {
|
||||||
|
const { getExposedMeta } = await import('../exposed');
|
||||||
|
const mainExposedMeta = getExposedMeta();
|
||||||
|
return [
|
||||||
|
`--main-exposed-meta=` + JSON.stringify(mainExposedMeta),
|
||||||
|
`--window-name=hidden-window`,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
function transformToAppUrl(url: URL) {
|
||||||
|
const params = url.searchParams;
|
||||||
|
return mainWindowOrigin + url.pathname + '?' + params.toString();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open a URL in a hidden window.
|
* Open a URL in a hidden window.
|
||||||
*/
|
*/
|
||||||
export async function openUrlInHiddenWindow(url: string) {
|
export async function openUrlInHiddenWindow(urlObj: URL) {
|
||||||
|
const url = transformToAppUrl(urlObj);
|
||||||
const win = new BrowserWindow({
|
const win = new BrowserWindow({
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 600,
|
height: 600,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: join(__dirname, './preload.js'),
|
preload: join(__dirname, './preload.js'),
|
||||||
|
additionalArguments: await getWindowAdditionalArguments(),
|
||||||
},
|
},
|
||||||
show: environment.isDebug,
|
show: environment.isDebug,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (environment.isDebug) {
|
||||||
|
win.webContents.openDevTools();
|
||||||
|
}
|
||||||
|
|
||||||
win.on('close', e => {
|
win.on('close', e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!win.isDestroyed()) {
|
if (win && !win.isDestroyed()) {
|
||||||
win.destroy();
|
win.destroy();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
logger.info('loading page at', url);
|
logger.info('loading page at', url);
|
||||||
await win.loadURL(url);
|
win.loadURL(url).catch(e => {
|
||||||
|
logger.error('failed to load url', e);
|
||||||
|
});
|
||||||
return win;
|
return win;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openUrlInMainWindow(url: string) {
|
// TODO(@pengx17): somehow the page won't load the url passed, help needed
|
||||||
|
export async function openUrlInMainWindow(urlObj: URL) {
|
||||||
|
const url = transformToAppUrl(urlObj);
|
||||||
|
logger.info('loading page at', url);
|
||||||
const mainWindow = await getMainWindow();
|
const mainWindow = await getMainWindow();
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
mainWindow.show();
|
|
||||||
await mainWindow.loadURL(url);
|
await mainWindow.loadURL(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Default route fallback for mobile
|
// Default route fallback for mobile
|
||||||
import { SignIn } from '@affine/core/pages/sign-in';
|
import { SignIn } from '@affine/core/pages/auth/sign-in';
|
||||||
|
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
// placeholder impl
|
// placeholder impl
|
||||||
|
|||||||
Reference in New Issue
Block a user