mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-10 20:02:26 +00:00
complete integration of direct share link as discussed in #35
This commit is contained in:
@@ -57,14 +57,22 @@ import {
|
||||
CommandItem,
|
||||
CommandList
|
||||
} from "@app/components/ui/command";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
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 { constructShareLink } from "@app/lib/shareLinks";
|
||||
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;
|
||||
@@ -75,6 +83,7 @@ type FormProps = {
|
||||
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()
|
||||
@@ -88,14 +97,18 @@ export default function CreateShareLinkForm({
|
||||
const { toast } = useToast();
|
||||
const { org } = useOrgContext();
|
||||
|
||||
const api = createApiClient(useEnvContext());
|
||||
const { env } = useEnvContext();
|
||||
const api = createApiClient({ env });
|
||||
|
||||
const [link, setLink] = useState<string | null>(null);
|
||||
const [directLink, setDirectLink] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [neverExpire, setNeverExpire] = useState(false);
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const [resources, setResources] = useState<
|
||||
{ resourceId: number; name: string }[]
|
||||
{ resourceId: number; name: string; resourceUrl: string }[]
|
||||
>([]);
|
||||
|
||||
const timeUnits = [
|
||||
@@ -139,7 +152,13 @@ export default function CreateShareLinkForm({
|
||||
});
|
||||
|
||||
if (res?.status === 200) {
|
||||
setResources(res.data.data.resources);
|
||||
setResources(
|
||||
res.data.data.resources.map((r) => ({
|
||||
resourceId: r.resourceId,
|
||||
name: r.name,
|
||||
resourceUrl: `${r.ssl ? "https://" : "http://"}${r.fullDomain}/`
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,6 +221,13 @@ export default function CreateShareLinkForm({
|
||||
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,
|
||||
@@ -306,6 +332,10 @@ export default function CreateShareLinkForm({
|
||||
"resourceName",
|
||||
r.name
|
||||
);
|
||||
form.setValue(
|
||||
"resourceUrl",
|
||||
r.resourceUrl
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CheckIcon
|
||||
@@ -462,12 +492,62 @@ export default function CreateShareLinkForm({
|
||||
<QRCodeCanvas value={link} size={200} />
|
||||
</div>
|
||||
|
||||
<div className="mx-auto">
|
||||
<CopyTextBox
|
||||
text={link}
|
||||
wrapText={false}
|
||||
/>
|
||||
</div>
|
||||
<Collapsible
|
||||
open={isOpen}
|
||||
onOpenChange={setIsOpen}
|
||||
className="space-y-2"
|
||||
>
|
||||
<div className="mx-auto">
|
||||
<CopyTextBox
|
||||
text={link}
|
||||
wrapText={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between space-x-4">
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
variant="text"
|
||||
size="sm"
|
||||
className="p-0 flex items-center justify-between w-full"
|
||||
>
|
||||
<h4 className="text-sm font-semibold">
|
||||
See alternative share
|
||||
links
|
||||
</h4>
|
||||
<div>
|
||||
<ChevronsUpDown className="h-4 w-4" />
|
||||
<span className="sr-only">
|
||||
Toggle
|
||||
</span>
|
||||
</div>
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
</div>
|
||||
<CollapsibleContent className="space-y-2">
|
||||
{directLink && (
|
||||
<div className="space-y-1">
|
||||
<div className="mx-auto">
|
||||
<CopyTextBox
|
||||
text={directLink}
|
||||
wrapText={false}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@ import { useRouter } from "next/navigation";
|
||||
// import CreateResourceForm from "./CreateResourceForm";
|
||||
import { useState } from "react";
|
||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||
import { formatAxiosError } from "@app/lib/api";;
|
||||
import { formatAxiosError } from "@app/lib/api";
|
||||
import { useToast } from "@app/hooks/useToast";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
@@ -109,15 +109,14 @@ export default function ShareLinksTable({
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>
|
||||
<button
|
||||
onClick={() =>
|
||||
deleteSharelink(
|
||||
resourceRow.accessTokenId
|
||||
)
|
||||
}
|
||||
className="text-red-500"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
deleteSharelink(
|
||||
resourceRow.accessTokenId
|
||||
);
|
||||
}}
|
||||
>
|
||||
<button className="text-red-500">
|
||||
Delete
|
||||
</button>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -30,6 +30,7 @@ export default function AccessToken({
|
||||
redirectUrl
|
||||
}: AccessTokenProps) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isValid, setIsValid] = useState(false);
|
||||
|
||||
const api = createApiClient(useEnvContext());
|
||||
|
||||
@@ -49,6 +50,7 @@ export default function AccessToken({
|
||||
});
|
||||
|
||||
if (res.data.data.session) {
|
||||
setIsValid(true);
|
||||
window.location.href = redirectUrl;
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -61,24 +63,47 @@ export default function AccessToken({
|
||||
check();
|
||||
}, [accessTokenId, accessToken]);
|
||||
|
||||
function renderTitle() {
|
||||
if (isValid) {
|
||||
return "Access Granted";
|
||||
} else {
|
||||
return "Access URL Invalid";
|
||||
}
|
||||
}
|
||||
|
||||
function renderContent() {
|
||||
if (isValid) {
|
||||
return (
|
||||
<div>
|
||||
You have been granted access to this resource. Redirecting
|
||||
you...
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div>
|
||||
This shared access URL is invalid. Please contact the
|
||||
resource owner for a new URL.
|
||||
<div className="text-center mt-4">
|
||||
<Button>
|
||||
<Link href="/">Go Home</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return loading ? (
|
||||
<div></div>
|
||||
) : (
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-center text-2xl font-bold">
|
||||
Access URL Invalid
|
||||
{renderTitle()}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
This shared access URL is invalid. Please contact the resource
|
||||
owner for a new URL.
|
||||
<div className="text-center mt-4">
|
||||
<Button>
|
||||
<Link href="/">Go Home</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardContent>{renderContent()}</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ const buttonVariants = cva(
|
||||
secondary:
|
||||
"bg-secondary border border-input text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
text: "",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
|
||||
11
src/components/ui/collapsible.tsx
Normal file
11
src/components/ui/collapsible.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root
|
||||
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
||||
|
||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|
||||
@@ -6,7 +6,8 @@ export function pullEnv(): Env {
|
||||
nextPort: process.env.NEXT_PORT as string,
|
||||
externalPort: process.env.SERVER_EXTERNAL_PORT as string,
|
||||
sessionCookieName: process.env.SESSION_COOKIE_NAME as string,
|
||||
resourceSessionCookieName: process.env.RESOURCE_SESSION_COOKIE_NAME as string
|
||||
resourceSessionCookieName: process.env.RESOURCE_SESSION_COOKIE_NAME as string,
|
||||
resourceAccessTokenParam: process.env.RESOURCE_ACCESS_TOKEN_PARAM as string
|
||||
},
|
||||
app: {
|
||||
environment: process.env.ENVIRONMENT as string,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { pullEnv } from "./pullEnv";
|
||||
|
||||
export function constructShareLink(
|
||||
resourceId: number,
|
||||
id: string,
|
||||
@@ -5,3 +7,12 @@ export function constructShareLink(
|
||||
) {
|
||||
return `${window.location.origin}/auth/resource/${resourceId}?token=${id}.${token}`;
|
||||
}
|
||||
|
||||
export function constructDirectShareLink(
|
||||
param: string,
|
||||
resourceUrl: string,
|
||||
id: string,
|
||||
token: string
|
||||
) {
|
||||
return `${resourceUrl}?${param}=${id}.${token}`;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ export type Env = {
|
||||
nextPort: string;
|
||||
sessionCookieName: string;
|
||||
resourceSessionCookieName: string;
|
||||
resourceAccessTokenParam: string;
|
||||
},
|
||||
email: {
|
||||
emailEnabled: boolean;
|
||||
|
||||
Reference in New Issue
Block a user