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 export const oidcClientCreatedSchema = oidcClientPublicSchema.extend({ client_secret: z.string(), }) export type OidcClientCreated = z.infer /** 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 } }