Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enhance: improve authentication logic #463

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/errorPage/ErrorPage.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const errorMsgs = {
500: '服务器异常,请稍后重试',
};

const ErrorPage = ({ statusCode, errorMsg, error = undefined }) => {
const ErrorPage = ({ statusCode, errorMsg = undefined, error = undefined }) => {
const router = useRouter();
const Icon = R.propOr(icons[500], statusCode)(icons);

Expand Down
5 changes: 3 additions & 2 deletions src/pages/_app.page.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import '@pingcap-inc/tidb-community-site-components/dist/index.css';
import Link from 'next/link';

import { fetcher as newFetcher } from '~/api';
import { withAuthentication } from '~/utils/auth.utils';

dayjs.extend(relativeTime);
// TODO: Need to sync with NextJS locale value
Expand Down Expand Up @@ -100,7 +101,7 @@ const App = ({ Component, pageProps, router }) => {
const { status, statusText, data } = err;
const errorMsg = data?.detail || statusText;

if ([403, 404].includes(status)) {
if ([404].includes(status)) {
setErrorStatus(status);
setErrorMsg(errorMsg);
} else {
Expand Down Expand Up @@ -152,7 +153,7 @@ const App = ({ Component, pageProps, router }) => {
return <ErrorPage statusCode={errorStatus} errorMsg={errorMsg} />;
}

const WrappedComponent = withLayout(Component);
const WrappedComponent = withLayout(withAuthentication(Component));

return (
<SWRConfig
Expand Down
12 changes: 5 additions & 7 deletions src/pages/blog/audits/index.page.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import Link from 'next/link';
import BlogLayout from '../BlogLayout.component';
import * as Styled from './index.styled';
import BlogList from '../_components/BlogList';
import { usePrincipal } from '../blog.hooks';
import { getPageQuery } from '~/utils/pagination.utils';
import useSWR from 'swr';
import { useRouter } from 'next/router';
import { AuthenticateState, blogAuthenticated, useAuthenticatedState } from '~/utils/auth.utils';

const status = 'PENDING';

Expand All @@ -23,9 +23,7 @@ export const getServerSideProps = async (ctx) => {
};

const PageContent = () => {
const { hasAuthority } = usePrincipal();
const hasPermission = hasAuthority('REVIEW_POST');

const authenticatedState = useAuthenticatedState();
const router = useRouter();
const { page, size } = getPageQuery(router.query);
const sort = 'lastModifiedAt,desc';
Expand All @@ -34,12 +32,10 @@ const PageContent = () => {
});

const error = blogsError;
const loading = !blogs || hasPermission === undefined;
const loading = !blogs || authenticatedState === AuthenticateState.LOADING;
if (error) return <ErrorPage error={error} />;
if (loading) return <Skeleton active />;

if (hasPermission === false) <ErrorPage statusCode={403} errorMsg={'您没有 REVIEW_POST 权限,无法查看本页面'} />;

return (
<BlogLayout>
<Styled.Content>
Expand All @@ -64,4 +60,6 @@ const Page = (props) => (
</>
);

Page.useAuthenticated = blogAuthenticated(['REVIEW_POST'], []);

export default Page;
5 changes: 3 additions & 2 deletions src/pages/blog/blog.hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const usePrincipalSsr = () => {
hasRole: () => false,
isAuthor: () => false,
isLogin: false,
isPrincipalValidating: false,
};
};

Expand All @@ -24,7 +25,7 @@ const UNAUTHORIZED = {

const usePrincipalBrowser = () => {
const { meData, isMeValidating } = useContext(MeContext);
const { data: principal } = useSWR([meData?.username], {
const { data: principal, isValidating: isPrincipalValidating } = useSWR([meData?.username], {
fetcher: () => api.blog.common.principal(),
});

Expand Down Expand Up @@ -64,7 +65,7 @@ const usePrincipalBrowser = () => {
}
}, [meData, principal, isMeValidating]);

return { roles, authorities, hasRole, hasAuthority, isAuthor, isLogin, id, loading };
return { roles, authorities, hasRole, hasAuthority, isAuthor, isLogin, id, loading, isPrincipalValidating };
};

export const usePrincipal = typeof window === 'undefined' ? usePrincipalSsr : usePrincipalBrowser;
21 changes: 6 additions & 15 deletions src/pages/member/layout/Layout.component.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,16 @@
import React, { useContext, useEffect } from 'react';
import React from 'react';
import { Col, Row } from 'antd';

import * as Styled from './layout.styled';
import Sidebar from './menu';
import { AuthContext } from '~/context';
import { CoreLayout } from '~/layouts';
import { CommunityHead } from '~/components';
import { useIsSmallScreen } from '~/hooks';
import { useRequestLoggedIn, withAuthentication } from '~/utils/auth.utils';

const Layout = ({ children }) => {
const { setIsAuthRequired, isAnonymous, login } = useContext(AuthContext);

useEffect(() => {
setIsAuthRequired(true);
return () => setIsAuthRequired(false);
}, [setIsAuthRequired]);

const { isSmallScreen } = useIsSmallScreen();

if (isAnonymous) {
login();
return null;
}

return (
<>
<CoreLayout MainWrapper={Styled.Wrapper}>
Expand All @@ -45,4 +33,7 @@ const Layout = ({ children }) => {
);
};

export default Layout;
Layout.displayName = 'MemberLayout';
Layout.useAuthenticated = useRequestLoggedIn;

export default withAuthentication(Layout);
11 changes: 4 additions & 7 deletions src/pages/my/settings/index.page.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import React, { useContext } from 'react';

import Content from './content';
import Layout from '~/pages/my/layout';
import { AuthContext, MeContext } from '~/context';
import { MeContext } from '~/context';
import { CommunityHead } from '~/components';
import { PageLoader } from '~/components';
import { getI18nProps } from '~/utils/i18n.utils';
import { useRequestLoggedIn } from '~/utils/auth.utils';

export const getServerSideProps = async (ctx) => {
const i18nProps = await getI18nProps(['common'])(ctx);
Expand All @@ -18,14 +19,8 @@ export const getServerSideProps = async (ctx) => {
};

const PageContent = ({ title }) => {
const { login, isAnonymous } = useContext(AuthContext);
const { meData } = useContext(MeContext);

if (isAnonymous) {
login();
return null;
}

if (!meData) {
return <PageLoader />;
}
Expand All @@ -48,4 +43,6 @@ const Page = () => {
);
};

Page.useAuthenticated = useRequestLoggedIn;

export default Page;
115 changes: 115 additions & 0 deletions src/utils/auth.utils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { NextPage } from 'next';
import { MeData } from '~/api/me';
import React, { createContext, useContext } from 'react';
import { authContext, MeContext } from '~/context';
import { SWRResponse } from 'swr';
import { ErrorPage } from '~/components';
import { usePrincipal } from '~/pages/blog/blog.hooks';

export type AuthenticateMethod = () => AuthenticateState;
export type NextAuthenticatedPage<P, IP = P> = NextPage<P, IP> & {
useAuthenticated?: AuthenticateMethod;
};

type MeContextType = {
meData: MeData | undefined;
mutateMe: SWRResponse<MeData>['mutate'];
isMeValidating: boolean;
};

type BlogPrincipalType = {
loading: boolean;
id: number;
authorities: string[];
roles: string[];
hasAuthority: (authority: string) => boolean;
hasRole: (role: string) => boolean;
isAuthor: (blog: unknown) => boolean;
isLogin: boolean;
isPrincipalValidating: boolean;
};

export const enum AuthenticateState {
OK = 'ok',
LOADING = 'loading',
UNAUTHORIZED = 'unauthorized',
FORBIDDEN = 'forbidden',
}

const AuthenticateContext = createContext<{
state: AuthenticateState;
}>({
state: AuthenticateState.OK,
});

export function withAuthentication<P = Record<string, unknown>, IP = P>(Page: NextAuthenticatedPage<P, IP>) {
const { useAuthenticated } = Page;
if (!useAuthenticated) {
return Page;
}

const AuthenticatedPage: NextPage<P, IP> = function (props: P) {
const state = useAuthenticated();
return (
<AuthenticateContext.Provider value={{ state }}>
{(() => {
switch (state) {
case AuthenticateState.OK:
case AuthenticateState.LOADING:
return <Page {...props} />;
case AuthenticateState.FORBIDDEN:
return <ErrorPage statusCode={403} errorMsg={undefined} />;
case AuthenticateState.UNAUTHORIZED:
authContext.login();
}
})()}
</AuthenticateContext.Provider>
);
};

AuthenticatedPage.displayName = `Authenticated${Page.displayName ?? 'Page'}`;

return AuthenticatedPage;
}

export const useAuthenticatedState = () => {
return useContext(AuthenticateContext).state;
};

export const useRequestLoggedIn: AuthenticateMethod = () => {
const { isMeValidating, meData } = useContext<MeContextType>(MeContext);
if (isMeValidating) {
return AuthenticateState.LOADING;
}

if (meData) {
return AuthenticateState.OK;
}

return AuthenticateState.UNAUTHORIZED;
};

export const blogAuthenticated = (auth: string[], role: string[]): AuthenticateMethod => {
return function useBlogAuthenticated() {
const state = useRequestLoggedIn();
const { hasRole, hasAuthority, isPrincipalValidating } = usePrincipal() as BlogPrincipalType;
if (state !== AuthenticateState.OK) {
return state;
}
if (isPrincipalValidating) {
return AuthenticateState.LOADING;
}
for (const item of auth) {
if (!hasAuthority(item)) {
return AuthenticateState.FORBIDDEN;
}
}
for (const item of role) {
if (!hasRole(item)) {
return AuthenticateState.FORBIDDEN;
}
}

return AuthenticateState.OK;
};
};