feat(reui): update ReUI components and documentation
Build and Push Auth Portal Docker Image / build-and-push (push) Successful in 2m30s
Build and Push Auth Portal Docker Image / create-release (push) Skipped

- Added new OIDC configuration options in `.env.example`.
- Expanded documentation in `AGENTS.md` to include OIDC endpoints and admin UI.
- Updated ReUI skill version and component count from 17 to 20 across various documentation files.
- Enhanced `README.md` and other related files to reflect the new component structure and usage guidelines.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-08-11 16:58:47 +07:00
co-authored by Cursor
parent cc38bf06f8
commit 0b6fa65b08
61 changed files with 2387 additions and 103 deletions
+26 -1
View File
@@ -1,5 +1,13 @@
import { z } from 'zod'
import { APP_IDS, APPS, appIdSchema, type AppId, type AppMeta } from './auth.js'
import {
APP_IDS,
APPS,
appAuthModeSchema,
appIdSchema,
type AppAuthMode,
type AppId,
type AppMeta,
} from './auth.js'
export const appSwitcherIconSchema = z.enum([
'server',
@@ -20,6 +28,8 @@ export const appSwitcherEntrySchema = z.object({
sort: z.number().int().optional(),
/** App-scoped tenant (e.g. EvoBGP UUID) — goes into JWT, not public switcher. */
tenantId: z.string().optional(),
/** jwt = fragment SSO; oidc = open base URL. */
authMode: appAuthModeSchema.optional(),
})
export const appSwitcherConfigSchema = z.object({
@@ -36,6 +46,15 @@ const DEFAULT_ICONS: Record<AppId, AppSwitcherIconName> = {
vps: 'server',
bgp: 'globe',
fw: 'server',
dns: 'globe',
}
const DEFAULT_AUTH_MODE: Record<AppId, AppAuthMode> = {
cfdm: 'jwt',
vps: 'jwt',
bgp: 'jwt',
fw: 'jwt',
dns: 'oidc',
}
/** Seed / fallback when DB is empty. */
@@ -50,6 +69,7 @@ export function defaultAppSwitcherConfig(): AppSwitcherConfig {
icon: DEFAULT_ICONS[app.id],
enabled: true,
sort: index,
authMode: DEFAULT_AUTH_MODE[app.id],
})),
}
}
@@ -77,6 +97,10 @@ export function normalizeAppSwitcherConfig(
enabled: existing?.enabled ?? true,
icon: existing?.icon ?? fallback.icon,
tenantId: existing?.tenantId?.trim() || undefined,
authMode:
existing?.authMode ??
fallback.authMode ??
DEFAULT_AUTH_MODE[id],
}
}).sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0))
@@ -122,5 +146,6 @@ export function appsMetaFromSwitcher(config: AppSwitcherConfig): AppMeta[] {
title: a.name,
description: a.subtitle ?? APPS.find((x) => x.id === a.id)?.description ?? '',
url: a.url,
authMode: a.authMode ?? DEFAULT_AUTH_MODE[a.id],
}))
}
+2 -1
View File
@@ -10,6 +10,7 @@ export const AUDIT_SOURCE_APPS = [
'cfdm',
'bgp',
'fw',
'dns',
] as const
export type AuditSourceApp = (typeof AUDIT_SOURCE_APPS)[number]
export const auditSourceAppSchema = z.enum(AUDIT_SOURCE_APPS)
@@ -105,7 +106,7 @@ export type AuditPurgeResponse = z.infer<typeof auditPurgeResponseSchema>
export const ingestAuditEventSchema = z.object({
event_id: z.string().min(1).max(128),
source_app: z.enum(['vps', 'cfdm', 'bgp', 'fw']),
source_app: z.enum(['vps', 'cfdm', 'bgp', 'fw', 'dns']),
action: z.string().min(1).max(200),
severity: auditSeveritySchema.optional(),
actor_user_id: z.string().nullable().optional(),
+29 -1
View File
@@ -1,9 +1,13 @@
import { z } from 'zod'
export const APP_IDS = ['cfdm', 'vps', 'bgp', 'fw'] as const
export const APP_IDS = ['cfdm', 'vps', 'bgp', 'fw', 'dns'] as const
export type AppId = (typeof APP_IDS)[number]
export const appIdSchema = z.enum(APP_IDS)
export const APP_AUTH_MODES = ['jwt', 'oidc'] as const
export type AppAuthMode = (typeof APP_AUTH_MODES)[number]
export const appAuthModeSchema = z.enum(APP_AUTH_MODES)
export const PERMISSION_ACTIONS = ['read', 'write', 'admin'] as const
export type PermissionAction = (typeof PERMISSION_ACTIONS)[number]
export const permissionActionSchema = z.enum(PERMISSION_ACTIONS)
@@ -13,6 +17,8 @@ export type AppMeta = {
title: string
description: string
url: string
/** jwt = fragment SSO; oidc = open base URL (external OIDC RP). */
authMode: AppAuthMode
}
export const APPS: AppMeta[] = [
@@ -21,24 +27,35 @@ export const APPS: AppMeta[] = [
title: 'Cloudflare Domain Manager',
description: 'Домены, DNS, сертификаты, группы и сервисы',
url: 'https://cfdm.shnt.top',
authMode: 'jwt',
},
{
id: 'vps',
title: 'VPS Tracker',
description: 'Серверы, аккаунты, платежи и синхронизация',
url: 'https://vps.shnt.top',
authMode: 'jwt',
},
{
id: 'bgp',
title: 'EvoBGP',
description: 'Модули, сеть, операции и мониторинг BGP',
url: 'https://bgp.shnt.top',
authMode: 'jwt',
},
{
id: 'fw',
title: 'EvoFirewall',
description: 'Централизованный firewall: агенты, списки, политики',
url: 'https://fw.shnt.top',
authMode: 'jwt',
},
{
id: 'dns',
title: 'Technitium DNS',
description: 'DNS-сервер: зоны, DHCP, блокировки (OIDC SSO)',
url: 'https://dns.shnt.top',
authMode: 'oidc',
},
]
@@ -127,6 +144,16 @@ export const PERMISSION_CATALOG: AppPermissionCatalog[] = [
section('settings', 'Настройки', 'Enroll seed и интеграции', ['admin']),
],
},
{
appId: 'dns',
title: 'Technitium DNS',
sections: [
section('console', 'Консоль', 'Полный доступ Administrators', ['admin']),
section('dns', 'DNS', 'Зоны и записи (DNS Administrators)'),
section('dhcp', 'DHCP', 'DHCP scopes (DHCP Administrators)'),
section('settings', 'Настройки', 'SSO и системные настройки', ['admin']),
],
},
]
export function permissionKey(
@@ -343,6 +370,7 @@ export const catalogResponseSchema = z.object({
title: z.string(),
description: z.string(),
url: z.string(),
authMode: appAuthModeSchema.optional(),
}),
),
permissions: z.array(
+101
View File
@@ -0,0 +1,101 @@
import { z } from 'zod'
export const OIDC_SCOPES = ['openid', 'profile', 'email', 'groups'] as const
export type OidcScope = (typeof OIDC_SCOPES)[number]
/** Remote groups for Technitium Group Map (and similar RPs). */
export const TECHNITIUM_OIDC_GROUPS = {
admins: 'technitium_admins',
dnsAdmins: 'technitium_dns_admins',
dhcpAdmins: 'technitium_dhcp_admins',
} as const
/**
* Map portal permissions / admin flag → OIDC group names for Technitium.
* Used in id_token + userinfo `groups` and `roles` claims.
*/
export function oidcGroupsForUser(input: {
isAdmin: boolean
permissions: readonly string[]
}): string[] {
const granted = new Set(input.permissions)
const groups: string[] = []
if (input.isAdmin || granted.has('dns:console:admin')) {
groups.push(TECHNITIUM_OIDC_GROUPS.admins)
}
if (
granted.has('dns:dns:write') ||
granted.has('dns:dns:admin') ||
granted.has('dns:console:admin') ||
input.isAdmin
) {
groups.push(TECHNITIUM_OIDC_GROUPS.dnsAdmins)
}
if (
granted.has('dns:dhcp:write') ||
granted.has('dns:dhcp:admin') ||
granted.has('dns:console:admin') ||
input.isAdmin
) {
groups.push(TECHNITIUM_OIDC_GROUPS.dhcpAdmins)
}
return [...new Set(groups)]
}
export const createOidcClientRequestSchema = z.object({
name: z.string().min(1).max(200),
redirect_uris: z.array(z.string().url()).min(1),
scopes: z
.array(z.enum(OIDC_SCOPES))
.min(1)
.default([...OIDC_SCOPES]),
enabled: z.boolean().default(true),
})
export type CreateOidcClientRequest = z.infer<
typeof createOidcClientRequestSchema
>
export const patchOidcClientRequestSchema = z.object({
name: z.string().min(1).max(200).optional(),
redirect_uris: z.array(z.string().url()).min(1).optional(),
scopes: z.array(z.enum(OIDC_SCOPES)).min(1).optional(),
enabled: z.boolean().optional(),
})
export type PatchOidcClientRequest = z.infer<
typeof patchOidcClientRequestSchema
>
export const oidcClientPublicSchema = z.object({
id: z.string(),
client_id: z.string(),
name: z.string(),
redirect_uris: z.array(z.string()),
scopes: z.array(z.string()),
enabled: z.boolean(),
created_at: z.string(),
updated_at: z.string(),
})
export type OidcClientPublic = z.infer<typeof oidcClientPublicSchema>
export const oidcClientCreatedSchema = oidcClientPublicSchema.extend({
client_secret: z.string(),
})
export type OidcClientCreated = z.infer<typeof oidcClientCreatedSchema>
/** True when return_to is portal OIDC authorize (no JWT fragment handoff). */
export function isPortalOidcAuthorizeUrl(
returnTo: string,
issuer: string,
): boolean {
try {
const url = new URL(returnTo)
const iss = new URL(issuer)
if (url.origin !== iss.origin) return false
return (
url.pathname === '/oauth/authorize' ||
url.pathname === '/oauth/authorize/'
)
} catch {
return false
}
}
+1
View File
@@ -1,4 +1,5 @@
export * from './contracts/auth.js'
export * from './contracts/app-switcher.js'
export * from './contracts/audit.js'
export * from './contracts/oidc.js'