@@ -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 (
|
||||
<>
|
||||
<div className="d-flex flex-column flex-shrink-1 flex-grow-1 justify-content-center align-items-center">
|
||||
<div
|
||||
className="mb-4 text-secondary"
|
||||
style={{ fontSize: '120px', lineHeight: 1.2 }}>
|
||||
(=‘x‘=)
|
||||
</div>
|
||||
<h4 className="text-center">{t('http_error', { code: httpCode })}</h4>
|
||||
<div className="text-center mb-3 fs-5">{t(`desc_${httpCode}`)}</div>
|
||||
<div className="text-center mb-3 fs-5">
|
||||
{errMsg || t(`desc_${httpCode}`)}
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<Link to="/" className="btn btn-link">
|
||||
{t('back_home')}
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<Container
|
||||
className="d-flex flex-column justify-content-center align-items-center"
|
||||
style={{ flex: 1 }}>
|
||||
<HttpErrorContent httpCode="404" />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { HttpErrorContent } from '@/components';
|
||||
|
||||
const Index = () => {
|
||||
return <HttpErrorContent httpCode="404" />;
|
||||
};
|
||||
|
||||
export default Index;
|
||||
@@ -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 (
|
||||
<Container
|
||||
className="d-flex flex-column justify-content-center align-items-center"
|
||||
style={{ flex: 1 }}>
|
||||
<HttpErrorContent httpCode="50X" />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { HttpErrorContent } from '@/components';
|
||||
|
||||
const Index = () => {
|
||||
return <HttpErrorContent httpCode="50X" />;
|
||||
};
|
||||
|
||||
export default Index;
|
||||
@@ -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 */}
|
||||
<div
|
||||
className="position-relative page-wrap"
|
||||
style={{ display: httpStatusCode ? 'contents' : 'block' }}
|
||||
onClick={imgViewer.checkClickForImgView}>
|
||||
{httpStatusCode ? (
|
||||
<div
|
||||
className="d-flex flex-column justify-content-center align-items-center"
|
||||
style={{ flex: 1 }}>
|
||||
<HttpErrorContent httpCode={httpStatusCode} />
|
||||
</div>
|
||||
<HttpErrorContent httpCode={httpStatusCode} />
|
||||
) : (
|
||||
<Outlet />
|
||||
)}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import Error50X from '@/pages/50X';
|
||||
// import Page404 from '@/pages/404';
|
||||
import { HttpErrorContent } from '@/components';
|
||||
|
||||
const Index = () => {
|
||||
return <Error50X />;
|
||||
const Index = ({ errCode = '50X', errMsg = '' }) => {
|
||||
return <HttpErrorContent httpCode={errCode} errMsg={errMsg} />;
|
||||
};
|
||||
|
||||
export default Index;
|
||||
|
||||
@@ -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 ? (
|
||||
<RouteErrorBoundary errCode={guardError.code} />
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
export default RouteGuard;
|
||||
|
||||
@@ -13,7 +13,7 @@ const routeWrapper = (routeNodes: RouteNode[], root: RouteNode[]) => {
|
||||
routeNodes.forEach((rn) => {
|
||||
if (rn.page === 'pages/Layout') {
|
||||
rn.element = rn.guard ? (
|
||||
<RouteGuard onEnter={rn.guard} path={rn.path}>
|
||||
<RouteGuard onEnter={rn.guard} path={rn.path} page={rn.page}>
|
||||
<Layout />
|
||||
</RouteGuard>
|
||||
) : (
|
||||
@@ -30,7 +30,7 @@ const routeWrapper = (routeNodes: RouteNode[], root: RouteNode[]) => {
|
||||
rn.element = (
|
||||
<Suspense>
|
||||
{rn.guard ? (
|
||||
<RouteGuard onEnter={rn.guard} path={rn.path}>
|
||||
<RouteGuard onEnter={rn.guard} path={rn.path} page={rn.page}>
|
||||
<Ctrl />
|
||||
</RouteGuard>
|
||||
) : (
|
||||
|
||||
@@ -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<IndexRouteObject, 'children'>;
|
||||
type NonIndexRouteNode = Omit<NonIndexRouteObject, 'children'>;
|
||||
@@ -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',
|
||||
|
||||
@@ -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') => {
|
||||
|
||||
@@ -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<NotFoundType>((set) => ({
|
||||
const Index = create<ErrorCodeType>((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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ interface IProps {
|
||||
update: (params: AdminSettingsSeo) => void;
|
||||
}
|
||||
|
||||
const siteInfo = create<IProps>((set) => ({
|
||||
const Index = create<IProps>((set) => ({
|
||||
seo: {
|
||||
robots: '',
|
||||
permalink: 1,
|
||||
@@ -25,4 +25,4 @@ const siteInfo = create<IProps>((set) => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
export default siteInfo;
|
||||
export default Index;
|
||||
|
||||
+22
-1
@@ -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;
|
||||
|
||||
+21
-16
@@ -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<T = any>(url: string, config?: APIconfig): Promise<T> {
|
||||
public get<T = any>(url: string, config?: ApiConfig): Promise<T> {
|
||||
return this.instance.get(url, config);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user