feat(app-switcher): централизовать ссылки приложений в portal settings
Build and Push Auth Portal Docker Image / build-and-push (push) Successful in 1m49s
Build and Push Auth Portal Docker Image / create-release (push) Skipped

Публичный GET и admin PUT/UI /admin/apps; каталог и chrome читают URL из store.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-07-18 20:59:01 +07:00
co-authored by Cursor
parent 83bfc0355d
commit 0fa4b7adea
18 changed files with 662 additions and 46 deletions
@@ -0,0 +1,96 @@
import { z } from 'zod'
import { APP_IDS, APPS, appIdSchema, type AppId, type AppMeta } from './auth.js'
export const appSwitcherIconSchema = z.enum([
'server',
'cloud',
'globe',
'dashboard',
'chart',
])
export const appSwitcherEntrySchema = z.object({
id: appIdSchema,
name: z.string().min(1),
subtitle: z.string().optional(),
url: z.string().url(),
icon: appSwitcherIconSchema,
shortcut: z.string().optional(),
enabled: z.boolean(),
sort: z.number().int().optional(),
})
export const appSwitcherConfigSchema = z.object({
menuLabel: z.string().min(1),
apps: z.array(appSwitcherEntrySchema).min(1),
})
export type AppSwitcherIconName = z.infer<typeof appSwitcherIconSchema>
export type AppSwitcherEntry = z.infer<typeof appSwitcherEntrySchema>
export type AppSwitcherConfig = z.infer<typeof appSwitcherConfigSchema>
const DEFAULT_ICONS: Record<AppId, AppSwitcherIconName> = {
cfdm: 'cloud',
vps: 'server',
bgp: 'globe',
}
/** Seed / fallback when DB is empty. */
export function defaultAppSwitcherConfig(): AppSwitcherConfig {
return {
menuLabel: 'Приложения',
apps: APPS.map((app, index) => ({
id: app.id,
name: app.title,
subtitle: app.description,
url: app.url,
icon: DEFAULT_ICONS[app.id],
enabled: true,
sort: index,
})),
}
}
export function parseAppSwitcherConfig(raw: unknown): AppSwitcherConfig {
const parsed = appSwitcherConfigSchema.safeParse(raw)
if (!parsed.success) return defaultAppSwitcherConfig()
return normalizeAppSwitcherConfig(parsed.data)
}
/** Ensure all APP_IDS present; sort; drop unknown. */
export function normalizeAppSwitcherConfig(
config: AppSwitcherConfig,
): AppSwitcherConfig {
const byId = new Map(config.apps.map((a) => [a.id, a]))
const defaults = defaultAppSwitcherConfig()
const apps = APP_IDS.map((id, index) => {
const existing = byId.get(id)
const fallback = defaults.apps.find((a) => a.id === id)!
return {
...fallback,
...existing,
id,
sort: existing?.sort ?? index,
enabled: existing?.enabled ?? true,
icon: existing?.icon ?? fallback.icon,
}
}).sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0))
return {
menuLabel: config.menuLabel || 'Приложения',
apps,
}
}
/** AppMeta list with URLs from switcher store (for /apps + catalog). */
export function appsMetaFromSwitcher(config: AppSwitcherConfig): AppMeta[] {
const normalized = normalizeAppSwitcherConfig(config)
return normalized.apps
.filter((a) => a.enabled !== false)
.map((a) => ({
id: a.id,
title: a.name,
description: a.subtitle ?? APPS.find((x) => x.id === a.id)?.description ?? '',
url: a.url,
}))
}
+2
View File
@@ -1 +1,3 @@
export * from './contracts/auth.js'
export * from './contracts/app-switcher.js'