"use client"; 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@app/components/ui/select"; import { useToast } from "@app/hooks/useToast"; import { zodResolver } from "@hookform/resolvers/zod"; import { InviteUserBody, InviteUserResponse } from "@server/routers/user"; import { AxiosResponse } from "axios"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; import CopyTextBox from "@app/components/CopyTextBox"; import { Credenza, CredenzaBody, CredenzaClose, CredenzaContent, CredenzaDescription, CredenzaFooter, CredenzaHeader, CredenzaTitle } from "@app/components/Credenza"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { ListRolesResponse } from "@server/routers/role"; import { formatAxiosError } from "@app/lib/api"; import { cn } from "@app/lib/cn"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { ListResourcesResponse } from "@server/routers/resource"; import { Popover, PopoverContent, PopoverTrigger } from "@app/components/ui/popover"; import { CaretSortIcon } from "@radix-ui/react-icons"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@app/components/ui/command"; import { CheckIcon, ChevronsUpDown } from "lucide-react"; import { register } from "module"; import { Label } from "@app/components/ui/label"; import { Checkbox } from "@app/components/ui/checkbox"; import { GenerateAccessTokenResponse } from "@server/routers/accessToken"; import { constructDirectShareLink, constructShareLink } from "@app/lib/shareLinks"; import { ShareLinkRow } from "./ShareLinksTable"; import { QRCodeCanvas, QRCodeSVG } from "qrcode.react"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@app/components/ui/collapsible"; type FormProps = { open: boolean; setOpen: (open: boolean) => void; onCreated?: (result: ShareLinkRow) => void; }; const formSchema = z.object({ resourceId: z.number({ message: "Please select a resource" }), resourceName: z.string(), resourceUrl: z.string(), timeUnit: z.string(), timeValue: z.coerce.number().int().positive().min(1), title: z.string().optional() }); export default function CreateShareLinkForm({ open, setOpen, onCreated }: FormProps) { const { toast } = useToast(); const { org } = useOrgContext(); const { env } = useEnvContext(); const api = createApiClient({ env }); const [link, setLink] = useState(null); const [directLink, setDirectLink] = useState(null); const [loading, setLoading] = useState(false); const [neverExpire, setNeverExpire] = useState(false); const [isOpen, setIsOpen] = useState(false); const [resources, setResources] = useState< { resourceId: number; name: string; resourceUrl: string }[] >([]); const timeUnits = [ { unit: "minutes", name: "Minutes" }, { unit: "hours", name: "Hours" }, { unit: "days", name: "Days" }, { unit: "weeks", name: "Weeks" }, { unit: "months", name: "Months" }, { unit: "years", name: "Years" } ]; const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { timeUnit: "days", timeValue: 30, title: "" } }); useEffect(() => { if (!open) { return; } async function fetchResources() { const res = await api .get< AxiosResponse >(`/org/${org?.org.orgId}/resources`) .catch((e) => { console.error(e); toast({ variant: "destructive", title: "Failed to fetch resources", description: formatAxiosError( e, "An error occurred while fetching the resources" ) }); }); if (res?.status === 200) { setResources( res.data.data.resources.filter((r) => { return r.http; }).map((r) => ({ resourceId: r.resourceId, name: r.name, resourceUrl: `${r.ssl ? "https://" : "http://"}${r.fullDomain}/` })) ); } } fetchResources(); }, [open]); async function onSubmit(values: z.infer) { setLoading(true); // convert time to seconds let timeInSeconds = values.timeValue; switch (values.timeUnit) { case "minutes": timeInSeconds *= 60; break; case "hours": timeInSeconds *= 60 * 60; break; case "days": timeInSeconds *= 60 * 60 * 24; break; case "weeks": timeInSeconds *= 60 * 60 * 24 * 7; break; case "months": timeInSeconds *= 60 * 60 * 24 * 30; break; case "years": timeInSeconds *= 60 * 60 * 24 * 365; break; } const res = await api .post>( `/resource/${values.resourceId}/access-token`, { validForSeconds: neverExpire ? undefined : timeInSeconds, title: values.title || `${values.resourceName || "Resource" + values.resourceId} Share Link` } ) .catch((e) => { console.error(e); toast({ variant: "destructive", title: "Failed to create share link", description: formatAxiosError( e, "An error occurred while creating the share link" ) }); }); if (res && res.data.data.accessTokenId) { const token = res.data.data; const link = constructShareLink( values.resourceId, token.accessTokenId, token.accessToken ); setLink(link); const directLink = constructDirectShareLink( env.server.resourceAccessTokenParam, values.resourceUrl, token.accessTokenId, token.accessToken ); setDirectLink(directLink); onCreated?.({ accessTokenId: token.accessTokenId, resourceId: token.resourceId, resourceName: values.resourceName, title: token.title, createdAt: token.createdAt, expiresAt: token.expiresAt }); } setLoading(false); } return ( <> { setOpen(val); setLink(null); setLoading(false); form.reset(); }} > Create Shareable Link Anyone with this link can access the resource
{!link && (
( Resource No resources found {resources.map( ( r ) => ( { form.setValue( "resourceId", r.resourceId ); form.setValue( "resourceName", r.name ); form.setValue( "resourceUrl", r.resourceUrl ); }} > { r.name } ) )} )} /> ( )} />
( )} /> ( )} />
setNeverExpire( val as boolean ) } />

Expiration time is how long the link will be usable and provide access to the resource. After this time, the link will no longer work, and users who used this link will lose access to the resource.

)} {link && (

You will only be able to see this link once. Make sure to copy it.

Anyone with this link can access the resource. Share it with care.

{directLink && (

This link does not require visiting in a browser to complete the redirect. It contains the access token directly in the URL, which can be useful for sharing with clients that do not support redirects.

)}
)}
); }