/* eslint-disable */ // @ts-nocheck import { fetchWithError } from "@keycloak/keycloak-admin-client"; import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation"; import { useFetch } from "../../../shared/keycloak-ui-shared"; import { PageSection } from "../../../shared/@patternfly/react-core"; import { useState } from "react"; import { useFormContext } from "react-hook-form"; import { useAdminClient } from "../../admin-client"; import { useRealm } from "../../context/realm-context/RealmContext"; import { addTrailingSlash, convertAttributeNameToForm } from "../../util"; import { getAuthorizationHeaders } from "../../utils/getAuthorizationHeaders"; import type { FormFields, SaveOptions } from "../ClientDetails"; import type { SsfClientTab } from "../routes/ClientSsfTab"; import { EmitEventsTab } from "./tabs/EmitEventsTab"; import { EventSearchTab } from "./tabs/EventSearchTab"; import { ReceiverTab } from "./tabs/ReceiverTab"; import { StreamTab, type SsfClientStream } from "./tabs/StreamTab"; import { SubjectsTab } from "./tabs/SubjectsTab"; const FALLBACK_DEFAULT_SUPPORTED_EVENTS = "CaepCredentialChange,CaepSessionRevoked"; const isPollDeliveryMethod = (method: string | undefined): boolean => method === "urn:ietf:rfc:8936" || method === "https://schemas.openid.net/secevent/risc/delivery-method/poll"; type SsfConfig = { defaultSupportedEvents?: string[]; availableSupportedEvents?: string[]; nativelyEmittedEvents?: string[]; defaultPushEndpointConnectTimeoutMillis?: number; defaultPushEndpointSocketTimeoutMillis?: number; defaultUserSubjectFormat?: string; }; const FALLBACK_DEFAULT_PUSH_CONNECT_TIMEOUT_MILLIS = 1000; const FALLBACK_DEFAULT_PUSH_SOCKET_TIMEOUT_MILLIS = 1000; const FALLBACK_DEFAULT_USER_SUBJECT_FORMAT = "iss_sub"; export type SsfTabProps = { save: (options?: SaveOptions) => void; client: ClientRepresentation; /** * Which sub-tab to render. The parent (ClientDetails) drives this * from the URL via RoutableTabs so each section is a deep-linkable * page on its own. */ activeTab: SsfClientTab; }; export const SsfTab = ({ save, client, activeTab }: SsfTabProps) => { const { adminClient } = useAdminClient(); const { realm } = useRealm(); const { watch, setValue } = useFormContext(); const [defaultSupportedEvents, setDefaultSupportedEvents] = useState( FALLBACK_DEFAULT_SUPPORTED_EVENTS, ); const [availableSupportedEvents, setAvailableSupportedEvents] = useState< string[] >([]); const [nativelyEmittedEvents, setNativelyEmittedEvents] = useState( [], ); const [defaultPushConnectTimeoutMillis, setDefaultPushConnectTimeoutMillis] = useState(FALLBACK_DEFAULT_PUSH_CONNECT_TIMEOUT_MILLIS); const [defaultPushSocketTimeoutMillis, setDefaultPushSocketTimeoutMillis] = useState(FALLBACK_DEFAULT_PUSH_SOCKET_TIMEOUT_MILLIS); const [defaultUserSubjectFormat, setDefaultUserSubjectFormat] = useState(FALLBACK_DEFAULT_USER_SUBJECT_FORMAT); const [clientStream, setClientStream] = useState( null, ); const [streamFetchKey, setStreamFetchKey] = useState(0); const [configFetchKey, setConfigFetchKey] = useState(0); const refresh = () => { setStreamFetchKey((k) => k + 1); setConfigFetchKey((k) => k + 1); }; useFetch( async () => { const response = await fetchWithError( `${addTrailingSlash( adminClient.baseUrl, )}admin/realms/${realm}/ssf/config`, { headers: getAuthorizationHeaders(await adminClient.getAccessToken()), }, ); if (!response.ok) { return null; } return (await response.json()) as SsfConfig; }, (config) => { if (config?.availableSupportedEvents) { setAvailableSupportedEvents(config.availableSupportedEvents); } if (config?.nativelyEmittedEvents) { setNativelyEmittedEvents(config.nativelyEmittedEvents); } if (typeof config?.defaultPushEndpointConnectTimeoutMillis === "number") { setDefaultPushConnectTimeoutMillis( config.defaultPushEndpointConnectTimeoutMillis, ); const connectField = convertAttributeNameToForm( "attributes.ssf.pushEndpointConnectTimeoutMillis", ); const currentConnect = watch(connectField); if (currentConnect === undefined || currentConnect === "") { setValue( connectField, String(config.defaultPushEndpointConnectTimeoutMillis), { shouldDirty: false }, ); } } if (typeof config?.defaultPushEndpointSocketTimeoutMillis === "number") { setDefaultPushSocketTimeoutMillis( config.defaultPushEndpointSocketTimeoutMillis, ); const socketField = convertAttributeNameToForm( "attributes.ssf.pushEndpointSocketTimeoutMillis", ); const currentSocket = watch(socketField); if (currentSocket === undefined || currentSocket === "") { setValue( socketField, String(config.defaultPushEndpointSocketTimeoutMillis), { shouldDirty: false }, ); } } if ( typeof config?.defaultUserSubjectFormat === "string" && config.defaultUserSubjectFormat.length > 0 ) { setDefaultUserSubjectFormat(config.defaultUserSubjectFormat); const userSubjectFormatField = convertAttributeNameToForm( "attributes.ssf.userSubjectFormat", ); const currentUserSubjectFormat = watch(userSubjectFormatField); if ( currentUserSubjectFormat === undefined || currentUserSubjectFormat === "" ) { setValue(userSubjectFormatField, config.defaultUserSubjectFormat, { shouldDirty: false, }); } } const events = config?.defaultSupportedEvents; if (!events || events.length === 0) { return; } const joined = events.join(","); setDefaultSupportedEvents(joined); const fieldName = convertAttributeNameToForm( "attributes.ssf.supportedEvents", ); const currentValue = watch(fieldName); if (currentValue === undefined || currentValue === "") { setValue(fieldName, joined, { shouldDirty: false }); } }, [configFetchKey], ); useFetch( async () => { if (!client.id) { return null; } // Use plain fetch instead of fetchWithError so we can handle the // expected 404 (no stream registered yet) without surfacing it as a // global error and redirecting away from the client details page. const response = await fetch( `${addTrailingSlash( adminClient.baseUrl, )}admin/realms/${realm}/ssf/clients/${client.clientId}/stream`, { headers: getAuthorizationHeaders(await adminClient.getAccessToken()), }, ); if (!response.ok) { return null; } return (await response.json()) as SsfClientStream; }, (stream) => { setClientStream(stream); if (stream?.status) { // Re-seed the form-bound status field so clicking "Refresh" // picks up receiver-driven status changes (e.g. the receiver // paused the stream via POST /streams/status) instead of // showing the stale value the page loaded with. setValue( convertAttributeNameToForm( "attributes.ssf.stream.status", ), stream.status, { shouldDirty: false }, ); } // Re-seed the form-bound delivery selector from the actual // stream's delivery.method so the dropdown matches reality. The // attribute defaults to "PUSH" for new clients and only gets // overwritten on save — without this, opening a client whose // stream was created as POLL still shows "Push" in the picker. if (stream?.delivery?.method) { setValue( convertAttributeNameToForm("attributes.ssf.delivery"), isPollDeliveryMethod(stream.delivery.method) ? "POLL" : "PUSH", { shouldDirty: false }, ); } }, [client.id, streamFetchKey], ); const resetFields = (names: string[]) => { for (const name of names) { setValue( convertAttributeNameToForm(`attributes.${name}`), client.attributes?.[name] || "", ); } }; const reset = () => resetFields([ "ssf.description", "ssf.streamAudience", "ssf.supportedEvents", "ssf.emitOnlyEvents", "ssf.profile", "ssf.userSubjectFormat", "ssf.defaultSubjects", "ssf.autoNotifyOnLogin", "ssf.requireServiceAccount", "ssf.requiredRole", "ssf.allowEmitEvents", "ssf.emitEventsRole", "ssf.minVerificationInterval", "ssf.autoVerifyStream", "ssf.verificationDelayMillis", "ssf.delivery", "ssf.pushEndpointConnectTimeoutMillis", "ssf.pushEndpointSocketTimeoutMillis", "ssf.maxEventAgeSeconds", "ssf.inactivityTimeoutSeconds", "ssf.subjectRemovalGraceSeconds", ]); return ( {activeTab === "receiver" && ( )} {activeTab === "stream" && ( )} {activeTab === "subjects" && } {activeTab === "event-search" && } {activeTab === "emit-events" && ( )} ); };