fix(modqueue): enforce role-gated access with not-allowed view

This commit is contained in:
plebeius
2026-02-16 19:17:38 +08:00
parent 6befc6bacd
commit 9c558d21c1
6 changed files with 99 additions and 15 deletions
+55 -6
View File
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import { Outlet, Route, Routes, useLocation, useParams } from 'react-router-dom';
import { useAccountComment } from '@plebbit/plebbit-react-hooks';
import { Navigate, Outlet, Route, Routes, useLocation, useParams } from 'react-router-dom';
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 { isAllView, isModView, isSubscriptionsView } from './lib/utils/view-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 useTheme from './hooks/use-theme';
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 styles from './app.module.css';
import FAQ from './views/faq';
import Home from './views/home';
import Rules from './views/rules';
import NotFound from './views/not-found';
import NotAllowed from './views/not-allowed';
import PendingPost from './views/pending-post';
import Post from './views/post';
import ModQueueView from './views/mod-queue';
@@ -37,6 +40,8 @@ import SettingsModal from './components/settings-modal';
// to prevent visible loading delays when switching themes
preloadThemeAssets();
const hasModQueueAccessRole = (role?: string): boolean => role === 'admin' || role === 'owner' || role === 'moderator';
const BoardLayout = () => {
const { accountCommentIndex, boardIdentifier } = useParams();
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 = () => (
<div className={styles.app}>
<Routes>
@@ -170,16 +218,16 @@ const App = () => (
<Route path='/mod/catalog/:timeFilterName?' element={null} />
<Route path='/mod/catalog/:timeFilterName?/settings' element={null} />
<Route path='/mod/modqueue' element={<ModQueueView />} />
<Route path='/mod/modqueue/settings' element={<ModQueueView />} />
<Route path='/mod/modqueue' element={<ModQueueRoute />} />
<Route path='/mod/modqueue/settings' element={<ModQueueRoute />} />
<Route path='/:boardIdentifier' element={null} />
<Route path='/:boardIdentifier/settings' element={null} />
<Route path='/:boardIdentifier/catalog' element={null} />
<Route path='/:boardIdentifier/catalog/settings' element={null} />
<Route path='/:boardIdentifier/modqueue' element={<ModQueueView />} />
<Route path='/:boardIdentifier/modqueue/settings' element={<ModQueueView />} />
<Route path='/:boardIdentifier/modqueue' element={<ModQueueRoute />} />
<Route path='/:boardIdentifier/modqueue/settings' element={<ModQueueRoute />} />
<Route path='/:boardIdentifier/thread/:commentCid' 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/settings' element={<PendingPost />} />
</Route>
<Route path='/not-allowed' element={<NotAllowed />} />
<Route path='/not-found' element={<NotFound />} />
<Route path='*' element={<NotFound />} />
</Route>
+1
View File
@@ -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;
+1
View File
@@ -0,0 +1 @@
export { default } from './not-allowed';
+30
View File
@@ -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;
+2 -9
View File
@@ -1,17 +1,10 @@
import { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { useSubplebbitField } from '../../hooks/use-stable-subplebbit';
import { useDirectories } from '../../hooks/use-directories';
import { getSubplebbitAddress } from '../../lib/utils/route-utils';
import { HomeLogo } from '../home';
import NotFoundImage from '../../components/not-found-image';
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 location = useLocation();
@@ -31,7 +24,7 @@ const NotFound = () => {
<div className={styles.boxOuter}>
<div className={styles.boxInner}>
<div className={styles.boxBar}>
<h2>404 Not Found</h2>
<h2>Not Found</h2>
</div>
<div className={styles.boxContent}>
<NotFoundImage />