"use client"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { Button } from "@app/components/ui/button"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@app/components/ui/form"; import { Input } from "@app/components/ui/input"; import { useForm } from "react-hook-form"; import { toast } from "@app/hooks/useToast"; import { useRouter, useParams } from "next/navigation"; import Link from "next/link"; import { SettingsContainer, SettingsSection, SettingsSectionHeader, SettingsSectionTitle, SettingsSectionDescription, SettingsSectionBody, SettingsSectionForm, SettingsSectionGrid } from "@app/components/Settings"; import { formatAxiosError } from "@app/lib/api"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useState, useEffect } from "react"; import IdpAutoProvisionUsersDescription from "@app/components/IdpAutoProvisionUsersDescription"; import { SwitchInput } from "@app/components/SwitchInput"; import { InfoSection, InfoSectionContent, InfoSections, InfoSectionTitle } from "@app/components/InfoSection"; import CopyToClipboard from "@app/components/CopyToClipboard"; import IdpTypeBadge from "@app/components/IdpTypeBadge"; import IdpIdentifierChangeDialog from "@app/components/IdpIdentifierChangeDialog"; import { useTranslations } from "next-intl"; export default function GeneralPage() { const { env } = useEnvContext(); const api = createApiClient({ env }); const router = useRouter(); const { idpId } = useParams(); const [loading, setLoading] = useState(false); const [initialLoading, setInitialLoading] = useState(true); const [variant, setVariant] = useState<"oidc" | "google" | "azure">("oidc"); const [originalIdentifierPath, setOriginalIdentifierPath] = useState(""); const [identifierConfirmOpen, setIdentifierConfirmOpen] = useState(false); const [pendingPayload, setPendingPayload] = useState | null>(null); const redirectUrl = `${env.app.dashboardUrl}/auth/idp/${idpId}/oidc/callback`; const t = useTranslations(); const OidcFormSchema = z.object({ name: z.string().min(2, { message: t("nameMin", { len: 2 }) }), clientId: z.string().min(1, { message: t("idpClientIdRequired") }), clientSecret: z .string() .min(1, { message: t("idpClientSecretRequired") }), authUrl: z.url({ message: t("idpErrorAuthUrlInvalid") }), tokenUrl: z.url({ message: t("idpErrorTokenUrlInvalid") }), identifierPath: z.string().min(1, { message: t("idpPathRequired") }), emailPath: z.string().optional(), namePath: z.string().optional(), scopes: z.string().min(1, { message: t("idpScopeRequired") }), autoProvision: z.boolean().default(false) }); const GoogleFormSchema = z.object({ name: z.string().min(2, { message: t("nameMin", { len: 2 }) }), clientId: z.string().min(1, { message: t("idpClientIdRequired") }), clientSecret: z .string() .min(1, { message: t("idpClientSecretRequired") }), autoProvision: z.boolean().default(false) }); const AzureFormSchema = z.object({ name: z.string().min(2, { message: t("nameMin", { len: 2 }) }), clientId: z.string().min(1, { message: t("idpClientIdRequired") }), clientSecret: z .string() .min(1, { message: t("idpClientSecretRequired") }), tenantId: z.string().min(1, { message: t("idpTenantIdRequired") }), autoProvision: z.boolean().default(false) }); type OidcFormValues = z.infer; type GoogleFormValues = z.infer; type AzureFormValues = z.infer; type GeneralFormValues = | OidcFormValues | GoogleFormValues | AzureFormValues; const getFormSchema = () => { switch (variant) { case "google": return GoogleFormSchema; case "azure": return AzureFormSchema; default: return OidcFormSchema; } }; const form = useForm({ resolver: zodResolver(getFormSchema()) as never, defaultValues: { name: "", clientId: "", clientSecret: "", authUrl: "", tokenUrl: "", identifierPath: "sub", emailPath: "email", namePath: "name", scopes: "openid profile email", autoProvision: true, tenantId: "" } }); useEffect(() => { form.clearErrors(); }, [variant, form]); useEffect(() => { const loadIdp = async () => { try { const res = await api.get(`/idp/${idpId}`); if (res.status === 200) { const data = res.data.data; const idpVariant = (data.idpOidcConfig?.variant as | "oidc" | "google" | "azure") || "oidc"; setVariant(idpVariant); setOriginalIdentifierPath( data.idpOidcConfig?.identifierPath ?? "sub" ); let tenantId = ""; if (idpVariant === "azure" && data.idpOidcConfig?.authUrl) { const tenantMatch = data.idpOidcConfig.authUrl.match( /login\.microsoftonline\.com\/([^/]+)\/oauth2/ ); if (tenantMatch) { tenantId = tenantMatch[1]; } } const formData: Record = { name: data.idp.name, clientId: data.idpOidcConfig.clientId, clientSecret: data.idpOidcConfig.clientSecret, autoProvision: data.idp.autoProvision }; if (idpVariant === "oidc") { formData.authUrl = data.idpOidcConfig.authUrl; formData.tokenUrl = data.idpOidcConfig.tokenUrl; formData.identifierPath = data.idpOidcConfig.identifierPath; formData.emailPath = data.idpOidcConfig.emailPath ?? undefined; formData.namePath = data.idpOidcConfig.namePath ?? undefined; formData.scopes = data.idpOidcConfig.scopes; } else if (idpVariant === "azure") { formData.tenantId = tenantId; } form.reset(formData as GeneralFormValues); } } catch (e) { toast({ title: t("error"), description: formatAxiosError(e), variant: "destructive" }); router.push("/admin/idp"); } finally { setInitialLoading(false); } }; loadIdp(); }, [idpId]); async function onSubmit(data: GeneralFormValues) { setLoading(true); try { const schema = getFormSchema(); const validationResult = schema.safeParse(data); if (!validationResult.success) { const errors = validationResult.error.flatten().fieldErrors; Object.keys(errors).forEach((key) => { const fieldName = key as keyof GeneralFormValues; const errorMessage = (errors as Record)[ key ]?.[0] || t("invalidValue"); form.setError(fieldName, { type: "manual", message: errorMessage }); }); setLoading(false); return; } let payload: Record = { name: data.name, clientId: data.clientId, clientSecret: data.clientSecret, autoProvision: data.autoProvision, variant }; if (variant === "oidc") { const oidcData = data as OidcFormValues; payload = { ...payload, authUrl: oidcData.authUrl, tokenUrl: oidcData.tokenUrl, identifierPath: oidcData.identifierPath, emailPath: oidcData.emailPath ?? "", namePath: oidcData.namePath ?? "", scopes: oidcData.scopes }; } else if (variant === "azure") { const azureData = data as AzureFormValues; const authUrl = `https://login.microsoftonline.com/${azureData.tenantId}/oauth2/v2.0/authorize`; const tokenUrl = `https://login.microsoftonline.com/${azureData.tenantId}/oauth2/v2.0/token`; payload = { ...payload, authUrl, tokenUrl, identifierPath: "email", emailPath: "email", namePath: "name", scopes: "openid profile email" }; } else if (variant === "google") { payload = { ...payload, authUrl: "https://accounts.google.com/o/oauth2/v2/auth", tokenUrl: "https://oauth2.googleapis.com/token", identifierPath: "email", emailPath: "email", namePath: "name", scopes: "openid profile email" }; } const nextIdentifierPath = variant === "oidc" ? (data as OidcFormValues).identifierPath : undefined; if ( typeof nextIdentifierPath === "string" && nextIdentifierPath !== originalIdentifierPath ) { setPendingPayload(payload); setIdentifierConfirmOpen(true); return; } await persistIdp(payload); } catch (e) { toast({ title: t("error"), description: formatAxiosError(e), variant: "destructive" }); } finally { setLoading(false); } } async function persistIdp(payload: Record) { const res = await api.post(`/idp/${idpId}/oidc`, payload); if (res.status === 200) { if (typeof payload.identifierPath === "string") { setOriginalIdentifierPath(payload.identifierPath); } toast({ title: t("success"), description: t("idpUpdatedDescription") }); router.refresh(); } } async function confirmIdentifierChange() { if (!pendingPayload) { return; } setLoading(true); try { await persistIdp(pendingPayload); setPendingPayload(null); } catch (e) { toast({ title: t("error"), description: formatAxiosError(e), variant: "destructive" }); } finally { setLoading(false); } } if (initialLoading) { return null; } return ( <> { setIdentifierConfirmOpen(open); if (!open) { setPendingPayload(null); } }} onConfirm={confirmIdentifierChange} /> {t("idpTitle")} {t("idpSettingsDescription")} {t("redirectUrl")}
{t("idpTypeLabel")}:
( {t("name")} {t("idpDisplayName")} )} />
{t("idpAutoProvisionUsers")}
{ form.setValue( "autoProvision", checked ); }} />
{form.watch("autoProvision") && ( {t.rich( "idpAdminAutoProvisionPoliciesTabHint", { policiesTabLink: ( chunks ) => ( {chunks} ) } )} )}
{variant === "google" && ( {t("idpGoogleConfiguration")} {t("idpGoogleConfigurationDescription")}
( {t("idpClientId")} {t( "idpGoogleClientIdDescription" )} )} /> ( {t("idpClientSecret")} {t( "idpGoogleClientSecretDescription" )} )} />
)} {variant === "azure" && ( {t("idpAzureConfiguration")} {t("idpAzureConfigurationDescription")}
( {t("idpTenantId")} {t( "idpAzureTenantIdDescription" )} )} /> ( {t("idpClientId")} {t( "idpAzureClientIdDescription" )} )} /> ( {t("idpClientSecret")} {t( "idpAzureClientSecretDescription" )} )} />
)} {variant === "oidc" && ( {t("idpOidcConfigure")} {t("idpOidcConfigureDescription")}
( {t("idpClientId")} {t( "idpClientIdDescription" )} )} /> ( {t( "idpClientSecret" )} {t( "idpClientSecretDescription" )} )} /> ( {t("idpAuthUrl")} {t( "idpAuthUrlDescription" )} )} /> ( {t("idpTokenUrl")} {t( "idpTokenUrlDescription" )} )} />
{t("idpToken")} {t("idpTokenDescription")}
( {t( "idpJmespathLabel" )} {t( "idpJmespathLabelDescription" )} )} /> ( {t( "idpJmespathEmailPathOptional" )} {t( "idpJmespathEmailPathOptionalDescription" )} )} /> ( {t( "idpJmespathNamePathOptional" )} {t( "idpJmespathNamePathOptionalDescription" )} )} /> ( {t( "idpOidcConfigureScopes" )} {t( "idpOidcConfigureScopesDescription" )} )} />
)}
); }