mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-06 03:49:56 +08:00
refactor(server): auth (#5895)
Remove `next-auth` and implement our own Authorization/Authentication system from scratch.
## Server
- [x] tokens
- [x] function
- [x] encryption
- [x] AuthController
- [x] /api/auth/sign-in
- [x] /api/auth/sign-out
- [x] /api/auth/session
- [x] /api/auth/session (WE SUPPORT MULTI-ACCOUNT!)
- [x] OAuthPlugin
- [x] OAuthController
- [x] /oauth/login
- [x] /oauth/callback
- [x] Providers
- [x] Google
- [x] GitHub
## Client
- [x] useSession
- [x] cloudSignIn
- [x] cloudSignOut
## NOTE:
Tests will be adding in the future
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { Button } from '../../ui/button';
|
||||
import { AuthPageContainer } from './auth-page-container';
|
||||
|
||||
export const ConfirmChangeEmail: FC<{
|
||||
onOpenAffine: () => void;
|
||||
}> = ({ onOpenAffine }) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
return (
|
||||
<AuthPageContainer
|
||||
title={t['com.affine.auth.change.email.page.success.title']()}
|
||||
subtitle={t['com.affine.auth.change.email.page.success.subtitle']()}
|
||||
>
|
||||
<Button type="primary" size="large" onClick={onOpenAffine}>
|
||||
{t['com.affine.auth.open.affine']()}
|
||||
</Button>
|
||||
</AuthPageContainer>
|
||||
);
|
||||
};
|
||||
@@ -37,7 +37,7 @@ function getCallbackUrl(location: Location) {
|
||||
try {
|
||||
const url =
|
||||
location.state?.callbackURL ||
|
||||
new URLSearchParams(location.search).get('callbackUrl');
|
||||
new URLSearchParams(location.search).get('redirect_uri');
|
||||
if (typeof url === 'string' && url) {
|
||||
if (!url.startsWith('http:') && !url.startsWith('https:')) {
|
||||
return url;
|
||||
|
||||
@@ -3,4 +3,5 @@ export interface User {
|
||||
name: string;
|
||||
email: string;
|
||||
image?: string | null;
|
||||
avatarUrl: string | null;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { SignOutIcon } from '@blocksuite/icons';
|
||||
import { Avatar } from '../../ui/avatar';
|
||||
import { Button, IconButton } from '../../ui/button';
|
||||
import { Tooltip } from '../../ui/tooltip';
|
||||
import type { User } from '../auth-components';
|
||||
import { NotFoundPattern } from './not-found-pattern';
|
||||
import {
|
||||
largeButtonEffect,
|
||||
@@ -12,11 +13,7 @@ import {
|
||||
} from './styles.css';
|
||||
|
||||
export interface NotFoundPageProps {
|
||||
user: {
|
||||
name: string;
|
||||
email: string;
|
||||
avatar: string;
|
||||
} | null;
|
||||
user?: User | null;
|
||||
onBack: () => void;
|
||||
onSignOut: () => void;
|
||||
}
|
||||
@@ -47,7 +44,7 @@ export const NotFoundPage = ({
|
||||
|
||||
{user ? (
|
||||
<div className={wrapper}>
|
||||
<Avatar url={user.avatar} name={user.name} />
|
||||
<Avatar url={user.avatarUrl ?? user.image} name={user.name} />
|
||||
<span style={{ margin: '0 12px' }}>{user.email}</span>
|
||||
<Tooltip content={t['404.signOut']()}>
|
||||
<IconButton onClick={onSignOut}>
|
||||
|
||||
@@ -384,6 +384,7 @@ export const createConfiguration: (
|
||||
{ context: '/api', target: 'http://localhost:3010' },
|
||||
{ context: '/socket.io', target: 'http://localhost:3010', ws: true },
|
||||
{ context: '/graphql', target: 'http://localhost:3010' },
|
||||
{ context: '/oauth', target: 'http://localhost:3010' },
|
||||
],
|
||||
} as DevServerConfiguration,
|
||||
} satisfies webpack.Configuration;
|
||||
|
||||
@@ -78,7 +78,6 @@
|
||||
"lottie-web": "^5.12.2",
|
||||
"mini-css-extract-plugin": "^2.8.0",
|
||||
"nanoid": "^5.0.6",
|
||||
"next-auth": "^4.24.5",
|
||||
"next-themes": "^0.2.1",
|
||||
"postcss-loader": "^8.1.0",
|
||||
"react": "18.2.0",
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import { atom } from 'jotai';
|
||||
import type { SessionContextValue } from 'next-auth/react';
|
||||
|
||||
export const sessionAtom = atom<SessionContextValue<true> | null>(null);
|
||||
@@ -24,7 +24,7 @@ export type AuthProps = {
|
||||
setAuthEmail: (state: AuthProps['email']) => void;
|
||||
setEmailType: (state: AuthProps['emailType']) => void;
|
||||
email: string;
|
||||
emailType: 'setPassword' | 'changePassword' | 'changeEmail';
|
||||
emailType: 'setPassword' | 'changePassword' | 'changeEmail' | 'verifyEmail';
|
||||
onSignedIn?: () => void;
|
||||
};
|
||||
|
||||
@@ -59,8 +59,10 @@ export const AuthModal: FC<AuthModalBaseProps & AuthProps> = ({
|
||||
emailType,
|
||||
}) => {
|
||||
const onSignedIn = useCallback(() => {
|
||||
setAuthState('signIn');
|
||||
setAuthEmail('');
|
||||
setOpen(false);
|
||||
}, [setOpen]);
|
||||
}, [setAuthState, setAuthEmail, setOpen]);
|
||||
|
||||
return (
|
||||
<AuthModalBase open={open} setOpen={setOpen}>
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import {
|
||||
useOAuthProviders,
|
||||
useServerFeatures,
|
||||
} from '@affine/core/hooks/affine/use-server-config';
|
||||
import { OAuthProviderType } from '@affine/graphql';
|
||||
import { GithubIcon, GoogleDuotoneIcon } from '@blocksuite/icons';
|
||||
import { type ReactElement, useCallback } from 'react';
|
||||
|
||||
import { useAuth } from './use-auth';
|
||||
|
||||
const OAuthProviderMap: Record<
|
||||
OAuthProviderType,
|
||||
{
|
||||
icon: ReactElement;
|
||||
}
|
||||
> = {
|
||||
[OAuthProviderType.Google]: {
|
||||
icon: <GoogleDuotoneIcon />,
|
||||
},
|
||||
|
||||
[OAuthProviderType.GitHub]: {
|
||||
icon: <GithubIcon />,
|
||||
},
|
||||
};
|
||||
|
||||
export function OAuth() {
|
||||
const { oauth } = useServerFeatures();
|
||||
|
||||
if (!oauth) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <OAuthProviders />;
|
||||
}
|
||||
|
||||
function OAuthProviders() {
|
||||
const providers = useOAuthProviders();
|
||||
|
||||
return providers.map(provider => (
|
||||
<OAuthProvider key={provider} provider={provider} />
|
||||
));
|
||||
}
|
||||
|
||||
function OAuthProvider({ provider }: { provider: OAuthProviderType }) {
|
||||
const { icon } = OAuthProviderMap[provider];
|
||||
const { oauthSignIn } = useAuth();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
oauthSignIn(provider);
|
||||
}, [provider, oauthSignIn]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={provider}
|
||||
type="primary"
|
||||
block
|
||||
size="extraLarge"
|
||||
style={{ marginTop: 30 }}
|
||||
icon={icon}
|
||||
onClick={onClick}
|
||||
>
|
||||
Continue with {provider}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
sendChangeEmailMutation,
|
||||
sendChangePasswordEmailMutation,
|
||||
sendSetPasswordEmailMutation,
|
||||
sendVerifyEmailMutation,
|
||||
} from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useSetAtom } from 'jotai/react';
|
||||
@@ -29,7 +30,9 @@ const useEmailTitle = (emailType: AuthPanelProps['emailType']) => {
|
||||
case 'changePassword':
|
||||
return t['com.affine.auth.reset.password']();
|
||||
case 'changeEmail':
|
||||
return t['com.affine.settings.email.action']();
|
||||
return t['com.affine.settings.email.action.change']();
|
||||
case 'verifyEmail':
|
||||
return t['com.affine.settings.email.action.verify']();
|
||||
}
|
||||
};
|
||||
const useContent = (emailType: AuthPanelProps['emailType'], email: string) => {
|
||||
@@ -41,7 +44,8 @@ const useContent = (emailType: AuthPanelProps['emailType'], email: string) => {
|
||||
case 'changePassword':
|
||||
return t['com.affine.auth.reset.password.message']();
|
||||
case 'changeEmail':
|
||||
return t['com.affine.auth.change.email.message']({
|
||||
case 'verifyEmail':
|
||||
return t['com.affine.auth.verify.email.message']({
|
||||
email,
|
||||
});
|
||||
}
|
||||
@@ -56,7 +60,8 @@ const useNotificationHint = (emailType: AuthPanelProps['emailType']) => {
|
||||
case 'changePassword':
|
||||
return t['com.affine.auth.sent.change.password.hint']();
|
||||
case 'changeEmail':
|
||||
return t['com.affine.auth.sent.change.email.hint']();
|
||||
case 'verifyEmail':
|
||||
return t['com.affine.auth.sent.verify.email.hint']();
|
||||
}
|
||||
};
|
||||
const useButtonContent = (emailType: AuthPanelProps['emailType']) => {
|
||||
@@ -68,7 +73,8 @@ const useButtonContent = (emailType: AuthPanelProps['emailType']) => {
|
||||
case 'changePassword':
|
||||
return t['com.affine.auth.send.reset.password.link']();
|
||||
case 'changeEmail':
|
||||
return t['com.affine.auth.send.change.email.link']();
|
||||
case 'verifyEmail':
|
||||
return t['com.affine.auth.send.verify.email.hint']();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -87,12 +93,17 @@ const useSendEmail = (emailType: AuthPanelProps['emailType']) => {
|
||||
useMutation({
|
||||
mutation: sendChangeEmailMutation,
|
||||
});
|
||||
const { trigger: sendVerifyEmail, isMutating: isVerifyEmailMutation } =
|
||||
useMutation({
|
||||
mutation: sendVerifyEmailMutation,
|
||||
});
|
||||
|
||||
return {
|
||||
loading:
|
||||
isChangePasswordMutating ||
|
||||
isSetPasswordMutating ||
|
||||
isChangeEmailMutating,
|
||||
isChangeEmailMutating ||
|
||||
isVerifyEmailMutation,
|
||||
sendEmail: useCallback(
|
||||
(email: string) => {
|
||||
let trigger: (args: {
|
||||
@@ -113,6 +124,10 @@ const useSendEmail = (emailType: AuthPanelProps['emailType']) => {
|
||||
trigger = sendChangeEmail;
|
||||
callbackUrl = 'changeEmail';
|
||||
break;
|
||||
case 'verifyEmail':
|
||||
trigger = sendVerifyEmail;
|
||||
callbackUrl = 'verify-email';
|
||||
break;
|
||||
}
|
||||
// TODO: add error handler
|
||||
return trigger({
|
||||
@@ -127,6 +142,7 @@ const useSendEmail = (emailType: AuthPanelProps['emailType']) => {
|
||||
sendChangeEmail,
|
||||
sendChangePasswordEmail,
|
||||
sendSetPasswordEmail,
|
||||
sendVerifyEmail,
|
||||
]
|
||||
),
|
||||
};
|
||||
|
||||
@@ -5,10 +5,9 @@ import {
|
||||
ModalHeader,
|
||||
} from '@affine/component/auth-components';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { useSession } from '@affine/core/hooks/affine/use-current-user';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useSession } from 'next-auth/react';
|
||||
import type { FC } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
@@ -25,7 +24,7 @@ export const SignInWithPassword: FC<AuthPanelProps> = ({
|
||||
onSignedIn,
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const { update } = useSession();
|
||||
const { reload } = useSession();
|
||||
|
||||
const [password, setPassword] = useState('');
|
||||
const [passwordError, setPasswordError] = useState(false);
|
||||
@@ -39,7 +38,6 @@ export const SignInWithPassword: FC<AuthPanelProps> = ({
|
||||
|
||||
const onSignIn = useAsyncCallback(async () => {
|
||||
const res = await signInCloud('credentials', {
|
||||
redirect: false,
|
||||
email,
|
||||
password,
|
||||
}).catch(console.error);
|
||||
@@ -48,9 +46,9 @@ export const SignInWithPassword: FC<AuthPanelProps> = ({
|
||||
return setPasswordError(true);
|
||||
}
|
||||
|
||||
await update();
|
||||
await reload();
|
||||
onSignedIn?.();
|
||||
}, [email, password, onSignedIn, update]);
|
||||
}, [email, password, onSignedIn, reload]);
|
||||
|
||||
const sendMagicLink = useAsyncCallback(async () => {
|
||||
if (allowSendEmail && verifyToken && !sendingEmail) {
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from '@affine/graphql';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ArrowDownBigIcon, GoogleDuotoneIcon } from '@blocksuite/icons';
|
||||
import { ArrowDownBigIcon } from '@blocksuite/icons';
|
||||
import { type FC, useState } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-s
|
||||
import { useMutation } from '../../../hooks/use-mutation';
|
||||
import { emailRegex } from '../../../utils/email-regex';
|
||||
import type { AuthPanelProps } from './index';
|
||||
import { OAuth } from './oauth';
|
||||
import * as style from './style.css';
|
||||
import { INTERNAL_BETA_URL, useAuth } from './use-auth';
|
||||
import { Captcha, useCaptcha } from './use-captcha';
|
||||
@@ -46,7 +47,6 @@ export const SignIn: FC<AuthPanelProps> = ({
|
||||
allowSendEmail,
|
||||
signIn,
|
||||
signUp,
|
||||
signInWithGoogle,
|
||||
} = useAuth();
|
||||
|
||||
const { trigger: verifyUser, isMutating } = useMutation({
|
||||
@@ -59,6 +59,10 @@ export const SignIn: FC<AuthPanelProps> = ({
|
||||
}
|
||||
|
||||
const onContinue = useAsyncCallback(async () => {
|
||||
if (!allowSendEmail) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateEmail(email)) {
|
||||
setIsValidEmail(false);
|
||||
return;
|
||||
@@ -99,13 +103,14 @@ export const SignIn: FC<AuthPanelProps> = ({
|
||||
const res = await signUp(email, verifyToken, challenge);
|
||||
if (res?.status === 403 && res?.url === INTERNAL_BETA_URL) {
|
||||
return setAuthState('noAccess');
|
||||
} else if (!res || res.status >= 400 || res.error) {
|
||||
} else if (!res || res.status >= 400) {
|
||||
return;
|
||||
}
|
||||
setAuthState('afterSignUpSendEmail');
|
||||
}
|
||||
}
|
||||
}, [
|
||||
allowSendEmail,
|
||||
subscriptionData,
|
||||
challenge,
|
||||
email,
|
||||
@@ -124,20 +129,7 @@ export const SignIn: FC<AuthPanelProps> = ({
|
||||
subTitle={t['com.affine.brand.affineCloud']()}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
block
|
||||
size="extraLarge"
|
||||
style={{
|
||||
marginTop: 30,
|
||||
}}
|
||||
icon={<GoogleDuotoneIcon />}
|
||||
onClick={useCallback(() => {
|
||||
signInWithGoogle();
|
||||
}, [signInWithGoogle])}
|
||||
>
|
||||
{t['Continue with Google']()}
|
||||
</Button>
|
||||
<OAuth />
|
||||
|
||||
<div className={style.authModalContent}>
|
||||
<AuthInput
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
import type { Notification } from '@affine/component/notification-center/index.jotai';
|
||||
import type { OAuthProviderType } from '@affine/graphql';
|
||||
import { atom, useAtom, useSetAtom } from 'jotai';
|
||||
import { type SignInResponse } from 'next-auth/react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { signInCloud } from '../../../utils/cloud-utils';
|
||||
@@ -11,10 +11,10 @@ const COUNT_DOWN_TIME = 60;
|
||||
export const INTERNAL_BETA_URL = `https://community.affine.pro/c/insider-general/`;
|
||||
|
||||
function handleSendEmailError(
|
||||
res: SignInResponse | undefined | void,
|
||||
res: Response | undefined | void,
|
||||
pushNotification: (notification: Notification) => void
|
||||
) {
|
||||
if (res?.error) {
|
||||
if (!res?.ok) {
|
||||
pushNotification({
|
||||
title: 'Send email error',
|
||||
message: 'Please back to home and try again',
|
||||
@@ -64,8 +64,13 @@ export const useAuth = () => {
|
||||
const [authStore, setAuthStore] = useAtom(authStoreAtom);
|
||||
const startResendCountDown = useSetAtom(countDownAtom);
|
||||
|
||||
const signIn = useCallback(
|
||||
async (email: string, verifyToken: string, challenge?: string) => {
|
||||
const sendEmailMagicLink = useCallback(
|
||||
async (
|
||||
signUp: boolean,
|
||||
email: string,
|
||||
verifyToken: string,
|
||||
challenge?: string
|
||||
) => {
|
||||
setAuthStore(prev => {
|
||||
return {
|
||||
...prev,
|
||||
@@ -76,18 +81,19 @@ export const useAuth = () => {
|
||||
const res = await signInCloud(
|
||||
'email',
|
||||
{
|
||||
email: email,
|
||||
callbackUrl: subscriptionData
|
||||
? subscriptionData.getRedirectUrl(false)
|
||||
: '/auth/signIn',
|
||||
redirect: false,
|
||||
email,
|
||||
},
|
||||
challenge
|
||||
? {
|
||||
challenge,
|
||||
token: verifyToken,
|
||||
}
|
||||
: { token: verifyToken }
|
||||
{
|
||||
...(challenge
|
||||
? {
|
||||
challenge,
|
||||
token: verifyToken,
|
||||
}
|
||||
: { token: verifyToken }),
|
||||
callbackUrl: subscriptionData
|
||||
? subscriptionData.getRedirectUrl(signUp)
|
||||
: '/auth/signIn',
|
||||
}
|
||||
).catch(console.error);
|
||||
|
||||
handleSendEmailError(res, pushNotification);
|
||||
@@ -107,47 +113,24 @@ export const useAuth = () => {
|
||||
|
||||
const signUp = useCallback(
|
||||
async (email: string, verifyToken: string, challenge?: string) => {
|
||||
setAuthStore(prev => {
|
||||
return {
|
||||
...prev,
|
||||
isMutating: true,
|
||||
};
|
||||
});
|
||||
|
||||
const res = await signInCloud(
|
||||
'email',
|
||||
{
|
||||
email: email,
|
||||
callbackUrl: subscriptionData
|
||||
? subscriptionData.getRedirectUrl(true)
|
||||
: '/auth/signUp',
|
||||
redirect: false,
|
||||
},
|
||||
challenge
|
||||
? {
|
||||
challenge,
|
||||
token: verifyToken,
|
||||
}
|
||||
: { token: verifyToken }
|
||||
).catch(console.error);
|
||||
|
||||
handleSendEmailError(res, pushNotification);
|
||||
|
||||
setAuthStore({
|
||||
isMutating: false,
|
||||
allowSendEmail: false,
|
||||
resendCountDown: COUNT_DOWN_TIME,
|
||||
});
|
||||
|
||||
startResendCountDown();
|
||||
|
||||
return res;
|
||||
return sendEmailMagicLink(true, email, verifyToken, challenge).catch(
|
||||
console.error
|
||||
);
|
||||
},
|
||||
[pushNotification, setAuthStore, startResendCountDown, subscriptionData]
|
||||
[sendEmailMagicLink]
|
||||
);
|
||||
|
||||
const signInWithGoogle = useCallback(() => {
|
||||
signInCloud('google').catch(console.error);
|
||||
const signIn = useCallback(
|
||||
async (email: string, verifyToken: string, challenge?: string) => {
|
||||
return sendEmailMagicLink(false, email, verifyToken, challenge).catch(
|
||||
console.error
|
||||
);
|
||||
},
|
||||
[sendEmailMagicLink]
|
||||
);
|
||||
|
||||
const oauthSignIn = useCallback((provider: OAuthProviderType) => {
|
||||
signInCloud(provider).catch(console.error);
|
||||
}, []);
|
||||
|
||||
const resetCountDown = useCallback(() => {
|
||||
@@ -165,6 +148,6 @@ export const useAuth = () => {
|
||||
isMutating: authStore.isMutating,
|
||||
signUp,
|
||||
signIn,
|
||||
signInWithGoogle,
|
||||
oauthSignIn,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,21 +3,21 @@ import { useLiveData } from '@toeverything/infra/livedata';
|
||||
import { Suspense, useEffect } from 'react';
|
||||
|
||||
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
|
||||
import { useCurrentUser } from '../../../hooks/affine/use-current-user';
|
||||
import { useSession } from '../../../hooks/affine/use-current-user';
|
||||
import { CurrentWorkspaceService } from '../../../modules/workspace/current-workspace';
|
||||
|
||||
const SyncAwarenessInnerLoggedIn = () => {
|
||||
const currentUser = useCurrentUser();
|
||||
const { user } = useSession();
|
||||
const currentWorkspace = useLiveData(
|
||||
useService(CurrentWorkspaceService).currentWorkspace
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentUser && currentWorkspace) {
|
||||
if (user && currentWorkspace) {
|
||||
currentWorkspace.blockSuiteWorkspace.awarenessStore.awareness.setLocalStateField(
|
||||
'user',
|
||||
{
|
||||
name: currentUser.name,
|
||||
name: user.name,
|
||||
// todo: add avatar?
|
||||
}
|
||||
);
|
||||
@@ -30,7 +30,7 @@ const SyncAwarenessInnerLoggedIn = () => {
|
||||
};
|
||||
}
|
||||
return;
|
||||
}, [currentUser, currentWorkspace]);
|
||||
}, [user, currentWorkspace]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
+30
-13
@@ -13,6 +13,7 @@ import {
|
||||
allBlobSizesQuery,
|
||||
removeAvatarMutation,
|
||||
SubscriptionPlan,
|
||||
updateUserProfileMutation,
|
||||
uploadAvatarMutation,
|
||||
} from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
@@ -58,11 +59,10 @@ export const UserAvatar = () => {
|
||||
async (file: File) => {
|
||||
try {
|
||||
const reducedFile = await validateAndReduceImage(file);
|
||||
await avatarTrigger({
|
||||
const data = await avatarTrigger({
|
||||
avatar: reducedFile, // Pass the reducedFile directly to the avatarTrigger
|
||||
});
|
||||
// XXX: This is a hack to force the user to update, since next-auth can not only use update function without params
|
||||
await user.update({ name: user.name });
|
||||
user.update({ avatarUrl: data.uploadAvatar.avatarUrl });
|
||||
pushNotification({
|
||||
title: 'Update user avatar success',
|
||||
type: 'success',
|
||||
@@ -82,8 +82,7 @@ export const UserAvatar = () => {
|
||||
async (e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
await removeAvatarTrigger();
|
||||
// XXX: This is a hack to force the user to update, since next-auth can not only use update function without params
|
||||
user.update({ name: user.name }).catch(console.error);
|
||||
user.update({ avatarUrl: null });
|
||||
},
|
||||
[removeAvatarTrigger, user]
|
||||
);
|
||||
@@ -97,9 +96,9 @@ export const UserAvatar = () => {
|
||||
<Avatar
|
||||
size={56}
|
||||
name={user.name}
|
||||
url={user.image}
|
||||
url={user.avatarUrl}
|
||||
hoverIcon={<CameraIcon />}
|
||||
onRemove={user.image ? handleRemoveUserAvatar : undefined}
|
||||
onRemove={user.avatarUrl ? handleRemoveUserAvatar : undefined}
|
||||
avatarTooltipOptions={{ content: t['Click to replace photo']() }}
|
||||
removeTooltipOptions={{ content: t['Remove photo']() }}
|
||||
data-testid="user-setting-avatar"
|
||||
@@ -115,14 +114,30 @@ export const AvatarAndName = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const user = useCurrentUser();
|
||||
const [input, setInput] = useState<string>(user.name);
|
||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||
|
||||
const { trigger: updateProfile } = useMutation({
|
||||
mutation: updateUserProfileMutation,
|
||||
});
|
||||
const allowUpdate = !!input && input !== user.name;
|
||||
const handleUpdateUserName = useCallback(() => {
|
||||
const handleUpdateUserName = useAsyncCallback(async () => {
|
||||
if (!allowUpdate) {
|
||||
return;
|
||||
}
|
||||
user.update({ name: input }).catch(console.error);
|
||||
}, [allowUpdate, input, user]);
|
||||
|
||||
try {
|
||||
const data = await updateProfile({
|
||||
input: { name: input },
|
||||
});
|
||||
user.update({ name: data.updateProfile.name });
|
||||
} catch (e) {
|
||||
pushNotification({
|
||||
title: 'Failed to update user name.',
|
||||
message: String(e),
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
}, [allowUpdate, input, user, updateProfile, pushNotification]);
|
||||
|
||||
return (
|
||||
<SettingRow
|
||||
@@ -222,9 +237,9 @@ export const AccountSetting: FC = () => {
|
||||
openModal: true,
|
||||
state: 'sendEmail',
|
||||
email: user.email,
|
||||
emailType: 'changeEmail',
|
||||
emailType: user.emailVerified ? 'changeEmail' : 'verifyEmail',
|
||||
});
|
||||
}, [setAuthModal, user.email]);
|
||||
}, [setAuthModal, user.email, user.emailVerified]);
|
||||
|
||||
const onPasswordButtonClick = useCallback(() => {
|
||||
setAuthModal({
|
||||
@@ -249,7 +264,9 @@ export const AccountSetting: FC = () => {
|
||||
<AvatarAndName />
|
||||
<SettingRow name={t['com.affine.settings.email']()} desc={user.email}>
|
||||
<Button onClick={onChangeEmail} className={styles.button}>
|
||||
{t['com.affine.settings.email.action']()}
|
||||
{user.emailVerified
|
||||
? t['com.affine.settings.email.action.change']()
|
||||
: t['com.affine.settings.email.action.verify']()}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
|
||||
+6
-1
@@ -49,7 +49,12 @@ export const UserInfo = ({
|
||||
})}
|
||||
onClick={onAccountSettingClick}
|
||||
>
|
||||
<Avatar size={28} name={user.name} url={user.image} className="avatar" />
|
||||
<Avatar
|
||||
size={28}
|
||||
name={user.name}
|
||||
url={user.avatarUrl}
|
||||
className="avatar"
|
||||
/>
|
||||
|
||||
<div className="content">
|
||||
<div className="name-container">
|
||||
|
||||
@@ -26,7 +26,7 @@ const UserInfo = () => {
|
||||
<Avatar
|
||||
size={28}
|
||||
name={user.name}
|
||||
url={user.image}
|
||||
url={user.avatarUrl}
|
||||
className={styles.avatar}
|
||||
/>
|
||||
|
||||
@@ -51,7 +51,7 @@ export const PublishPageUserAvatar = () => {
|
||||
const location = useLocation();
|
||||
|
||||
const handleSignOut = useAsyncCallback(async () => {
|
||||
await signOutCloud({ callbackUrl: location.pathname });
|
||||
await signOutCloud(location.pathname);
|
||||
}, [location.pathname]);
|
||||
|
||||
const menuItem = useMemo(() => {
|
||||
@@ -84,7 +84,7 @@ export const PublishPageUserAvatar = () => {
|
||||
}}
|
||||
>
|
||||
<div className={styles.iconWrapper} data-testid="share-page-user-avatar">
|
||||
<Avatar size={24} url={user.image} name={user.name} />
|
||||
<Avatar size={24} url={user.avatarUrl} name={user.name} />
|
||||
</div>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
@@ -25,7 +25,7 @@ const SignInButton = () => {
|
||||
<StyledSignInButton
|
||||
data-testid="sign-in-button"
|
||||
onClick={useCallback(() => {
|
||||
signInCloud().catch(console.error);
|
||||
signInCloud('email').catch(console.error);
|
||||
}, [])}
|
||||
>
|
||||
<div className="circle">
|
||||
|
||||
+5
-6
@@ -1,5 +1,6 @@
|
||||
import { Divider } from '@affine/component/ui/divider';
|
||||
import { MenuItem } from '@affine/component/ui/menu';
|
||||
import { useSession } from '@affine/core/hooks/affine/use-current-user';
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Logo1Icon } from '@blocksuite/icons';
|
||||
@@ -7,9 +8,7 @@ import { WorkspaceManager } from '@toeverything/infra';
|
||||
import { useService } from '@toeverything/infra/di';
|
||||
import { useLiveData } from '@toeverything/infra/livedata';
|
||||
import { useSetAtom } from 'jotai';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import {
|
||||
authAtom,
|
||||
@@ -68,9 +67,9 @@ export const UserWithWorkspaceList = ({
|
||||
}: {
|
||||
onEventEnd?: () => void;
|
||||
}) => {
|
||||
const { data: session, status } = useSession();
|
||||
const { user, status } = useSession();
|
||||
|
||||
const isAuthenticated = useMemo(() => status === 'authenticated', [status]);
|
||||
const isAuthenticated = status === 'authenticated';
|
||||
|
||||
const setOpenCreateWorkspaceModal = useSetAtom(openCreateWorkspaceModalAtom);
|
||||
const setDisableCloudOpen = useSetAtom(openDisableCloudAlertModalAtom);
|
||||
@@ -124,7 +123,7 @@ export const UserWithWorkspaceList = ({
|
||||
<div className={styles.workspaceListWrapper}>
|
||||
{isAuthenticated ? (
|
||||
<UserAccountItem
|
||||
email={session?.user.email ?? 'Unknown User'}
|
||||
email={user?.email ?? 'Unknown User'}
|
||||
onEventEnd={onEventEnd}
|
||||
/>
|
||||
) : (
|
||||
|
||||
+2
-4
@@ -1,6 +1,7 @@
|
||||
import { ScrollableContainer } from '@affine/component';
|
||||
import { Divider } from '@affine/component/ui/divider';
|
||||
import { WorkspaceList } from '@affine/component/workspace-list';
|
||||
import { useSession } from '@affine/core/hooks/affine/use-current-user';
|
||||
import {
|
||||
useWorkspaceAvatar,
|
||||
useWorkspaceName,
|
||||
@@ -12,8 +13,6 @@ import { WorkspaceManager, type WorkspaceMetadata } from '@toeverything/infra';
|
||||
import { useService } from '@toeverything/infra/di';
|
||||
import { useLiveData } from '@toeverything/infra/livedata';
|
||||
import { useSetAtom } from 'jotai';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
@@ -119,10 +118,9 @@ export const AFFiNEWorkspaceList = ({
|
||||
|
||||
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
|
||||
// TODO: AFFiNE Cloud support
|
||||
const { status } = useSession();
|
||||
|
||||
const isAuthenticated = useMemo(() => status === 'authenticated', [status]);
|
||||
const isAuthenticated = status === 'authenticated';
|
||||
|
||||
const cloudWorkspaces = useMemo(
|
||||
() =>
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { useSession } from './use-current-user';
|
||||
|
||||
export function useCurrentLoginStatus():
|
||||
| 'authenticated'
|
||||
| 'unauthenticated'
|
||||
| 'loading' {
|
||||
export function useCurrentLoginStatus() {
|
||||
const session = useSession();
|
||||
return session.status;
|
||||
}
|
||||
|
||||
@@ -1,42 +1,83 @@
|
||||
import { type User } from '@affine/component/auth-components';
|
||||
import type { DefaultSession, Session } from 'next-auth';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { getSession, useSession } from 'next-auth/react';
|
||||
import { useEffect, useMemo, useReducer } from 'react';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { getBaseUrl } from '@affine/graphql';
|
||||
import { useMemo, useReducer } from 'react';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { SessionFetchErrorRightAfterLoginOrSignUp } from '../../unexpected-application-state/errors';
|
||||
import { useAsyncCallback } from '../affine-async-hooks';
|
||||
|
||||
export type CheckedUser = User & {
|
||||
const logger = new DebugLogger('auth');
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
hasPassword: boolean;
|
||||
update: ReturnType<typeof useSession>['update'];
|
||||
avatarUrl: string | null;
|
||||
emailVerified: string | null;
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
user?: User | null;
|
||||
status: 'authenticated' | 'unauthenticated' | 'loading';
|
||||
reload: () => Promise<void>;
|
||||
}
|
||||
|
||||
export type CheckedUser = Session['user'] & {
|
||||
update: (changes?: Partial<User>) => void;
|
||||
};
|
||||
|
||||
declare module 'next-auth' {
|
||||
interface Session {
|
||||
user: {
|
||||
name: string;
|
||||
email: string;
|
||||
id: string;
|
||||
hasPassword: boolean;
|
||||
} & Omit<NonNullable<DefaultSession['user']>, 'name' | 'email'>;
|
||||
export async function getSession(
|
||||
url: string = getBaseUrl() + '/api/auth/session'
|
||||
) {
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
|
||||
if (res.ok) {
|
||||
return (await res.json()) as { user?: User | null };
|
||||
}
|
||||
|
||||
logger.error('Failed to fetch session', res.statusText);
|
||||
return { user: null };
|
||||
} catch (e) {
|
||||
logger.error('Failed to fetch session', e);
|
||||
return { user: null };
|
||||
}
|
||||
}
|
||||
|
||||
export function useSession(): Session {
|
||||
const { data, mutate, isLoading } = useSWR('session', () => getSession());
|
||||
|
||||
return {
|
||||
user: data?.user,
|
||||
status: isLoading
|
||||
? 'loading'
|
||||
: data?.user
|
||||
? 'authenticated'
|
||||
: 'unauthenticated',
|
||||
reload: async () => {
|
||||
return mutate().then(e => {
|
||||
console.error(e);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
type UpdateSessionAction =
|
||||
| {
|
||||
type: 'update';
|
||||
payload: Session;
|
||||
payload?: Partial<User>;
|
||||
}
|
||||
| {
|
||||
type: 'fetchError';
|
||||
payload: null;
|
||||
};
|
||||
|
||||
function updateSessionReducer(prevState: Session, action: UpdateSessionAction) {
|
||||
function updateSessionReducer(prevState: User, action: UpdateSessionAction) {
|
||||
const { type, payload } = action;
|
||||
switch (type) {
|
||||
case 'update':
|
||||
return payload;
|
||||
return { ...prevState, ...payload };
|
||||
case 'fetchError':
|
||||
return prevState;
|
||||
}
|
||||
@@ -49,11 +90,11 @@ function updateSessionReducer(prevState: Session, action: UpdateSessionAction) {
|
||||
* If network error or API response error, it will use the cached value.
|
||||
*/
|
||||
export function useCurrentUser(): CheckedUser {
|
||||
const { data, update } = useSession();
|
||||
const session = useSession();
|
||||
|
||||
const [session, dispatcher] = useReducer(
|
||||
const [user, dispatcher] = useReducer(
|
||||
updateSessionReducer,
|
||||
data,
|
||||
session.user,
|
||||
firstSession => {
|
||||
if (!firstSession) {
|
||||
// barely possible.
|
||||
@@ -64,10 +105,10 @@ export function useCurrentUser(): CheckedUser {
|
||||
() => {
|
||||
getSession()
|
||||
.then(session => {
|
||||
if (session) {
|
||||
if (session.user) {
|
||||
dispatcher({
|
||||
type: 'update',
|
||||
payload: session,
|
||||
payload: session.user,
|
||||
});
|
||||
}
|
||||
})
|
||||
@@ -77,35 +118,30 @@ export function useCurrentUser(): CheckedUser {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return firstSession;
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
const update = useAsyncCallback(
|
||||
async (changes?: Partial<User>) => {
|
||||
dispatcher({
|
||||
type: 'update',
|
||||
payload: data,
|
||||
payload: changes,
|
||||
});
|
||||
} else {
|
||||
dispatcher({
|
||||
type: 'fetchError',
|
||||
payload: null,
|
||||
});
|
||||
}
|
||||
}, [data, update]);
|
||||
|
||||
const user = session.user;
|
||||
await session.reload();
|
||||
},
|
||||
[dispatcher, session]
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
image: user.image,
|
||||
hasPassword: user?.hasPassword ?? false,
|
||||
return useMemo(
|
||||
() => ({
|
||||
...user,
|
||||
update,
|
||||
};
|
||||
// spread the user object to make sure the hook will not be re-rendered when user ref changed but the properties not.
|
||||
}, [user.id, user.name, user.email, user.image, user.hasPassword, update]);
|
||||
}),
|
||||
// only list the things will change as deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[user.id, user.avatarUrl, user.name, update]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useSession } from './use-current-user';
|
||||
|
||||
export const useDeleteCollectionInfo = () => {
|
||||
const user = useSession().data?.user;
|
||||
const { user } = useSession();
|
||||
|
||||
return useMemo(
|
||||
() => (user ? { userName: user.name ?? '', userId: user.id } : null),
|
||||
() => (user ? { userName: user.name, userId: user.id } : null),
|
||||
[user]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ServerFeature } from '@affine/graphql';
|
||||
import { serverConfigQuery } from '@affine/graphql';
|
||||
import { oauthProvidersQuery, serverConfigQuery } from '@affine/graphql';
|
||||
import type { BareFetcher, Middleware } from 'swr';
|
||||
|
||||
import { useQueryImmutable } from '../use-query';
|
||||
@@ -44,6 +44,21 @@ export const useServerFeatures = (): ServerFeatureRecord => {
|
||||
}, {} as ServerFeatureRecord);
|
||||
};
|
||||
|
||||
export const useOAuthProviders = () => {
|
||||
const { data, error } = useQueryImmutable(
|
||||
{ query: oauthProvidersQuery },
|
||||
{
|
||||
use: [errorHandler],
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return data.serverConfig.oauthProviders;
|
||||
};
|
||||
|
||||
export const useServerBaseUrl = () => {
|
||||
const config = useServerConfig();
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { NotFoundPage } from '@affine/component/not-found-page';
|
||||
import { useSession } from '@affine/core/hooks/affine/use-current-user';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useSession } from 'next-auth/react';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
@@ -10,7 +9,7 @@ import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||
import { signOutCloud } from '../utils/cloud-utils';
|
||||
|
||||
export const PageNotFound = (): ReactElement => {
|
||||
const { data: session } = useSession();
|
||||
const { user } = useSession();
|
||||
const { jumpToIndex } = useNavigateHelper();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
@@ -25,22 +24,12 @@ export const PageNotFound = (): ReactElement => {
|
||||
|
||||
const onConfirmSignOut = useAsyncCallback(async () => {
|
||||
setOpen(false);
|
||||
await signOutCloud({
|
||||
callbackUrl: '/signIn',
|
||||
});
|
||||
await signOutCloud('/signIn');
|
||||
}, [setOpen]);
|
||||
return (
|
||||
<>
|
||||
<NotFoundPage
|
||||
user={
|
||||
session?.user
|
||||
? {
|
||||
name: session.user.name || '',
|
||||
email: session.user.email || '',
|
||||
avatar: session.user.image || '',
|
||||
}
|
||||
: null
|
||||
}
|
||||
user={user}
|
||||
onBack={handleBackButtonClick}
|
||||
onSignOut={handleOpenSignOutModal}
|
||||
/>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
changeEmailMutation,
|
||||
changePasswordMutation,
|
||||
sendVerifyChangeEmailMutation,
|
||||
verifyEmailMutation,
|
||||
} from '@affine/graphql';
|
||||
import { fetcher } from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
@@ -42,6 +43,7 @@ const authTypeSchema = z.enum([
|
||||
'changeEmail',
|
||||
'confirm-change-email',
|
||||
'subscription-redirect',
|
||||
'verify-email',
|
||||
]);
|
||||
|
||||
export const AuthPage = (): ReactElement | null => {
|
||||
@@ -73,12 +75,10 @@ export const AuthPage = (): ReactElement | null => {
|
||||
// FIXME: There is not notification
|
||||
if (res?.sendVerifyChangeEmail) {
|
||||
pushNotification({
|
||||
title: t['com.affine.auth.sent.change.email.hint'](),
|
||||
title: t['com.affine.auth.sent.verify.email.hint'](),
|
||||
type: 'success',
|
||||
});
|
||||
}
|
||||
|
||||
if (!res?.sendVerifyChangeEmail) {
|
||||
} else {
|
||||
pushNotification({
|
||||
title: t['com.affine.auth.sent.change.email.fail'](),
|
||||
type: 'error',
|
||||
@@ -156,6 +156,9 @@ export const AuthPage = (): ReactElement | null => {
|
||||
case 'subscription-redirect': {
|
||||
return <SubscriptionRedirect />;
|
||||
}
|
||||
case 'verify-email': {
|
||||
return <ConfirmChangeEmail onOpenAffine={onOpenAffine} />;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -171,20 +174,37 @@ export const loader: LoaderFunction = async args => {
|
||||
if (args.params.authType === 'confirm-change-email') {
|
||||
const url = new URL(args.request.url);
|
||||
const searchParams = url.searchParams;
|
||||
const token = searchParams.get('token');
|
||||
const token = searchParams.get('token') ?? '';
|
||||
const email = decodeURIComponent(searchParams.get('email') ?? '');
|
||||
const res = await fetcher({
|
||||
query: changeEmailMutation,
|
||||
variables: {
|
||||
token: token || '',
|
||||
token: token,
|
||||
email: email,
|
||||
},
|
||||
}).catch(console.error);
|
||||
// TODO: Add error handling
|
||||
if (!res?.changeEmail) {
|
||||
return redirect('/expired');
|
||||
}
|
||||
} else if (args.params.authType === 'verify-email') {
|
||||
const url = new URL(args.request.url);
|
||||
const searchParams = url.searchParams;
|
||||
const token = searchParams.get('token') ?? '';
|
||||
const res = await fetcher({
|
||||
query: verifyEmailMutation,
|
||||
variables: {
|
||||
token: token,
|
||||
},
|
||||
}).catch(console.error);
|
||||
|
||||
if (!res?.verifyEmail) {
|
||||
return redirect('/expired');
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
const { jumpToExpired } = useNavigateHelper();
|
||||
|
||||
@@ -1,34 +1,43 @@
|
||||
import { getSession } from 'next-auth/react';
|
||||
import { OAuthProviderType } from '@affine/graphql';
|
||||
import { type LoaderFunction } from 'react-router-dom';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getSession } from '../hooks/affine/use-current-user';
|
||||
import { signInCloud, signOutCloud } from '../utils/cloud-utils';
|
||||
|
||||
const supportedProvider = z.enum(['google']);
|
||||
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 callback_url = searchParams.get('callback_url');
|
||||
if (!callback_url) {
|
||||
const redirectUri =
|
||||
searchParams.get('redirect_uri') ??
|
||||
/* backward compatibility */ searchParams.get('callback_url');
|
||||
|
||||
if (!redirectUri) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const session = await getSession();
|
||||
|
||||
if (session) {
|
||||
if (session.user) {
|
||||
// already signed in, need to sign out first
|
||||
await signOutCloud({
|
||||
callbackUrl: request.url, // retry
|
||||
});
|
||||
await signOutCloud(request.url);
|
||||
}
|
||||
|
||||
const maybeProvider = supportedProvider.safeParse(provider);
|
||||
if (maybeProvider.success) {
|
||||
const provider = maybeProvider.data;
|
||||
await signInCloud(provider, {
|
||||
callbackUrl: callback_url,
|
||||
let provider = maybeProvider.data;
|
||||
// BACKWARD COMPATIBILITY
|
||||
if (provider === 'google') {
|
||||
provider = OAuthProviderType.Google;
|
||||
}
|
||||
await signInCloud(provider, undefined, {
|
||||
redirectUri,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -216,9 +216,7 @@ export const SignOutConfirmModal = () => {
|
||||
|
||||
const onConfirm = useAsyncCallback(async () => {
|
||||
setOpen(false);
|
||||
await signOutCloud({
|
||||
redirect: false,
|
||||
});
|
||||
await signOutCloud();
|
||||
|
||||
// if current workspace is affine cloud, switch to local workspace
|
||||
if (currentWorkspace?.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
import { useSession } from '@affine/core/hooks/affine/use-current-user';
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { affine } from '@affine/electron-api';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CLOUD_WORKSPACE_CHANGED_BROADCAST_CHANNEL_KEY } from '@affine/workspace-impl';
|
||||
import { useAtom, useSetAtom } from 'jotai';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { SessionProvider, useSession } from 'next-auth/react';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import {
|
||||
type PropsWithChildren,
|
||||
startTransition,
|
||||
@@ -13,13 +12,11 @@ import {
|
||||
useRef,
|
||||
} from 'react';
|
||||
|
||||
import { sessionAtom } from '../atoms/cloud-user';
|
||||
import { useOnceSignedInEvents } from '../atoms/event';
|
||||
|
||||
const SessionDefence = (props: PropsWithChildren) => {
|
||||
export const CloudSessionProvider = (props: PropsWithChildren) => {
|
||||
const session = useSession();
|
||||
const prevSession = useRef<ReturnType<typeof useSession>>();
|
||||
const [sessionInAtom, setSession] = useAtom(sessionAtom);
|
||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||
const onceSignedInEvents = useOnceSignedInEvents();
|
||||
const t = useAFFiNEI18N();
|
||||
@@ -32,10 +29,6 @@ const SessionDefence = (props: PropsWithChildren) => {
|
||||
}, [onceSignedInEvents]);
|
||||
|
||||
useEffect(() => {
|
||||
if (sessionInAtom !== session && session.status === 'authenticated') {
|
||||
setSession(session);
|
||||
}
|
||||
|
||||
if (prevSession.current !== session && session.status !== 'loading') {
|
||||
// unauthenticated -> authenticated
|
||||
if (
|
||||
@@ -55,22 +48,7 @@ const SessionDefence = (props: PropsWithChildren) => {
|
||||
}
|
||||
prevSession.current = session;
|
||||
}
|
||||
}, [
|
||||
session,
|
||||
sessionInAtom,
|
||||
prevSession,
|
||||
setSession,
|
||||
pushNotification,
|
||||
refreshAfterSignedInEvents,
|
||||
t,
|
||||
]);
|
||||
}, [session, prevSession, pushNotification, refreshAfterSignedInEvents, t]);
|
||||
|
||||
return props.children;
|
||||
};
|
||||
|
||||
export const CloudSessionProvider = ({ children }: PropsWithChildren) => {
|
||||
return (
|
||||
<SessionProvider refetchOnWindowFocus>
|
||||
<SessionDefence>{children}</SessionDefence>
|
||||
</SessionProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import {
|
||||
generateRandUTF16Chars,
|
||||
getBaseUrl,
|
||||
OAuthProviderType,
|
||||
SPAN_ID_BYTES,
|
||||
TRACE_ID_BYTES,
|
||||
traceReporter,
|
||||
} from '@affine/graphql';
|
||||
import { CLOUD_WORKSPACE_CHANGED_BROADCAST_CHANNEL_KEY } from '@affine/workspace-impl';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { signIn, signOut } from 'next-auth/react';
|
||||
|
||||
type TraceParams = {
|
||||
startTime: string;
|
||||
@@ -43,62 +43,95 @@ function onRejectHandleTrace<T>(
|
||||
return Promise.reject(res);
|
||||
}
|
||||
|
||||
export const signInCloud: typeof signIn = async (provider, ...rest) => {
|
||||
type Providers = 'credentials' | 'email' | OAuthProviderType;
|
||||
|
||||
export const signInCloud = async (
|
||||
provider: Providers,
|
||||
credentials?: { email: string; password?: string },
|
||||
searchParams: Record<string, any> = {}
|
||||
): Promise<Response | undefined> => {
|
||||
const traceParams = genTraceParams();
|
||||
if (environment.isDesktop) {
|
||||
if (provider === 'google') {
|
||||
|
||||
if (provider === 'credentials' || provider === 'email') {
|
||||
if (!credentials) {
|
||||
throw new Error('Invalid Credentials');
|
||||
}
|
||||
|
||||
return signIn(credentials, searchParams)
|
||||
.then(res => onResolveHandleTrace(res, traceParams))
|
||||
.catch(err => onRejectHandleTrace(err, traceParams));
|
||||
} else if (OAuthProviderType[provider]) {
|
||||
if (environment.isDesktop) {
|
||||
open(
|
||||
`${
|
||||
runtimeConfig.serverUrlPrefix
|
||||
}/desktop-signin?provider=google&callback_url=${buildCallbackUrl(
|
||||
}/desktop-signin?provider=${provider}&redirect_uri=${buildRedirectUri(
|
||||
'/open-app/signin-redirect'
|
||||
)}`,
|
||||
'_target'
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
const [options, ...tail] = rest;
|
||||
const callbackUrl =
|
||||
runtimeConfig.serverUrlPrefix +
|
||||
(provider === 'email'
|
||||
? '/open-app/signin-redirect'
|
||||
: location.pathname);
|
||||
return signIn(
|
||||
provider,
|
||||
{
|
||||
...options,
|
||||
callbackUrl: buildCallbackUrl(callbackUrl),
|
||||
},
|
||||
...tail
|
||||
)
|
||||
.then(res => onResolveHandleTrace(res, traceParams))
|
||||
.catch(err => onRejectHandleTrace(err, traceParams));
|
||||
location.href = `${
|
||||
runtimeConfig.serverUrlPrefix
|
||||
}/oauth/login?provider=${provider}&redirect_uri=${encodeURIComponent(
|
||||
searchParams.redirectUri ?? location.pathname
|
||||
)}`;
|
||||
}
|
||||
|
||||
return;
|
||||
} else {
|
||||
return signIn(provider, ...rest)
|
||||
.then(res => onResolveHandleTrace(res, traceParams))
|
||||
.catch(err => onRejectHandleTrace(err, traceParams));
|
||||
throw new Error('Invalid Provider');
|
||||
}
|
||||
};
|
||||
|
||||
export const signOutCloud: typeof signOut = async options => {
|
||||
async function signIn(
|
||||
credential: { email: string; password?: string },
|
||||
searchParams: Record<string, any> = {}
|
||||
) {
|
||||
const url = new URL(getBaseUrl() + '/api/auth/sign-in');
|
||||
|
||||
for (const key in searchParams) {
|
||||
url.searchParams.set(key, searchParams[key]);
|
||||
}
|
||||
|
||||
const redirectUri =
|
||||
runtimeConfig.serverUrlPrefix +
|
||||
(environment.isDesktop
|
||||
? buildRedirectUri('/open-app/signin-redirect')
|
||||
: location.pathname);
|
||||
|
||||
url.searchParams.set('redirect_uri', redirectUri);
|
||||
|
||||
return fetch(url.toString(), {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(credential),
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const signOutCloud = async (redirectUri?: string) => {
|
||||
const traceParams = genTraceParams();
|
||||
return signOut({
|
||||
callbackUrl: '/',
|
||||
...options,
|
||||
})
|
||||
return fetch(getBaseUrl() + '/api/auth/sign-out')
|
||||
.then(result => {
|
||||
if (result) {
|
||||
if (result.ok) {
|
||||
new BroadcastChannel(
|
||||
CLOUD_WORKSPACE_CHANGED_BROADCAST_CHANNEL_KEY
|
||||
).postMessage(1);
|
||||
|
||||
if (redirectUri && location.href !== redirectUri) {
|
||||
setTimeout(() => {
|
||||
location.href = redirectUri;
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
return onResolveHandleTrace(result, traceParams);
|
||||
})
|
||||
.catch(err => onRejectHandleTrace(err, traceParams));
|
||||
};
|
||||
|
||||
export function buildCallbackUrl(callbackUrl: string) {
|
||||
export function buildRedirectUri(callbackUrl: string) {
|
||||
const params: string[][] = [];
|
||||
if (environment.isDesktop && window.appInfo.schema) {
|
||||
params.push(['schema', window.appInfo.schema]);
|
||||
|
||||
@@ -8,7 +8,6 @@ import { logger } from './logger';
|
||||
import {
|
||||
getMainWindow,
|
||||
handleOpenUrlInHiddenWindow,
|
||||
removeCookie,
|
||||
setCookie,
|
||||
} from './main-window';
|
||||
|
||||
@@ -82,28 +81,16 @@ async function handleOauthJwt(url: string) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isSecure = CLOUD_BASE_URL.startsWith('https://');
|
||||
|
||||
// set token to cookie
|
||||
await setCookie({
|
||||
url: CLOUD_BASE_URL,
|
||||
httpOnly: true,
|
||||
value: token,
|
||||
secure: true,
|
||||
name: isSecure
|
||||
? '__Secure-next-auth.session-token'
|
||||
: 'next-auth.session-token',
|
||||
name: 'sid',
|
||||
expirationDate: Math.floor(Date.now() / 1000 + 3600 * 24 * 7),
|
||||
});
|
||||
|
||||
// force reset next-auth.callback-url
|
||||
// there could be incorrect callback-url in cookie that will cause auth failure
|
||||
// so we need to reset it to empty to mitigate this issue
|
||||
await removeCookie(
|
||||
CLOUD_BASE_URL,
|
||||
isSecure ? '__Secure-next-auth.callback-url' : 'next-auth.callback-url'
|
||||
);
|
||||
|
||||
let hiddenWindow: BrowserWindow | null = null;
|
||||
|
||||
ipcMain.once('affine:login', () => {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
mutation changeEmail($token: String!) {
|
||||
changeEmail(token: $token) {
|
||||
mutation changeEmail($token: String!, $email: String!) {
|
||||
changeEmail(token: $token, email: $email) {
|
||||
id
|
||||
name
|
||||
avatarUrl
|
||||
email
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
mutation changePassword($token: String!, $newPassword: String!) {
|
||||
changePassword(token: $token, newPassword: $newPassword) {
|
||||
id
|
||||
name
|
||||
avatarUrl
|
||||
email
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ query earlyAccessUsers {
|
||||
email
|
||||
avatarUrl
|
||||
emailVerified
|
||||
createdAt
|
||||
subscription {
|
||||
plan
|
||||
recurring
|
||||
|
||||
@@ -5,7 +5,6 @@ query getCurrentUser {
|
||||
email
|
||||
emailVerified
|
||||
avatarUrl
|
||||
createdAt
|
||||
token {
|
||||
sessionToken
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
query oauthProviders {
|
||||
serverConfig {
|
||||
oauthProviders
|
||||
}
|
||||
}
|
||||
@@ -101,11 +101,9 @@ export const changeEmailMutation = {
|
||||
definitionName: 'changeEmail',
|
||||
containsFile: false,
|
||||
query: `
|
||||
mutation changeEmail($token: String!) {
|
||||
changeEmail(token: $token) {
|
||||
mutation changeEmail($token: String!, $email: String!) {
|
||||
changeEmail(token: $token, email: $email) {
|
||||
id
|
||||
name
|
||||
avatarUrl
|
||||
email
|
||||
}
|
||||
}`,
|
||||
@@ -120,9 +118,6 @@ export const changePasswordMutation = {
|
||||
mutation changePassword($token: String!, $newPassword: String!) {
|
||||
changePassword(token: $token, newPassword: $newPassword) {
|
||||
id
|
||||
name
|
||||
avatarUrl
|
||||
email
|
||||
}
|
||||
}`,
|
||||
};
|
||||
@@ -212,7 +207,6 @@ query earlyAccessUsers {
|
||||
email
|
||||
avatarUrl
|
||||
emailVerified
|
||||
createdAt
|
||||
subscription {
|
||||
plan
|
||||
recurring
|
||||
@@ -248,7 +242,6 @@ query getCurrentUser {
|
||||
email
|
||||
emailVerified
|
||||
avatarUrl
|
||||
createdAt
|
||||
token {
|
||||
sessionToken
|
||||
}
|
||||
@@ -324,6 +317,19 @@ query getMembersByWorkspaceId($workspaceId: String!, $skip: Int!, $take: Int!) {
|
||||
}`,
|
||||
};
|
||||
|
||||
export const oauthProvidersQuery = {
|
||||
id: 'oauthProvidersQuery' as const,
|
||||
operationName: 'oauthProviders',
|
||||
definitionName: 'serverConfig',
|
||||
containsFile: false,
|
||||
query: `
|
||||
query oauthProviders {
|
||||
serverConfig {
|
||||
oauthProviders
|
||||
}
|
||||
}`,
|
||||
};
|
||||
|
||||
export const getPublicWorkspaceQuery = {
|
||||
id: 'getPublicWorkspaceQuery' as const,
|
||||
operationName: 'getPublicWorkspace',
|
||||
@@ -627,8 +633,8 @@ export const sendChangeEmailMutation = {
|
||||
definitionName: 'sendChangeEmail',
|
||||
containsFile: false,
|
||||
query: `
|
||||
mutation sendChangeEmail($email: String!, $callbackUrl: String!) {
|
||||
sendChangeEmail(email: $email, callbackUrl: $callbackUrl)
|
||||
mutation sendChangeEmail($callbackUrl: String!) {
|
||||
sendChangeEmail(callbackUrl: $callbackUrl)
|
||||
}`,
|
||||
};
|
||||
|
||||
@@ -638,8 +644,8 @@ export const sendChangePasswordEmailMutation = {
|
||||
definitionName: 'sendChangePasswordEmail',
|
||||
containsFile: false,
|
||||
query: `
|
||||
mutation sendChangePasswordEmail($email: String!, $callbackUrl: String!) {
|
||||
sendChangePasswordEmail(email: $email, callbackUrl: $callbackUrl)
|
||||
mutation sendChangePasswordEmail($callbackUrl: String!) {
|
||||
sendChangePasswordEmail(callbackUrl: $callbackUrl)
|
||||
}`,
|
||||
};
|
||||
|
||||
@@ -649,8 +655,8 @@ export const sendSetPasswordEmailMutation = {
|
||||
definitionName: 'sendSetPasswordEmail',
|
||||
containsFile: false,
|
||||
query: `
|
||||
mutation sendSetPasswordEmail($email: String!, $callbackUrl: String!) {
|
||||
sendSetPasswordEmail(email: $email, callbackUrl: $callbackUrl)
|
||||
mutation sendSetPasswordEmail($callbackUrl: String!) {
|
||||
sendSetPasswordEmail(callbackUrl: $callbackUrl)
|
||||
}`,
|
||||
};
|
||||
|
||||
@@ -665,6 +671,17 @@ mutation sendVerifyChangeEmail($token: String!, $email: String!, $callbackUrl: S
|
||||
}`,
|
||||
};
|
||||
|
||||
export const sendVerifyEmailMutation = {
|
||||
id: 'sendVerifyEmailMutation' as const,
|
||||
operationName: 'sendVerifyEmail',
|
||||
definitionName: 'sendVerifyEmail',
|
||||
containsFile: false,
|
||||
query: `
|
||||
mutation sendVerifyEmail($callbackUrl: String!) {
|
||||
sendVerifyEmail(callbackUrl: $callbackUrl)
|
||||
}`,
|
||||
};
|
||||
|
||||
export const serverConfigQuery = {
|
||||
id: 'serverConfigQuery' as const,
|
||||
operationName: 'serverConfig',
|
||||
@@ -695,36 +712,6 @@ mutation setWorkspacePublicById($id: ID!, $public: Boolean!) {
|
||||
}`,
|
||||
};
|
||||
|
||||
export const signInMutation = {
|
||||
id: 'signInMutation' as const,
|
||||
operationName: 'signIn',
|
||||
definitionName: 'signIn',
|
||||
containsFile: false,
|
||||
query: `
|
||||
mutation signIn($email: String!, $password: String!) {
|
||||
signIn(email: $email, password: $password) {
|
||||
token {
|
||||
token
|
||||
}
|
||||
}
|
||||
}`,
|
||||
};
|
||||
|
||||
export const signUpMutation = {
|
||||
id: 'signUpMutation' as const,
|
||||
operationName: 'signUp',
|
||||
definitionName: 'signUp',
|
||||
containsFile: false,
|
||||
query: `
|
||||
mutation signUp($name: String!, $email: String!, $password: String!) {
|
||||
signUp(name: $name, email: $email, password: $password) {
|
||||
token {
|
||||
token
|
||||
}
|
||||
}
|
||||
}`,
|
||||
};
|
||||
|
||||
export const subscriptionQuery = {
|
||||
id: 'subscriptionQuery' as const,
|
||||
operationName: 'subscription',
|
||||
@@ -766,6 +753,20 @@ mutation updateSubscription($recurring: SubscriptionRecurring!, $idempotencyKey:
|
||||
}`,
|
||||
};
|
||||
|
||||
export const updateUserProfileMutation = {
|
||||
id: 'updateUserProfileMutation' as const,
|
||||
operationName: 'updateUserProfile',
|
||||
definitionName: 'updateProfile',
|
||||
containsFile: false,
|
||||
query: `
|
||||
mutation updateUserProfile($input: UpdateUserInput!) {
|
||||
updateProfile(input: $input) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}`,
|
||||
};
|
||||
|
||||
export const uploadAvatarMutation = {
|
||||
id: 'uploadAvatarMutation' as const,
|
||||
operationName: 'uploadAvatar',
|
||||
@@ -782,6 +783,17 @@ mutation uploadAvatar($avatar: Upload!) {
|
||||
}`,
|
||||
};
|
||||
|
||||
export const verifyEmailMutation = {
|
||||
id: 'verifyEmailMutation' as const,
|
||||
operationName: 'verifyEmail',
|
||||
definitionName: 'verifyEmail',
|
||||
containsFile: false,
|
||||
query: `
|
||||
mutation verifyEmail($token: String!) {
|
||||
verifyEmail(token: $token)
|
||||
}`,
|
||||
};
|
||||
|
||||
export const enabledFeaturesQuery = {
|
||||
id: 'enabledFeaturesQuery' as const,
|
||||
operationName: 'enabledFeatures',
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
mutation sendChangeEmail($email: String!, $callbackUrl: String!) {
|
||||
sendChangeEmail(email: $email, callbackUrl: $callbackUrl)
|
||||
mutation sendChangeEmail($callbackUrl: String!) {
|
||||
sendChangeEmail(callbackUrl: $callbackUrl)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
mutation sendChangePasswordEmail($email: String!, $callbackUrl: String!) {
|
||||
sendChangePasswordEmail(email: $email, callbackUrl: $callbackUrl)
|
||||
mutation sendChangePasswordEmail($callbackUrl: String!) {
|
||||
sendChangePasswordEmail(callbackUrl: $callbackUrl)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
mutation sendSetPasswordEmail($email: String!, $callbackUrl: String!) {
|
||||
sendSetPasswordEmail(email: $email, callbackUrl: $callbackUrl)
|
||||
mutation sendSetPasswordEmail($callbackUrl: String!) {
|
||||
sendSetPasswordEmail(callbackUrl: $callbackUrl)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
mutation sendVerifyEmail($callbackUrl: String!) {
|
||||
sendVerifyEmail(callbackUrl: $callbackUrl)
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
mutation signIn($email: String!, $password: String!) {
|
||||
signIn(email: $email, password: $password) {
|
||||
token {
|
||||
token
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
mutation signUp($name: String!, $email: String!, $password: String!) {
|
||||
signUp(name: $name, email: $email, password: $password) {
|
||||
token {
|
||||
token
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
mutation updateUserProfile($input: UpdateUserInput!) {
|
||||
updateProfile(input: $input) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mutation verifyEmail($token: String!) {
|
||||
verifyEmail(token: $token)
|
||||
}
|
||||
@@ -57,6 +57,11 @@ export enum InvoiceStatus {
|
||||
Void = 'Void',
|
||||
}
|
||||
|
||||
export enum OAuthProviderType {
|
||||
GitHub = 'GitHub',
|
||||
Google = 'Google',
|
||||
}
|
||||
|
||||
/** User permission in workspace */
|
||||
export enum Permission {
|
||||
Admin = 'Admin',
|
||||
@@ -77,6 +82,7 @@ export enum ServerDeploymentType {
|
||||
}
|
||||
|
||||
export enum ServerFeature {
|
||||
OAuth = 'OAuth',
|
||||
Payment = 'Payment',
|
||||
}
|
||||
|
||||
@@ -104,6 +110,11 @@ export enum SubscriptionStatus {
|
||||
Unpaid = 'Unpaid',
|
||||
}
|
||||
|
||||
export interface UpdateUserInput {
|
||||
/** User name */
|
||||
name: InputMaybe<Scalars['String']['input']>;
|
||||
}
|
||||
|
||||
export interface UpdateWorkspaceInput {
|
||||
id: Scalars['ID']['input'];
|
||||
/** is Public workspace */
|
||||
@@ -176,17 +187,12 @@ export type CancelSubscriptionMutation = {
|
||||
|
||||
export type ChangeEmailMutationVariables = Exact<{
|
||||
token: Scalars['String']['input'];
|
||||
email: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
export type ChangeEmailMutation = {
|
||||
__typename?: 'Mutation';
|
||||
changeEmail: {
|
||||
__typename?: 'UserType';
|
||||
id: string;
|
||||
name: string;
|
||||
avatarUrl: string | null;
|
||||
email: string;
|
||||
};
|
||||
changeEmail: { __typename?: 'UserType'; id: string; email: string };
|
||||
};
|
||||
|
||||
export type ChangePasswordMutationVariables = Exact<{
|
||||
@@ -196,13 +202,7 @@ export type ChangePasswordMutationVariables = Exact<{
|
||||
|
||||
export type ChangePasswordMutation = {
|
||||
__typename?: 'Mutation';
|
||||
changePassword: {
|
||||
__typename?: 'UserType';
|
||||
id: string;
|
||||
name: string;
|
||||
avatarUrl: string | null;
|
||||
email: string;
|
||||
};
|
||||
changePassword: { __typename?: 'UserType'; id: string };
|
||||
};
|
||||
|
||||
export type CreateCheckoutSessionMutationVariables = Exact<{
|
||||
@@ -270,8 +270,7 @@ export type EarlyAccessUsersQuery = {
|
||||
name: string;
|
||||
email: string;
|
||||
avatarUrl: string | null;
|
||||
emailVerified: string | null;
|
||||
createdAt: string | null;
|
||||
emailVerified: boolean;
|
||||
subscription: {
|
||||
__typename?: 'UserSubscription';
|
||||
plan: SubscriptionPlan;
|
||||
@@ -301,10 +300,9 @@ export type GetCurrentUserQuery = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
emailVerified: string | null;
|
||||
emailVerified: boolean;
|
||||
avatarUrl: string | null;
|
||||
createdAt: string | null;
|
||||
token: { __typename?: 'TokenType'; sessionToken: string | null };
|
||||
token: { __typename?: 'tokenType'; sessionToken: string | null };
|
||||
} | null;
|
||||
};
|
||||
|
||||
@@ -365,11 +363,21 @@ export type GetMembersByWorkspaceIdQuery = {
|
||||
permission: Permission;
|
||||
inviteId: string;
|
||||
accepted: boolean;
|
||||
emailVerified: string | null;
|
||||
emailVerified: boolean | null;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
export type OauthProvidersQueryVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
export type OauthProvidersQuery = {
|
||||
__typename?: 'Query';
|
||||
serverConfig: {
|
||||
__typename?: 'ServerConfigType';
|
||||
oauthProviders: Array<OAuthProviderType>;
|
||||
};
|
||||
};
|
||||
|
||||
export type GetPublicWorkspaceQueryVariables = Exact<{
|
||||
id: Scalars['String']['input'];
|
||||
}>;
|
||||
@@ -386,18 +394,14 @@ export type GetUserQueryVariables = Exact<{
|
||||
export type GetUserQuery = {
|
||||
__typename?: 'Query';
|
||||
user:
|
||||
| {
|
||||
__typename: 'LimitedUserType';
|
||||
email: string;
|
||||
hasPassword: boolean | null;
|
||||
}
|
||||
| { __typename: 'LimitedUserType'; email: string; hasPassword: boolean }
|
||||
| {
|
||||
__typename: 'UserType';
|
||||
id: string;
|
||||
name: string;
|
||||
avatarUrl: string | null;
|
||||
email: string;
|
||||
hasPassword: boolean | null;
|
||||
hasPassword: boolean;
|
||||
}
|
||||
| null;
|
||||
};
|
||||
@@ -628,7 +632,6 @@ export type RevokePublicPageMutation = {
|
||||
};
|
||||
|
||||
export type SendChangeEmailMutationVariables = Exact<{
|
||||
email: Scalars['String']['input'];
|
||||
callbackUrl: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
@@ -638,7 +641,6 @@ export type SendChangeEmailMutation = {
|
||||
};
|
||||
|
||||
export type SendChangePasswordEmailMutationVariables = Exact<{
|
||||
email: Scalars['String']['input'];
|
||||
callbackUrl: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
@@ -648,7 +650,6 @@ export type SendChangePasswordEmailMutation = {
|
||||
};
|
||||
|
||||
export type SendSetPasswordEmailMutationVariables = Exact<{
|
||||
email: Scalars['String']['input'];
|
||||
callbackUrl: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
@@ -668,6 +669,15 @@ export type SendVerifyChangeEmailMutation = {
|
||||
sendVerifyChangeEmail: boolean;
|
||||
};
|
||||
|
||||
export type SendVerifyEmailMutationVariables = Exact<{
|
||||
callbackUrl: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
export type SendVerifyEmailMutation = {
|
||||
__typename?: 'Mutation';
|
||||
sendVerifyEmail: boolean;
|
||||
};
|
||||
|
||||
export type ServerConfigQueryVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
export type ServerConfigQuery = {
|
||||
@@ -692,33 +702,6 @@ export type SetWorkspacePublicByIdMutation = {
|
||||
updateWorkspace: { __typename?: 'WorkspaceType'; id: string };
|
||||
};
|
||||
|
||||
export type SignInMutationVariables = Exact<{
|
||||
email: Scalars['String']['input'];
|
||||
password: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
export type SignInMutation = {
|
||||
__typename?: 'Mutation';
|
||||
signIn: {
|
||||
__typename?: 'UserType';
|
||||
token: { __typename?: 'TokenType'; token: string };
|
||||
};
|
||||
};
|
||||
|
||||
export type SignUpMutationVariables = Exact<{
|
||||
name: Scalars['String']['input'];
|
||||
email: Scalars['String']['input'];
|
||||
password: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
export type SignUpMutation = {
|
||||
__typename?: 'Mutation';
|
||||
signUp: {
|
||||
__typename?: 'UserType';
|
||||
token: { __typename?: 'TokenType'; token: string };
|
||||
};
|
||||
};
|
||||
|
||||
export type SubscriptionQueryVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
export type SubscriptionQuery = {
|
||||
@@ -755,6 +738,15 @@ export type UpdateSubscriptionMutation = {
|
||||
};
|
||||
};
|
||||
|
||||
export type UpdateUserProfileMutationVariables = Exact<{
|
||||
input: UpdateUserInput;
|
||||
}>;
|
||||
|
||||
export type UpdateUserProfileMutation = {
|
||||
__typename?: 'Mutation';
|
||||
updateProfile: { __typename?: 'UserType'; id: string; name: string };
|
||||
};
|
||||
|
||||
export type UploadAvatarMutationVariables = Exact<{
|
||||
avatar: Scalars['Upload']['input'];
|
||||
}>;
|
||||
@@ -770,6 +762,15 @@ export type UploadAvatarMutation = {
|
||||
};
|
||||
};
|
||||
|
||||
export type VerifyEmailMutationVariables = Exact<{
|
||||
token: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
export type VerifyEmailMutation = {
|
||||
__typename?: 'Mutation';
|
||||
verifyEmail: boolean;
|
||||
};
|
||||
|
||||
export type EnabledFeaturesQueryVariables = Exact<{
|
||||
id: Scalars['String']['input'];
|
||||
}>;
|
||||
@@ -938,6 +939,11 @@ export type Queries =
|
||||
variables: GetMembersByWorkspaceIdQueryVariables;
|
||||
response: GetMembersByWorkspaceIdQuery;
|
||||
}
|
||||
| {
|
||||
name: 'oauthProvidersQuery';
|
||||
variables: OauthProvidersQueryVariables;
|
||||
response: OauthProvidersQuery;
|
||||
}
|
||||
| {
|
||||
name: 'getPublicWorkspaceQuery';
|
||||
variables: GetPublicWorkspaceQueryVariables;
|
||||
@@ -1145,31 +1151,36 @@ export type Mutations =
|
||||
variables: SendVerifyChangeEmailMutationVariables;
|
||||
response: SendVerifyChangeEmailMutation;
|
||||
}
|
||||
| {
|
||||
name: 'sendVerifyEmailMutation';
|
||||
variables: SendVerifyEmailMutationVariables;
|
||||
response: SendVerifyEmailMutation;
|
||||
}
|
||||
| {
|
||||
name: 'setWorkspacePublicByIdMutation';
|
||||
variables: SetWorkspacePublicByIdMutationVariables;
|
||||
response: SetWorkspacePublicByIdMutation;
|
||||
}
|
||||
| {
|
||||
name: 'signInMutation';
|
||||
variables: SignInMutationVariables;
|
||||
response: SignInMutation;
|
||||
}
|
||||
| {
|
||||
name: 'signUpMutation';
|
||||
variables: SignUpMutationVariables;
|
||||
response: SignUpMutation;
|
||||
}
|
||||
| {
|
||||
name: 'updateSubscriptionMutation';
|
||||
variables: UpdateSubscriptionMutationVariables;
|
||||
response: UpdateSubscriptionMutation;
|
||||
}
|
||||
| {
|
||||
name: 'updateUserProfileMutation';
|
||||
variables: UpdateUserProfileMutationVariables;
|
||||
response: UpdateUserProfileMutation;
|
||||
}
|
||||
| {
|
||||
name: 'uploadAvatarMutation';
|
||||
variables: UploadAvatarMutationVariables;
|
||||
response: UploadAvatarMutation;
|
||||
}
|
||||
| {
|
||||
name: 'verifyEmailMutation';
|
||||
variables: VerifyEmailMutationVariables;
|
||||
response: VerifyEmailMutation;
|
||||
}
|
||||
| {
|
||||
name: 'setWorkspaceExperimentalFeatureMutation';
|
||||
variables: SetWorkspaceExperimentalFeatureMutationVariables;
|
||||
|
||||
@@ -406,10 +406,12 @@
|
||||
"com.affine.appearanceSettings.windowFrame.description": "Customise appearance of Windows Client.",
|
||||
"com.affine.appearanceSettings.windowFrame.frameless": "Frameless",
|
||||
"com.affine.appearanceSettings.windowFrame.title": "Window frame style",
|
||||
"com.affine.auth.change.email.message": "Your current email is {{email}}. We’ll send a temporary verification link to this email.",
|
||||
"com.affine.auth.verify.email.message": "Your current email is {{email}}. We’ll send a temporary verification link to this email.",
|
||||
"com.affine.auth.change.email.page.subtitle": "Please enter your new email address below. We will send a verification link to this email address to complete the process.",
|
||||
"com.affine.auth.change.email.page.success.subtitle": "Congratulations! You have successfully updated the email address associated with your AFFiNE Cloud account.",
|
||||
"com.affine.auth.change.email.page.success.title": "Email address updated!",
|
||||
"com.affine.auth.verify.email.page.success.title": "Email address verified!",
|
||||
"com.affine.auth.verify.email.page.success.subtitle": "Congratulations! You have successfully verified the email address associated with your AFFiNE Cloud account.",
|
||||
"com.affine.auth.change.email.page.title": "Change email address",
|
||||
"com.affine.auth.create.count": "Create Account",
|
||||
"com.affine.auth.desktop.signing.in": "Signing in...",
|
||||
@@ -430,11 +432,11 @@
|
||||
"com.affine.auth.reset.password.message": "You will receive an email with a link to reset your password. Please check your inbox.",
|
||||
"com.affine.auth.reset.password.page.success": "Password reset successful",
|
||||
"com.affine.auth.reset.password.page.title": "Reset your AFFiNE Cloud password",
|
||||
"com.affine.auth.send.change.email.link": "Send verification link",
|
||||
"com.affine.auth.send.verify.email.hint": "Send verification link",
|
||||
"com.affine.auth.send.reset.password.link": "Send reset link",
|
||||
"com.affine.auth.send.set.password.link": "Send set link",
|
||||
"com.affine.auth.sent": "Sent",
|
||||
"com.affine.auth.sent.change.email.hint": "Verification link has been sent.",
|
||||
"com.affine.auth.sent.verify.email.hint": "Verification link has been sent.",
|
||||
"com.affine.auth.sent.change.email.fail": "The verification link failed to be sent, please try again later.",
|
||||
"com.affine.auth.sent.change.password.hint": "Reset password link has been sent.",
|
||||
"com.affine.auth.sent.reset.password.success.message": "Your password has upgraded! You can sign in AFFiNE Cloud with new password!",
|
||||
@@ -951,7 +953,8 @@
|
||||
"com.affine.settings.auto-check-description": "If enabled, it will automatically check for new versions at regular intervals.",
|
||||
"com.affine.settings.auto-download-description": " If enabled, new versions will be automatically downloaded to the current device.",
|
||||
"com.affine.settings.email": "Email",
|
||||
"com.affine.settings.email.action": "Change Email",
|
||||
"com.affine.settings.email.action.change": "Change Email",
|
||||
"com.affine.settings.email.action.verify": "Verify Email",
|
||||
"com.affine.settings.member-tooltip": "Enable AFFiNE Cloud to collaborate with others",
|
||||
"com.affine.settings.noise-style": "Noise background on the sidebar",
|
||||
"com.affine.settings.noise-style-description": "Use background noise effect on the sidebar.",
|
||||
|
||||
@@ -406,7 +406,7 @@
|
||||
"com.affine.appearanceSettings.windowFrame.description": "Personnalisez l'apparence de l'application Windows",
|
||||
"com.affine.appearanceSettings.windowFrame.frameless": "Sans Bords",
|
||||
"com.affine.appearanceSettings.windowFrame.title": "Style de fenêtre",
|
||||
"com.affine.auth.change.email.message": "Votre email actuel est {{email}}. Nous enverrons un lien de vérification temporaire à cette addresse.",
|
||||
"com.affine.auth.verify.email.message": "Votre email actuel est {{email}}. Nous enverrons un lien de vérification temporaire à cette addresse.",
|
||||
"com.affine.auth.change.email.page.subtitle": "Rentrez votre nouvelle adresse mail en dessous. Nous enverrons un lien de vérification à cette adresse mail pour compléter le processus",
|
||||
"com.affine.auth.change.email.page.success.subtitle": "Félicitation ! Vous avez réussi à mettre à jour votre adresse mail associé avec votre compte AFFiNE cloud ",
|
||||
"com.affine.auth.change.email.page.success.title": "Adresse mail mise à jour !",
|
||||
@@ -430,11 +430,11 @@
|
||||
"com.affine.auth.reset.password.message": "Vous allez recevoir un mail avec un lien pour réinitialiser votre mot de passe. Merci de vérifier votre boite de réception",
|
||||
"com.affine.auth.reset.password.page.success": "Mot de passe réinitialisé avec succès",
|
||||
"com.affine.auth.reset.password.page.title": "Réinitialiser votre mot de passe AFFiNE Cloud",
|
||||
"com.affine.auth.send.change.email.link": "Envoyer un lien de vérification",
|
||||
"com.affine.auth.send.verify.email.hint": "Envoyer un lien de vérification",
|
||||
"com.affine.auth.send.reset.password.link": "Envoyer un lien de réinitialisation",
|
||||
"com.affine.auth.send.set.password.link": "Envoyer un lien pour définir votre mot de passe",
|
||||
"com.affine.auth.sent": "Envoyé",
|
||||
"com.affine.auth.sent.change.email.hint": "Le lien de vérification a été envoyé",
|
||||
"com.affine.auth.sent.verify.email.hint": "Le lien de vérification a été envoyé",
|
||||
"com.affine.auth.sent.change.password.hint": "Le lien de réinitialisation de mot de passe a été envoyé",
|
||||
"com.affine.auth.sent.reset.password.success.message": "Votre mot de passe a été changé ! Vous pouvez à nouveau vous connecter à AFFiNE Cloud avec votre nouveau mot de passe ! ",
|
||||
"com.affine.auth.sent.set.password.hint": "Le lien pour définir votre mot de passe à été envoyé",
|
||||
@@ -788,7 +788,7 @@
|
||||
"com.affine.settings.auto-check-description": "Si activé, l'option cherchera automatiquement pour les nouvelles versions à intervalles réguliers",
|
||||
"com.affine.settings.auto-download-description": "Si activé, les nouvelles versions seront automatiquement téléchargées sur l'appareil actuel",
|
||||
"com.affine.settings.email": "Email",
|
||||
"com.affine.settings.email.action": "Changer l'Email",
|
||||
"com.affine.settings.email.action.change": "Changer l'Email",
|
||||
"com.affine.settings.member-tooltip": "Activer AFFiNE Cloud pour collaborer avec d'autres personnes",
|
||||
"com.affine.settings.noise-style": "Bruit d'arrière-plan de la barre latérale",
|
||||
"com.affine.settings.noise-style-description": "Utiliser l'effet de bruit d'arrière-plan sur la barre latérale",
|
||||
|
||||
@@ -406,7 +406,7 @@
|
||||
"com.affine.appearanceSettings.windowFrame.description": "Windows 클라이언트의 모양을 사용자 정의합니다.",
|
||||
"com.affine.appearanceSettings.windowFrame.frameless": "프레임 없이",
|
||||
"com.affine.appearanceSettings.windowFrame.title": "윈도우 프레임 스타일",
|
||||
"com.affine.auth.change.email.message": "현재 이메일은 {{email}}입니다. 이 이메일 주소로 임시 인증 링크를 보내 드리겠습니다.",
|
||||
"com.affine.auth.verify.email.message": "현재 이메일은 {{email}}입니다. 이 이메일 주소로 임시 인증 링크를 보내 드리겠습니다.",
|
||||
"com.affine.auth.change.email.page.subtitle": "아래에 새 이메일 주소를 입력하세요. 절차를 완료하기 위해 이 이메일 주소로 인증 링크를 보내드립니다.",
|
||||
"com.affine.auth.change.email.page.success.subtitle": "축하합니다! AFFiNE Cloud 계정과 연결된 이메일 주소를 성공적으로 업데이트했습니다.",
|
||||
"com.affine.auth.change.email.page.success.title": "이메일 주소를 업데이트했습니다!",
|
||||
@@ -430,11 +430,11 @@
|
||||
"com.affine.auth.reset.password.message": "비밀번호를 재설정할 수 있는 링크가 포함된 이메일을 받게 됩니다. 받은 편지함을 확인해 주세요.",
|
||||
"com.affine.auth.reset.password.page.success": "비밀번호 재설정 성공",
|
||||
"com.affine.auth.reset.password.page.title": "AFFiNE Cloud 비밀번호 재설정",
|
||||
"com.affine.auth.send.change.email.link": "인증 링크 전송",
|
||||
"com.affine.auth.send.verify.email.hint": "인증 링크 전송",
|
||||
"com.affine.auth.send.reset.password.link": "재설정 링크 전송",
|
||||
"com.affine.auth.send.set.password.link": "설정 링크 전송",
|
||||
"com.affine.auth.sent": "보냄",
|
||||
"com.affine.auth.sent.change.email.hint": "인증 링크를 보냈습니다.",
|
||||
"com.affine.auth.sent.verify.email.hint": "인증 링크를 보냈습니다.",
|
||||
"com.affine.auth.sent.change.password.hint": "비밀번호 재설정 링크를 보냈습니다.",
|
||||
"com.affine.auth.sent.reset.password.success.message": "비밀번호가 업그레이드했습니다! 새 비밀번호로 AFFiNE Cloud에 로그인할 수 있습니다!",
|
||||
"com.affine.auth.sent.set.password.hint": "비밀번호 설정 링크를 보냈습니다.",
|
||||
@@ -908,7 +908,7 @@
|
||||
"com.affine.settings.auto-check-description": "이 기능을 활성화하면, 정기적으로 새 버전을 자동으로 확인합니다.",
|
||||
"com.affine.settings.auto-download-description": "이 기능을 활성화하면, 새 버전이 현재 디바이스에 자동으로 다운로드됩니다.",
|
||||
"com.affine.settings.email": "이메일",
|
||||
"com.affine.settings.email.action": "이메일 변경",
|
||||
"com.affine.settings.email.action.change": "이메일 변경",
|
||||
"com.affine.settings.member-tooltip": "다른 사람들과 협업할 수 있는 AFFiNE Cloud 활성화",
|
||||
"com.affine.settings.noise-style": "Noise background on the sidebar",
|
||||
"com.affine.settings.noise-style-description": "Use background noise effect on the sidebar.",
|
||||
|
||||
@@ -337,11 +337,11 @@
|
||||
"com.affine.auth.reset.password": "Redefinir Senha",
|
||||
"com.affine.auth.reset.password.message": "Você receberá um email com um link para redefinir sua senha. Por favor verifique sua caixa de entrada.",
|
||||
"com.affine.auth.reset.password.page.title": "Redefina sua senha da AFFiNE Cloud",
|
||||
"com.affine.auth.send.change.email.link": "Envie um link de verificação",
|
||||
"com.affine.auth.send.verify.email.hint": "Envie um link de verificação",
|
||||
"com.affine.auth.send.reset.password.link": "Enviar link de redefinição",
|
||||
"com.affine.auth.send.set.password.link": "Enviar link de definição",
|
||||
"com.affine.auth.sent": "Enviado",
|
||||
"com.affine.auth.sent.change.email.hint": "Link de verificação foi enviado.",
|
||||
"com.affine.auth.sent.verify.email.hint": "Link de verificação foi enviado.",
|
||||
"com.affine.auth.sent.change.password.hint": "Link de redefinição de senha foi enviado.",
|
||||
"com.affine.auth.set.email.save": "Salvar Email",
|
||||
"com.affine.auth.set.password.page.title": "Defina sua senha para AFFiNE Cloud",
|
||||
@@ -422,7 +422,7 @@
|
||||
"com.affine.settings.auto-check-description": "Se ativado, ele verificará automaticamente novas versões em intervalos regulares.",
|
||||
"com.affine.settings.auto-download-description": "Se ativado, novas versões serão baixadas automaticamente para o dispositivo atual.",
|
||||
"com.affine.settings.email": "Email",
|
||||
"com.affine.settings.email.action": "Mudar Email",
|
||||
"com.affine.settings.email.action.change": "Mudar Email",
|
||||
"com.affine.settings.password": "Senha",
|
||||
"com.affine.settings.password.action.change": "Mudar senha",
|
||||
"com.affine.settings.profile": "Meu Perfil",
|
||||
|
||||
@@ -318,10 +318,10 @@
|
||||
"com.affine.auth.reset.password": "Восстановить пароль",
|
||||
"com.affine.auth.reset.password.message": "Вы получите письмо со ссылкой для восстановления пароля. Пожалуйста, проверьте свой почтовый ящик.",
|
||||
"com.affine.auth.reset.password.page.title": "Восстановить пароль AFFiNE Cloud",
|
||||
"com.affine.auth.send.change.email.link": "Отправить ссылку для подтверждения",
|
||||
"com.affine.auth.send.verify.email.hint": "Отправить ссылку для подтверждения",
|
||||
"com.affine.auth.send.reset.password.link": "Отправить ссылку для восстановления",
|
||||
"com.affine.auth.sent": "Отправлено",
|
||||
"com.affine.auth.sent.change.email.hint": "Ссылка для подтверждения отправлена.",
|
||||
"com.affine.auth.sent.verify.email.hint": "Ссылка для подтверждения отправлена.",
|
||||
"com.affine.auth.sent.change.password.hint": "Ссылка для восстановления пароля отправлена.",
|
||||
"com.affine.auth.sent.set.password.hint": "Ссылка для установки пароля отправлена.",
|
||||
"com.affine.auth.set.email.save": "Сохранить электронную почту",
|
||||
|
||||
@@ -394,7 +394,7 @@
|
||||
"com.affine.appearanceSettings.windowFrame.description": "自定义 Windows 客户端外观。",
|
||||
"com.affine.appearanceSettings.windowFrame.frameless": "无边框",
|
||||
"com.affine.appearanceSettings.windowFrame.title": "视窗样式",
|
||||
"com.affine.auth.change.email.message": "您当前的邮箱是 {{email}}。我们将向此邮箱发送一个临时的验证链接。",
|
||||
"com.affine.auth.verify.email.message": "您当前的邮箱是 {{email}}。我们将向此邮箱发送一个临时的验证链接。",
|
||||
"com.affine.auth.change.email.page.subtitle": "请在下方输入您的新电子邮件地址。我们将把验证链接发送至该电子邮件地址以完成此过程。",
|
||||
"com.affine.auth.change.email.page.success.subtitle": "恭喜!您已更新了与 AFFiNE Cloud 账户关联的电子邮件地址。",
|
||||
"com.affine.auth.change.email.page.success.title": "邮箱地址已更新!",
|
||||
@@ -418,11 +418,11 @@
|
||||
"com.affine.auth.reset.password.message": "您将收到一封电子邮件,以便重置密码。请在收件箱中查收。",
|
||||
"com.affine.auth.reset.password.page.success": "密码重置成功",
|
||||
"com.affine.auth.reset.password.page.title": "重置您的 AFFiNE Cloud 密码",
|
||||
"com.affine.auth.send.change.email.link": "发送验证链接",
|
||||
"com.affine.auth.send.verify.email.hint": "发送验证链接",
|
||||
"com.affine.auth.send.reset.password.link": "发送重置链接",
|
||||
"com.affine.auth.send.set.password.link": "发送设置链接",
|
||||
"com.affine.auth.sent": "已发送",
|
||||
"com.affine.auth.sent.change.email.hint": "验证链接已发送",
|
||||
"com.affine.auth.sent.verify.email.hint": "验证链接已发送",
|
||||
"com.affine.auth.sent.change.password.hint": "重置密码链接已发送。",
|
||||
"com.affine.auth.sent.reset.password.success.message": "您的密码已更新!您可以使用新密码登录 AFFiNE Cloud!",
|
||||
"com.affine.auth.sent.set.password.hint": "设置密码链接已发送。",
|
||||
@@ -821,7 +821,8 @@
|
||||
"com.affine.settings.auto-check-description": "如果启用,它将定期自动检查新版本。",
|
||||
"com.affine.settings.auto-download-description": "如果启用,新版本将自动下载到当前设备。",
|
||||
"com.affine.settings.email": "电子邮件",
|
||||
"com.affine.settings.email.action": "更改邮箱",
|
||||
"com.affine.settings.email.action.change": "更改邮箱",
|
||||
"com.affine.settings.email.action.verify": "验证邮箱",
|
||||
"com.affine.settings.member-tooltip": "启用 AFFiNE Cloud 以与他人协作",
|
||||
"com.affine.settings.noise-style": "侧边栏的噪点背景",
|
||||
"com.affine.settings.noise-style-description": "在侧边栏使用噪点背景效果。",
|
||||
|
||||
@@ -336,11 +336,11 @@
|
||||
"com.affine.auth.reset.password": "重設密碼",
|
||||
"com.affine.auth.reset.password.message": "您將收到一封電子郵件,其中包含重設密碼的連結。請檢查您的收件箱。",
|
||||
"com.affine.auth.reset.password.page.title": "重設您的 AFFiNE Cloud 密碼",
|
||||
"com.affine.auth.send.change.email.link": "發送驗證連結",
|
||||
"com.affine.auth.send.verify.email.hint": "發送驗證連結",
|
||||
"com.affine.auth.send.reset.password.link": "發送重設連結",
|
||||
"com.affine.auth.send.set.password.link": "發送設定連結",
|
||||
"com.affine.auth.sent": "已發送",
|
||||
"com.affine.auth.sent.change.email.hint": "驗證連結已發送。",
|
||||
"com.affine.auth.sent.verify.email.hint": "驗證連結已發送。",
|
||||
"com.affine.auth.sent.change.password.hint": "重設密碼連結已發送。",
|
||||
"com.affine.auth.sent.set.password.hint": "設定密碼連結已發送。",
|
||||
"com.affine.auth.set.email.save": "保存電子郵件地址",
|
||||
@@ -438,7 +438,8 @@
|
||||
"com.affine.settings.auto-check-description": "若啟用,將定期自動檢測新版本。",
|
||||
"com.affine.settings.auto-download-description": "若啟用,將自動下載新版本。",
|
||||
"com.affine.settings.email": "電子郵件地址",
|
||||
"com.affine.settings.email.action": "更改電子郵件地址",
|
||||
"com.affine.settings.email.action.change": "更改電子郵件地址",
|
||||
"com.affine.settings.email.action.verify": "验证電子郵件地址",
|
||||
"com.affine.settings.member-tooltip": "啟用 AFFiNE Cloud 以與他人協作",
|
||||
"com.affine.settings.noise-style": "側欄背景雜訊效果",
|
||||
"com.affine.settings.noise-style-description": "在側欄背景使用雜訊效果。",
|
||||
|
||||
@@ -88,7 +88,7 @@ switch (platform) {
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported architecture on Android ${arch}`)
|
||||
loadError = new Error(`Unsupported architecture on Android ${arch}`)
|
||||
}
|
||||
break
|
||||
case 'win32':
|
||||
@@ -136,7 +136,7 @@ switch (platform) {
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported architecture on Windows: ${arch}`)
|
||||
loadError = new Error(`Unsupported architecture on Windows: ${arch}`)
|
||||
}
|
||||
break
|
||||
case 'darwin':
|
||||
@@ -177,22 +177,37 @@ switch (platform) {
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported architecture on macOS: ${arch}`)
|
||||
loadError = new Error(`Unsupported architecture on macOS: ${arch}`)
|
||||
}
|
||||
break
|
||||
case 'freebsd':
|
||||
if (arch !== 'x64') {
|
||||
throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
|
||||
}
|
||||
localFileExisted = existsSync(join(__dirname, 'affine.freebsd-x64.node'))
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./affine.freebsd-x64.node')
|
||||
} else {
|
||||
nativeBinding = require('@affine/native-freebsd-x64')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
switch (arch) {
|
||||
case 'x64':
|
||||
localFileExisted = existsSync(join(__dirname, 'affine.freebsd-x64.node'))
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./affine.freebsd-x64.node')
|
||||
} else {
|
||||
nativeBinding = require('@affine/native-freebsd-x64')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'arm64':
|
||||
localFileExisted = existsSync(join(__dirname, 'affine.freebsd-arm64.node'))
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./affine.freebsd-arm64.node')
|
||||
} else {
|
||||
nativeBinding = require('@affine/native-freebsd-arm64')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
default:
|
||||
loadError = new Error(`Unsupported architecture on FreeBSD: ${arch}`)
|
||||
}
|
||||
break
|
||||
case 'linux':
|
||||
@@ -298,25 +313,43 @@ switch (platform) {
|
||||
}
|
||||
}
|
||||
break
|
||||
case 's390x':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'affine.linux-s390x-gnu.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./affine.linux-s390x-gnu.node')
|
||||
} else {
|
||||
nativeBinding = require('@affine/native-linux-s390x-gnu')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported architecture on Linux: ${arch}`)
|
||||
loadError = new Error(`Unsupported architecture on Linux: ${arch}`)
|
||||
}
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
|
||||
loadError = new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
|
||||
}
|
||||
|
||||
if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) {
|
||||
try {
|
||||
nativeBinding = require('./affine.wasi.cjs')
|
||||
} catch {
|
||||
// ignore
|
||||
} catch (err) {
|
||||
if (process.env.NAPI_RS_FORCE_WASI) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
if (!nativeBinding) {
|
||||
try {
|
||||
nativeBinding = require('@affine/native-wasm32-wasi')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
if (process.env.NAPI_RS_FORCE_WASI) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/* eslint-disable simple-import-sort/imports */
|
||||
// Auto generated, do not edit manually
|
||||
import json_0 from './onboarding/W-d9_llZ6rE-qoTiHKTk4.snapshot.json';
|
||||
import json_1 from './onboarding/info.json';
|
||||
import json_2 from './onboarding/blob.json';
|
||||
import json_0 from './onboarding/info.json';
|
||||
import json_1 from './onboarding/blob.json';
|
||||
import json_2 from './onboarding/W-d9_llZ6rE-qoTiHKTk4.snapshot.json';
|
||||
|
||||
export const onboarding = {
|
||||
'W-d9_llZ6rE-qoTiHKTk4.snapshot.json': json_0,
|
||||
'info.json': json_1,
|
||||
'blob.json': json_2
|
||||
'info.json': json_0,
|
||||
'blob.json': json_1,
|
||||
'W-d9_llZ6rE-qoTiHKTk4.snapshot.json': json_2
|
||||
}
|
||||
@@ -21,7 +21,6 @@
|
||||
"is-svg": "^5.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nanoid": "^5.0.6",
|
||||
"next-auth": "^4.24.5",
|
||||
"socket.io-client": "^4.7.4",
|
||||
"y-protocols": "^1.0.6",
|
||||
"yjs": "^13.6.12"
|
||||
|
||||
@@ -2,6 +2,7 @@ import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import {
|
||||
createWorkspaceMutation,
|
||||
deleteWorkspaceMutation,
|
||||
findGraphQLError,
|
||||
getWorkspacesQuery,
|
||||
} from '@affine/graphql';
|
||||
import { fetcher } from '@affine/graphql';
|
||||
@@ -16,7 +17,6 @@ import {
|
||||
import { globalBlockSuiteSchema } from '@toeverything/infra';
|
||||
import { difference } from 'lodash-es';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { getSession } from 'next-auth/react';
|
||||
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
||||
|
||||
import { IndexedDBBlobStorage } from '../local/blob-indexeddb';
|
||||
@@ -27,13 +27,11 @@ import { CLOUD_WORKSPACE_CHANGED_BROADCAST_CHANNEL_KEY } from './consts';
|
||||
import { AffineStaticSyncStorage } from './sync';
|
||||
|
||||
async function getCloudWorkspaceList() {
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const { workspaces } = await fetcher({
|
||||
query: getWorkspacesQuery,
|
||||
}).catch(() => {
|
||||
return { workspaces: [] };
|
||||
});
|
||||
const ids = workspaces.map(({ id }) => id);
|
||||
return ids.map(id => ({
|
||||
@@ -41,10 +39,13 @@ async function getCloudWorkspaceList() {
|
||||
flavour: WorkspaceFlavour.AFFINE_CLOUD,
|
||||
}));
|
||||
} catch (err) {
|
||||
if (err instanceof Array && err[0]?.message === 'Forbidden resource') {
|
||||
console.log(err);
|
||||
const e = findGraphQLError(err, e => e.extensions.code === 401);
|
||||
if (e) {
|
||||
// user not logged in
|
||||
return [];
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user