mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-31 08:25:35 +00:00
feat(admin): let admins restrict which public groups a user may access
Public groups have always been bindable by every user: CanBindGroup returned true for any non-exclusive group, and user_allowed_groups only ever carried the exclusive groups an admin had granted. Admins had no way to hand a single user a subset of the public groups short of converting a group to exclusive, which changes it for everyone already using it. A user now carries restrict_public_groups. While it is false, which is the default and what every existing row migrates to, nothing changes: every public group stays bindable. Once an admin turns it on for a user, that user's public groups are narrowed to the ones listed in user_allowed_groups, the same table that already gates exclusive groups. The flag is an administrative control, so it rides on the admin user DTO only and leaves the shape of the end-user endpoints alone. The model plaza filter honours the flag as well, so a restricted user is not shown groups they would be refused when binding a key. Anonymous visitors have no user record and keep the previous view. Enforcement rides on the existing CanBindGroup choke point, so it covers key creation, key updates, and per-request authorization together. An API key bound to a group that is later withdrawn stops working at request time rather than lingering as a key that can be listed but not used. The admin dialog gains a toggle over the public group list. Turning it off re-checks every public group, so an admin cannot save a list that reads as restrictive while the restriction itself is disabled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
efb46db0a9
commit
b56c61eccd
@@ -95,11 +95,23 @@
|
||||
|
||||
<!-- 公开分组区域 -->
|
||||
<div v-if="publicGroups.length > 0">
|
||||
<div class="mb-3 flex items-center gap-2">
|
||||
<div class="mb-3 flex flex-wrap items-center gap-2">
|
||||
<div class="h-1.5 w-1.5 rounded-full bg-green-500"></div>
|
||||
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300">{{ t('admin.users.publicGroups') }}</h4>
|
||||
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||
{{ restrictPublicGroups ? t('admin.users.publicGroupsRestricted') : t('admin.users.publicGroups') }}
|
||||
</h4>
|
||||
<span class="text-xs text-gray-400">({{ publicGroupConfigs.length }})</span>
|
||||
<label class="ml-auto flex cursor-pointer items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="restrictPublicGroups"
|
||||
@change="toggleRestrictPublicGroups"
|
||||
class="h-4 w-4 cursor-pointer rounded border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-dark-500"
|
||||
/>
|
||||
{{ t('admin.users.restrictPublicGroups') }}
|
||||
</label>
|
||||
</div>
|
||||
<p class="mb-3 text-xs text-gray-500 dark:text-gray-400">{{ t('admin.users.restrictPublicGroupsHint') }}</p>
|
||||
<div class="grid gap-3">
|
||||
<div
|
||||
v-for="config in publicGroupConfigs"
|
||||
@@ -107,9 +119,19 @@
|
||||
class="relative overflow-hidden rounded-xl border-2 border-green-200 bg-green-50/50 p-4 dark:border-green-800/50 dark:bg-green-900/10"
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<!-- 复选框(禁用状态) -->
|
||||
<!-- 未开启限制时公开分组恒可用,此处仅作展示;开启后才是真实开关 -->
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex h-5 w-5 items-center justify-center rounded-md border-2 border-green-400 bg-green-500 dark:border-green-600 dark:bg-green-600">
|
||||
<input
|
||||
v-if="restrictPublicGroups"
|
||||
type="checkbox"
|
||||
:checked="config.isSelected"
|
||||
@change="togglePublicGroup(config.groupId)"
|
||||
class="h-5 w-5 cursor-pointer rounded-md border-2 border-green-400 text-green-600 focus:ring-green-500 dark:border-green-600"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="flex h-5 w-5 items-center justify-center rounded-md border-2 border-green-400 bg-green-500 dark:border-green-600 dark:bg-green-600"
|
||||
>
|
||||
<svg class="h-full w-full text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
@@ -207,6 +229,7 @@ const groupConfigs = ref<GroupRateConfig[]>([])
|
||||
const originalGroupRates = ref<Record<number, number>>({}) // 记录原始专属倍率,用于检测删除
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const restrictPublicGroups = ref(false)
|
||||
|
||||
// 分离专属分组和公开分组
|
||||
const exclusiveGroups = computed(() => groups.value.filter((g) => g.is_exclusive))
|
||||
@@ -234,6 +257,7 @@ const load = async () => {
|
||||
// 初始化配置
|
||||
const userAllowedGroups = props.user?.allowed_groups || []
|
||||
const userGroupRates = props.user?.group_rates || {}
|
||||
restrictPublicGroups.value = props.user?.restrict_public_groups ?? false
|
||||
|
||||
// 保存原始专属倍率,用于检测删除操作
|
||||
originalGroupRates.value = { ...userGroupRates }
|
||||
@@ -246,8 +270,9 @@ const load = async () => {
|
||||
defaultRate: g.rate_multiplier,
|
||||
customRate: userGroupRates[g.id] ?? null,
|
||||
// 专属分组:检查是否在 allowed_groups 中
|
||||
// 公开分组:始终选中
|
||||
isSelected: g.is_exclusive ? userAllowedGroups.includes(g.id) : true,
|
||||
// 公开分组:未开启限制时恒可用;开启后同样以 allowed_groups 为准
|
||||
isSelected:
|
||||
g.is_exclusive || restrictPublicGroups.value ? userAllowedGroups.includes(g.id) : true,
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('Failed to load groups:', error)
|
||||
@@ -263,6 +288,23 @@ const toggleExclusiveGroup = (groupId: number) => {
|
||||
}
|
||||
}
|
||||
|
||||
const togglePublicGroup = (groupId: number) => {
|
||||
const config = groupConfigs.value.find((c) => c.groupId === groupId)
|
||||
if (config && !config.isExclusive) {
|
||||
config.isSelected = !config.isSelected
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭限制时把公开分组全部勾回,避免保存出一份"限制已关但只勾了两个"的误导状态。
|
||||
const toggleRestrictPublicGroups = () => {
|
||||
restrictPublicGroups.value = !restrictPublicGroups.value
|
||||
if (!restrictPublicGroups.value) {
|
||||
for (const config of groupConfigs.value) {
|
||||
if (!config.isExclusive) config.isSelected = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updateCustomRate = (groupId: number, value: string) => {
|
||||
const config = groupConfigs.value.find((c) => c.groupId === groupId)
|
||||
if (config) {
|
||||
@@ -280,8 +322,11 @@ const handleSave = async () => {
|
||||
submitting.value = true
|
||||
|
||||
try {
|
||||
// 构建 allowed_groups(仅包含专属分组中被勾选的)
|
||||
const allowedGroups = groupConfigs.value.filter((c) => c.isExclusive && c.isSelected).map((c) => c.groupId)
|
||||
// 构建 allowed_groups:专属分组中被勾选的,以及开启限制后被勾选的公开分组。
|
||||
// 未开启限制时不写入公开分组,保持该表"额外授予"的原有语义。
|
||||
const allowedGroups = groupConfigs.value
|
||||
.filter((c) => c.isSelected && (c.isExclusive || restrictPublicGroups.value))
|
||||
.map((c) => c.groupId)
|
||||
|
||||
// 构建 group_rates
|
||||
// - 有新专属倍率: 设置为该值
|
||||
@@ -301,6 +346,7 @@ const handleSave = async () => {
|
||||
|
||||
await adminAPI.users.update(props.user.id, {
|
||||
allowed_groups: allowedGroups,
|
||||
restrict_public_groups: restrictPublicGroups.value,
|
||||
group_rates: Object.keys(groupRates).length > 0 ? groupRates : undefined,
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user