"use client"; import { Button } from "@app/components/ui/button"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { toast } from "@app/hooks/useToast"; import { useState, useEffect, useActionState } from "react"; import { Form } from "@/components/ui/form"; import { Label } from "@/components/ui/label"; import { z } from "zod"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { formatAxiosError } from "@app/lib/api"; import { AxiosResponse } from "axios"; import { useRouter } from "next/navigation"; import { SettingsSection, SettingsSectionHeader, SettingsSectionTitle, SettingsSectionDescription, SettingsSectionBody, SettingsSectionForm } from "@app/components/Settings"; import { useTranslations } from "next-intl"; import { GetLoginPageResponse } from "@server/routers/loginPage/types"; import { ListDomainsResponse } from "@server/routers/domain"; import { DomainRow } from "@app/components/DomainsTable"; import { toUnicode } from "punycode"; import { Globe, Trash2 } from "lucide-react"; import CertificateStatus from "@app/components/CertificateStatus"; import { Credenza, CredenzaBody, CredenzaClose, CredenzaContent, CredenzaDescription, CredenzaFooter, CredenzaHeader, CredenzaTitle } from "@app/components/Credenza"; import DomainPicker from "@app/components/DomainPicker"; import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils"; import { InfoPopup } from "@app/components/ui/info-popup"; import { build } from "@server/build"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; // Auth page form schema const AuthPageFormSchema = z.object({ authPageDomainId: z.string().optional(), authPageSubdomain: z.string().optional() }); interface AuthPageSettingsProps { onSaveSuccess?: () => void; onSaveError?: (error: any) => void; loginPage: GetLoginPageResponse | null; } export interface AuthPageSettingsRef { saveAuthSettings: () => Promise; hasUnsavedChanges: () => boolean; } function AuthPageSettings({ onSaveSuccess, onSaveError, loginPage: defaultLoginPage }: AuthPageSettingsProps) { const { org } = useOrgContext(); const api = createApiClient(useEnvContext()); const router = useRouter(); const t = useTranslations(); const { env } = useEnvContext(); const { isPaidUser } = usePaidStatus(); // Auth page domain state const [loginPage, setLoginPage] = useState(defaultLoginPage); const [, formAction, isSubmitting] = useActionState(onSubmit, null); const [loginPageExists, setLoginPageExists] = useState( Boolean(defaultLoginPage) ); const [editDomainOpen, setEditDomainOpen] = useState(false); const [baseDomains, setBaseDomains] = useState([]); const [selectedDomain, setSelectedDomain] = useState<{ domainId: string; subdomain?: string; fullDomain: string; baseDomain: string; } | null>(null); const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); const form = useForm({ resolver: zodResolver(AuthPageFormSchema), defaultValues: { authPageDomainId: loginPage?.domainId || "", authPageSubdomain: loginPage?.subdomain || "" }, mode: "onChange" }); // Fetch login page and domains data useEffect(() => { const fetchDomains = async () => { try { const res = await api.get>( `/org/${org?.org.orgId}/domains/` ); if (res.status === 200) { const rawDomains = res.data.data.domains as DomainRow[]; const domains = rawDomains.map((domain) => ({ ...domain, baseDomain: toUnicode(domain.baseDomain) })); setBaseDomains(domains); } } catch (err) { console.error("Failed to fetch domains:", err); } }; if (org?.org.orgId) { fetchDomains(); } }, []); // Handle domain selection from modal function handleDomainSelection(domain: { domainId: string; subdomain?: string; fullDomain: string; baseDomain: string; }) { form.setValue("authPageDomainId", domain.domainId); form.setValue("authPageSubdomain", domain.subdomain || ""); setEditDomainOpen(false); // Update loginPage state to show the selected domain immediately const sanitizedSubdomain = domain.subdomain ? finalizeSubdomainSanitize(domain.subdomain) : ""; const sanitizedFullDomain = sanitizedSubdomain ? `${sanitizedSubdomain}.${domain.baseDomain}` : domain.baseDomain; // Only update loginPage state if a login page already exists if (loginPageExists && loginPage) { setLoginPage({ ...loginPage, domainId: domain.domainId, subdomain: sanitizedSubdomain, fullDomain: sanitizedFullDomain }); } setHasUnsavedChanges(true); } // Clear auth page domain function clearAuthPageDomain() { form.setValue("authPageDomainId", ""); form.setValue("authPageSubdomain", ""); setLoginPage(null); setHasUnsavedChanges(true); } async function onSubmit() { const isValid = await form.trigger(); if (!isValid) return; const data = form.getValues(); try { // Handle auth page domain if (data.authPageDomainId) { if (isPaidUser(tierMatrix.loginPageDomain)) { const sanitizedSubdomain = data.authPageSubdomain ? finalizeSubdomainSanitize(data.authPageSubdomain) : ""; if (loginPageExists) { // Login page exists on server - need to update it // First, we need to get the loginPageId from the server since loginPage might be null locally let loginPageId: number; if (loginPage) { // We have the loginPage data locally loginPageId = loginPage.loginPageId; } else { // User cleared selection locally, but login page still exists on server // We need to fetch it to get the loginPageId const fetchRes = await api.get< AxiosResponse >(`/org/${org?.org.orgId}/login-page`); loginPageId = fetchRes.data.data.loginPageId; } // Update existing auth page domain const updateRes = await api.post( `/org/${org?.org.orgId}/login-page/${loginPageId}`, { domainId: data.authPageDomainId, subdomain: sanitizedSubdomain || null } ); if (updateRes.status === 201) { setLoginPage(updateRes.data.data); setLoginPageExists(true); } } else { // No login page exists on server - create new one const createRes = await api.put( `/org/${org?.org.orgId}/login-page`, { domainId: data.authPageDomainId, subdomain: sanitizedSubdomain || null } ); if (createRes.status === 201) { setLoginPage(createRes.data.data); setLoginPageExists(true); } } } } else if (loginPageExists) { // Delete existing auth page domain if no domain selected let loginPageId: number; if (loginPage) { // We have the loginPage data locally loginPageId = loginPage.loginPageId; } else { // User cleared selection locally, but login page still exists on server // We need to fetch it to get the loginPageId const fetchRes = await api.get< AxiosResponse >(`/org/${org?.org.orgId}/login-page`); loginPageId = fetchRes.data.data.loginPageId; } await api.delete( `/org/${org?.org.orgId}/login-page/${loginPageId}` ); setLoginPage(null); setLoginPageExists(false); } setHasUnsavedChanges(false); router.refresh(); onSaveSuccess?.(); toast({ variant: "default", title: t("success"), description: t("authPageDomainUpdated") }); } catch (e) { toast({ variant: "destructive", title: t("authPageErrorUpdate"), description: formatAxiosError( e, t("authPageErrorUpdateMessage") ) }); onSaveError?.(e); } } return ( <> {t("customDomain")} {t("authPageDescription")}
{loginPage && !loginPage.domainId ? ( ) : loginPage?.fullDomain ? ( {`${window.location.protocol}//${loginPage.fullDomain}`} ) : form.watch( "authPageDomainId" ) ? ( // Show selected domain from form state when no loginPage exists yet (() => { const selectedDomainId = form.watch( "authPageDomainId" ); const selectedSubdomain = form.watch( "authPageSubdomain" ); const domain = baseDomains.find( (d) => d.domainId === selectedDomainId ); if (domain) { const sanitizedSubdomain = selectedSubdomain ? finalizeSubdomainSanitize( selectedSubdomain ) : ""; const fullDomain = sanitizedSubdomain ? `${sanitizedSubdomain}.${domain.baseDomain}` : domain.baseDomain; return fullDomain; } return t("noDomainSet"); })() ) : ( t("noDomainSet") )}
{form.watch("authPageDomainId") && ( )}
{!form.watch("authPageDomainId") && (
{t( "addDomainToEnableCustomAuthPages" )}
)} {env.flags.usePangolinDns && (build === "enterprise" || !isPaidUser( tierMatrix.loginPageDomain )) && loginPage?.domainId && loginPage?.fullDomain && !hasUnsavedChanges && ( )}
{/* Domain Picker Modal */} setEditDomainOpen(setOpen)} > {loginPage ? t("editAuthPageDomain") : t("setAuthPageDomain")} {t("selectDomainForOrgAuthPage")} { const selected = res === null ? null : { domainId: res.domainId, subdomain: res.subdomain, fullDomain: res.fullDomain, baseDomain: res.baseDomain }; setSelectedDomain(selected); }} /> ); } AuthPageSettings.displayName = "AuthPageSettings"; export default AuthPageSettings;