mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-01 15:49:08 +00:00
add pass access token in headers
This commit is contained in:
105
src/app/[orgId]/settings/share-links/AccessTokenUsage.tsx
Normal file
105
src/app/[orgId]/settings/share-links/AccessTokenUsage.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Check, Copy, Info, InfoIcon } from "lucide-react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||
import CopyTextBox from "@app/components/CopyTextBox";
|
||||
|
||||
interface AccessTokenSectionProps {
|
||||
token: string;
|
||||
tokenId: string;
|
||||
resourceUrl: string;
|
||||
}
|
||||
|
||||
export default function AccessTokenSection({
|
||||
token,
|
||||
tokenId,
|
||||
resourceUrl
|
||||
}: AccessTokenSectionProps) {
|
||||
const { env } = useEnvContext();
|
||||
|
||||
const [copied, setCopied] = useState<string | null>(null);
|
||||
|
||||
const copyToClipboard = (text: string, type: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
setCopied(type);
|
||||
setTimeout(() => setCopied(null), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-start space-x-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Your access token can be passed in two ways: as a query
|
||||
parameter or in the request headers. These must be passed
|
||||
from the client on every request for authenticated access.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="token" className="w-full mt-4">
|
||||
<TabsList className="grid grid-cols-2">
|
||||
<TabsTrigger value="token">Access Token</TabsTrigger>
|
||||
<TabsTrigger value="usage">Usage Examples</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="token" className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<div className="font-bold">Token ID</div>
|
||||
<CopyToClipboard text={tokenId} isLink={false} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="font-bold">Token</div>
|
||||
<CopyToClipboard text={token} isLink={false} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="usage" className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-medium">Request Headers</h3>
|
||||
<CopyTextBox
|
||||
text={`${env.server.resourceAccessTokenHeadersId}: ${tokenId}
|
||||
${env.server.resourceAccessTokenHeadersToken}: ${token}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-medium">Query Parameter</h3>
|
||||
<CopyTextBox
|
||||
text={`${resourceUrl}?${env.server.resourceAccessTokenParam}=${tokenId}.${token}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Alert variant="neutral">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">
|
||||
Important Note
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
For security reasons, using headers is recommended
|
||||
over query parameters when possible, as query
|
||||
parameters may be logged in server logs or browser
|
||||
history.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="text-sm text-muted-foreground mt-4">
|
||||
Keep your access token secure. Do not share it in publicly
|
||||
accessible areas or client-side code.
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import { Button } from "@app/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
@@ -20,7 +19,6 @@ import {
|
||||
} from "@app/components/ui/select";
|
||||
import { toast } 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";
|
||||
@@ -37,7 +35,6 @@ import {
|
||||
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";
|
||||
@@ -58,12 +55,9 @@ import {
|
||||
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";
|
||||
@@ -73,6 +67,7 @@ import {
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger
|
||||
} from "@app/components/ui/collapsible";
|
||||
import AccessTokenSection from "./AccessTokenUsage";
|
||||
|
||||
type FormProps = {
|
||||
open: boolean;
|
||||
@@ -100,7 +95,8 @@ export default function CreateShareLinkForm({
|
||||
const api = createApiClient({ env });
|
||||
|
||||
const [link, setLink] = useState<string | null>(null);
|
||||
const [directLink, setDirectLink] = useState<string | null>(null);
|
||||
const [accessTokenId, setAccessTokenId] = useState<string | null>(null);
|
||||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [neverExpire, setNeverExpire] = useState(false);
|
||||
|
||||
@@ -226,12 +222,9 @@ export default function CreateShareLinkForm({
|
||||
const token = res.data.data;
|
||||
const link = constructShareLink(token.accessToken);
|
||||
setLink(link);
|
||||
const directLink = constructDirectShareLink(
|
||||
env.server.resourceAccessTokenParam,
|
||||
values.resourceUrl,
|
||||
token.accessToken
|
||||
);
|
||||
setDirectLink(directLink);
|
||||
|
||||
setAccessToken(token.accessToken);
|
||||
setAccessTokenId(token.accessTokenId);
|
||||
|
||||
const resource = resources.find(
|
||||
(r) => r.resourceId === values.resourceId
|
||||
@@ -515,8 +508,7 @@ export default function CreateShareLinkForm({
|
||||
className="p-0 flex items-center justify-between w-full"
|
||||
>
|
||||
<h4 className="text-sm font-semibold">
|
||||
See alternative share
|
||||
links
|
||||
See Access Token Usage
|
||||
</h4>
|
||||
<div>
|
||||
<ChevronsUpDown className="h-4 w-4" />
|
||||
@@ -528,26 +520,21 @@ export default function CreateShareLinkForm({
|
||||
</CollapsibleTrigger>
|
||||
</div>
|
||||
<CollapsibleContent className="space-y-2">
|
||||
{directLink && (
|
||||
{accessTokenId && accessToken && (
|
||||
<div className="space-y-2">
|
||||
<div className="mx-auto">
|
||||
<CopyTextBox
|
||||
text={directLink}
|
||||
wrapText={false}
|
||||
<AccessTokenSection
|
||||
tokenId={
|
||||
accessTokenId
|
||||
}
|
||||
token={accessToken}
|
||||
resourceUrl={
|
||||
form.getValues(
|
||||
"resourceUrl"
|
||||
)
|
||||
}
|
||||
/>
|
||||
</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>
|
||||
|
||||
@@ -617,7 +617,7 @@ PersistentKeepalive = 5`;
|
||||
</InfoSection>
|
||||
</InfoSections>
|
||||
|
||||
<Alert variant="default" className="">
|
||||
<Alert variant="neutral" className="">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">
|
||||
Save Your Credentials
|
||||
@@ -777,7 +777,7 @@ PersistentKeepalive = 5`;
|
||||
<SettingsSectionBody>
|
||||
<CopyTextBox text={wgConfig} />
|
||||
|
||||
<Alert variant="default">
|
||||
<Alert variant="neutral">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">
|
||||
Save Your Credentials
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function CopyTextBox({
|
||||
>
|
||||
<pre
|
||||
ref={textRef}
|
||||
className={`p-4 pr-16 text-sm w-full ${
|
||||
className={`p-2 pr-16 text-sm w-full ${
|
||||
wrapText
|
||||
? "whitespace-pre-wrap break-words"
|
||||
: "overflow-x-auto"
|
||||
@@ -41,10 +41,10 @@ export default function CopyTextBox({
|
||||
<code className="block w-full">{text}</code>
|
||||
</pre>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
type="button"
|
||||
className="absolute top-1 right-1 z-10 bg-card"
|
||||
className="absolute top-0.5 right-0 z-10 bg-card"
|
||||
onClick={copyToClipboard}
|
||||
aria-label="Copy to clipboard"
|
||||
>
|
||||
|
||||
@@ -4,15 +4,16 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@app/lib/cn";
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border-2 p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||
"relative w-full rounded-lg p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-card text-foreground",
|
||||
default: "bg-card border text-foreground",
|
||||
neutral: "bg-card border-2 text-foreground",
|
||||
destructive:
|
||||
"border-destructive/50 bg-destructive/10 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||
"border-destructive/50 border-2 bg-destructive/10 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||
success:
|
||||
"border-green-500/50 bg-green-500/10 text-green-500 dark:border-success [&>svg]:text-green-500",
|
||||
"border-green-500/50 border-2 bg-green-500/10 text-green-500 dark:border-success [&>svg]:text-green-500",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -9,12 +9,16 @@ export function pullEnv(): Env {
|
||||
resourceAccessTokenParam: process.env
|
||||
.RESOURCE_ACCESS_TOKEN_PARAM as string,
|
||||
resourceSessionRequestParam: process.env
|
||||
.RESOURCE_SESSION_REQUEST_PARAM as string
|
||||
.RESOURCE_SESSION_REQUEST_PARAM as string,
|
||||
resourceAccessTokenHeadersId: process.env
|
||||
.RESOURCE_ACCESS_TOKEN_HEADERS_ID as string,
|
||||
resourceAccessTokenHeadersToken: process.env
|
||||
.RESOURCE_ACCESS_TOKEN_HEADERS_TOKEN as string
|
||||
},
|
||||
app: {
|
||||
environment: process.env.ENVIRONMENT as string,
|
||||
version: process.env.APP_VERSION as string,
|
||||
dashboardUrl: process.env.DASHBOARD_URL as string,
|
||||
dashboardUrl: process.env.DASHBOARD_URL as string
|
||||
},
|
||||
email: {
|
||||
emailEnabled: process.env.EMAIL_ENABLED === "true" ? true : false
|
||||
|
||||
@@ -3,11 +3,3 @@ export function constructShareLink(
|
||||
) {
|
||||
return `${window.location.origin}/s/${token!}`;
|
||||
}
|
||||
|
||||
export function constructDirectShareLink(
|
||||
param: string,
|
||||
resourceUrl: string,
|
||||
token: string
|
||||
) {
|
||||
return `${resourceUrl}?${param}=${token}`;
|
||||
}
|
||||
|
||||
@@ -3,22 +3,24 @@ export type Env = {
|
||||
environment: string;
|
||||
version: string;
|
||||
dashboardUrl: string;
|
||||
},
|
||||
};
|
||||
server: {
|
||||
externalPort: string;
|
||||
nextPort: string;
|
||||
sessionCookieName: string;
|
||||
resourceAccessTokenParam: string;
|
||||
resourceSessionRequestParam: string;
|
||||
},
|
||||
resourceAccessTokenHeadersId: string;
|
||||
resourceAccessTokenHeadersToken: string;
|
||||
};
|
||||
email: {
|
||||
emailEnabled: boolean;
|
||||
},
|
||||
};
|
||||
flags: {
|
||||
disableSignupWithoutInvite: boolean;
|
||||
disableUserCreateOrg: boolean;
|
||||
emailVerificationRequired: boolean;
|
||||
allowRawResources: boolean;
|
||||
allowBaseDomainResources: boolean;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user