mirror of
https://github.com/fosrl/pangolin.git
synced 2025-05-13 13:50:40 +01:00
add pass access token in headers
This commit is contained in:
parent
74d6b3d902
commit
6cc4bc2645
14 changed files with 333 additions and 161 deletions
|
@ -11,10 +11,12 @@ import { verifyPassword } from "./password";
|
||||||
import { encodeHexLowerCase } from "@oslojs/encoding";
|
import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||||
import { sha256 } from "@oslojs/crypto/sha2";
|
import { sha256 } from "@oslojs/crypto/sha2";
|
||||||
|
|
||||||
export async function verifyResourceAccessTokenSHA256({
|
export async function verifyResourceAccessToken({
|
||||||
accessToken
|
accessToken,
|
||||||
|
accessTokenId
|
||||||
}: {
|
}: {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
|
accessTokenId?: string;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
valid: boolean;
|
valid: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
@ -25,6 +27,10 @@ export async function verifyResourceAccessTokenSHA256({
|
||||||
sha256(new TextEncoder().encode(accessToken))
|
sha256(new TextEncoder().encode(accessToken))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let tokenItem: ResourceAccessToken | undefined;
|
||||||
|
let resource: Resource | undefined;
|
||||||
|
|
||||||
|
if (!accessTokenId) {
|
||||||
const [res] = await db
|
const [res] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(resourceAccessToken)
|
.from(resourceAccessToken)
|
||||||
|
@ -34,8 +40,48 @@ export async function verifyResourceAccessTokenSHA256({
|
||||||
eq(resourceAccessToken.resourceId, resources.resourceId)
|
eq(resourceAccessToken.resourceId, resources.resourceId)
|
||||||
);
|
);
|
||||||
|
|
||||||
const tokenItem = res?.resourceAccessToken;
|
tokenItem = res?.resourceAccessToken;
|
||||||
const resource = res?.resources;
|
resource = res?.resources;
|
||||||
|
} else {
|
||||||
|
const [res] = await db
|
||||||
|
.select()
|
||||||
|
.from(resourceAccessToken)
|
||||||
|
.where(and(eq(resourceAccessToken.accessTokenId, accessTokenId)))
|
||||||
|
.innerJoin(
|
||||||
|
resources,
|
||||||
|
eq(resourceAccessToken.resourceId, resources.resourceId)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res && res.resourceAccessToken) {
|
||||||
|
if (res.resourceAccessToken.tokenHash?.startsWith("$argon")) {
|
||||||
|
const validCode = await verifyPassword(
|
||||||
|
accessToken,
|
||||||
|
res.resourceAccessToken.tokenHash
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!validCode) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: "Invalid access token"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const tokenHash = encodeHexLowerCase(
|
||||||
|
sha256(new TextEncoder().encode(accessToken))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.resourceAccessToken.tokenHash !== tokenHash) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: "Invalid access token"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenItem = res?.resourceAccessToken;
|
||||||
|
resource = res?.resources;
|
||||||
|
}
|
||||||
|
|
||||||
if (!tokenItem || !resource) {
|
if (!tokenItem || !resource) {
|
||||||
return {
|
return {
|
||||||
|
@ -60,61 +106,3 @@ export async function verifyResourceAccessTokenSHA256({
|
||||||
resource
|
resource
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function verifyResourceAccessToken({
|
|
||||||
resource,
|
|
||||||
accessTokenId,
|
|
||||||
accessToken
|
|
||||||
}: {
|
|
||||||
resource: Resource;
|
|
||||||
accessTokenId: string;
|
|
||||||
accessToken: string;
|
|
||||||
}): Promise<{
|
|
||||||
valid: boolean;
|
|
||||||
error?: string;
|
|
||||||
tokenItem?: ResourceAccessToken;
|
|
||||||
}> {
|
|
||||||
const [result] = await db
|
|
||||||
.select()
|
|
||||||
.from(resourceAccessToken)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(resourceAccessToken.resourceId, resource.resourceId),
|
|
||||||
eq(resourceAccessToken.accessTokenId, accessTokenId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
const tokenItem = result;
|
|
||||||
|
|
||||||
if (!tokenItem) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
error: "Access token does not exist for resource"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const validCode = await verifyPassword(accessToken, tokenItem.tokenHash);
|
|
||||||
|
|
||||||
if (!validCode) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
error: "Invalid access token"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
tokenItem.expiresAt &&
|
|
||||||
!isWithinExpirationDate(new Date(tokenItem.expiresAt))
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
error: "Access token has expired"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
valid: true,
|
|
||||||
tokenItem
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
@ -66,6 +66,10 @@ const configSchema = z.object({
|
||||||
internal_hostname: z.string().transform((url) => url.toLowerCase()),
|
internal_hostname: z.string().transform((url) => url.toLowerCase()),
|
||||||
session_cookie_name: z.string(),
|
session_cookie_name: z.string(),
|
||||||
resource_access_token_param: z.string(),
|
resource_access_token_param: z.string(),
|
||||||
|
resource_access_token_headers: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
token: z.string()
|
||||||
|
}),
|
||||||
resource_session_request_param: z.string(),
|
resource_session_request_param: z.string(),
|
||||||
dashboard_session_length_hours: z
|
dashboard_session_length_hours: z
|
||||||
.number()
|
.number()
|
||||||
|
@ -239,6 +243,10 @@ export class Config {
|
||||||
: "false";
|
: "false";
|
||||||
process.env.RESOURCE_ACCESS_TOKEN_PARAM =
|
process.env.RESOURCE_ACCESS_TOKEN_PARAM =
|
||||||
parsedConfig.data.server.resource_access_token_param;
|
parsedConfig.data.server.resource_access_token_param;
|
||||||
|
process.env.RESOURCE_ACCESS_TOKEN_HEADERS_ID =
|
||||||
|
parsedConfig.data.server.resource_access_token_headers.id;
|
||||||
|
process.env.RESOURCE_ACCESS_TOKEN_HEADERS_TOKEN =
|
||||||
|
parsedConfig.data.server.resource_access_token_headers.token;
|
||||||
process.env.RESOURCE_SESSION_REQUEST_PARAM =
|
process.env.RESOURCE_SESSION_REQUEST_PARAM =
|
||||||
parsedConfig.data.server.resource_session_request_param;
|
parsedConfig.data.server.resource_session_request_param;
|
||||||
process.env.FLAGS_ALLOW_BASE_DOMAIN_RESOURCES = parsedConfig.data.flags
|
process.env.FLAGS_ALLOW_BASE_DOMAIN_RESOURCES = parsedConfig.data.flags
|
||||||
|
|
|
@ -41,12 +41,13 @@ const cache = new NodeCache({
|
||||||
|
|
||||||
const verifyResourceSessionSchema = z.object({
|
const verifyResourceSessionSchema = z.object({
|
||||||
sessions: z.record(z.string()).optional(),
|
sessions: z.record(z.string()).optional(),
|
||||||
|
headers: z.record(z.string()).optional(),
|
||||||
|
query: z.record(z.string()).optional(),
|
||||||
originalRequestURL: z.string().url(),
|
originalRequestURL: z.string().url(),
|
||||||
scheme: z.string(),
|
scheme: z.string(),
|
||||||
host: z.string(),
|
host: z.string(),
|
||||||
path: z.string(),
|
path: z.string(),
|
||||||
method: z.string(),
|
method: z.string(),
|
||||||
accessToken: z.string().optional(),
|
|
||||||
tls: z.boolean(),
|
tls: z.boolean(),
|
||||||
requestIp: z.string().optional()
|
requestIp: z.string().optional()
|
||||||
});
|
});
|
||||||
|
@ -85,7 +86,8 @@ export async function verifyResourceSession(
|
||||||
originalRequestURL,
|
originalRequestURL,
|
||||||
requestIp,
|
requestIp,
|
||||||
path,
|
path,
|
||||||
accessToken: token
|
headers,
|
||||||
|
query
|
||||||
} = parsedBody.data;
|
} = parsedBody.data;
|
||||||
|
|
||||||
const clientIp = requestIp?.split(":")[0];
|
const clientIp = requestIp?.split(":")[0];
|
||||||
|
@ -183,12 +185,32 @@ export async function verifyResourceSession(
|
||||||
resource.resourceId
|
resource.resourceId
|
||||||
)}?redirect=${encodeURIComponent(originalRequestURL)}`;
|
)}?redirect=${encodeURIComponent(originalRequestURL)}`;
|
||||||
|
|
||||||
// check for access token
|
// check for access token in headers
|
||||||
let validAccessToken: ResourceAccessToken | undefined;
|
if (
|
||||||
if (token) {
|
headers &&
|
||||||
const [accessTokenId, accessToken] = token.split(".");
|
headers[
|
||||||
|
config.getRawConfig().server.resource_access_token_headers.id
|
||||||
|
] &&
|
||||||
|
headers[
|
||||||
|
config.getRawConfig().server.resource_access_token_headers.token
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
const accessTokenId =
|
||||||
|
headers[
|
||||||
|
config.getRawConfig().server.resource_access_token_headers
|
||||||
|
.id
|
||||||
|
];
|
||||||
|
const accessToken =
|
||||||
|
headers[
|
||||||
|
config.getRawConfig().server.resource_access_token_headers
|
||||||
|
.token
|
||||||
|
];
|
||||||
|
|
||||||
const { valid, error, tokenItem } = await verifyResourceAccessToken(
|
const { valid, error, tokenItem } = await verifyResourceAccessToken(
|
||||||
{ resource, accessTokenId, accessToken }
|
{
|
||||||
|
accessToken,
|
||||||
|
accessTokenId
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -206,16 +228,43 @@ export async function verifyResourceSession(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (valid && tokenItem) {
|
if (valid && tokenItem) {
|
||||||
validAccessToken = tokenItem;
|
return allowed(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!sessions) {
|
if (
|
||||||
return await createAccessTokenSession(
|
query &&
|
||||||
res,
|
query[config.getRawConfig().server.resource_access_token_param]
|
||||||
resource,
|
) {
|
||||||
tokenItem
|
const token =
|
||||||
|
query[config.getRawConfig().server.resource_access_token_param];
|
||||||
|
|
||||||
|
const [accessTokenId, accessToken] = token.split(".");
|
||||||
|
|
||||||
|
const { valid, error, tokenItem } = await verifyResourceAccessToken(
|
||||||
|
{
|
||||||
|
accessToken,
|
||||||
|
accessTokenId
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
logger.debug("Access token invalid: " + error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!valid) {
|
||||||
|
if (config.getRawConfig().app.log_failed_attempts) {
|
||||||
|
logger.info(
|
||||||
|
`Resource access token is invalid. Resource ID: ${
|
||||||
|
resource.resourceId
|
||||||
|
}. IP: ${clientIp}.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (valid && tokenItem) {
|
||||||
|
return allowed(res);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sessions) {
|
if (!sessions) {
|
||||||
|
@ -321,16 +370,6 @@ export async function verifyResourceSession(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// At this point we have checked all sessions, but since the access token is
|
|
||||||
// valid, we should allow access and create a new session.
|
|
||||||
if (validAccessToken) {
|
|
||||||
return await createAccessTokenSession(
|
|
||||||
res,
|
|
||||||
resource,
|
|
||||||
validAccessToken
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug("No more auth to check, resource not allowed");
|
logger.debug("No more auth to check, resource not allowed");
|
||||||
|
|
||||||
if (config.getRawConfig().app.log_failed_attempts) {
|
if (config.getRawConfig().app.log_failed_attempts) {
|
||||||
|
@ -360,8 +399,7 @@ function extractResourceSessionToken(
|
||||||
ssl ? "_s" : ""
|
ssl ? "_s" : ""
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const all: { cookieName: string; token: string; priority: number }[] =
|
const all: { cookieName: string; token: string; priority: number }[] = [];
|
||||||
[];
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(sessions)) {
|
for (const [key, value] of Object.entries(sessions)) {
|
||||||
const parts = key.split(".");
|
const parts = key.split(".");
|
||||||
|
|
|
@ -11,8 +11,7 @@ import { fromError } from "zod-validation-error";
|
||||||
import { createResourceSession } from "@server/auth/sessions/resource";
|
import { createResourceSession } from "@server/auth/sessions/resource";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import {
|
import {
|
||||||
verifyResourceAccessToken,
|
verifyResourceAccessToken
|
||||||
verifyResourceAccessTokenSHA256
|
|
||||||
} from "@server/auth/verifyResourceAccessToken";
|
} from "@server/auth/verifyResourceAccessToken";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import stoi from "@server/lib/stoi";
|
import stoi from "@server/lib/stoi";
|
||||||
|
@ -98,7 +97,6 @@ export async function authWithAccessToken(
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await verifyResourceAccessToken({
|
const res = await verifyResourceAccessToken({
|
||||||
resource: foundResource,
|
|
||||||
accessTokenId,
|
accessTokenId,
|
||||||
accessToken
|
accessToken
|
||||||
});
|
});
|
||||||
|
@ -108,7 +106,7 @@ export async function authWithAccessToken(
|
||||||
error = res.error;
|
error = res.error;
|
||||||
resource = foundResource;
|
resource = foundResource;
|
||||||
} else {
|
} else {
|
||||||
const res = await verifyResourceAccessTokenSHA256({
|
const res = await verifyResourceAccessToken({
|
||||||
accessToken
|
accessToken
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -110,9 +110,12 @@ export async function traefikConfigProvider(
|
||||||
userSessionCookieName:
|
userSessionCookieName:
|
||||||
config.getRawConfig().server
|
config.getRawConfig().server
|
||||||
.session_cookie_name,
|
.session_cookie_name,
|
||||||
|
|
||||||
|
// deprecated
|
||||||
accessTokenQueryParam:
|
accessTokenQueryParam:
|
||||||
config.getRawConfig().server
|
config.getRawConfig().server
|
||||||
.resource_access_token_param,
|
.resource_access_token_param,
|
||||||
|
|
||||||
resourceSessionRequestParam:
|
resourceSessionRequestParam:
|
||||||
config.getRawConfig().server
|
config.getRawConfig().server
|
||||||
.resource_session_request_param
|
.resource_session_request_param
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import db from "@server/db";
|
import db from "@server/db";
|
||||||
|
import { configFilePath1, configFilePath2 } from "@server/lib/consts";
|
||||||
import { sql } from "drizzle-orm";
|
import { sql } from "drizzle-orm";
|
||||||
|
import fs from "fs";
|
||||||
|
import yaml from "js-yaml";
|
||||||
|
|
||||||
const version = "1.2.0";
|
const version = "1.2.0";
|
||||||
|
|
||||||
|
@ -19,5 +22,48 @@ export default async function migration() {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Determine which config file exists
|
||||||
|
const filePaths = [configFilePath1, configFilePath2];
|
||||||
|
let filePath = "";
|
||||||
|
for (const path of filePaths) {
|
||||||
|
if (fs.existsSync(path)) {
|
||||||
|
filePath = path;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filePath) {
|
||||||
|
throw new Error(
|
||||||
|
`No config file found (expected config.yml or config.yaml).`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read and parse the YAML file
|
||||||
|
let rawConfig: any;
|
||||||
|
const fileContents = fs.readFileSync(filePath, "utf8");
|
||||||
|
rawConfig = yaml.load(fileContents);
|
||||||
|
|
||||||
|
if (!rawConfig.flags) {
|
||||||
|
rawConfig.flags = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
rawConfig.server.resource_access_token_headers = {
|
||||||
|
id: "P-Access-Token-ID",
|
||||||
|
token: "P-Access-Token"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Write the updated YAML back to the file
|
||||||
|
const updatedYaml = yaml.dump(rawConfig);
|
||||||
|
fs.writeFileSync(filePath, updatedYaml, "utf8");
|
||||||
|
|
||||||
|
console.log(`Added new config option: resource_access_token_headers`);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(
|
||||||
|
`Unable to add new config option: resource_access_token_headers. Please add it manually. https://docs.fossorial.io/Pangolin/Configuration/config`
|
||||||
|
);
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`${version} migration complete`);
|
console.log(`${version} migration complete`);
|
||||||
}
|
}
|
||||||
|
|
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 {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormDescription,
|
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
|
@ -20,7 +19,6 @@ import {
|
||||||
} from "@app/components/ui/select";
|
} from "@app/components/ui/select";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { InviteUserBody, InviteUserResponse } from "@server/routers/user";
|
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
@ -37,7 +35,6 @@ import {
|
||||||
CredenzaTitle
|
CredenzaTitle
|
||||||
} from "@app/components/Credenza";
|
} from "@app/components/Credenza";
|
||||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||||
import { ListRolesResponse } from "@server/routers/role";
|
|
||||||
import { formatAxiosError } from "@app/lib/api";
|
import { formatAxiosError } from "@app/lib/api";
|
||||||
import { cn } from "@app/lib/cn";
|
import { cn } from "@app/lib/cn";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
|
@ -58,12 +55,9 @@ import {
|
||||||
CommandList
|
CommandList
|
||||||
} from "@app/components/ui/command";
|
} from "@app/components/ui/command";
|
||||||
import { CheckIcon, ChevronsUpDown } 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 { Checkbox } from "@app/components/ui/checkbox";
|
||||||
import { GenerateAccessTokenResponse } from "@server/routers/accessToken";
|
import { GenerateAccessTokenResponse } from "@server/routers/accessToken";
|
||||||
import {
|
import {
|
||||||
constructDirectShareLink,
|
|
||||||
constructShareLink
|
constructShareLink
|
||||||
} from "@app/lib/shareLinks";
|
} from "@app/lib/shareLinks";
|
||||||
import { ShareLinkRow } from "./ShareLinksTable";
|
import { ShareLinkRow } from "./ShareLinksTable";
|
||||||
|
@ -73,6 +67,7 @@ import {
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger
|
CollapsibleTrigger
|
||||||
} from "@app/components/ui/collapsible";
|
} from "@app/components/ui/collapsible";
|
||||||
|
import AccessTokenSection from "./AccessTokenUsage";
|
||||||
|
|
||||||
type FormProps = {
|
type FormProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
@ -100,7 +95,8 @@ export default function CreateShareLinkForm({
|
||||||
const api = createApiClient({ env });
|
const api = createApiClient({ env });
|
||||||
|
|
||||||
const [link, setLink] = useState<string | null>(null);
|
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 [loading, setLoading] = useState(false);
|
||||||
const [neverExpire, setNeverExpire] = useState(false);
|
const [neverExpire, setNeverExpire] = useState(false);
|
||||||
|
|
||||||
|
@ -226,12 +222,9 @@ export default function CreateShareLinkForm({
|
||||||
const token = res.data.data;
|
const token = res.data.data;
|
||||||
const link = constructShareLink(token.accessToken);
|
const link = constructShareLink(token.accessToken);
|
||||||
setLink(link);
|
setLink(link);
|
||||||
const directLink = constructDirectShareLink(
|
|
||||||
env.server.resourceAccessTokenParam,
|
setAccessToken(token.accessToken);
|
||||||
values.resourceUrl,
|
setAccessTokenId(token.accessTokenId);
|
||||||
token.accessToken
|
|
||||||
);
|
|
||||||
setDirectLink(directLink);
|
|
||||||
|
|
||||||
const resource = resources.find(
|
const resource = resources.find(
|
||||||
(r) => r.resourceId === values.resourceId
|
(r) => r.resourceId === values.resourceId
|
||||||
|
@ -515,8 +508,7 @@ export default function CreateShareLinkForm({
|
||||||
className="p-0 flex items-center justify-between w-full"
|
className="p-0 flex items-center justify-between w-full"
|
||||||
>
|
>
|
||||||
<h4 className="text-sm font-semibold">
|
<h4 className="text-sm font-semibold">
|
||||||
See alternative share
|
See Access Token Usage
|
||||||
links
|
|
||||||
</h4>
|
</h4>
|
||||||
<div>
|
<div>
|
||||||
<ChevronsUpDown className="h-4 w-4" />
|
<ChevronsUpDown className="h-4 w-4" />
|
||||||
|
@ -528,26 +520,21 @@ export default function CreateShareLinkForm({
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
</div>
|
</div>
|
||||||
<CollapsibleContent className="space-y-2">
|
<CollapsibleContent className="space-y-2">
|
||||||
{directLink && (
|
{accessTokenId && accessToken && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="mx-auto">
|
<div className="mx-auto">
|
||||||
<CopyTextBox
|
<AccessTokenSection
|
||||||
text={directLink}
|
tokenId={
|
||||||
wrapText={false}
|
accessTokenId
|
||||||
|
}
|
||||||
|
token={accessToken}
|
||||||
|
resourceUrl={
|
||||||
|
form.getValues(
|
||||||
|
"resourceUrl"
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
|
|
|
@ -617,7 +617,7 @@ PersistentKeepalive = 5`;
|
||||||
</InfoSection>
|
</InfoSection>
|
||||||
</InfoSections>
|
</InfoSections>
|
||||||
|
|
||||||
<Alert variant="default" className="">
|
<Alert variant="neutral" className="">
|
||||||
<InfoIcon className="h-4 w-4" />
|
<InfoIcon className="h-4 w-4" />
|
||||||
<AlertTitle className="font-semibold">
|
<AlertTitle className="font-semibold">
|
||||||
Save Your Credentials
|
Save Your Credentials
|
||||||
|
@ -777,7 +777,7 @@ PersistentKeepalive = 5`;
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
<CopyTextBox text={wgConfig} />
|
<CopyTextBox text={wgConfig} />
|
||||||
|
|
||||||
<Alert variant="default">
|
<Alert variant="neutral">
|
||||||
<InfoIcon className="h-4 w-4" />
|
<InfoIcon className="h-4 w-4" />
|
||||||
<AlertTitle className="font-semibold">
|
<AlertTitle className="font-semibold">
|
||||||
Save Your Credentials
|
Save Your Credentials
|
||||||
|
|
|
@ -32,7 +32,7 @@ export default function CopyTextBox({
|
||||||
>
|
>
|
||||||
<pre
|
<pre
|
||||||
ref={textRef}
|
ref={textRef}
|
||||||
className={`p-4 pr-16 text-sm w-full ${
|
className={`p-2 pr-16 text-sm w-full ${
|
||||||
wrapText
|
wrapText
|
||||||
? "whitespace-pre-wrap break-words"
|
? "whitespace-pre-wrap break-words"
|
||||||
: "overflow-x-auto"
|
: "overflow-x-auto"
|
||||||
|
@ -41,10 +41,10 @@ export default function CopyTextBox({
|
||||||
<code className="block w-full">{text}</code>
|
<code className="block w-full">{text}</code>
|
||||||
</pre>
|
</pre>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="ghost"
|
||||||
size="icon"
|
size="sm"
|
||||||
type="button"
|
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}
|
onClick={copyToClipboard}
|
||||||
aria-label="Copy to clipboard"
|
aria-label="Copy to clipboard"
|
||||||
>
|
>
|
||||||
|
|
|
@ -4,15 +4,16 @@ import { cva, type VariantProps } from "class-variance-authority";
|
||||||
import { cn } from "@app/lib/cn";
|
import { cn } from "@app/lib/cn";
|
||||||
|
|
||||||
const alertVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-card text-foreground",
|
default: "bg-card border text-foreground",
|
||||||
|
neutral: "bg-card border-2 text-foreground",
|
||||||
destructive:
|
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:
|
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: {
|
defaultVariants: {
|
||||||
|
|
|
@ -9,12 +9,16 @@ export function pullEnv(): Env {
|
||||||
resourceAccessTokenParam: process.env
|
resourceAccessTokenParam: process.env
|
||||||
.RESOURCE_ACCESS_TOKEN_PARAM as string,
|
.RESOURCE_ACCESS_TOKEN_PARAM as string,
|
||||||
resourceSessionRequestParam: process.env
|
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: {
|
app: {
|
||||||
environment: process.env.ENVIRONMENT as string,
|
environment: process.env.ENVIRONMENT as string,
|
||||||
version: process.env.APP_VERSION as string,
|
version: process.env.APP_VERSION as string,
|
||||||
dashboardUrl: process.env.DASHBOARD_URL as string,
|
dashboardUrl: process.env.DASHBOARD_URL as string
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
emailEnabled: process.env.EMAIL_ENABLED === "true" ? true : false
|
emailEnabled: process.env.EMAIL_ENABLED === "true" ? true : false
|
||||||
|
|
|
@ -3,11 +3,3 @@ export function constructShareLink(
|
||||||
) {
|
) {
|
||||||
return `${window.location.origin}/s/${token!}`;
|
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;
|
environment: string;
|
||||||
version: string;
|
version: string;
|
||||||
dashboardUrl: string;
|
dashboardUrl: string;
|
||||||
},
|
};
|
||||||
server: {
|
server: {
|
||||||
externalPort: string;
|
externalPort: string;
|
||||||
nextPort: string;
|
nextPort: string;
|
||||||
sessionCookieName: string;
|
sessionCookieName: string;
|
||||||
resourceAccessTokenParam: string;
|
resourceAccessTokenParam: string;
|
||||||
resourceSessionRequestParam: string;
|
resourceSessionRequestParam: string;
|
||||||
},
|
resourceAccessTokenHeadersId: string;
|
||||||
|
resourceAccessTokenHeadersToken: string;
|
||||||
|
};
|
||||||
email: {
|
email: {
|
||||||
emailEnabled: boolean;
|
emailEnabled: boolean;
|
||||||
},
|
};
|
||||||
flags: {
|
flags: {
|
||||||
disableSignupWithoutInvite: boolean;
|
disableSignupWithoutInvite: boolean;
|
||||||
disableUserCreateOrg: boolean;
|
disableUserCreateOrg: boolean;
|
||||||
emailVerificationRequired: boolean;
|
emailVerificationRequired: boolean;
|
||||||
allowRawResources: boolean;
|
allowRawResources: boolean;
|
||||||
allowBaseDomainResources: boolean;
|
allowBaseDomainResources: boolean;
|
||||||
}
|
};
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue