mirror of
https://github.com/fosrl/pangolin.git
synced 2025-07-31 08:04:54 +02:00
complete integration of direct share link as discussed in #35
This commit is contained in:
parent
bfd1b21f9c
commit
a2ed7c7117
15 changed files with 215 additions and 37 deletions
|
@ -12,6 +12,7 @@ server:
|
||||||
secure_cookies: false
|
secure_cookies: false
|
||||||
session_cookie_name: p_session
|
session_cookie_name: p_session
|
||||||
resource_session_cookie_name: p_resource_session
|
resource_session_cookie_name: p_resource_session
|
||||||
|
resource_access_token_param: p_token
|
||||||
|
|
||||||
traefik:
|
traefik:
|
||||||
cert_resolver: letsencrypt
|
cert_resolver: letsencrypt
|
||||||
|
|
|
@ -9,9 +9,10 @@ server:
|
||||||
internal_port: 3001
|
internal_port: 3001
|
||||||
next_port: 3002
|
next_port: 3002
|
||||||
internal_hostname: pangolin
|
internal_hostname: pangolin
|
||||||
secure_cookies: false
|
secure_cookies: true
|
||||||
session_cookie_name: p_session
|
session_cookie_name: p_session
|
||||||
resource_session_cookie_name: p_resource_session
|
resource_session_cookie_name: p_resource_session
|
||||||
|
resource_access_token_param: p_token
|
||||||
|
|
||||||
traefik:
|
traefik:
|
||||||
cert_resolver: letsencrypt
|
cert_resolver: letsencrypt
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@fosrl/pangolin",
|
"name": "@fosrl/pangolin",
|
||||||
"version": "1.0.0-beta.4",
|
"version": "1.0.0-beta.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Tunneled Reverse Proxy Management Server with Identity and Access Control and Dashboard UI",
|
"description": "Tunneled Reverse Proxy Management Server with Identity and Access Control and Dashboard UI",
|
||||||
|
@ -26,6 +26,7 @@
|
||||||
"@oslojs/encoding": "1.1.0",
|
"@oslojs/encoding": "1.1.0",
|
||||||
"@radix-ui/react-avatar": "1.1.2",
|
"@radix-ui/react-avatar": "1.1.2",
|
||||||
"@radix-ui/react-checkbox": "1.1.3",
|
"@radix-ui/react-checkbox": "1.1.3",
|
||||||
|
"@radix-ui/react-collapsible": "1.1.2",
|
||||||
"@radix-ui/react-dialog": "1.1.4",
|
"@radix-ui/react-dialog": "1.1.4",
|
||||||
"@radix-ui/react-dropdown-menu": "2.1.4",
|
"@radix-ui/react-dropdown-menu": "2.1.4",
|
||||||
"@radix-ui/react-icons": "1.3.2",
|
"@radix-ui/react-icons": "1.3.2",
|
||||||
|
|
|
@ -32,7 +32,8 @@ const environmentSchema = z.object({
|
||||||
internal_hostname: z.string().transform((url) => url.toLowerCase()),
|
internal_hostname: z.string().transform((url) => url.toLowerCase()),
|
||||||
secure_cookies: z.boolean(),
|
secure_cookies: z.boolean(),
|
||||||
session_cookie_name: z.string(),
|
session_cookie_name: z.string(),
|
||||||
resource_session_cookie_name: z.string()
|
resource_session_cookie_name: z.string(),
|
||||||
|
resource_access_token_param: z.string()
|
||||||
}),
|
}),
|
||||||
traefik: z.object({
|
traefik: z.object({
|
||||||
http_entrypoint: z.string(),
|
http_entrypoint: z.string(),
|
||||||
|
@ -186,6 +187,7 @@ export class Config {
|
||||||
?.disable_user_create_org
|
?.disable_user_create_org
|
||||||
? "true"
|
? "true"
|
||||||
: "false";
|
: "false";
|
||||||
|
process.env.RESOURCE_ACCESS_TOKEN_PARAM = parsedConfig.data.server.resource_access_token_param;
|
||||||
|
|
||||||
this.rawConfig = parsedConfig.data;
|
this.rawConfig = parsedConfig.data;
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,7 +56,7 @@ export async function traefikConfigProvider(
|
||||||
config.getRawConfig().server.resource_session_cookie_name,
|
config.getRawConfig().server.resource_session_cookie_name,
|
||||||
userSessionCookieName:
|
userSessionCookieName:
|
||||||
config.getRawConfig().server.session_cookie_name,
|
config.getRawConfig().server.session_cookie_name,
|
||||||
accessTokenQueryParam: "p_token"
|
accessTokenQueryParam: config.getRawConfig().server.resource_access_token_param,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { loadAppVersion } from "@server/lib/loadAppVersion";
|
||||||
import m1 from "./scripts/1.0.0-beta1";
|
import m1 from "./scripts/1.0.0-beta1";
|
||||||
import m2 from "./scripts/1.0.0-beta2";
|
import m2 from "./scripts/1.0.0-beta2";
|
||||||
import m3 from "./scripts/1.0.0-beta3";
|
import m3 from "./scripts/1.0.0-beta3";
|
||||||
|
import m4 from "./scripts/1.0.0-beta5";
|
||||||
|
|
||||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||||
|
@ -17,7 +18,8 @@ import m3 from "./scripts/1.0.0-beta3";
|
||||||
const migrations = [
|
const migrations = [
|
||||||
{ version: "1.0.0-beta.1", run: m1 },
|
{ version: "1.0.0-beta.1", run: m1 },
|
||||||
{ version: "1.0.0-beta.2", run: m2 },
|
{ version: "1.0.0-beta.2", run: m2 },
|
||||||
{ version: "1.0.0-beta.3", run: m3 }
|
{ version: "1.0.0-beta.3", run: m3 },
|
||||||
|
{ version: "1.0.0-beta.5", run: m4 }
|
||||||
// Add new migrations here as they are created
|
// Add new migrations here as they are created
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
|
42
server/setup/scripts/1.0.0-beta5.ts
Normal file
42
server/setup/scripts/1.0.0-beta5.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import { configFilePath1, configFilePath2 } from "@server/lib/consts";
|
||||||
|
import fs from "fs";
|
||||||
|
import yaml from "js-yaml";
|
||||||
|
|
||||||
|
export default async function migration() {
|
||||||
|
console.log("Running setup script 1.0.0-beta.5...");
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// Validate the structure
|
||||||
|
if (!rawConfig.server) {
|
||||||
|
throw new Error(`Invalid config file: server is missing.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the config
|
||||||
|
rawConfig.server.resource_access_token_param = "p_token";
|
||||||
|
|
||||||
|
// Write the updated YAML back to the file
|
||||||
|
const updatedYaml = yaml.dump(rawConfig);
|
||||||
|
fs.writeFileSync(filePath, updatedYaml, "utf8");
|
||||||
|
|
||||||
|
console.log("Done.");
|
||||||
|
}
|
|
@ -57,14 +57,22 @@ import {
|
||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList
|
CommandList
|
||||||
} from "@app/components/ui/command";
|
} from "@app/components/ui/command";
|
||||||
import { CheckIcon } from "lucide-react";
|
import { CheckIcon, ChevronsUpDown } from "lucide-react";
|
||||||
import { register } from "module";
|
import { register } from "module";
|
||||||
import { Label } from "@app/components/ui/label";
|
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 { constructShareLink } from "@app/lib/shareLinks";
|
import {
|
||||||
|
constructDirectShareLink,
|
||||||
|
constructShareLink
|
||||||
|
} from "@app/lib/shareLinks";
|
||||||
import { ShareLinkRow } from "./ShareLinksTable";
|
import { ShareLinkRow } from "./ShareLinksTable";
|
||||||
import { QRCodeCanvas, QRCodeSVG } from "qrcode.react";
|
import { QRCodeCanvas, QRCodeSVG } from "qrcode.react";
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger
|
||||||
|
} from "@app/components/ui/collapsible";
|
||||||
|
|
||||||
type FormProps = {
|
type FormProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
@ -75,6 +83,7 @@ type FormProps = {
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
resourceId: z.number({ message: "Please select a resource" }),
|
resourceId: z.number({ message: "Please select a resource" }),
|
||||||
resourceName: z.string(),
|
resourceName: z.string(),
|
||||||
|
resourceUrl: z.string(),
|
||||||
timeUnit: z.string(),
|
timeUnit: z.string(),
|
||||||
timeValue: z.coerce.number().int().positive().min(1),
|
timeValue: z.coerce.number().int().positive().min(1),
|
||||||
title: z.string().optional()
|
title: z.string().optional()
|
||||||
|
@ -88,14 +97,18 @@ export default function CreateShareLinkForm({
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { org } = useOrgContext();
|
const { org } = useOrgContext();
|
||||||
|
|
||||||
const api = createApiClient(useEnvContext());
|
const { env } = useEnvContext();
|
||||||
|
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 [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [neverExpire, setNeverExpire] = useState(false);
|
const [neverExpire, setNeverExpire] = useState(false);
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const [resources, setResources] = useState<
|
const [resources, setResources] = useState<
|
||||||
{ resourceId: number; name: string }[]
|
{ resourceId: number; name: string; resourceUrl: string }[]
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
const timeUnits = [
|
const timeUnits = [
|
||||||
|
@ -139,7 +152,13 @@ export default function CreateShareLinkForm({
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res?.status === 200) {
|
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
|
token.accessToken
|
||||||
);
|
);
|
||||||
setLink(link);
|
setLink(link);
|
||||||
|
const directLink = constructDirectShareLink(
|
||||||
|
env.server.resourceAccessTokenParam,
|
||||||
|
values.resourceUrl,
|
||||||
|
token.accessTokenId,
|
||||||
|
token.accessToken
|
||||||
|
);
|
||||||
|
setDirectLink(directLink);
|
||||||
onCreated?.({
|
onCreated?.({
|
||||||
accessTokenId: token.accessTokenId,
|
accessTokenId: token.accessTokenId,
|
||||||
resourceId: token.resourceId,
|
resourceId: token.resourceId,
|
||||||
|
@ -306,6 +332,10 @@ export default function CreateShareLinkForm({
|
||||||
"resourceName",
|
"resourceName",
|
||||||
r.name
|
r.name
|
||||||
);
|
);
|
||||||
|
form.setValue(
|
||||||
|
"resourceUrl",
|
||||||
|
r.resourceUrl
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
|
@ -462,12 +492,62 @@ export default function CreateShareLinkForm({
|
||||||
<QRCodeCanvas value={link} size={200} />
|
<QRCodeCanvas value={link} size={200} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mx-auto">
|
<Collapsible
|
||||||
<CopyTextBox
|
open={isOpen}
|
||||||
text={link}
|
onOpenChange={setIsOpen}
|
||||||
wrapText={false}
|
className="space-y-2"
|
||||||
/>
|
>
|
||||||
</div>
|
<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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -24,7 +24,7 @@ import { useRouter } from "next/navigation";
|
||||||
// import CreateResourceForm from "./CreateResourceForm";
|
// import CreateResourceForm from "./CreateResourceForm";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
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 { useToast } from "@app/hooks/useToast";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
@ -109,15 +109,14 @@ export default function ShareLinksTable({
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem
|
||||||
<button
|
onClick={() => {
|
||||||
onClick={() =>
|
deleteSharelink(
|
||||||
deleteSharelink(
|
resourceRow.accessTokenId
|
||||||
resourceRow.accessTokenId
|
);
|
||||||
)
|
}}
|
||||||
}
|
>
|
||||||
className="text-red-500"
|
<button className="text-red-500">
|
||||||
>
|
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
|
@ -30,6 +30,7 @@ export default function AccessToken({
|
||||||
redirectUrl
|
redirectUrl
|
||||||
}: AccessTokenProps) {
|
}: AccessTokenProps) {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [isValid, setIsValid] = useState(false);
|
||||||
|
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
|
|
||||||
|
@ -49,6 +50,7 @@ export default function AccessToken({
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.data.data.session) {
|
if (res.data.data.session) {
|
||||||
|
setIsValid(true);
|
||||||
window.location.href = redirectUrl;
|
window.location.href = redirectUrl;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -61,24 +63,47 @@ export default function AccessToken({
|
||||||
check();
|
check();
|
||||||
}, [accessTokenId, accessToken]);
|
}, [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 ? (
|
return loading ? (
|
||||||
<div></div>
|
<div></div>
|
||||||
) : (
|
) : (
|
||||||
<Card className="w-full max-w-md">
|
<Card className="w-full max-w-md">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-center text-2xl font-bold">
|
<CardTitle className="text-center text-2xl font-bold">
|
||||||
Access URL Invalid
|
{renderTitle()}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>{renderContent()}</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>
|
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ const buttonVariants = cva(
|
||||||
secondary:
|
secondary:
|
||||||
"bg-secondary border border-input text-secondary-foreground hover:bg-secondary/80",
|
"bg-secondary border border-input text-secondary-foreground hover:bg-secondary/80",
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
text: "",
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
},
|
},
|
||||||
size: {
|
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,
|
nextPort: process.env.NEXT_PORT as string,
|
||||||
externalPort: process.env.SERVER_EXTERNAL_PORT as string,
|
externalPort: process.env.SERVER_EXTERNAL_PORT as string,
|
||||||
sessionCookieName: process.env.SESSION_COOKIE_NAME 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: {
|
app: {
|
||||||
environment: process.env.ENVIRONMENT as string,
|
environment: process.env.ENVIRONMENT as string,
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { pullEnv } from "./pullEnv";
|
||||||
|
|
||||||
export function constructShareLink(
|
export function constructShareLink(
|
||||||
resourceId: number,
|
resourceId: number,
|
||||||
id: string,
|
id: string,
|
||||||
|
@ -5,3 +7,12 @@ export function constructShareLink(
|
||||||
) {
|
) {
|
||||||
return `${window.location.origin}/auth/resource/${resourceId}?token=${id}.${token}`;
|
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;
|
nextPort: string;
|
||||||
sessionCookieName: string;
|
sessionCookieName: string;
|
||||||
resourceSessionCookieName: string;
|
resourceSessionCookieName: string;
|
||||||
|
resourceAccessTokenParam: string;
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
emailEnabled: boolean;
|
emailEnabled: boolean;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue