mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
fix: hide batch image entry without allowed key
This commit is contained in:
@@ -195,6 +195,7 @@ import { useAdminSettingsStore, useAppStore, useAuthStore, useOnboardingStore }
|
||||
import VersionBadge from '@/components/common/VersionBadge.vue'
|
||||
import { sanitizeSvg } from '@/utils/sanitize'
|
||||
import { FeatureFlags, makeSidebarFlag } from '@/utils/featureFlags'
|
||||
import { useBatchImageAccess } from '@/composables/useBatchImageAccess'
|
||||
|
||||
interface NavItem {
|
||||
path: string
|
||||
@@ -240,6 +241,7 @@ const appStore = useAppStore()
|
||||
const authStore = useAuthStore()
|
||||
const onboardingStore = useOnboardingStore()
|
||||
const adminSettingsStore = useAdminSettingsStore()
|
||||
const { canUseBatchImage, refreshBatchImageAccess } = useBatchImageAccess()
|
||||
|
||||
const sidebarCollapsed = computed(() => appStore.sidebarCollapsed)
|
||||
const mobileOpen = computed(() => appStore.mobileOpen)
|
||||
@@ -683,6 +685,7 @@ const flagAffiliate = makeSidebarFlag(FeatureFlags.affiliate)
|
||||
const flagRiskControl = makeSidebarFlag(FeatureFlags.riskControl)
|
||||
const flagOpsMonitoring = () => adminSettingsStore.opsMonitoringEnabled
|
||||
const flagAdminPayment = () => adminSettingsStore.paymentEnabled
|
||||
const flagBatchImageAccess = () => canUseBatchImage.value
|
||||
|
||||
// buildSelfNavItems 构造用户自己的导航项(用户端主菜单和管理员的"我的账户"子菜单共享这组声明)。
|
||||
// withDashboard=true 时包含仪表盘(用户端),false 时不含(管理员的个人区已经有独立仪表盘入口)。
|
||||
@@ -696,7 +699,7 @@ function buildSelfNavItems(withDashboard: boolean): NavItem[] {
|
||||
}
|
||||
items.push(
|
||||
{ path: '/keys', label: t('nav.apiKeys'), icon: KeyIcon },
|
||||
{ path: '/batch-image', label: t('nav.batchImage'), icon: BatchImageIcon, hideInSimpleMode: true },
|
||||
{ path: '/batch-image', label: t('nav.batchImage'), icon: BatchImageIcon, hideInSimpleMode: true, featureFlag: flagBatchImageAccess },
|
||||
{ path: '/usage', label: t('nav.usage'), icon: ChartIcon, hideInSimpleMode: true },
|
||||
{ path: '/available-channels', label: t('nav.availableChannels'), icon: ChannelIcon, hideInSimpleMode: true, featureFlag: flagAvailableChannels },
|
||||
{ path: '/monitor', label: t('nav.channelStatus'), icon: SignalIcon, featureFlag: flagChannelMonitor },
|
||||
@@ -917,6 +920,7 @@ watch(
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
void refreshBatchImageAccess()
|
||||
if (isAdmin.value) {
|
||||
adminSettingsStore.fetch()
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
/>
|
||||
</button>
|
||||
|
||||
<button @click="router.push('/batch-image')" class="group flex w-full items-center gap-4 rounded-xl bg-gray-50 p-4 text-left transition-all duration-200 hover:bg-gray-100 dark:bg-dark-800/50 dark:hover:bg-dark-800">
|
||||
<button v-if="canUseBatchImage" @click="router.push('/batch-image')" class="group flex w-full items-center gap-4 rounded-xl bg-gray-50 p-4 text-left transition-all duration-200 hover:bg-gray-100 dark:bg-dark-800/50 dark:hover:bg-dark-800">
|
||||
<div class="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-xl bg-sky-100 transition-transform group-hover:scale-105 dark:bg-sky-900/30">
|
||||
<Icon name="sparkles" size="lg" class="text-sky-600 dark:text-sky-400" />
|
||||
</div>
|
||||
@@ -68,9 +68,16 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import { useBatchImageAccess } from '@/composables/useBatchImageAccess'
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
const { canUseBatchImage, refreshBatchImageAccess } = useBatchImageAccess()
|
||||
|
||||
onMounted(() => {
|
||||
void refreshBatchImageAccess()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { keysAPI } from '@/api/keys'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import type { ApiKey } from '@/types'
|
||||
|
||||
const loaded = ref(false)
|
||||
const loading = ref(false)
|
||||
const hasAllowedBatchImageKey = ref(false)
|
||||
let pendingLoad: Promise<boolean> | null = null
|
||||
const pageSize = 100
|
||||
|
||||
function keyAllowsBatchImage(key: ApiKey): boolean {
|
||||
return (
|
||||
key.status === 'active' &&
|
||||
key.group?.platform === 'gemini' &&
|
||||
key.group?.allow_batch_image_generation === true
|
||||
)
|
||||
}
|
||||
|
||||
async function loadBatchImageAccess(force = false): Promise<boolean> {
|
||||
const authStore = useAuthStore()
|
||||
if (!authStore.isAuthenticated) {
|
||||
loaded.value = true
|
||||
hasAllowedBatchImageKey.value = false
|
||||
return false
|
||||
}
|
||||
|
||||
if (loaded.value && !force) {
|
||||
return hasAllowedBatchImageKey.value
|
||||
}
|
||||
|
||||
if (pendingLoad && !force) {
|
||||
return pendingLoad
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
pendingLoad = (async () => {
|
||||
let page = 1
|
||||
while (true) {
|
||||
const response = await keysAPI.list(page, pageSize, {
|
||||
status: 'active',
|
||||
sort_by: 'created_at',
|
||||
sort_order: 'desc'
|
||||
})
|
||||
|
||||
if ((response.items || []).some(keyAllowsBatchImage)) {
|
||||
hasAllowedBatchImageKey.value = true
|
||||
loaded.value = true
|
||||
return true
|
||||
}
|
||||
|
||||
if (page >= response.pages || (response.items || []).length === 0) {
|
||||
hasAllowedBatchImageKey.value = false
|
||||
loaded.value = true
|
||||
return false
|
||||
}
|
||||
|
||||
page += 1
|
||||
}
|
||||
})()
|
||||
.catch(() => {
|
||||
hasAllowedBatchImageKey.value = false
|
||||
loaded.value = true
|
||||
return false
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
pendingLoad = null
|
||||
})
|
||||
|
||||
return pendingLoad
|
||||
}
|
||||
|
||||
export function useBatchImageAccess() {
|
||||
const canUseBatchImage = computed(() => hasAllowedBatchImageKey.value)
|
||||
|
||||
return {
|
||||
canUseBatchImage,
|
||||
batchImageAccessLoaded: computed(() => loaded.value),
|
||||
batchImageAccessLoading: computed(() => loading.value),
|
||||
refreshBatchImageAccess: loadBatchImageAccess,
|
||||
}
|
||||
}
|
||||
@@ -225,6 +225,7 @@
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<button
|
||||
v-if="canUseBatchImage"
|
||||
type="button"
|
||||
class="group flex items-center gap-3 rounded-lg bg-gray-50 p-3 text-left transition-colors hover:bg-sky-50 dark:bg-dark-800/50 dark:hover:bg-sky-900/20"
|
||||
@click="router.push('/batch-image')"
|
||||
@@ -361,6 +362,7 @@ import DateRangePicker from '@/components/common/DateRangePicker.vue'
|
||||
import Select from '@/components/common/Select.vue'
|
||||
import ModelDistributionChart from '@/components/charts/ModelDistributionChart.vue'
|
||||
import TokenUsageTrend from '@/components/charts/TokenUsageTrend.vue'
|
||||
import { useBatchImageAccess } from '@/composables/useBatchImageAccess'
|
||||
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
@@ -387,6 +389,7 @@ ChartJS.register(
|
||||
|
||||
const appStore = useAppStore()
|
||||
const router = useRouter()
|
||||
const { canUseBatchImage, refreshBatchImageAccess } = useBatchImageAccess()
|
||||
const stats = ref<DashboardStats | null>(null)
|
||||
const loading = ref(false)
|
||||
const chartsLoading = ref(false)
|
||||
@@ -746,6 +749,7 @@ const loadChartData = async () => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void refreshBatchImageAccess()
|
||||
loadDashboardStats()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -55,6 +55,28 @@ Covered by automated tests and smoke:
|
||||
- Stale pre-provider jobs can be failed and released.
|
||||
- Completed job download only returns successful outputs.
|
||||
|
||||
## Access Control And Visibility
|
||||
|
||||
The batch image feature has two independent gates:
|
||||
|
||||
- Global runtime gate: `BATCH_IMAGE_ENABLED` controls whether `/v1/images/batches*` is available at all. If disabled, the backend returns `404 BATCH_IMAGE_DISABLED` regardless of group settings. This value is loaded at application startup, so changing the server environment requires restarting/redeploying the app container.
|
||||
- Group/API-key gate: `groups.allow_batch_image_generation` controls whether a user's API key may use the feature. If the global gate is enabled but the API key's group is not allowed, the backend returns `403 BATCH_IMAGE_GROUP_DISABLED`.
|
||||
|
||||
Frontend visibility follows the same group/API-key gate for user-facing entry points:
|
||||
|
||||
- Sidebar `/batch-image` entry is shown only when the current user has at least one active Gemini API key whose group has `allow_batch_image_generation=true`.
|
||||
- User dashboard quick action is hidden under the same condition.
|
||||
- Admin dashboard's shortcut to the user-facing batch image page is also hidden under the same current-user API-key condition; admin group configuration remains available under group management.
|
||||
- The frontend check pages through active keys in batches of 100 and stops as soon as it finds an allowed key. The result is cached in a shared composable for sidebar/dashboard reuse, and API errors fail closed by hiding the entry.
|
||||
|
||||
This frontend hiding is only a UX affordance. Backend authorization remains the source of truth, so direct API calls without an allowed group still fail.
|
||||
|
||||
Quick action origin:
|
||||
|
||||
- `UserDashboardQuickActions.vue` is an upstream dashboard component. The batch image button was added by the custom batch image work to fit into the existing quick action surface.
|
||||
- The admin dashboard quick action block and the batch image shortcut inside it were added by the custom batch image work.
|
||||
- The sidebar batch image module entry was added by the custom batch image work.
|
||||
|
||||
## Residual Risks
|
||||
|
||||
- Real provider failure combinations should still be tested with controlled fake/fixture provider outputs: malformed output JSONL, missing image bytes, provider cancelled after partial success, and delayed output indexing.
|
||||
@@ -64,4 +86,3 @@ Covered by automated tests and smoke:
|
||||
## Recommendation
|
||||
|
||||
Proceed to broader review with Claude and/or manual exploratory testing. Before production enablement, add one integration test for cancel/settle concurrency and one for persistent settlement billing failure recovery.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user