mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
refactor(auth): authenticate user in main window (#8032)
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { atom } from 'jotai';
|
||||
|
||||
import type { AuthProps } from '../components/affine/auth';
|
||||
import type { SettingProps } from '../components/affine/setting-modal';
|
||||
import type { ActiveTab } from '../components/affine/setting-modal/types';
|
||||
// modal atoms
|
||||
@@ -37,18 +36,37 @@ export const openSettingModalAtom = atom<SettingAtom>({
|
||||
open: false,
|
||||
});
|
||||
|
||||
export type AuthAtom = {
|
||||
openModal: boolean;
|
||||
state: AuthProps['state'];
|
||||
email?: string;
|
||||
emailType?: AuthProps['emailType'];
|
||||
};
|
||||
export type AuthAtomData =
|
||||
| { state: 'signIn' }
|
||||
| {
|
||||
state: 'afterSignUpSendEmail';
|
||||
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,
|
||||
state: 'signIn',
|
||||
email: '',
|
||||
emailType: 'changeEmail',
|
||||
});
|
||||
|
||||
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 { AuthService } from '@affine/core/modules/cloud';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import type { AuthPanelProps } from './index';
|
||||
@@ -17,10 +17,9 @@ import * as style from './style.css';
|
||||
import { Captcha, useCaptcha } from './use-captcha';
|
||||
|
||||
export const AfterSignInSendEmail = ({
|
||||
setAuthState,
|
||||
setAuthData: setAuth,
|
||||
email,
|
||||
onSignedIn,
|
||||
}: AuthPanelProps) => {
|
||||
}: AuthPanelProps<'afterSignInSendEmail'>) => {
|
||||
const [resendCountDown, setResendCountDown] = useState(60);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -37,22 +36,9 @@ export const AfterSignInSendEmail = ({
|
||||
|
||||
const t = useI18n();
|
||||
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();
|
||||
|
||||
if (loginStatus === 'authenticated') {
|
||||
onSignedIn?.();
|
||||
}
|
||||
|
||||
const onResendClick = useAsyncCallback(async () => {
|
||||
setIsSending(true);
|
||||
try {
|
||||
@@ -70,12 +56,12 @@ export const AfterSignInSendEmail = ({
|
||||
}, [authService, challenge, email, verifyToken]);
|
||||
|
||||
const onSignInWithPasswordClick = useCallback(() => {
|
||||
setAuthState('signInWithPassword');
|
||||
}, [setAuthState]);
|
||||
setAuth({ state: 'signInWithPassword' });
|
||||
}, [setAuth]);
|
||||
|
||||
const onBackBottomClick = useCallback(() => {
|
||||
setAuthState('signIn');
|
||||
}, [setAuthState]);
|
||||
setAuth({ state: 'signIn' });
|
||||
}, [setAuth]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import type { FC } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
@@ -17,11 +17,9 @@ import type { AuthPanelProps } from './index';
|
||||
import * as style from './style.css';
|
||||
import { Captcha, useCaptcha } from './use-captcha';
|
||||
|
||||
export const AfterSignUpSendEmail: FC<AuthPanelProps> = ({
|
||||
setAuthState,
|
||||
email,
|
||||
onSignedIn,
|
||||
}) => {
|
||||
export const AfterSignUpSendEmail: FC<
|
||||
AuthPanelProps<'afterSignUpSendEmail'>
|
||||
> = ({ setAuthData, email }) => {
|
||||
const [resendCountDown, setResendCountDown] = useState(60);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -37,19 +35,6 @@ export const AfterSignUpSendEmail: FC<AuthPanelProps> = ({
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
const t = useI18n();
|
||||
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();
|
||||
|
||||
@@ -117,8 +102,8 @@ export const AfterSignUpSendEmail: FC<AuthPanelProps> = ({
|
||||
|
||||
<BackButton
|
||||
onClick={useCallback(() => {
|
||||
setAuthState('signIn');
|
||||
}, [setAuthState])}
|
||||
setAuthData({ state: 'signIn' });
|
||||
}, [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 { 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 { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
import { AfterSignInSendEmail } from './after-sign-in-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 { SignInWithPassword } from './sign-in-with-password';
|
||||
|
||||
export type AuthProps = {
|
||||
state:
|
||||
| 'signIn'
|
||||
| '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;
|
||||
};
|
||||
type AuthAtomType<T extends AuthAtomData['state']> = Extract<
|
||||
AuthAtomData,
|
||||
{ state: T }
|
||||
>;
|
||||
|
||||
export type AuthPanelProps = {
|
||||
email: string;
|
||||
setAuthState: AuthProps['setAuthState'];
|
||||
setAuthEmail: AuthProps['setAuthEmail'];
|
||||
setEmailType: AuthProps['setEmailType'];
|
||||
emailType: AuthProps['emailType'];
|
||||
onSignedIn?: () => void;
|
||||
};
|
||||
// return field in B that is not in A
|
||||
type Difference<
|
||||
A extends Record<string, any>,
|
||||
B extends Record<string, any>,
|
||||
> = Pick<B, Exclude<keyof B, keyof A>>;
|
||||
|
||||
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: {
|
||||
[k in AuthProps['state']]: FC<AuthPanelProps>;
|
||||
[k in AuthAtomData['state']]: FC<AuthPanelProps<k>>;
|
||||
} = {
|
||||
signIn: SignIn,
|
||||
afterSignUpSendEmail: AfterSignUpSendEmail,
|
||||
@@ -44,58 +43,100 @@ const config: {
|
||||
sendEmail: SendEmail,
|
||||
};
|
||||
|
||||
export const AuthModal: FC<AuthModalBaseProps & AuthProps> = ({
|
||||
open,
|
||||
state,
|
||||
setOpen,
|
||||
email,
|
||||
setAuthEmail,
|
||||
setAuthState,
|
||||
setEmailType,
|
||||
emailType,
|
||||
}) => {
|
||||
const onSignedIn = useCallback(() => {
|
||||
setAuthState('signIn');
|
||||
setAuthEmail('');
|
||||
setOpen(false);
|
||||
}, [setAuthState, setAuthEmail, setOpen]);
|
||||
export function AuthModal() {
|
||||
const t = useI18n();
|
||||
const [authAtomValue, setAuthAtom] = useAtom(authAtom);
|
||||
const authService = useService(AuthService);
|
||||
const setOpen = useCallback(
|
||||
(open: boolean) => {
|
||||
setAuthAtom(prev => ({ ...prev, openModal: open }));
|
||||
},
|
||||
[setAuthAtom]
|
||||
);
|
||||
|
||||
const signIn = useAsyncCallback(
|
||||
async ({
|
||||
method,
|
||||
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 (
|
||||
<AuthModalBase open={open} setOpen={setOpen}>
|
||||
<AuthPanel
|
||||
state={state}
|
||||
email={email}
|
||||
setAuthEmail={setAuthEmail}
|
||||
setAuthState={setAuthState}
|
||||
setEmailType={setEmailType}
|
||||
emailType={emailType}
|
||||
onSignedIn={onSignedIn}
|
||||
/>
|
||||
<AuthModalBase open={authAtomValue.openModal} setOpen={setOpen}>
|
||||
<AuthPanel />
|
||||
</AuthModalBase>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const AuthPanel: FC<AuthProps> = ({
|
||||
state,
|
||||
email,
|
||||
setAuthEmail,
|
||||
setAuthState,
|
||||
setEmailType,
|
||||
emailType,
|
||||
onSignedIn,
|
||||
}) => {
|
||||
const CurrentPanel = useMemo(() => {
|
||||
return config[state];
|
||||
}, [state]);
|
||||
export function AuthPanel() {
|
||||
const t = useI18n();
|
||||
const [authAtomValue, setAuthAtom] = useAtom(authAtom);
|
||||
const authService = useService(AuthService);
|
||||
const loginStatus = useLiveData(authService.session.status$);
|
||||
const previousLoginStatus = useRef(loginStatus);
|
||||
|
||||
return (
|
||||
<CurrentPanel
|
||||
email={email}
|
||||
setAuthState={setAuthState}
|
||||
setAuthEmail={setAuthEmail}
|
||||
setEmailType={setEmailType}
|
||||
emailType={emailType}
|
||||
onSignedIn={onSignedIn}
|
||||
/>
|
||||
const setAuthData = useCallback(
|
||||
(updates: Partial<AuthAtomData>) => {
|
||||
// @ts-expect-error checked in impls
|
||||
setAuthAtom(prev => ({
|
||||
...prev,
|
||||
...updates,
|
||||
}));
|
||||
},
|
||||
[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 { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { track } from '@affine/core/mixpanel';
|
||||
import { popupWindow } from '@affine/core/utils';
|
||||
import { apis } from '@affine/electron-api';
|
||||
import { OAuthProviderType } from '@affine/graphql';
|
||||
import { GithubIcon, GoogleDuotoneIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
@@ -59,7 +61,12 @@ function OAuthProvider({ provider }: { provider: OAuthProviderType }) {
|
||||
const onClick = useAsyncCallback(async () => {
|
||||
try {
|
||||
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) {
|
||||
console.error(err);
|
||||
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 type { AuthPanelProps } from './index';
|
||||
|
||||
const useEmailTitle = (emailType: AuthPanelProps['emailType']) => {
|
||||
const useEmailTitle = (emailType: AuthPanelProps<'sendEmail'>['emailType']) => {
|
||||
const t = useI18n();
|
||||
|
||||
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();
|
||||
|
||||
switch (emailType) {
|
||||
@@ -49,7 +51,9 @@ const useNotificationHint = (emailType: AuthPanelProps['emailType']) => {
|
||||
return t['com.affine.auth.sent.verify.email.hint']();
|
||||
}
|
||||
};
|
||||
const useButtonContent = (emailType: AuthPanelProps['emailType']) => {
|
||||
const useButtonContent = (
|
||||
emailType: AuthPanelProps<'sendEmail'>['emailType']
|
||||
) => {
|
||||
const t = useI18n();
|
||||
|
||||
switch (emailType) {
|
||||
@@ -63,7 +67,7 @@ const useButtonContent = (emailType: AuthPanelProps['emailType']) => {
|
||||
}
|
||||
};
|
||||
|
||||
const useSendEmail = (emailType: AuthPanelProps['emailType']) => {
|
||||
const useSendEmail = (emailType: AuthPanelProps<'sendEmail'>['emailType']) => {
|
||||
const {
|
||||
trigger: sendChangePasswordEmail,
|
||||
isMutating: isChangePasswordMutating,
|
||||
@@ -134,10 +138,10 @@ const useSendEmail = (emailType: AuthPanelProps['emailType']) => {
|
||||
};
|
||||
|
||||
export const SendEmail = ({
|
||||
setAuthState,
|
||||
setAuthData,
|
||||
email,
|
||||
emailType,
|
||||
}: AuthPanelProps) => {
|
||||
}: AuthPanelProps<'sendEmail'>) => {
|
||||
const t = useI18n();
|
||||
const serverConfig = useService(ServerConfigService).serverConfig;
|
||||
|
||||
@@ -160,8 +164,8 @@ export const SendEmail = ({
|
||||
}, [email, hint, sendEmail]);
|
||||
|
||||
const onBack = useCallback(() => {
|
||||
setAuthState('signIn');
|
||||
}, [setAuthState]);
|
||||
setAuthData({ state: 'signIn' });
|
||||
}, [setAuthData]);
|
||||
|
||||
if (!passwordLimits) {
|
||||
// TODO(@eyhn): loading & error UI
|
||||
|
||||
@@ -16,11 +16,9 @@ import type { AuthPanelProps } from './index';
|
||||
import * as styles from './style.css';
|
||||
import { useCaptcha } from './use-captcha';
|
||||
|
||||
export const SignInWithPassword: FC<AuthPanelProps> = ({
|
||||
setAuthState,
|
||||
setEmailType,
|
||||
export const SignInWithPassword: FC<AuthPanelProps<'signInWithPassword'>> = ({
|
||||
setAuthData,
|
||||
email,
|
||||
onSignedIn,
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const authService = useService(AuthService);
|
||||
@@ -40,14 +38,13 @@ export const SignInWithPassword: FC<AuthPanelProps> = ({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
onSignedIn?.();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setPasswordError(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [isLoading, authService, email, password, onSignedIn]);
|
||||
}, [isLoading, authService, email, password]);
|
||||
|
||||
const sendMagicLink = useAsyncCallback(async () => {
|
||||
if (sendingEmail) return;
|
||||
@@ -55,7 +52,7 @@ export const SignInWithPassword: FC<AuthPanelProps> = ({
|
||||
try {
|
||||
if (verifyToken) {
|
||||
await authService.sendEmailMagicLink(email, verifyToken, challenge);
|
||||
setAuthState('afterSignInSendEmail');
|
||||
setAuthData({ state: 'afterSignInSendEmail' });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -65,12 +62,11 @@ export const SignInWithPassword: FC<AuthPanelProps> = ({
|
||||
// TODO(@eyhn): handle error better
|
||||
}
|
||||
setSendingEmail(false);
|
||||
}, [sendingEmail, verifyToken, authService, email, challenge, setAuthState]);
|
||||
}, [sendingEmail, verifyToken, authService, email, challenge, setAuthData]);
|
||||
|
||||
const sendChangePasswordEmail = useCallback(() => {
|
||||
setEmailType('changePassword');
|
||||
setAuthState('sendEmail');
|
||||
}, [setAuthState, setEmailType]);
|
||||
setAuthData({ state: 'sendEmail', emailType: 'changePassword' });
|
||||
}, [setAuthData]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -140,8 +136,8 @@ export const SignInWithPassword: FC<AuthPanelProps> = ({
|
||||
</Wrapper>
|
||||
<BackButton
|
||||
onClick={useCallback(() => {
|
||||
setAuthState('signIn');
|
||||
}, [setAuthState])}
|
||||
setAuthData({ state: 'signIn' });
|
||||
}, [setAuthData])}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { notify } from '@affine/component';
|
||||
import { AuthInput, ModalHeader } from '@affine/component/auth-components';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { authAtom } from '@affine/core/atoms';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { track } from '@affine/core/mixpanel';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import { ArrowRightBigIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import type { FC } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { AuthService } from '../../../modules/cloud';
|
||||
@@ -24,36 +22,19 @@ function validateEmail(email: string) {
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
export const SignIn: FC<AuthPanelProps> = ({
|
||||
setAuthState,
|
||||
setAuthEmail,
|
||||
email,
|
||||
onSignedIn,
|
||||
export const SignIn: FC<AuthPanelProps<'signIn'>> = ({
|
||||
setAuthData: setAuthState,
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const authService = useService(AuthService);
|
||||
const [searchParams] = useSearchParams();
|
||||
const [isMutating, setIsMutating] = useState(false);
|
||||
const [verifyToken, challenge] = useCaptcha();
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
const [isValidEmail, setIsValidEmail] = useState(true);
|
||||
const { openModal } = useAtomValue(authAtom);
|
||||
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 () => {
|
||||
if (!validateEmail(email)) {
|
||||
setIsValidEmail(false);
|
||||
@@ -61,10 +42,8 @@ export const SignIn: FC<AuthPanelProps> = ({
|
||||
}
|
||||
|
||||
setIsValidEmail(true);
|
||||
|
||||
setIsMutating(true);
|
||||
|
||||
setAuthEmail(email);
|
||||
try {
|
||||
const { hasPassword, registered } =
|
||||
await authService.checkUserByEmail(email);
|
||||
@@ -74,16 +53,25 @@ export const SignIn: FC<AuthPanelProps> = ({
|
||||
// provider password sign-in if user has by default
|
||||
// If with payment, onl support email sign in to avoid redirect to affine app
|
||||
if (hasPassword) {
|
||||
setAuthState('signInWithPassword');
|
||||
setAuthState({
|
||||
state: 'signInWithPassword',
|
||||
email,
|
||||
});
|
||||
} else {
|
||||
track.$.$.auth.signIn();
|
||||
await authService.sendEmailMagicLink(email, verifyToken, challenge);
|
||||
setAuthState('afterSignInSendEmail');
|
||||
setAuthState({
|
||||
state: 'afterSignInSendEmail',
|
||||
email,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await authService.sendEmailMagicLink(email, verifyToken, challenge);
|
||||
track.$.$.auth.signUp();
|
||||
setAuthState('afterSignUpSendEmail');
|
||||
setAuthState({
|
||||
state: 'afterSignUpSendEmail',
|
||||
email,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -96,7 +84,7 @@ export const SignIn: FC<AuthPanelProps> = ({
|
||||
}
|
||||
|
||||
setIsMutating(false);
|
||||
}, [authService, challenge, email, setAuthEmail, setAuthState, verifyToken]);
|
||||
}, [authService, challenge, email, setAuthState, verifyToken]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -111,13 +99,7 @@ export const SignIn: FC<AuthPanelProps> = ({
|
||||
<AuthInput
|
||||
label={t['com.affine.settings.email']()}
|
||||
placeholder={t['com.affine.auth.sign.email.placeholder']()}
|
||||
value={email}
|
||||
onChange={useCallback(
|
||||
(value: string) => {
|
||||
setAuthEmail(value);
|
||||
},
|
||||
[setAuthEmail]
|
||||
)}
|
||||
onChange={setEmail}
|
||||
error={!isValidEmail}
|
||||
errorHint={
|
||||
isValidEmail ? '' : t['com.affine.auth.sign.email.error']()
|
||||
|
||||
@@ -195,6 +195,7 @@ export const AccountSetting: FC = () => {
|
||||
setAuthModal({
|
||||
openModal: true,
|
||||
state: 'sendEmail',
|
||||
// @ts-expect-error accont email is always defined
|
||||
email: account.email,
|
||||
emailType: account.info?.emailVerified ? 'changeEmail' : 'verifyEmail',
|
||||
});
|
||||
@@ -204,6 +205,7 @@ export const AccountSetting: FC = () => {
|
||||
setAuthModal({
|
||||
openModal: true,
|
||||
state: 'sendEmail',
|
||||
// @ts-expect-error accont email is always defined
|
||||
email: account.email,
|
||||
emailType: account.info?.hasPassword ? 'changePassword' : 'setPassword',
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { useAtom } from 'jotai/react';
|
||||
import { useSetAtom } from 'jotai/react';
|
||||
import {
|
||||
type MouseEvent,
|
||||
Suspense,
|
||||
@@ -81,7 +81,7 @@ export const UserInfo = ({ onAccountSettingClick, active }: UserInfoProps) => {
|
||||
|
||||
export const SignInButton = () => {
|
||||
const t = useI18n();
|
||||
const [, setAuthModal] = useAtom(authAtom);
|
||||
const setAuthModal = useSetAtom(authAtom);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { AIProvider } from '@affine/core/blocksuite/presets/ai';
|
||||
import { buildAppUrl, popupWindow } from '@affine/core/utils';
|
||||
import { apis, appInfo } from '@affine/electron-api';
|
||||
import { appInfo } from '@affine/electron-api';
|
||||
import type { OAuthProviderType } from '@affine/graphql';
|
||||
import {
|
||||
ApplicationFocused,
|
||||
@@ -97,11 +96,7 @@ export class AuthService extends Service {
|
||||
email,
|
||||
// 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
|
||||
callbackUrl: buildAppUrl('/magic-link', {
|
||||
desktop: environment.isDesktop,
|
||||
openInHiddenWindow: true,
|
||||
redirectFromWeb: true,
|
||||
}),
|
||||
callbackUrl: `/magic-link?client=${environment.isDesktop ? appInfo?.schema : 'web'}`,
|
||||
}),
|
||||
headers: {
|
||||
'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', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ provider }),
|
||||
body: JSON.stringify({ provider, redirect_uri: redirectUrl }),
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to sign in with ${provider}`);
|
||||
}
|
||||
|
||||
let { url } = await res.json();
|
||||
|
||||
// change `state=xxx` to `state={state:xxx,native:true}`
|
||||
@@ -140,13 +144,19 @@ export class AuthService extends Service {
|
||||
);
|
||||
url = oauthUrl.toString();
|
||||
|
||||
if (environment.isDesktop) {
|
||||
await apis?.ui.openExternal(url);
|
||||
} else {
|
||||
popupWindow(url);
|
||||
}
|
||||
return 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 }) {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
import { SignOutModal } from '../components/affine/sign-out-modal';
|
||||
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||
import { AuthService } from '../modules/cloud';
|
||||
import { SignIn } from './sign-in';
|
||||
import { SignIn } from './auth/sign-in';
|
||||
|
||||
export const PageNotFound = ({
|
||||
noPermission,
|
||||
|
||||
@@ -22,9 +22,9 @@ import type { LoaderFunction } from 'react-router-dom';
|
||||
import { redirect, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useMutation } from '../hooks/use-mutation';
|
||||
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||
import { AuthService, ServerConfigService } from '../modules/cloud';
|
||||
import { useMutation } from '../../hooks/use-mutation';
|
||||
import { RouteLogic, useNavigateHelper } from '../../hooks/use-navigate-helper';
|
||||
import { AuthService, ServerConfigService } from '../../modules/cloud';
|
||||
|
||||
const authTypeSchema = z.enum([
|
||||
'onboarding',
|
||||
62
packages/frontend/core/src/pages/auth/desktop-signin.tsx
Normal file
62
packages/frontend/core/src/pages/auth/desktop-signin.tsx
Normal file
@@ -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;
|
||||
};
|
||||
72
packages/frontend/core/src/pages/auth/magic-link.tsx
Normal file
72
packages/frontend/core/src/pages/auth/magic-link.tsx
Normal file
@@ -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;
|
||||
};
|
||||
73
packages/frontend/core/src/pages/auth/oauth-callback.tsx
Normal file
73
packages/frontend/core/src/pages/auth/oauth-callback.tsx
Normal file
@@ -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;
|
||||
};
|
||||
@@ -1,20 +1,16 @@
|
||||
import { AffineOtherPageLayout } from '@affine/component/affine-other-page-layout';
|
||||
import { SignInPageContainer } from '@affine/component/auth-components';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { appInfo } from '@affine/electron-api';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { authAtom } from '../atoms';
|
||||
import type { AuthProps } from '../components/affine/auth';
|
||||
import { AuthPanel } 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 = () => {
|
||||
const [{ state, email = '', emailType = 'changePassword' }, setAuthAtom] =
|
||||
useAtom(authAtom);
|
||||
const session = useService(AuthService).session;
|
||||
const status = useLiveData(session.status$);
|
||||
const isRevalidating = useLiveData(session.isRevalidating$);
|
||||
@@ -24,6 +20,10 @@ export const SignIn = () => {
|
||||
const isLoggedIn = status === 'authenticated' && !isRevalidating;
|
||||
|
||||
useEffect(() => {
|
||||
if (environment.isDesktop && appInfo?.windowName === 'hidden-window') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isLoggedIn) {
|
||||
const redirectUri = searchParams.get('redirect_uri');
|
||||
if (redirectUri) {
|
||||
@@ -36,40 +36,12 @@ export const SignIn = () => {
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [jumpToIndex, navigate, setAuthAtom, 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]
|
||||
);
|
||||
}, [jumpToIndex, navigate, isLoggedIn, searchParams]);
|
||||
|
||||
return (
|
||||
<SignInPageContainer>
|
||||
<div style={{ maxWidth: '400px', width: '100%' }}>
|
||||
<AuthPanel
|
||||
state={state}
|
||||
email={email}
|
||||
emailType={emailType}
|
||||
setEmailType={onSetEmailType}
|
||||
setAuthState={onSetAuthState}
|
||||
setAuthEmail={onSetAuthEmail}
|
||||
/>
|
||||
<AuthPanel />
|
||||
</div>
|
||||
</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 { EditorModeSwitch } from '@affine/core/components/blocksuite/block-suite-mode-switch';
|
||||
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 { DocCollection } from '@blocksuite/store';
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ import type { ReactElement } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import type { SettingAtom } from '../atoms';
|
||||
import { authAtom, openSettingModalAtom, openSignOutModalAtom } from '../atoms';
|
||||
import { AuthModal as Auth } from '../components/affine/auth';
|
||||
import { openSettingModalAtom, openSignOutModalAtom } from '../atoms';
|
||||
import { AuthModal } from '../components/affine/auth';
|
||||
import { AiLoginRequiredModal } from '../components/affine/auth/ai-login-required';
|
||||
import { HistoryTipsModal } from '../components/affine/history-tips-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() {
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
|
||||
|
||||
@@ -54,10 +54,6 @@ export const topLevelRoutes = [
|
||||
path: '/admin-panel',
|
||||
lazy: () => import('./pages/admin-panel'),
|
||||
},
|
||||
{
|
||||
path: '/auth/:authType',
|
||||
lazy: () => import('./pages/auth'),
|
||||
},
|
||||
{
|
||||
path: '/expired',
|
||||
lazy: () => import('./pages/expired'),
|
||||
@@ -66,14 +62,6 @@ export const topLevelRoutes = [
|
||||
path: '/invite/:inviteId',
|
||||
lazy: () => import('./pages/invite'),
|
||||
},
|
||||
{
|
||||
path: '/signIn',
|
||||
lazy: () => import('./pages/sign-in'),
|
||||
},
|
||||
{
|
||||
path: '/magic-link',
|
||||
lazy: () => import('./pages/magic-link'),
|
||||
},
|
||||
{
|
||||
path: '/upgrade-success',
|
||||
lazy: () => import('./pages/upgrade-success'),
|
||||
@@ -111,18 +99,33 @@ export const topLevelRoutes = [
|
||||
lazy: () => import('./pages/import-template'),
|
||||
},
|
||||
{
|
||||
path: '/oauth/callback',
|
||||
lazy: () => import('./pages/oauth-callback'),
|
||||
path: '/auth/:authType',
|
||||
lazy: () => import(/* webpackChunkName: "auth" */ './pages/auth/auth'),
|
||||
},
|
||||
{
|
||||
path: '/open-app/:action',
|
||||
lazy: () => import('./pages/open-app'),
|
||||
path: '/signIn',
|
||||
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
|
||||
// TODO(@forehalo): remove
|
||||
{
|
||||
path: '/desktop-signin',
|
||||
lazy: () => import('./pages/desktop-signin'),
|
||||
lazy: () => import('./pages/auth/desktop-signin'),
|
||||
},
|
||||
{
|
||||
path: '/open-app/:action',
|
||||
lazy: () => import('./pages/open-app'),
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
|
||||
Reference in New Issue
Block a user