"use client"; import { useState } from "react"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import * as z from "zod"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { LockIcon, Binary, Key, User, Send, AtSign } from "lucide-react"; import { InputOTP, InputOTPGroup, InputOTPSlot } from "@app/components/ui/input-otp"; import { useRouter } from "next/navigation"; import { Alert, AlertDescription } from "@app/components/ui/alert"; import LoginForm, { LoginFormIDP } from "@app/components/LoginForm"; import ResourceAccessDenied from "@app/components/ResourceAccessDenied"; import { resourcePasswordProxy, resourcePincodeProxy, resourceWhitelistProxy, resourceAccessProxy } from "@app/actions/server"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import Link from "next/link"; import Image from "next/image"; import BrandingLogo from "@app/components/BrandingLogo"; import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext"; import { useTranslations } from "next-intl"; import { build } from "@server/build"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; const pinSchema = z.object({ pin: z .string() .length(6, { message: "PIN must be exactly 6 digits" }) .regex(/^\d+$/, { message: "PIN must only contain numbers" }) }); const passwordSchema = z.object({ password: z.string().min(1, { message: "Password must be at least 1 character long" }) }); const requestOtpSchema = z.object({ email: z.string().email() }); const submitOtpSchema = z.object({ email: z.string().email(), otp: z.string().min(1, { message: "OTP must be at least 1 character long" }) }); type ResourceAuthPortalProps = { methods: { password: boolean; pincode: boolean; sso: boolean; whitelist: boolean; }; resource: { name: string; id: number; }; redirect: string; idps?: LoginFormIDP[]; orgId?: string; }; export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { const router = useRouter(); const t = useTranslations(); const { isUnlocked } = useLicenseStatusContext(); const getNumMethods = () => { let colLength = 0; if (props.methods.pincode) colLength++; if (props.methods.password) colLength++; if (props.methods.sso) colLength++; if (props.methods.whitelist) colLength++; return colLength; }; const [numMethods, setNumMethods] = useState(getNumMethods()); const [passwordError, setPasswordError] = useState(null); const [pincodeError, setPincodeError] = useState(null); const [whitelistError, setWhitelistError] = useState(null); const [accessDenied, setAccessDenied] = useState(false); const [loadingLogin, setLoadingLogin] = useState(false); const [otpState, setOtpState] = useState<"idle" | "otp_sent">("idle"); const { env } = useEnvContext(); const { supporterStatus } = useSupporterStatusContext(); function getDefaultSelectedMethod() { if (props.methods.sso) { return "sso"; } if (props.methods.password) { return "password"; } if (props.methods.pincode) { return "pin"; } if (props.methods.whitelist) { return "whitelist"; } } const [activeTab, setActiveTab] = useState(getDefaultSelectedMethod()); const pinForm = useForm({ resolver: zodResolver(pinSchema), defaultValues: { pin: "" } }); const passwordForm = useForm({ resolver: zodResolver(passwordSchema), defaultValues: { password: "" } }); const requestOtpForm = useForm({ resolver: zodResolver(requestOtpSchema), defaultValues: { email: "" } }); const submitOtpForm = useForm({ resolver: zodResolver(submitOtpSchema), defaultValues: { email: "", otp: "" } }); function appendRequestToken(url: string, token: string) { const fullUrl = new URL(url); fullUrl.searchParams.append( env.server.resourceSessionRequestParam, token ); return fullUrl.toString(); } const onWhitelistSubmit = async (values: any) => { setLoadingLogin(true); setWhitelistError(null); try { const response = await resourceWhitelistProxy(props.resource.id, { email: values.email, otp: values.otp }); if (response.error) { setWhitelistError(response.message); return; } const data = response.data!; if (data.otpSent) { setOtpState("otp_sent"); submitOtpForm.setValue("email", values.email); toast({ title: t("otpEmailSent"), description: t("otpEmailSentDescription") }); return; } const session = data.session; if (session) { window.location.href = appendRequestToken( props.redirect, session ); } } catch (e: any) { console.error(e); setWhitelistError( t("otpEmailErrorAuthenticate", { defaultValue: "An unexpected error occurred. Please try again." }) ); } finally { setLoadingLogin(false); } }; const onPinSubmit = async (values: z.infer) => { setLoadingLogin(true); setPincodeError(null); try { const response = await resourcePincodeProxy(props.resource.id, { pincode: values.pin }); if (response.error) { setPincodeError(response.message); return; } const session = response.data!.session; if (session) { window.location.href = appendRequestToken( props.redirect, session ); } } catch (e: any) { console.error(e); setPincodeError( t("pincodeErrorAuthenticate", { defaultValue: "An unexpected error occurred. Please try again." }) ); } finally { setLoadingLogin(false); } }; const onPasswordSubmit = async (values: z.infer) => { setLoadingLogin(true); setPasswordError(null); try { const response = await resourcePasswordProxy(props.resource.id, { password: values.password }); if (response.error) { setPasswordError(response.message); return; } const session = response.data!.session; if (session) { window.location.href = appendRequestToken( props.redirect, session ); } } catch (e: any) { console.error(e); setPasswordError( t("passwordErrorAuthenticate", { defaultValue: "An unexpected error occurred. Please try again." }) ); } finally { setLoadingLogin(false); } }; async function handleSSOAuth() { let isAllowed = false; try { const response = await resourceAccessProxy(props.resource.id); if (response.error) { setAccessDenied(true); } else { isAllowed = true; } } catch (e) { setAccessDenied(true); } if (isAllowed) { // window.location.href = props.redirect; router.refresh(); } } function getTitle() { if ( isUnlocked() && build !== "oss" && env.branding.resourceAuthPage?.titleText ) { return env.branding.resourceAuthPage.titleText; } return t("authenticationRequired"); } function getSubtitle(resourceName: string) { if ( isUnlocked() && build !== "oss" && env.branding.resourceAuthPage?.subtitleText ) { return env.branding.resourceAuthPage.subtitleText .split("{{resourceName}}") .join(resourceName); } return numMethods > 1 ? t("authenticationMethodChoose", { name: resourceName }) : t("authenticationRequest", { name: resourceName }); } const logoWidth = isUnlocked() ? env.branding.logo?.authPage?.width || 100 : 100; const logoHeight = isUnlocked() ? env.branding.logo?.authPage?.height || 100 : 100; return (
{!accessDenied ? (
{isUnlocked() && build === "enterprise" ? ( !env.branding.resourceAuthPage?.hidePoweredBy && (
{t("poweredBy")}{" "} {env.branding.appName || "Pangolin"}
) ) : (
{t("poweredBy")}{" "} Pangolin
)} {isUnlocked() && build !== "oss" && env.branding?.resourceAuthPage?.showLogo && (
)} {getTitle()} {getSubtitle(props.resource.name)}
{numMethods > 1 && ( {props.methods.pincode && ( {" "} PIN )} {props.methods.password && ( {" "} {t("password")} )} {props.methods.sso && ( {" "} {t("user")} )} {props.methods.whitelist && ( {" "} {t("email")} )} )} {props.methods.pincode && (
( {t( "pincodeInput" )}
)} /> {pincodeError && ( {pincodeError} )}
)} {props.methods.password && (
( {t("password")} )} /> {passwordError && ( {passwordError} )}
)} {props.methods.sso && ( await handleSSOAuth() } /> )} {props.methods.whitelist && ( {otpState === "idle" && (
( {t("email")} {t( "otpEmailDescription" )} )} /> {whitelistError && ( {whitelistError} )} )} {otpState === "otp_sent" && (
( {t( "otpEmail" )} )} /> {whitelistError && ( {whitelistError} )} )}
)}
{supporterStatus?.visible && (
{t("noSupportKey")}
)}
) : ( )}
); }