mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00: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:
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user