Files
auth-portal/packages/shared/src/contracts/oidc.ts
T
DenozordecandCursor 0b6fa65b08
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
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]>
2026-08-11 16:58:47 +07:00

102 lines
2.8 KiB
TypeScript

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
}
}