diff --git a/ui/src/components/HttpErrorContent/index.tsx b/ui/src/components/HttpErrorContent/index.tsx index e1c40d25..abbb976c 100644 --- a/ui/src/components/HttpErrorContent/index.tsx +++ b/ui/src/components/HttpErrorContent/index.tsx @@ -1,30 +1,48 @@ -import { memo } from 'react'; +import { memo, useEffect } from 'react'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { usePageTags } from '@/hooks'; -const Index = ({ httpCode = '' }) => { +const Index = ({ httpCode = '', errMsg = '' }) => { const { t } = useTranslation('translation', { keyPrefix: 'page_error' }); + useEffect(() => { + // auto height of container + const pageWrap = document.querySelector('.page-wrap'); + if (pageWrap) { + // @ts-ignore + pageWrap.style.display = 'contents'; + } + + return () => { + if (pageWrap) { + // @ts-ignore + pageWrap.style.display = 'block'; + } + }; + }, []); usePageTags({ title: t(`http_${httpCode}`, { keyPrefix: 'page_title' }), }); + return ( - <> +
(=‘x‘=)

{t('http_error', { code: httpCode })}

-
{t(`desc_${httpCode}`)}
+
+ {errMsg || t(`desc_${httpCode}`)} +
{t('back_home')}
- +
); }; diff --git a/ui/src/pages/404/index.jsx b/ui/src/pages/404/index.jsx deleted file mode 100644 index 0f088abc..00000000 --- a/ui/src/pages/404/index.jsx +++ /dev/null @@ -1,27 +0,0 @@ -/* eslint-disable import/no-unresolved */ -import { useEffect } from 'react'; -import { Container } from 'react-bootstrap'; - -import { HttpErrorContent } from '@/components'; - -const Index = () => { - useEffect(() => { - // auto height of container - const pageWrap = document.querySelector('.page-wrap'); - pageWrap.style.display = 'contents'; - - return () => { - pageWrap.style.display = 'block'; - }; - }, []); - - return ( - - - - ); -}; - -export default Index; diff --git a/ui/src/pages/404/index.tsx b/ui/src/pages/404/index.tsx new file mode 100644 index 00000000..e6d800ab --- /dev/null +++ b/ui/src/pages/404/index.tsx @@ -0,0 +1,7 @@ +import { HttpErrorContent } from '@/components'; + +const Index = () => { + return ; +}; + +export default Index; diff --git a/ui/src/pages/50X/index.jsx b/ui/src/pages/50X/index.jsx deleted file mode 100644 index fe142ad6..00000000 --- a/ui/src/pages/50X/index.jsx +++ /dev/null @@ -1,27 +0,0 @@ -/* eslint-disable import/no-unresolved */ -import { useEffect } from 'react'; -import { Container } from 'react-bootstrap'; - -import { HttpErrorContent } from '@/components'; - -const Index = () => { - useEffect(() => { - // auto height of container - const pageWrap = document.querySelector('.page-wrap'); - pageWrap.style.display = 'contents'; - - return () => { - pageWrap.style.display = 'block'; - }; - }, []); - - return ( - - - - ); -}; - -export default Index; diff --git a/ui/src/pages/50X/index.tsx b/ui/src/pages/50X/index.tsx new file mode 100644 index 00000000..0dcc49f9 --- /dev/null +++ b/ui/src/pages/50X/index.tsx @@ -0,0 +1,7 @@ +import { HttpErrorContent } from '@/components'; + +const Index = () => { + return ; +}; + +export default Index; diff --git a/ui/src/pages/Layout/index.tsx b/ui/src/pages/Layout/index.tsx index c83dfb84..97a682f2 100644 --- a/ui/src/pages/Layout/index.tsx +++ b/ui/src/pages/Layout/index.tsx @@ -4,7 +4,7 @@ import { HelmetProvider } from 'react-helmet-async'; import { SWRConfig } from 'swr'; -import { toastStore, loginToContinueStore, errorCode } from '@/stores'; +import { toastStore, loginToContinueStore, errorCodeStore } from '@/stores'; import { Header, Footer, @@ -23,8 +23,7 @@ const Layout: FC = () => { const closeToast = () => { toastClear(); }; - const { code: httpStatusCode, reset: httpStatusReset } = errorCode(); - + const { code: httpStatusCode, reset: httpStatusReset } = errorCodeStore(); const imgViewer = useImgViewer(); const { show: showLoginToContinueModal } = loginToContinueStore(); @@ -43,14 +42,9 @@ const Layout: FC = () => { {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
{httpStatusCode ? ( -
- -
+ ) : ( )} diff --git a/ui/src/router/RouteErrorBoundary.tsx b/ui/src/router/RouteErrorBoundary.tsx index d70683e3..2fe5b1fa 100644 --- a/ui/src/router/RouteErrorBoundary.tsx +++ b/ui/src/router/RouteErrorBoundary.tsx @@ -1,8 +1,7 @@ -import Error50X from '@/pages/50X'; -// import Page404 from '@/pages/404'; +import { HttpErrorContent } from '@/components'; -const Index = () => { - return ; +const Index = ({ errCode = '50X', errMsg = '' }) => { + return ; }; export default Index; diff --git a/ui/src/router/RouteGuard.tsx b/ui/src/router/RouteGuard.tsx index 60d1249c..153bca51 100644 --- a/ui/src/router/RouteGuard.tsx +++ b/ui/src/router/RouteGuard.tsx @@ -1,41 +1,53 @@ import { FC, ReactNode, useEffect } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate, useLoaderData } from 'react-router-dom'; import { floppyNavigation } from '@/utils'; import { TGuardFunc } from '@/utils/guard'; -const Index: FC<{ +import RouteErrorBoundary from './RouteErrorBoundary'; + +const RouteGuard: FC<{ children: ReactNode; - onEnter?: TGuardFunc; + onEnter: TGuardFunc; path?: string; -}> = ({ - children, - onEnter, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - path, -}) => { + page?: string; +}> = ({ children, onEnter, path, page }) => { const navigate = useNavigate(); const location = useLocation(); - const callGuards = () => { - if (onEnter) { - const gr = onEnter(); - const redirectUrl = gr.redirect; - if (redirectUrl) { - floppyNavigation.navigate(redirectUrl, () => { - navigate(redirectUrl, { replace: true }); - }); - } + const loaderData = useLoaderData(); + const gr = onEnter({ + loaderData, + path, + page, + }); + + let guardError; + const errCode = gr.error?.code; + if (errCode === '403' || errCode === '404' || errCode === '50X') { + guardError = { + code: errCode, + msg: gr.error?.msg, + }; + } + const handleGuardRedirect = () => { + const redirectUrl = gr.redirect; + if (redirectUrl) { + floppyNavigation.navigate(redirectUrl, () => { + navigate(redirectUrl, { replace: true }); + }); } }; useEffect(() => { - callGuards(); + handleGuardRedirect(); }, [location]); return ( <> - {/* Route Guard */} - {children} + {gr.ok ? children : null} + {!gr.ok && guardError ? ( + + ) : null} ); }; -export default Index; +export default RouteGuard; diff --git a/ui/src/router/index.tsx b/ui/src/router/index.tsx index 2e30ce75..50b6d2ef 100644 --- a/ui/src/router/index.tsx +++ b/ui/src/router/index.tsx @@ -13,7 +13,7 @@ const routeWrapper = (routeNodes: RouteNode[], root: RouteNode[]) => { routeNodes.forEach((rn) => { if (rn.page === 'pages/Layout') { rn.element = rn.guard ? ( - + ) : ( @@ -30,7 +30,7 @@ const routeWrapper = (routeNodes: RouteNode[], root: RouteNode[]) => { rn.element = ( {rn.guard ? ( - + ) : ( diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index 3b5b2e3b..5a350d75 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -2,6 +2,8 @@ import type { IndexRouteObject, NonIndexRouteObject } from 'react-router-dom'; import { guard } from '@/utils'; import type { TGuardFunc } from '@/utils/guard'; +import { editCheck } from '@/services'; +import { isEditable } from '@/utils/guard'; type IndexRouteNode = Omit; type NonIndexRouteNode = Omit; @@ -70,6 +72,13 @@ const routes: RouteNode[] = [ { path: 'posts/:qid/:aid/edit', page: 'pages/Questions/EditAnswer', + loader: async ({ params }) => { + const ret = await editCheck(params.aid as string, true); + return ret; + }, + guard: (args) => { + return isEditable(args); + }, }, { path: '/search', diff --git a/ui/src/services/client/revision.ts b/ui/src/services/client/revision.ts index 06b5ce49..41201cb3 100644 --- a/ui/src/services/client/revision.ts +++ b/ui/src/services/client/revision.ts @@ -1,9 +1,11 @@ import request from '@/utils/request'; import * as Type from '@/common/interface'; -export const editCheck = (id: string) => { +export const editCheck = (id: string, passingError: boolean = false) => { const apiUrl = `/answer/api/v1/revisions/edit/check?id=${id}`; - return request.get(apiUrl); + return request.get(apiUrl, { + passingError, + }); }; export const revisionAudit = (id: string, operation: 'approve' | 'reject') => { diff --git a/ui/src/stores/errorCode.ts b/ui/src/stores/errorCode.ts index 6da8f8d0..aa2bcea1 100644 --- a/ui/src/stores/errorCode.ts +++ b/ui/src/stores/errorCode.ts @@ -2,24 +2,26 @@ import create from 'zustand'; type codeType = '403' | '404' | '50X' | ''; -interface NotFoundType { +interface ErrorCodeType { code: codeType; - update: (code: codeType) => void; + msg: string; + update: (code: codeType, msg?: string) => void; reset: () => void; } -const notFound = create((set) => ({ +const Index = create((set) => ({ code: '', - update: (code: codeType) => { + msg: '', + update: (code: codeType, msg: string = '') => { set(() => { - return { code }; + return { code, msg }; }); }, reset: () => { set(() => { - return { code: '' }; + return { code: '', msg: '' }; }); }, })); -export default notFound; +export default Index; diff --git a/ui/src/stores/index.ts b/ui/src/stores/index.ts index a6c3b143..247f1d64 100644 --- a/ui/src/stores/index.ts +++ b/ui/src/stores/index.ts @@ -10,7 +10,7 @@ import pageTagStore from './pageTags'; import customizeStore from './customize'; import themeSettingStore from './themeSetting'; import loginToContinueStore from './loginToContinue'; -import errorCode from './errorCode'; +import errorCodeStore from './errorCode'; export { toastStore, @@ -24,5 +24,5 @@ export { themeSettingStore, seoSettingStore, loginToContinueStore, - errorCode, + errorCodeStore, }; diff --git a/ui/src/stores/seoSetting.ts b/ui/src/stores/seoSetting.ts index 6c15e9cf..c714a8f5 100644 --- a/ui/src/stores/seoSetting.ts +++ b/ui/src/stores/seoSetting.ts @@ -7,7 +7,7 @@ interface IProps { update: (params: AdminSettingsSeo) => void; } -const siteInfo = create((set) => ({ +const Index = create((set) => ({ seo: { robots: '', permalink: 1, @@ -25,4 +25,4 @@ const siteInfo = create((set) => ({ }), })); -export default siteInfo; +export default Index; diff --git a/ui/src/utils/guard.ts b/ui/src/utils/guard.ts index 7c852986..0b231b1d 100644 --- a/ui/src/utils/guard.ts +++ b/ui/src/utils/guard.ts @@ -30,8 +30,16 @@ type TLoginState = { export type TGuardResult = { ok: boolean; redirect?: string; + error?: { + code?: number | string; + msg?: string; + }; }; -export type TGuardFunc = () => TGuardResult; +export type TGuardFunc = (args: { + loaderData?: any; + path?: string; + page?: string; +}) => TGuardResult; export const deriveLoginState = (): TLoginState => { const ls: TLoginState = { @@ -189,6 +197,19 @@ export const isAdminOrModerator = () => { return gr; }; +export const isEditable = (args) => { + const loaderData = args?.loaderData || {}; + const gr: TGuardResult = { ok: true }; + if (loaderData.code === 400) { + gr.ok = false; + gr.error = { + code: '403', + msg: loaderData.msg, + }; + } + return gr; +}; + export const allowNewRegistration = () => { const gr: TGuardResult = { ok: true }; const loginSetting = loginSettingStore.getState().login; diff --git a/ui/src/utils/request.ts b/ui/src/utils/request.ts index acb96556..8ba18831 100644 --- a/ui/src/utils/request.ts +++ b/ui/src/utils/request.ts @@ -2,7 +2,7 @@ import axios, { AxiosResponse } from 'axios'; import type { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios'; import { Modal } from '@/components'; -import { loggedUserInfoStore, toastStore, errorCode } from '@/stores'; +import { loggedUserInfoStore, toastStore, errorCodeStore } from '@/stores'; import { LOGGED_TOKEN_STORAGE_KEY, IGNORE_PATH_LIST } from '@/common/constants'; import { RouteAlias } from '@/router/alias'; import { getCurrentLang } from '@/utils/localize'; @@ -16,8 +16,11 @@ const baseConfig = { withCredentials: true, }; -interface APIconfig extends AxiosRequestConfig { - allow404: boolean; +interface ApiConfig extends AxiosRequestConfig { + // Configure whether to allow takeover of 404 errors + allow404?: boolean; + // Configure whether to pass errors directly + passingError?: boolean; } class Request { @@ -52,14 +55,17 @@ class Request { return data; }, (error) => { - const { status, data: respData } = error.response || {}; - const { data = {}, msg = '', reason = '' } = respData || {}; - - // console.log('response error:', error); - + const { + status, + data: errModel, + config: errConfig, + } = error.response || {}; + const { data = {}, msg = '' } = errModel || {}; if (status === 400) { - // show error message - if (data instanceof Object && data.err_type) { + if (data.err_type && errConfig?.passingError) { + return errModel; + } + if (data.err_type) { if (data.err_type === 'toast') { // toast error message toastStore.getState().show({ @@ -90,7 +96,6 @@ class Request { return Promise.reject({ code: status, msg, - reason, isError: true, list: data, }); @@ -107,7 +112,7 @@ class Request { // 401: Re-login required if (status === 401) { // clear userinfo - errorCode.getState().reset(); + errorCodeStore.getState().reset(); loggedUserInfoStore.getState().clear(); floppyNavigation.navigateToLogin(); return Promise.reject(false); @@ -140,7 +145,7 @@ class Request { return Promise.reject(false); } if (error.config?.url.includes('/admin/api')) { - errorCode.getState().update('403'); + errorCodeStore.getState().update('403'); return Promise.reject(false); } @@ -157,14 +162,14 @@ class Request { if (isIgnoredPath(IGNORE_PATH_LIST)) { return Promise.reject(false); } - errorCode.getState().update('404'); + errorCodeStore.getState().update('404'); return Promise.reject(false); } if (status >= 500) { if (isIgnoredPath(IGNORE_PATH_LIST)) { return Promise.reject(false); } - errorCode.getState().update('50X'); + errorCodeStore.getState().update('50X'); console.error( `Request failed with status code ${status}, ${msg || ''}`, ); @@ -178,7 +183,7 @@ class Request { return this.instance.request(config); } - public get(url: string, config?: APIconfig): Promise { + public get(url: string, config?: ApiConfig): Promise { return this.instance.get(url, config); }