mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(modqueue): enforce role-gated access with not-allowed view
This commit is contained in:
+55
-6
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Outlet, Route, Routes, useLocation, useParams } from 'react-router-dom';
|
import { Navigate, Outlet, Route, Routes, useLocation, useParams } from 'react-router-dom';
|
||||||
import { useAccountComment } from '@plebbit/plebbit-react-hooks';
|
import { useAccount, useAccountComment, useSubplebbit } from '@plebbit/plebbit-react-hooks';
|
||||||
|
import useAccountsStore from '@plebbit/plebbit-react-hooks/dist/stores/accounts';
|
||||||
import { initSnow, removeSnow } from './lib/snow';
|
import { initSnow, removeSnow } from './lib/snow';
|
||||||
import { isAllView, isModView, isSubscriptionsView } from './lib/utils/view-utils';
|
import { isAllView, isModView, isSubscriptionsView } from './lib/utils/view-utils';
|
||||||
import { preloadThemeAssets } from './lib/utils/preload-utils';
|
import { preloadThemeAssets } from './lib/utils/preload-utils';
|
||||||
@@ -10,12 +11,14 @@ import useSpecialThemeStore from './stores/use-special-theme-store';
|
|||||||
import useIsMobile from './hooks/use-is-mobile';
|
import useIsMobile from './hooks/use-is-mobile';
|
||||||
import useTheme from './hooks/use-theme';
|
import useTheme from './hooks/use-theme';
|
||||||
import { useDirectories } from './hooks/use-directories';
|
import { useDirectories } from './hooks/use-directories';
|
||||||
|
import { useResolvedSubplebbitAddress } from './hooks/use-resolved-subplebbit-address';
|
||||||
import { getSubplebbitAddress, isPostRoute, isPendingPostRoute, isModQueueRoute } from './lib/utils/route-utils';
|
import { getSubplebbitAddress, isPostRoute, isPendingPostRoute, isModQueueRoute } from './lib/utils/route-utils';
|
||||||
import styles from './app.module.css';
|
import styles from './app.module.css';
|
||||||
import FAQ from './views/faq';
|
import FAQ from './views/faq';
|
||||||
import Home from './views/home';
|
import Home from './views/home';
|
||||||
import Rules from './views/rules';
|
import Rules from './views/rules';
|
||||||
import NotFound from './views/not-found';
|
import NotFound from './views/not-found';
|
||||||
|
import NotAllowed from './views/not-allowed';
|
||||||
import PendingPost from './views/pending-post';
|
import PendingPost from './views/pending-post';
|
||||||
import Post from './views/post';
|
import Post from './views/post';
|
||||||
import ModQueueView from './views/mod-queue';
|
import ModQueueView from './views/mod-queue';
|
||||||
@@ -37,6 +40,8 @@ import SettingsModal from './components/settings-modal';
|
|||||||
// to prevent visible loading delays when switching themes
|
// to prevent visible loading delays when switching themes
|
||||||
preloadThemeAssets();
|
preloadThemeAssets();
|
||||||
|
|
||||||
|
const hasModQueueAccessRole = (role?: string): boolean => role === 'admin' || role === 'owner' || role === 'moderator';
|
||||||
|
|
||||||
const BoardLayout = () => {
|
const BoardLayout = () => {
|
||||||
const { accountCommentIndex, boardIdentifier } = useParams();
|
const { accountCommentIndex, boardIdentifier } = useParams();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -147,6 +152,49 @@ const GlobalLayout = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ModQueueRoute = () => {
|
||||||
|
const { boardIdentifier } = useParams();
|
||||||
|
const account = useAccount();
|
||||||
|
const accountAddress = account?.author?.address;
|
||||||
|
const subplebbitAddress = useResolvedSubplebbitAddress();
|
||||||
|
const subplebbit = useSubplebbit({ subplebbitAddress });
|
||||||
|
|
||||||
|
const accountSubplebbitAddresses = useAccountsStore(
|
||||||
|
(state) => {
|
||||||
|
const activeAccountId = state.activeAccountId;
|
||||||
|
const activeAccount = activeAccountId ? state.accounts[activeAccountId] : undefined;
|
||||||
|
const accountSubplebbits = activeAccount?.subplebbits || {};
|
||||||
|
return Object.keys(accountSubplebbits);
|
||||||
|
},
|
||||||
|
(prev, next) => {
|
||||||
|
if (prev.length !== next.length) return false;
|
||||||
|
return prev.every((val, idx) => val === next[idx]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accountAddress) {
|
||||||
|
return <Navigate to='/not-allowed' replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!boardIdentifier) {
|
||||||
|
return accountSubplebbitAddresses.length > 0 ? <ModQueueView /> : <Navigate to='/not-allowed' replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for board role metadata before enforcing access to avoid false redirects during initial load.
|
||||||
|
const boardState = subplebbit?.state;
|
||||||
|
const isBoardLoading = !subplebbit || !boardState || (boardState !== 'succeeded' && boardState !== 'failed');
|
||||||
|
if (isBoardLoading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountRole = subplebbit?.roles?.[accountAddress]?.role;
|
||||||
|
return hasModQueueAccessRole(accountRole) ? <ModQueueView /> : <Navigate to='/not-allowed' replace />;
|
||||||
|
};
|
||||||
|
|
||||||
const App = () => (
|
const App = () => (
|
||||||
<div className={styles.app}>
|
<div className={styles.app}>
|
||||||
<Routes>
|
<Routes>
|
||||||
@@ -170,16 +218,16 @@ const App = () => (
|
|||||||
<Route path='/mod/catalog/:timeFilterName?' element={null} />
|
<Route path='/mod/catalog/:timeFilterName?' element={null} />
|
||||||
<Route path='/mod/catalog/:timeFilterName?/settings' element={null} />
|
<Route path='/mod/catalog/:timeFilterName?/settings' element={null} />
|
||||||
|
|
||||||
<Route path='/mod/modqueue' element={<ModQueueView />} />
|
<Route path='/mod/modqueue' element={<ModQueueRoute />} />
|
||||||
<Route path='/mod/modqueue/settings' element={<ModQueueView />} />
|
<Route path='/mod/modqueue/settings' element={<ModQueueRoute />} />
|
||||||
|
|
||||||
<Route path='/:boardIdentifier' element={null} />
|
<Route path='/:boardIdentifier' element={null} />
|
||||||
<Route path='/:boardIdentifier/settings' element={null} />
|
<Route path='/:boardIdentifier/settings' element={null} />
|
||||||
<Route path='/:boardIdentifier/catalog' element={null} />
|
<Route path='/:boardIdentifier/catalog' element={null} />
|
||||||
<Route path='/:boardIdentifier/catalog/settings' element={null} />
|
<Route path='/:boardIdentifier/catalog/settings' element={null} />
|
||||||
|
|
||||||
<Route path='/:boardIdentifier/modqueue' element={<ModQueueView />} />
|
<Route path='/:boardIdentifier/modqueue' element={<ModQueueRoute />} />
|
||||||
<Route path='/:boardIdentifier/modqueue/settings' element={<ModQueueView />} />
|
<Route path='/:boardIdentifier/modqueue/settings' element={<ModQueueRoute />} />
|
||||||
|
|
||||||
<Route path='/:boardIdentifier/thread/:commentCid' element={<Post />} />
|
<Route path='/:boardIdentifier/thread/:commentCid' element={<Post />} />
|
||||||
<Route path='/:boardIdentifier/thread/:commentCid/settings' element={<Post />} />
|
<Route path='/:boardIdentifier/thread/:commentCid/settings' element={<Post />} />
|
||||||
@@ -187,6 +235,7 @@ const App = () => (
|
|||||||
<Route path='/pending/:accountCommentIndex' element={<PendingPost />} />
|
<Route path='/pending/:accountCommentIndex' element={<PendingPost />} />
|
||||||
<Route path='/pending/:accountCommentIndex/settings' element={<PendingPost />} />
|
<Route path='/pending/:accountCommentIndex/settings' element={<PendingPost />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path='/not-allowed' element={<NotAllowed />} />
|
||||||
<Route path='/not-found' element={<NotFound />} />
|
<Route path='/not-found' element={<NotFound />} />
|
||||||
<Route path='*' element={<NotFound />} />
|
<Route path='*' element={<NotFound />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './not-found-image';
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { NOT_FOUND_IMAGES } from '../../generated/asset-manifest';
|
||||||
|
|
||||||
|
const NotFoundImage = () => {
|
||||||
|
const [imagePath] = useState(() => NOT_FOUND_IMAGES[Math.floor(Math.random() * NOT_FOUND_IMAGES.length)]);
|
||||||
|
|
||||||
|
return <img src={imagePath} alt='' />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotFoundImage;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './not-allowed';
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { HomeLogo } from '../home';
|
||||||
|
import NotFoundImage from '../../components/not-found-image';
|
||||||
|
import styles from '../not-found/not-found.module.css';
|
||||||
|
|
||||||
|
const NotAllowed = () => {
|
||||||
|
return (
|
||||||
|
<div className={styles.wrapper}>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<HomeLogo />
|
||||||
|
<div className={styles.boxOuter}>
|
||||||
|
<div className={styles.boxInner}>
|
||||||
|
<div className={styles.boxBar}>
|
||||||
|
<h2>Not Allowed</h2>
|
||||||
|
</div>
|
||||||
|
<div className={styles.boxContent}>
|
||||||
|
<NotFoundImage />
|
||||||
|
<br />
|
||||||
|
<div className={styles.backToBoard}>
|
||||||
|
[<Link to='/'>Back to home</Link>]
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotAllowed;
|
||||||
@@ -1,17 +1,10 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { useSubplebbitField } from '../../hooks/use-stable-subplebbit';
|
import { useSubplebbitField } from '../../hooks/use-stable-subplebbit';
|
||||||
import { useDirectories } from '../../hooks/use-directories';
|
import { useDirectories } from '../../hooks/use-directories';
|
||||||
import { getSubplebbitAddress } from '../../lib/utils/route-utils';
|
import { getSubplebbitAddress } from '../../lib/utils/route-utils';
|
||||||
import { HomeLogo } from '../home';
|
import { HomeLogo } from '../home';
|
||||||
|
import NotFoundImage from '../../components/not-found-image';
|
||||||
import styles from './not-found.module.css';
|
import styles from './not-found.module.css';
|
||||||
import { NOT_FOUND_IMAGES } from '../../generated/asset-manifest';
|
|
||||||
|
|
||||||
const NotFoundImage = () => {
|
|
||||||
const [imagePath] = useState(() => NOT_FOUND_IMAGES[Math.floor(Math.random() * NOT_FOUND_IMAGES.length)]);
|
|
||||||
|
|
||||||
return <img src={imagePath} alt='' />;
|
|
||||||
};
|
|
||||||
|
|
||||||
const NotFound = () => {
|
const NotFound = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -31,7 +24,7 @@ const NotFound = () => {
|
|||||||
<div className={styles.boxOuter}>
|
<div className={styles.boxOuter}>
|
||||||
<div className={styles.boxInner}>
|
<div className={styles.boxInner}>
|
||||||
<div className={styles.boxBar}>
|
<div className={styles.boxBar}>
|
||||||
<h2>404 Not Found</h2>
|
<h2>Not Found</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.boxContent}>
|
<div className={styles.boxContent}>
|
||||||
<NotFoundImage />
|
<NotFoundImage />
|
||||||
|
|||||||
Reference in New Issue
Block a user