feat: add granular user permissions (balance, subscription, promo_group, referral, send_offer)

Split users:edit into fine-grained permissions. Guard UI sections
with hasPermission checks so moderators only see what they can do.
This commit is contained in:
Fringg
2026-02-25 04:42:40 +03:00
parent 80bfaca457
commit 3d6987f761
5 changed files with 176 additions and 138 deletions

View File

@@ -2618,7 +2618,12 @@
"manage": "Manage",
"ban": "Ban",
"unban": "Unban",
"assign": "Assign"
"assign": "Assign",
"promo_group": "Promo group",
"balance": "Balance",
"subscription": "Subscription",
"send_offer": "Send offer",
"referral": "Referral %"
},
"toggleSection": "Toggle all {{section}} permissions",
"selectedPermissions_one": "{{count}} permission selected",

View File

@@ -2407,7 +2407,12 @@
"manage": "مدیریت",
"ban": "مسدود کردن",
"unban": "رفع مسدودیت",
"assign": "اختصاص"
"assign": "اختصاص",
"promo_group": "گروه تبلیغاتی",
"balance": "موجودی",
"subscription": "اشتراک",
"send_offer": "ارسال پیشنهاد",
"referral": "درصد ارجاع"
},
"toggleSection": "تغییر وضعیت تمام مجوزهای {{section}}",
"selectedPermissions_other": "{{count}} مجوز انتخاب شده",

View File

@@ -3166,7 +3166,12 @@
"manage": "Управление",
"ban": "Бан",
"unban": "Разбан",
"assign": "Назначение"
"assign": "Назначение",
"promo_group": "Промо-группа",
"balance": "Баланс",
"subscription": "Подписка",
"send_offer": "Промо-предложение",
"referral": "Реф. процент"
},
"toggleSection": "Переключить все права {{section}}",
"selectedPermissions_one": "{{count}} разрешение выбрано",

View File

@@ -2406,7 +2406,12 @@
"manage": "管理",
"ban": "封禁",
"unban": "解封",
"assign": "分配"
"assign": "分配",
"promo_group": "促销组",
"balance": "余额",
"subscription": "订阅",
"send_offer": "发送优惠",
"referral": "推荐佣金"
},
"toggleSection": "切换所有 {{section}} 权限",
"selectedPermissions_other": "已选择 {{count}} 个权限",

View File

@@ -386,7 +386,7 @@ export default function AdminUserDetail() {
useEffect(() => {
if (activeTab === 'info') {
loadReferrals();
if (hasPermission('promo_groups:read')) loadPromoGroups();
if (hasPermission('users:promo_group')) loadPromoGroups();
}
if (activeTab === 'sync' && hasPermission('users:sync')) loadSyncStatus();
if (activeTab === 'subscription') {
@@ -935,7 +935,7 @@ export default function AdminUserDetail() {
<div className="rounded-xl bg-dark-800/50 p-3">
<div className="mb-1 flex items-center justify-between">
<span className="text-xs text-dark-500">{t('admin.users.detail.promoGroup')}</span>
{hasPermission('promo_groups:read') && (
{hasPermission('users:promo_group') && (
<button
onClick={() => setEditingPromoGroup(!editingPromoGroup)}
className="text-xs text-accent-400 transition-colors hover:text-accent-300"
@@ -989,6 +989,7 @@ export default function AdminUserDetail() {
<span className="text-sm font-medium text-dark-200">
{t('admin.users.detail.referral.title')}
</span>
{hasPermission('users:referral') && (
<button
onClick={() => {
if (!editingReferralCommission) {
@@ -1000,6 +1001,7 @@ export default function AdminUserDetail() {
>
{editingReferralCommission ? t('common.cancel') : t('common.edit')}
</button>
)}
</div>
<div className="grid grid-cols-3 gap-3 text-center">
<div>
@@ -1358,6 +1360,7 @@ export default function AdminUserDetail() {
)}
{/* Actions */}
{hasPermission('users:subscription') && (
<div className="rounded-xl bg-dark-800/50 p-4">
<div className="mb-3 font-medium text-dark-200">
{t('admin.users.detail.subscription.actions')}
@@ -1368,11 +1371,15 @@ export default function AdminUserDetail() {
onChange={(e) => setSubAction(e.target.value)}
className="input"
>
<option value="extend">{t('admin.users.detail.subscription.extend')}</option>
<option value="extend">
{t('admin.users.detail.subscription.extend')}
</option>
<option value="change_tariff">
{t('admin.users.detail.subscription.changeTariff')}
</option>
<option value="cancel">{t('admin.users.detail.subscription.cancel')}</option>
<option value="cancel">
{t('admin.users.detail.subscription.cancel')}
</option>
<option value="activate">
{t('admin.users.detail.subscription.activate')}
</option>
@@ -1422,8 +1429,9 @@ export default function AdminUserDetail() {
</button>
</div>
</div>
)}
</>
) : (
) : hasPermission('users:subscription') ? (
<div className="rounded-xl bg-dark-800/50 p-4">
<div className="mb-4 text-center text-dark-400">
{t('admin.users.detail.subscription.noActive')}
@@ -1463,6 +1471,12 @@ export default function AdminUserDetail() {
</button>
</div>
</div>
) : (
<div className="rounded-xl bg-dark-800/50 p-4">
<div className="text-center text-dark-400">
{t('admin.users.detail.subscription.noActive')}
</div>
</div>
)}
{/* Panel Info */}
@@ -1788,6 +1802,7 @@ export default function AdminUserDetail() {
</div>
{/* Add/subtract form */}
{hasPermission('users:balance') && (
<div className="space-y-3 rounded-xl bg-dark-800/50 p-4">
<input
type="number"
@@ -1821,6 +1836,7 @@ export default function AdminUserDetail() {
</button>
</div>
</div>
)}
{/* Active promo offer */}
{user.promo_offer_discount_percent > 0 && (
@@ -1869,6 +1885,7 @@ export default function AdminUserDetail() {
)}
{/* Send promo offer */}
{hasPermission('users:send_offer') && (
<div className="rounded-xl bg-dark-800/50 p-4">
<div className="mb-3 text-sm font-medium text-dark-200">
{t('admin.users.detail.sendOffer')}
@@ -1901,6 +1918,7 @@ export default function AdminUserDetail() {
</button>
</div>
</div>
)}
{/* Recent transactions */}
{user.recent_transactions.length > 0 && (