fix: guard user detail API calls with RBAC permission checks

Hide sync tab and skip sync/promo-groups API calls when moderator
lacks users:sync and promo_groups:read permissions.
This commit is contained in:
Fringg
2026-02-25 04:29:35 +03:00
parent 85c93158fb
commit bc5d832e0d

View File

@@ -20,6 +20,7 @@ import { promoOffersApi } from '../api/promoOffers';
import { ticketsApi } from '../api/tickets'; import { ticketsApi } from '../api/tickets';
import { AdminBackButton } from '../components/admin'; import { AdminBackButton } from '../components/admin';
import { createNumberInputHandler, toNumber } from '../utils/inputHelpers'; import { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
import { usePermissionStore } from '../store/permissions';
// ============ Helpers ============ // ============ Helpers ============
@@ -135,6 +136,7 @@ export default function AdminUserDetail() {
const navigate = useNavigate(); const navigate = useNavigate();
const notify = useNotify(); const notify = useNotify();
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const hasPermission = usePermissionStore((s) => s.hasPermission);
const localeMap: Record<string, string> = { ru: 'ru-RU', en: 'en-US', zh: 'zh-CN', fa: 'fa-IR' }; const localeMap: Record<string, string> = { ru: 'ru-RU', en: 'en-US', zh: 'zh-CN', fa: 'fa-IR' };
const locale = localeMap[i18n.language] || 'ru-RU'; const locale = localeMap[i18n.language] || 'ru-RU';
@@ -384,9 +386,9 @@ export default function AdminUserDetail() {
useEffect(() => { useEffect(() => {
if (activeTab === 'info') { if (activeTab === 'info') {
loadReferrals(); loadReferrals();
loadPromoGroups(); if (hasPermission('promo_groups:read')) loadPromoGroups();
} }
if (activeTab === 'sync') loadSyncStatus(); if (activeTab === 'sync' && hasPermission('users:sync')) loadSyncStatus();
if (activeTab === 'subscription') { if (activeTab === 'subscription') {
loadTariffs(); loadTariffs();
loadSubscriptionData(); loadSubscriptionData();
@@ -400,6 +402,7 @@ export default function AdminUserDetail() {
loadReferrals, loadReferrals,
loadSubscriptionData, loadSubscriptionData,
loadPromoGroups, loadPromoGroups,
hasPermission,
]); ]);
const handleUpdateBalance = async (isAdd: boolean) => { const handleUpdateBalance = async (isAdd: boolean) => {
@@ -831,7 +834,9 @@ export default function AdminUserDetail() {
className="scrollbar-hide -mx-4 mb-6 flex gap-2 overflow-x-auto px-4 py-1" className="scrollbar-hide -mx-4 mb-6 flex gap-2 overflow-x-auto px-4 py-1"
style={{ WebkitOverflowScrolling: 'touch' }} style={{ WebkitOverflowScrolling: 'touch' }}
> >
{(['info', 'subscription', 'balance', 'sync', 'tickets'] as const).map((tab) => ( {(['info', 'subscription', 'balance', 'sync', 'tickets'] as const)
.filter((tab) => tab !== 'sync' || hasPermission('users:sync'))
.map((tab) => (
<button <button
key={tab} key={tab}
onClick={() => setActiveTab(tab)} onClick={() => setActiveTab(tab)}
@@ -930,6 +935,7 @@ export default function AdminUserDetail() {
<div className="rounded-xl bg-dark-800/50 p-3"> <div className="rounded-xl bg-dark-800/50 p-3">
<div className="mb-1 flex items-center justify-between"> <div className="mb-1 flex items-center justify-between">
<span className="text-xs text-dark-500">{t('admin.users.detail.promoGroup')}</span> <span className="text-xs text-dark-500">{t('admin.users.detail.promoGroup')}</span>
{hasPermission('promo_groups:read') && (
<button <button
onClick={() => setEditingPromoGroup(!editingPromoGroup)} onClick={() => setEditingPromoGroup(!editingPromoGroup)}
className="text-xs text-accent-400 transition-colors hover:text-accent-300" className="text-xs text-accent-400 transition-colors hover:text-accent-300"
@@ -938,6 +944,7 @@ export default function AdminUserDetail() {
? t('common.cancel') ? t('common.cancel')
: t('admin.users.detail.changePromoGroup')} : t('admin.users.detail.changePromoGroup')}
</button> </button>
)}
</div> </div>
{editingPromoGroup ? ( {editingPromoGroup ? (
<div className="mt-2 space-y-2"> <div className="mt-2 space-y-2">