Resolve verified V1 frontend feedback by improving channel workflows, auth behavior, API key interactions, user filtering, layout persistence, subscription quota handling, i18n text, pricing metadata, and stale frontend cache recovery. - Add a global frontend cache version cleanup to prevent old frontend localStorage from causing page errors after upgrades. - Fix channel copy refresh, model mapping input focus loss, create-channel fetch-model title state, upstream model update confirmation, and batch test toast behavior. - Respect password login settings and improve Turnstile, forgot-password, registration, and invite-link flows. - Make user role/status filtering server-side and preserve table page size in URLs. - Improve API key edit validation feedback and prefetch real keys for reliable copy actions. - Fix rankings access fail-open behavior, double scrollbars, subscription received amount conversion/display, token i18n wording, model deletion confirmation grammar, and Claude pricing context inference. - Add clearer Playground model/group loading errors. Validation: - bun run typecheck - bun run i18n:sync - gofmt on modified Go files - go test ./controller ./model -run '^$'
212 lines
5.7 KiB
TypeScript
Vendored
212 lines
5.7 KiB
TypeScript
Vendored
/*
|
|
Copyright (C) 2023-2026 QuantumNous
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as
|
|
published by the Free Software Foundation, either version 3 of the
|
|
License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
For commercial licensing, please contact support@quantumnous.com
|
|
*/
|
|
import { getStatus } from '@/lib/api'
|
|
|
|
export type ModuleAccess = { enabled: boolean; requireAuth: boolean }
|
|
|
|
export type HeaderNavModule = 'rankings' | 'pricing'
|
|
|
|
export type HeaderNavModules = {
|
|
home: boolean
|
|
console: boolean
|
|
pricing: ModuleAccess
|
|
rankings: ModuleAccess
|
|
docs: boolean
|
|
about: boolean
|
|
[key: string]: boolean | ModuleAccess
|
|
}
|
|
|
|
const DEFAULT_HEADER_NAV_MODULES: HeaderNavModules = {
|
|
home: true,
|
|
console: true,
|
|
pricing: { enabled: true, requireAuth: false },
|
|
rankings: { enabled: true, requireAuth: false },
|
|
docs: true,
|
|
about: true,
|
|
}
|
|
|
|
const DEFAULTS: Record<HeaderNavModule, ModuleAccess> = {
|
|
pricing: DEFAULT_HEADER_NAV_MODULES.pricing,
|
|
rankings: DEFAULT_HEADER_NAV_MODULES.rankings,
|
|
}
|
|
|
|
function cloneHeaderNavDefaults(): HeaderNavModules {
|
|
return {
|
|
...DEFAULT_HEADER_NAV_MODULES,
|
|
pricing: { ...DEFAULT_HEADER_NAV_MODULES.pricing },
|
|
rankings: { ...DEFAULT_HEADER_NAV_MODULES.rankings },
|
|
}
|
|
}
|
|
|
|
export function parseHeaderNavBoolean(
|
|
raw: unknown,
|
|
fallback: boolean
|
|
): boolean {
|
|
if (typeof raw === 'boolean') return raw
|
|
if (typeof raw === 'number') {
|
|
if (raw === 1) return true
|
|
if (raw === 0) return false
|
|
return fallback
|
|
}
|
|
if (typeof raw === 'string') {
|
|
const normalized = raw.trim().toLowerCase()
|
|
if (normalized === 'true' || normalized === '1') return true
|
|
if (normalized === 'false' || normalized === '0') return false
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
function parseAccess(raw: unknown, fallback: ModuleAccess): ModuleAccess {
|
|
if (
|
|
typeof raw === 'boolean' ||
|
|
typeof raw === 'number' ||
|
|
typeof raw === 'string'
|
|
) {
|
|
return {
|
|
enabled: parseHeaderNavBoolean(raw, fallback.enabled),
|
|
requireAuth: fallback.requireAuth,
|
|
}
|
|
}
|
|
if (raw && typeof raw === 'object') {
|
|
const r = raw as Record<string, unknown>
|
|
return {
|
|
enabled: parseHeaderNavBoolean(r.enabled, fallback.enabled),
|
|
requireAuth: parseHeaderNavBoolean(r.requireAuth, fallback.requireAuth),
|
|
}
|
|
}
|
|
return { ...fallback }
|
|
}
|
|
|
|
function parseHeaderNavRecord(raw: unknown): Record<string, unknown> | null {
|
|
if (!raw || String(raw).trim() === '') return null
|
|
if (raw && typeof raw === 'object') return raw as Record<string, unknown>
|
|
|
|
try {
|
|
return JSON.parse(String(raw)) as Record<string, unknown>
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export function parseHeaderNavModules(raw: unknown): HeaderNavModules {
|
|
const result = cloneHeaderNavDefaults()
|
|
const parsed = parseHeaderNavRecord(raw)
|
|
if (!parsed) return result
|
|
|
|
Object.entries(parsed).forEach(([key, value]) => {
|
|
if (key === 'pricing') {
|
|
result.pricing = parseAccess(value, result.pricing)
|
|
return
|
|
}
|
|
if (key === 'rankings') {
|
|
result.rankings = parseAccess(value, result.rankings)
|
|
return
|
|
}
|
|
|
|
const fallback = result[key]
|
|
if (
|
|
typeof fallback === 'boolean' ||
|
|
typeof value === 'boolean' ||
|
|
typeof value === 'number' ||
|
|
typeof value === 'string'
|
|
) {
|
|
result[key] = parseHeaderNavBoolean(
|
|
value,
|
|
typeof fallback === 'boolean' ? fallback : true
|
|
)
|
|
}
|
|
})
|
|
|
|
return result
|
|
}
|
|
|
|
export function parseHeaderNavModulesFromStatus(
|
|
status: Record<string, unknown> | null
|
|
): HeaderNavModules {
|
|
return parseHeaderNavModules(status?.HeaderNavModules)
|
|
}
|
|
|
|
function getCachedStatus(): Record<string, unknown> | null {
|
|
try {
|
|
if (typeof window === 'undefined') return null
|
|
const raw = window.localStorage.getItem('status')
|
|
return raw ? (JSON.parse(raw) as Record<string, unknown>) : null
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
function cacheStatus(status: Record<string, unknown> | null): void {
|
|
try {
|
|
if (typeof window !== 'undefined' && status) {
|
|
window.localStorage.setItem('status', JSON.stringify(status))
|
|
}
|
|
} catch {
|
|
/* empty */
|
|
}
|
|
}
|
|
|
|
export function getModuleAccessFromStatus(
|
|
status: Record<string, unknown> | null,
|
|
module: HeaderNavModule
|
|
): ModuleAccess {
|
|
return parseHeaderNavModulesFromStatus(status)[module] ?? DEFAULTS[module]
|
|
}
|
|
|
|
export function getModuleAccess(module: HeaderNavModule): ModuleAccess {
|
|
return getModuleAccessFromStatus(getCachedStatus(), module)
|
|
}
|
|
|
|
export async function getFreshModuleAccess(
|
|
module: HeaderNavModule
|
|
): Promise<ModuleAccess> {
|
|
try {
|
|
const status = (await getStatus()) as Record<string, unknown> | null
|
|
cacheStatus(status)
|
|
return getModuleAccessFromStatus(status, module)
|
|
} catch {
|
|
return { enabled: false, requireAuth: true }
|
|
}
|
|
}
|
|
|
|
export function isSidebarModuleEnabled(
|
|
section: string,
|
|
module: string
|
|
): boolean {
|
|
const status = getCachedStatus()
|
|
if (!status) return true
|
|
|
|
const raw = status.SidebarModulesAdmin
|
|
if (!raw || String(raw).trim() === '') return true
|
|
|
|
try {
|
|
const parsed = JSON.parse(String(raw)) as Record<
|
|
string,
|
|
Record<string, boolean>
|
|
>
|
|
const sectionConfig = parsed[section]
|
|
if (!sectionConfig) return true
|
|
if (sectionConfig.enabled === false) return false
|
|
if (sectionConfig[module] === false) return false
|
|
return true
|
|
} catch {
|
|
return true
|
|
}
|
|
}
|