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:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user