feat(reui): update ReUI components and documentation
- 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:
@@ -142,6 +142,46 @@ export function migrateSchema(sqlite: Sqlite): void {
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_source ON audit_log(source_app, created_at);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_audit_log_event_id ON audit_log(event_id) WHERE event_id IS NOT NULL;
|
||||
`)
|
||||
|
||||
sqlite.exec(`
|
||||
CREATE TABLE IF NOT EXISTS oidc_clients (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
client_id TEXT NOT NULL UNIQUE,
|
||||
client_secret_hash TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
redirect_uris_json TEXT NOT NULL,
|
||||
scopes_json TEXT NOT NULL,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oidc_auth_codes (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
code_hash TEXT NOT NULL UNIQUE,
|
||||
client_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
redirect_uri TEXT NOT NULL,
|
||||
scope TEXT NOT NULL,
|
||||
nonce TEXT,
|
||||
code_challenge TEXT,
|
||||
code_challenge_method TEXT,
|
||||
expires_at TEXT NOT NULL,
|
||||
used_at TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oidc_signing_keys (
|
||||
kid TEXT PRIMARY KEY NOT NULL,
|
||||
private_pem TEXT NOT NULL,
|
||||
public_jwk_json TEXT NOT NULL,
|
||||
active INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_oidc_auth_codes_client ON oidc_auth_codes(client_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_oidc_auth_codes_user ON oidc_auth_codes(user_id);
|
||||
`)
|
||||
}
|
||||
|
||||
export function healthCheck(sqlite: Sqlite): void {
|
||||
@@ -152,3 +192,4 @@ export * from './schema/index.js'
|
||||
export * from './users.js'
|
||||
export * from './settings.js'
|
||||
export * from './audit-log.js'
|
||||
export * from './oidc.js'
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
import { and, eq, isNull } from 'drizzle-orm'
|
||||
import { randomBytes, randomUUID } from 'node:crypto'
|
||||
import type { AppDb } from './index.js'
|
||||
import { oidcAuthCodes, oidcClients, oidcSigningKeys } from './schema/index.js'
|
||||
import { hashToken } from './users.js'
|
||||
|
||||
export type OidcClientRow = typeof oidcClients.$inferSelect
|
||||
export type OidcAuthCodeRow = typeof oidcAuthCodes.$inferSelect
|
||||
export type OidcSigningKeyRow = typeof oidcSigningKeys.$inferSelect
|
||||
|
||||
export function parseJsonStringArray(raw: string): string[] {
|
||||
try {
|
||||
const v = JSON.parse(raw) as unknown
|
||||
if (!Array.isArray(v)) return []
|
||||
return v.filter((x): x is string => typeof x === 'string')
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export function listOidcClients(db: AppDb): OidcClientRow[] {
|
||||
return db
|
||||
.select()
|
||||
.from(oidcClients)
|
||||
.all()
|
||||
.sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1))
|
||||
}
|
||||
|
||||
export function getOidcClientById(
|
||||
db: AppDb,
|
||||
id: string,
|
||||
): OidcClientRow | undefined {
|
||||
return db.select().from(oidcClients).where(eq(oidcClients.id, id)).get()
|
||||
}
|
||||
|
||||
export function getOidcClientByClientId(
|
||||
db: AppDb,
|
||||
clientId: string,
|
||||
): OidcClientRow | undefined {
|
||||
return db
|
||||
.select()
|
||||
.from(oidcClients)
|
||||
.where(eq(oidcClients.clientId, clientId))
|
||||
.get()
|
||||
}
|
||||
|
||||
export function createOidcClient(
|
||||
db: AppDb,
|
||||
input: {
|
||||
name: string
|
||||
clientSecretHash: string
|
||||
redirectUris: string[]
|
||||
scopes: string[]
|
||||
enabled: boolean
|
||||
clientId?: string
|
||||
},
|
||||
): OidcClientRow {
|
||||
const now = new Date().toISOString()
|
||||
const id = randomUUID()
|
||||
const clientId = input.clientId ?? randomUUID()
|
||||
db.insert(oidcClients)
|
||||
.values({
|
||||
id,
|
||||
clientId,
|
||||
clientSecretHash: input.clientSecretHash,
|
||||
name: input.name,
|
||||
redirectUrisJson: JSON.stringify(input.redirectUris),
|
||||
scopesJson: JSON.stringify(input.scopes),
|
||||
enabled: input.enabled,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.run()
|
||||
return getOidcClientById(db, id)!
|
||||
}
|
||||
|
||||
export function updateOidcClient(
|
||||
db: AppDb,
|
||||
id: string,
|
||||
patch: {
|
||||
name?: string
|
||||
redirectUris?: string[]
|
||||
scopes?: string[]
|
||||
enabled?: boolean
|
||||
clientSecretHash?: string
|
||||
},
|
||||
): OidcClientRow | undefined {
|
||||
const existing = getOidcClientById(db, id)
|
||||
if (!existing) return undefined
|
||||
const now = new Date().toISOString()
|
||||
db.update(oidcClients)
|
||||
.set({
|
||||
name: patch.name ?? existing.name,
|
||||
redirectUrisJson: patch.redirectUris
|
||||
? JSON.stringify(patch.redirectUris)
|
||||
: existing.redirectUrisJson,
|
||||
scopesJson: patch.scopes
|
||||
? JSON.stringify(patch.scopes)
|
||||
: existing.scopesJson,
|
||||
enabled: patch.enabled ?? existing.enabled,
|
||||
clientSecretHash: patch.clientSecretHash ?? existing.clientSecretHash,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(eq(oidcClients.id, id))
|
||||
.run()
|
||||
return getOidcClientById(db, id)
|
||||
}
|
||||
|
||||
export function deleteOidcClient(db: AppDb, id: string): boolean {
|
||||
const result = db.delete(oidcClients).where(eq(oidcClients.id, id)).run()
|
||||
return result.changes > 0
|
||||
}
|
||||
|
||||
export function generateOidcClientSecret(): string {
|
||||
return randomBytes(32).toString('base64url')
|
||||
}
|
||||
|
||||
export function createOidcAuthCode(
|
||||
db: AppDb,
|
||||
input: {
|
||||
rawCode: string
|
||||
clientId: string
|
||||
userId: string
|
||||
redirectUri: string
|
||||
scope: string
|
||||
nonce?: string | null
|
||||
codeChallenge?: string | null
|
||||
codeChallengeMethod?: string | null
|
||||
expiresAt: Date
|
||||
},
|
||||
): string {
|
||||
const id = randomUUID()
|
||||
db.insert(oidcAuthCodes)
|
||||
.values({
|
||||
id,
|
||||
codeHash: hashToken(input.rawCode),
|
||||
clientId: input.clientId,
|
||||
userId: input.userId,
|
||||
redirectUri: input.redirectUri,
|
||||
scope: input.scope,
|
||||
nonce: input.nonce ?? null,
|
||||
codeChallenge: input.codeChallenge ?? null,
|
||||
codeChallengeMethod: input.codeChallengeMethod ?? null,
|
||||
expiresAt: input.expiresAt.toISOString(),
|
||||
usedAt: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
})
|
||||
.run()
|
||||
return id
|
||||
}
|
||||
|
||||
export function consumeOidcAuthCode(
|
||||
db: AppDb,
|
||||
rawCode: string,
|
||||
): OidcAuthCodeRow | undefined {
|
||||
const codeHash = hashToken(rawCode)
|
||||
const row = db
|
||||
.select()
|
||||
.from(oidcAuthCodes)
|
||||
.where(
|
||||
and(eq(oidcAuthCodes.codeHash, codeHash), isNull(oidcAuthCodes.usedAt)),
|
||||
)
|
||||
.get()
|
||||
if (!row) return undefined
|
||||
const now = new Date().toISOString()
|
||||
if (row.expiresAt < now) return undefined
|
||||
db.update(oidcAuthCodes)
|
||||
.set({ usedAt: now })
|
||||
.where(eq(oidcAuthCodes.id, row.id))
|
||||
.run()
|
||||
return row
|
||||
}
|
||||
|
||||
export function getActiveOidcSigningKey(
|
||||
db: AppDb,
|
||||
): OidcSigningKeyRow | undefined {
|
||||
return db
|
||||
.select()
|
||||
.from(oidcSigningKeys)
|
||||
.where(eq(oidcSigningKeys.active, true))
|
||||
.all()
|
||||
.sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1))[0]
|
||||
}
|
||||
|
||||
export function listOidcSigningKeys(db: AppDb): OidcSigningKeyRow[] {
|
||||
return db.select().from(oidcSigningKeys).all()
|
||||
}
|
||||
|
||||
export function insertOidcSigningKey(
|
||||
db: AppDb,
|
||||
input: {
|
||||
kid: string
|
||||
privatePem: string
|
||||
publicJwkJson: string
|
||||
},
|
||||
): OidcSigningKeyRow {
|
||||
for (const key of listOidcSigningKeys(db)) {
|
||||
if (key.active) {
|
||||
db.update(oidcSigningKeys)
|
||||
.set({ active: false })
|
||||
.where(eq(oidcSigningKeys.kid, key.kid))
|
||||
.run()
|
||||
}
|
||||
}
|
||||
db.insert(oidcSigningKeys)
|
||||
.values({
|
||||
kid: input.kid,
|
||||
privatePem: input.privatePem,
|
||||
publicJwkJson: input.publicJwkJson,
|
||||
active: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
})
|
||||
.run()
|
||||
return db
|
||||
.select()
|
||||
.from(oidcSigningKeys)
|
||||
.where(eq(oidcSigningKeys.kid, input.kid))
|
||||
.get()!
|
||||
}
|
||||
@@ -64,3 +64,40 @@ export const auditLog = sqliteTable('audit_log', {
|
||||
ip: text('ip'),
|
||||
createdAt: text('created_at').notNull(),
|
||||
})
|
||||
|
||||
export const oidcClients = sqliteTable('oidc_clients', {
|
||||
id: text('id').primaryKey(),
|
||||
clientId: text('client_id').notNull().unique(),
|
||||
clientSecretHash: text('client_secret_hash').notNull(),
|
||||
name: text('name').notNull(),
|
||||
redirectUrisJson: text('redirect_uris_json').notNull(),
|
||||
scopesJson: text('scopes_json').notNull(),
|
||||
enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true),
|
||||
createdAt: text('created_at').notNull(),
|
||||
updatedAt: text('updated_at').notNull(),
|
||||
})
|
||||
|
||||
export const oidcAuthCodes = sqliteTable('oidc_auth_codes', {
|
||||
id: text('id').primaryKey(),
|
||||
codeHash: text('code_hash').notNull().unique(),
|
||||
clientId: text('client_id').notNull(),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
redirectUri: text('redirect_uri').notNull(),
|
||||
scope: text('scope').notNull(),
|
||||
nonce: text('nonce'),
|
||||
codeChallenge: text('code_challenge'),
|
||||
codeChallengeMethod: text('code_challenge_method'),
|
||||
expiresAt: text('expires_at').notNull(),
|
||||
usedAt: text('used_at'),
|
||||
createdAt: text('created_at').notNull(),
|
||||
})
|
||||
|
||||
export const oidcSigningKeys = sqliteTable('oidc_signing_keys', {
|
||||
kid: text('kid').primaryKey(),
|
||||
privatePem: text('private_pem').notNull(),
|
||||
publicJwkJson: text('public_jwk_json').notNull(),
|
||||
active: integer('active', { mode: 'boolean' }).notNull().default(true),
|
||||
createdAt: text('created_at').notNull(),
|
||||
})
|
||||
|
||||
@@ -177,6 +177,24 @@ export function revokeRefreshSession(db: AppDb, rawToken: string): void {
|
||||
.run()
|
||||
}
|
||||
|
||||
/** Active refresh session → user (for OIDC authorize cookie). */
|
||||
export function getUserByRefreshToken(
|
||||
db: AppDb,
|
||||
rawToken: string,
|
||||
): UserRow | undefined {
|
||||
const now = new Date().toISOString()
|
||||
const session = db
|
||||
.select()
|
||||
.from(refreshSessions)
|
||||
.where(eq(refreshSessions.tokenHash, hashToken(rawToken)))
|
||||
.get()
|
||||
if (!session || session.revokedAt) return undefined
|
||||
if (session.expiresAt < now) return undefined
|
||||
const user = getUserById(db, session.userId)
|
||||
if (!user || user.disabled) return undefined
|
||||
return user
|
||||
}
|
||||
|
||||
export type ActiveSessionRow = {
|
||||
id: string
|
||||
userId: string
|
||||
|
||||
@@ -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],
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,4 +1,5 @@
|
||||
export * from './contracts/auth.js'
|
||||
export * from './contracts/app-switcher.js'
|
||||
export * from './contracts/audit.js'
|
||||
export * from './contracts/oidc.js'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user