diff --git a/src/components/errorPage/ErrorPage.component.js b/src/components/errorPage/ErrorPage.component.js index f5a073782..56ed05585 100644 --- a/src/components/errorPage/ErrorPage.component.js +++ b/src/components/errorPage/ErrorPage.component.js @@ -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); diff --git a/src/pages/_app.page.js b/src/pages/_app.page.js index f0a1d4c23..5a866293c 100644 --- a/src/pages/_app.page.js +++ b/src/pages/_app.page.js @@ -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 @@ -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 { @@ -152,7 +153,7 @@ const App = ({ Component, pageProps, router }) => { return ; } - const WrappedComponent = withLayout(Component); + const WrappedComponent = withLayout(withAuthentication(Component)); return ( { }; 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'; @@ -34,12 +32,10 @@ const PageContent = () => { }); const error = blogsError; - const loading = !blogs || hasPermission === undefined; + const loading = !blogs || authenticatedState === AuthenticateState.LOADING; if (error) return ; if (loading) return ; - if (hasPermission === false) ; - return ( @@ -64,4 +60,6 @@ const Page = (props) => ( ); +Page.useAuthenticated = blogAuthenticated(['REVIEW_POST'], []); + export default Page; diff --git a/src/pages/blog/blog.hooks.js b/src/pages/blog/blog.hooks.js index 9b8aa5193..e52ebd81c 100644 --- a/src/pages/blog/blog.hooks.js +++ b/src/pages/blog/blog.hooks.js @@ -13,6 +13,7 @@ const usePrincipalSsr = () => { hasRole: () => false, isAuthor: () => false, isLogin: false, + isPrincipalValidating: false, }; }; @@ -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(), }); @@ -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; diff --git a/src/pages/member/layout/Layout.component.js b/src/pages/member/layout/Layout.component.js index cb8079151..311bf20e8 100644 --- a/src/pages/member/layout/Layout.component.js +++ b/src/pages/member/layout/Layout.component.js @@ -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 ( <> @@ -45,4 +33,7 @@ const Layout = ({ children }) => { ); }; -export default Layout; +Layout.displayName = 'MemberLayout'; +Layout.useAuthenticated = useRequestLoggedIn; + +export default withAuthentication(Layout); diff --git a/src/pages/my/settings/index.page.js b/src/pages/my/settings/index.page.js index b386c9ca3..3a0c52d34 100644 --- a/src/pages/my/settings/index.page.js +++ b/src/pages/my/settings/index.page.js @@ -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); @@ -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 ; } @@ -48,4 +43,6 @@ const Page = () => { ); }; +Page.useAuthenticated = useRequestLoggedIn; + export default Page; diff --git a/src/utils/auth.utils.tsx b/src/utils/auth.utils.tsx new file mode 100644 index 000000000..df1d846b7 --- /dev/null +++ b/src/utils/auth.utils.tsx @@ -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 = NextPage & { + useAuthenticated?: AuthenticateMethod; +}; + +type MeContextType = { + meData: MeData | undefined; + mutateMe: SWRResponse['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

, IP = P>(Page: NextAuthenticatedPage) { + const { useAuthenticated } = Page; + if (!useAuthenticated) { + return Page; + } + + const AuthenticatedPage: NextPage = function (props: P) { + const state = useAuthenticated(); + return ( + + {(() => { + switch (state) { + case AuthenticateState.OK: + case AuthenticateState.LOADING: + return ; + case AuthenticateState.FORBIDDEN: + return ; + case AuthenticateState.UNAUTHORIZED: + authContext.login(); + } + })()} + + ); + }; + + AuthenticatedPage.displayName = `Authenticated${Page.displayName ?? 'Page'}`; + + return AuthenticatedPage; +} + +export const useAuthenticatedState = () => { + return useContext(AuthenticateContext).state; +}; + +export const useRequestLoggedIn: AuthenticateMethod = () => { + const { isMeValidating, meData } = useContext(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; + }; +};