"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 { formatAxiosError } from "@app/lib/api"; import { AxiosResponse } from "axios"; import LoginForm, { LoginFormIDP } from "@app/components/LoginForm"; import { AuthWithPasswordResponse, AuthWithWhitelistResponse } from "@server/routers/resource"; import ResourceAccessDenied from "@app/components/ResourceAccessDenied"; 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 { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext"; import { useTranslations } from "next-intl"; 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[]; }; export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { const router = useRouter(); const t = useTranslations(); 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 api = createApiClient({ env }); 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 = (values: any) => { setLoadingLogin(true); api.post>( `/auth/resource/${props.resource.id}/whitelist`, { email: values.email, otp: values.otp } ) .then((res) => { setWhitelistError(null); if (res.data.data.otpSent) { setOtpState("otp_sent"); submitOtpForm.setValue("email", values.email); toast({ title: t("otpEmailSent"), description: t("otpEmailSentDescription") }); return; } const session = res.data.data.session; if (session) { window.location.href = appendRequestToken( props.redirect, session ); } }) .catch((e) => { console.error(e); setWhitelistError( formatAxiosError(e, t("otpEmailErrorAuthenticate")) ); }) .then(() => setLoadingLogin(false)); }; const onPinSubmit = (values: z.infer) => { setLoadingLogin(true); api.post>( `/auth/resource/${props.resource.id}/pincode`, { pincode: values.pin } ) .then((res) => { setPincodeError(null); const session = res.data.data.session; if (session) { window.location.href = appendRequestToken( props.redirect, session ); } }) .catch((e) => { console.error(e); setPincodeError( formatAxiosError(e, t("pincodeErrorAuthenticate")) ); }) .then(() => setLoadingLogin(false)); }; const onPasswordSubmit = (values: z.infer) => { setLoadingLogin(true); api.post>( `/auth/resource/${props.resource.id}/password`, { password: values.password } ) .then((res) => { setPasswordError(null); const session = res.data.data.session; if (session) { window.location.href = appendRequestToken( props.redirect, session ); } }) .catch((e) => { console.error(e); setPasswordError( formatAxiosError(e, t("passwordErrorAuthenticate")) ); }) .finally(() => setLoadingLogin(false)); }; async function handleSSOAuth() { let isAllowed = false; try { await api.get(`/resource/${props.resource.id}`); isAllowed = true; } catch (e) { setAccessDenied(true); } if (isAllowed) { // window.location.href = props.redirect; router.refresh(); } } function getTitle() { return t("authenticationRequired"); } function getSubtitle(resourceName: string) { return numMethods > 1 ? t("authenticationMethodChoose", { name: props.resource.name }) : t("authenticationRequest", { name: props.resource.name }); } return (
{!accessDenied ? (
{t("poweredBy")}{" "} Pangolin
{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")}
)}
) : ( )}
); }