"use client"; import { useEffect, useState, useSyncExternalStore } from "react"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import * as z from "zod"; import { Card, CardContent, CardDescription, CardFooter, 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, ArrowLeft, ArrowRight, Lock } 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/utils"; import { AxiosResponse } from "axios"; import LoginForm from "@app/components/LoginForm"; import { AuthWithPasswordResponse } from "@server/routers/resource"; import { redirect } from "next/dist/server/api-utils"; import ResourceAccessDenied from "./ResourceAccessDenied"; import { createApiClient } from "@app/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useToast } from "@app/hooks/useToast"; const pin = z .string() .length(6, { message: "PIN must be exactly 6 digits" }) .regex(/^\d+$/, { message: "PIN must only contain numbers" }); const pinSchema = z.object({ pin }); const pinRequestOtpSchema = z.object({ pin, email: z.string().email() }); const pinOtpSchema = z.object({ pin, email: z.string().email(), otp: z.string() }); const password = z.string().min(1, { message: "Password must be at least 1 character long" }); const passwordSchema = z.object({ password }); const passwordRequestOtpSchema = z.object({ password, email: z.string().email() }); const passwordOtpSchema = z.object({ password, email: z.string().email(), otp: z.string() }); type ResourceAuthPortalProps = { methods: { password: boolean; pincode: boolean; sso: boolean; }; resource: { name: string; id: number; }; redirect: string; }; export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { const router = useRouter(); const { toast } = useToast(); const getNumMethods = () => { let colLength = 0; if (props.methods.pincode) colLength++; if (props.methods.password) colLength++; if (props.methods.sso) colLength++; return colLength; }; const [numMethods, setNumMethods] = useState(getNumMethods()); const [passwordError, setPasswordError] = useState(null); const [pincodeError, setPincodeError] = useState(null); const [accessDenied, setAccessDenied] = useState(false); const [loadingLogin, setLoadingLogin] = useState(false); const [otpState, setOtpState] = useState< "idle" | "otp_requested" | "otp_sent" >("idle"); const api = createApiClient(useEnvContext()); function getDefaultSelectedMethod() { if (props.methods.sso) { return "sso"; } if (props.methods.password) { return "password"; } if (props.methods.pincode) { return "pin"; } } const [activeTab, setActiveTab] = useState(getDefaultSelectedMethod()); const pinForm = useForm>({ resolver: zodResolver(pinSchema), defaultValues: { pin: "" } }); const pinRequestOtpForm = useForm>({ resolver: zodResolver(pinRequestOtpSchema), defaultValues: { pin: "", email: "" } }); const pinOtpForm = useForm>({ resolver: zodResolver(pinOtpSchema), defaultValues: { pin: "", email: "", otp: "" } }); const passwordForm = useForm>({ resolver: zodResolver(passwordSchema), defaultValues: { password: "" } }); const passwordRequestOtpForm = useForm< z.infer >({ resolver: zodResolver(passwordRequestOtpSchema), defaultValues: { password: "", email: "" } }); const passwordOtpForm = useForm>({ resolver: zodResolver(passwordOtpSchema), defaultValues: { password: "", email: "", otp: "" } }); const onPinSubmit = (values: any) => { setLoadingLogin(true); api.post>( `/auth/resource/${props.resource.id}/pincode`, { pincode: values.pin, email: values.email, otp: values.otp } ) .then((res) => { setPincodeError(null); if (res.data.data.otpRequested) { setOtpState("otp_requested"); pinRequestOtpForm.setValue("pin", values.pin); return; } else if (res.data.data.otpSent) { pinOtpForm.setValue("email", values.email); pinOtpForm.setValue("pin", values.pin); toast({ title: "OTP Sent", description: `OTP sent to ${values.email}` }); setOtpState("otp_sent"); return; } const session = res.data.data.session; if (session) { window.location.href = props.redirect; } }) .catch((e) => { console.error(e); setPincodeError( formatAxiosError(e, "Failed to authenticate with pincode") ); }) .then(() => setLoadingLogin(false)); }; const resetPasswordForms = () => { passwordForm.reset(); passwordRequestOtpForm.reset(); passwordOtpForm.reset(); setOtpState("idle"); setPasswordError(null); }; const resetPinForms = () => { pinForm.reset(); pinRequestOtpForm.reset(); pinOtpForm.reset(); setOtpState("idle"); setPincodeError(null); } const onPasswordSubmit = (values: any) => { setLoadingLogin(true); api.post>( `/auth/resource/${props.resource.id}/password`, { password: values.password, email: values.email, otp: values.otp } ) .then((res) => { setPasswordError(null); if (res.data.data.otpRequested) { setOtpState("otp_requested"); passwordRequestOtpForm.setValue( "password", values.password ); return; } else if (res.data.data.otpSent) { passwordOtpForm.setValue("email", values.email); passwordOtpForm.setValue("password", values.password); toast({ title: "OTP Sent", description: `OTP sent to ${values.email}` }); setOtpState("otp_sent"); return; } const session = res.data.data.session; if (session) { window.location.href = props.redirect; } }) .catch((e) => { console.error(e); setPasswordError( formatAxiosError(e, "Failed to authenticate with password") ); }) .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; } } return (
{!accessDenied ? (
Powered by Fossorial
Authentication Required {numMethods > 1 ? `Choose your preferred method to access ${props.resource.name}` : `You must authenticate to access ${props.resource.name}`} {numMethods > 1 && ( {props.methods.pincode && ( {" "} PIN )} {props.methods.password && ( {" "} Password )} {props.methods.sso && ( {" "} User )} )} {props.methods.pincode && ( {otpState === "idle" && (
( 6-digit PIN Code
)} /> {pincodeError && ( {pincodeError} )} )} {otpState === "otp_requested" && (
( Email A one-time code will be sent to this email. )} /> {pincodeError && ( {pincodeError} )} )} {otpState === "otp_sent" && (
( One-Time Password (OTP) )} /> {pincodeError && ( {pincodeError} )} )}
)} {props.methods.password && ( {otpState === "idle" && (
( Password )} /> {passwordError && ( {passwordError} )} )} {otpState === "otp_requested" && (
( Email A one-time code will be sent to this email. )} /> {passwordError && ( {passwordError} )} )} {otpState === "otp_sent" && (
( One-Time Password (OTP) )} /> {passwordError && ( {passwordError} )} )}
)} {props.methods.sso && ( await handleSSOAuth() } /> )}
{/* {activeTab === "sso" && (

Don't have an account?{" "} Sign up

)} */}
) : ( )}
); }