mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-29 18:13:47 +00:00
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:
@@ -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,23 +834,25 @@ 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)
|
||||||
<button
|
.filter((tab) => tab !== 'sync' || hasPermission('users:sync'))
|
||||||
key={tab}
|
.map((tab) => (
|
||||||
onClick={() => setActiveTab(tab)}
|
<button
|
||||||
className={`shrink-0 whitespace-nowrap rounded-xl px-4 py-2.5 text-sm font-medium transition-all ${
|
key={tab}
|
||||||
activeTab === tab
|
onClick={() => setActiveTab(tab)}
|
||||||
? 'bg-accent-500/15 text-accent-400 ring-1 ring-accent-500/30'
|
className={`shrink-0 whitespace-nowrap rounded-xl px-4 py-2.5 text-sm font-medium transition-all ${
|
||||||
: 'bg-dark-800/50 text-dark-400 active:bg-dark-700'
|
activeTab === tab
|
||||||
}`}
|
? 'bg-accent-500/15 text-accent-400 ring-1 ring-accent-500/30'
|
||||||
>
|
: 'bg-dark-800/50 text-dark-400 active:bg-dark-700'
|
||||||
{tab === 'info' && t('admin.users.detail.tabs.info')}
|
}`}
|
||||||
{tab === 'subscription' && t('admin.users.detail.tabs.subscription')}
|
>
|
||||||
{tab === 'balance' && t('admin.users.detail.tabs.balance')}
|
{tab === 'info' && t('admin.users.detail.tabs.info')}
|
||||||
{tab === 'sync' && t('admin.users.detail.tabs.sync')}
|
{tab === 'subscription' && t('admin.users.detail.tabs.subscription')}
|
||||||
{tab === 'tickets' && t('admin.users.detail.tabs.tickets')}
|
{tab === 'balance' && t('admin.users.detail.tabs.balance')}
|
||||||
</button>
|
{tab === 'sync' && t('admin.users.detail.tabs.sync')}
|
||||||
))}
|
{tab === 'tickets' && t('admin.users.detail.tabs.tickets')}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
@@ -930,14 +935,16 @@ 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>
|
||||||
<button
|
{hasPermission('promo_groups:read') && (
|
||||||
onClick={() => setEditingPromoGroup(!editingPromoGroup)}
|
<button
|
||||||
className="text-xs text-accent-400 transition-colors hover:text-accent-300"
|
onClick={() => setEditingPromoGroup(!editingPromoGroup)}
|
||||||
>
|
className="text-xs text-accent-400 transition-colors hover:text-accent-300"
|
||||||
{editingPromoGroup
|
>
|
||||||
? t('common.cancel')
|
{editingPromoGroup
|
||||||
: t('admin.users.detail.changePromoGroup')}
|
? t('common.cancel')
|
||||||
</button>
|
: t('admin.users.detail.changePromoGroup')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{editingPromoGroup ? (
|
{editingPromoGroup ? (
|
||||||
<div className="mt-2 space-y-2">
|
<div className="mt-2 space-y-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user