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:
liuyi
2024-03-12 10:00:09 +00:00
parent af49e8cc41
commit fb3a0e7b8f
148 changed files with 3407 additions and 2851 deletions
+4 -15
View File
@@ -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}
/>
+26 -6
View File
@@ -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;