add pass access token in headers

This commit is contained in:
miloschwartz
2025-04-05 22:28:47 -04:00
parent 74d6b3d902
commit 6cc4bc2645
14 changed files with 333 additions and 161 deletions

View 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>
</>
);
}

View File

@@ -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>

View File

@@ -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

View File

@@ -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"
>

View File

@@ -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: {

View File

@@ -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

View File

@@ -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}`;
}

View File

@@ -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;
}
};
};