fix: enforce header nav access control for public modules (#4889)
This commit is contained in:
Vendored
+148
-17
@@ -16,28 +16,135 @@ 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'
|
||||
|
||||
type ModuleAccess = { enabled: boolean; requireAuth: boolean }
|
||||
export type ModuleAccess = { enabled: boolean; requireAuth: boolean }
|
||||
|
||||
const DEFAULTS: Record<string, ModuleAccess> = {
|
||||
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') return { enabled: raw, requireAuth: fallback.requireAuth }
|
||||
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: typeof r.enabled === 'boolean' ? r.enabled : fallback.enabled,
|
||||
requireAuth: typeof r.requireAuth === 'boolean' ? r.requireAuth : fallback.requireAuth,
|
||||
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 {
|
||||
@@ -45,22 +152,43 @@ function getCachedStatus(): Record<string, unknown> | null {
|
||||
}
|
||||
}
|
||||
|
||||
export function getModuleAccess(module: 'rankings' | 'pricing'): ModuleAccess {
|
||||
const status = getCachedStatus()
|
||||
if (!status) return DEFAULTS[module]
|
||||
|
||||
const rawNav = status.HeaderNavModules
|
||||
if (!rawNav || String(rawNav).trim() === '') return DEFAULTS[module]
|
||||
|
||||
function cacheStatus(status: Record<string, unknown> | null): void {
|
||||
try {
|
||||
const parsed = JSON.parse(String(rawNav)) as Record<string, unknown>
|
||||
return parseAccess(parsed[module], DEFAULTS[module])
|
||||
if (typeof window !== 'undefined' && status) {
|
||||
window.localStorage.setItem('status', JSON.stringify(status))
|
||||
}
|
||||
} catch {
|
||||
return DEFAULTS[module]
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
export function isSidebarModuleEnabled(section: string, module: string): boolean {
|
||||
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 getModuleAccess(module)
|
||||
}
|
||||
}
|
||||
|
||||
export function isSidebarModuleEnabled(
|
||||
section: string,
|
||||
module: string
|
||||
): boolean {
|
||||
const status = getCachedStatus()
|
||||
if (!status) return true
|
||||
|
||||
@@ -68,7 +196,10 @@ export function isSidebarModuleEnabled(section: string, module: string): boolean
|
||||
if (!raw || String(raw).trim() === '') return true
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(String(raw)) as Record<string, Record<string, boolean>>
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user