mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(mod-queue): show board button from live roles (#1079)
* fix(mod-queue): show board button from live roles * fix(mod-queue): require live role for board access
This commit is contained in:
+1
-2
@@ -4,6 +4,7 @@ import { useAccount, useAccountComment, useCommunity } from '@bitsocialnet/bitso
|
|||||||
import { initSnow, removeSnow } from './lib/snow';
|
import { initSnow, removeSnow } from './lib/snow';
|
||||||
import { isAllView, isCatalogView, isModView, isSubscriptionsView } from './lib/utils/view-utils';
|
import { isAllView, isCatalogView, isModView, isSubscriptionsView } from './lib/utils/view-utils';
|
||||||
import { preloadReplyModal, preloadThemeAssets } from './lib/utils/preload-utils';
|
import { preloadReplyModal, preloadThemeAssets } from './lib/utils/preload-utils';
|
||||||
|
import { hasModQueueAccessRole } from './lib/utils/mod-access';
|
||||||
import useReplyModalStore from './stores/use-reply-modal-store';
|
import useReplyModalStore from './stores/use-reply-modal-store';
|
||||||
import useCreateBoardModalStore from './stores/use-create-board-modal-store';
|
import useCreateBoardModalStore from './stores/use-create-board-modal-store';
|
||||||
import useSpecialThemeStore from './stores/use-special-theme-store';
|
import useSpecialThemeStore from './stores/use-special-theme-store';
|
||||||
@@ -60,8 +61,6 @@ const SettingsModal = lazy(() => import('./components/settings-modal'));
|
|||||||
preloadThemeAssets();
|
preloadThemeAssets();
|
||||||
preloadReplyModal();
|
preloadReplyModal();
|
||||||
|
|
||||||
const hasModQueueAccessRole = (role?: string): boolean => role === 'admin' || role === 'owner' || role === 'moderator';
|
|
||||||
|
|
||||||
const BoardLayout = () => {
|
const BoardLayout = () => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const { accountCommentIndex, boardIdentifier, pageNumber } = params;
|
const { accountCommentIndex, boardIdentifier, pageNumber } = params;
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { canAccessBoardModQueue, hasModQueueAccessRole } from '../mod-access';
|
||||||
|
|
||||||
|
describe('hasModQueueAccessRole', () => {
|
||||||
|
it('accepts board moderation roles', () => {
|
||||||
|
expect(hasModQueueAccessRole('owner')).toBe(true);
|
||||||
|
expect(hasModQueueAccessRole('admin')).toBe(true);
|
||||||
|
expect(hasModQueueAccessRole('moderator')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects missing and unrelated roles', () => {
|
||||||
|
expect(hasModQueueAccessRole(undefined)).toBe(false);
|
||||||
|
expect(hasModQueueAccessRole('viewer')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('canAccessBoardModQueue', () => {
|
||||||
|
it('allows access to the global queue when at least one moderated board is cached', () => {
|
||||||
|
expect(
|
||||||
|
canAccessBoardModQueue({
|
||||||
|
accountCommunityAddresses: ['music-posting.eth'],
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects access to the global queue when no moderated boards are cached', () => {
|
||||||
|
expect(
|
||||||
|
canAccessBoardModQueue({
|
||||||
|
accountCommunityAddresses: [],
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows access when the current board role is moderator even without cached board membership', () => {
|
||||||
|
expect(
|
||||||
|
canAccessBoardModQueue({
|
||||||
|
boardAddress: '12D3KooWNFgjQWX2EUEs7pixdjkWSLh21EZ9NeYnV8iMaCyYhLGJ',
|
||||||
|
accountCommunityAddresses: [],
|
||||||
|
accountRole: 'moderator',
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects board-scoped access when only the cached moderated board matches by alias', () => {
|
||||||
|
expect(
|
||||||
|
canAccessBoardModQueue({
|
||||||
|
boardAddress: 'music-posting.eth',
|
||||||
|
accountCommunityAddresses: ['music-posting.bso'],
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects access when neither the role nor moderated board list matches', () => {
|
||||||
|
expect(
|
||||||
|
canAccessBoardModQueue({
|
||||||
|
boardAddress: 'music-posting.eth',
|
||||||
|
accountCommunityAddresses: ['tech-posting.eth'],
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export const hasModQueueAccessRole = (role?: string): boolean => role === 'admin' || role === 'owner' || role === 'moderator';
|
||||||
|
|
||||||
|
interface BoardModQueueAccessArgs {
|
||||||
|
boardAddress?: string;
|
||||||
|
accountCommunityAddresses: string[];
|
||||||
|
accountRole?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canAccessBoardModQueue = ({ boardAddress, accountCommunityAddresses, accountRole }: BoardModQueueAccessArgs): boolean => {
|
||||||
|
if (hasModQueueAccessRole(accountRole)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!boardAddress) {
|
||||||
|
return accountCommunityAddresses.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
import React, { useMemo, useState, useEffect, useCallback, memo } from 'react';
|
import React, { useMemo, useState, useEffect, useCallback, memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useParams, Link } from 'react-router-dom';
|
import { useParams, Link } from 'react-router-dom';
|
||||||
import { useFeed, Comment, usePublishCommentModeration, useEditedComment, useCommunity } from '@bitsocialnet/bitsocial-react-hooks';
|
import { useFeed, Comment, usePublishCommentModeration, useEditedComment, useCommunity, useAccount } from '@bitsocialnet/bitsocial-react-hooks';
|
||||||
import { Virtuoso } from 'react-virtuoso';
|
import { Virtuoso } from 'react-virtuoso';
|
||||||
import styles from './mod-queue.module.css';
|
import styles from './mod-queue.module.css';
|
||||||
import useModQueueStore from '../../stores/use-mod-queue-store';
|
import useModQueueStore from '../../stores/use-mod-queue-store';
|
||||||
import LoadingEllipsis from '../../components/loading-ellipsis';
|
import LoadingEllipsis from '../../components/loading-ellipsis';
|
||||||
import ErrorDisplay from '../../components/error-display/error-display';
|
import ErrorDisplay from '../../components/error-display/error-display';
|
||||||
import { useFeedStateString } from '../../hooks/use-state-string';
|
import { useFeedStateString } from '../../hooks/use-state-string';
|
||||||
import { getSubplebbitAddress, getBoardPath, extractDirectoryFromTitle } from '../../lib/utils/route-utils';
|
import { getSubplebbitAddress, getBoardPath, extractDirectoryFromTitle, areSameBoardAddress } from '../../lib/utils/route-utils';
|
||||||
import { useDirectories, DirectoryCommunity } from '../../hooks/use-directories';
|
import { useDirectories, DirectoryCommunity } from '../../hooks/use-directories';
|
||||||
import getShortAddress from '../../lib/get-short-address';
|
import getShortAddress from '../../lib/get-short-address';
|
||||||
import { BOARD_CODE_GROUPS } from '../../constants/board-codes';
|
import { BOARD_CODE_GROUPS } from '../../constants/board-codes';
|
||||||
@@ -28,6 +28,7 @@ import { useAccountCommunityAddresses } from '../../hooks/use-account-community-
|
|||||||
import useIsMobile from '../../hooks/use-is-mobile';
|
import useIsMobile from '../../hooks/use-is-mobile';
|
||||||
import { useCurrentTime } from '../../hooks/use-current-time';
|
import { useCurrentTime } from '../../hooks/use-current-time';
|
||||||
import { Post } from '../post/post';
|
import { Post } from '../post/post';
|
||||||
|
import { canAccessBoardModQueue, hasModQueueAccessRole } from '../../lib/utils/mod-access';
|
||||||
import capitalize from 'lodash/capitalize';
|
import capitalize from 'lodash/capitalize';
|
||||||
import lowerCase from 'lodash/lowerCase';
|
import lowerCase from 'lodash/lowerCase';
|
||||||
import { PageFooterDesktop, PageFooterMobile, StyleOnlyFooterFirstRow } from '../../components/footer';
|
import { PageFooterDesktop, PageFooterMobile, StyleOnlyFooterFirstRow } from '../../components/footer';
|
||||||
@@ -715,6 +716,8 @@ const ModQueueButtonContent = ({ feed, alertThresholdSeconds, boardIdentifier, i
|
|||||||
export const ModQueueButton = ({ boardIdentifier, isMobile }: ModQueueButtonProps) => {
|
export const ModQueueButton = ({ boardIdentifier, isMobile }: ModQueueButtonProps) => {
|
||||||
const { getAlertThresholdSeconds } = useModQueueStore();
|
const { getAlertThresholdSeconds } = useModQueueStore();
|
||||||
|
|
||||||
|
const account = useAccount();
|
||||||
|
const accountAddress = account?.author?.address;
|
||||||
const accountCommunityAddresses = useAccountCommunityAddresses();
|
const accountCommunityAddresses = useAccountCommunityAddresses();
|
||||||
|
|
||||||
const directories = useDirectories();
|
const directories = useDirectories();
|
||||||
@@ -725,6 +728,7 @@ export const ModQueueButton = ({ boardIdentifier, isMobile }: ModQueueButtonProp
|
|||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}, [boardIdentifier, directories]);
|
}, [boardIdentifier, directories]);
|
||||||
|
const community = useCommunity({ communityAddress: resolvedAddress });
|
||||||
|
|
||||||
const communityAddresses = useMemo(() => {
|
const communityAddresses = useMemo(() => {
|
||||||
if (resolvedAddress) {
|
if (resolvedAddress) {
|
||||||
@@ -733,11 +737,25 @@ export const ModQueueButton = ({ boardIdentifier, isMobile }: ModQueueButtonProp
|
|||||||
return accountCommunityAddresses;
|
return accountCommunityAddresses;
|
||||||
}, [resolvedAddress, accountCommunityAddresses]);
|
}, [resolvedAddress, accountCommunityAddresses]);
|
||||||
|
|
||||||
// If specific board, check if user is mod using resolved address
|
const accountRole = accountAddress ? community?.roles?.[accountAddress]?.role : undefined;
|
||||||
const isModOfBoard = resolvedAddress ? accountCommunityAddresses.includes(resolvedAddress) : true;
|
const hasBoardAccessFromAccountCommunities = resolvedAddress
|
||||||
|
? accountCommunityAddresses.some((address) => areSameBoardAddress(address, resolvedAddress))
|
||||||
|
: accountCommunityAddresses.length > 0;
|
||||||
|
const hasBoardAccess = canAccessBoardModQueue({
|
||||||
|
boardAddress: resolvedAddress,
|
||||||
|
accountCommunityAddresses,
|
||||||
|
accountRole,
|
||||||
|
});
|
||||||
|
const isBoardAccessLoading =
|
||||||
|
Boolean(resolvedAddress) &&
|
||||||
|
Boolean(accountAddress) &&
|
||||||
|
!hasModQueueAccessRole(accountRole) &&
|
||||||
|
!hasBoardAccessFromAccountCommunities &&
|
||||||
|
community?.state !== 'succeeded' &&
|
||||||
|
community?.state !== 'failed';
|
||||||
|
|
||||||
// Only fetch if we have addresses to check and permissions
|
// Only fetch if we have addresses to check and permissions
|
||||||
const shouldFetch = communityAddresses.length > 0 && isModOfBoard;
|
const shouldFetch = !isBoardAccessLoading && communityAddresses.length > 0 && hasBoardAccess;
|
||||||
|
|
||||||
const feedAddresses = shouldFetch ? communityAddresses : [];
|
const feedAddresses = shouldFetch ? communityAddresses : [];
|
||||||
const feedOptions = useMemo(
|
const feedOptions = useMemo(
|
||||||
|
|||||||
Reference in New Issue
Block a user