mirror of
https://github.com/fosrl/pangolin.git
synced 2026-06-25 08:41:55 +00:00
Shape the ssh/vnc/rdp login ui to match auth
This commit is contained in:
@@ -1,13 +1,6 @@
|
|||||||
import ThemeSwitcher from "@app/components/ThemeSwitcher";
|
import ThemeSwitcher from "@app/components/ThemeSwitcher";
|
||||||
import { Separator } from "@app/components/ui/separator";
|
import AuthFooter from "@app/components/AuthFooter";
|
||||||
import { priv } from "@app/lib/api";
|
|
||||||
import { pullEnv } from "@app/lib/pullEnv";
|
|
||||||
import { build } from "@server/build";
|
|
||||||
import { GetLicenseStatusResponse } from "@server/routers/license/types";
|
|
||||||
import { AxiosResponse } from "axios";
|
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import { getTranslations } from "next-intl/server";
|
|
||||||
import { cache } from "react";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: {
|
title: {
|
||||||
@@ -22,29 +15,6 @@ type AuthLayoutProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function AuthLayout({ children }: AuthLayoutProps) {
|
export default async function AuthLayout({ children }: AuthLayoutProps) {
|
||||||
const env = pullEnv();
|
|
||||||
const t = await getTranslations();
|
|
||||||
let hideFooter = false;
|
|
||||||
|
|
||||||
let licenseStatus: GetLicenseStatusResponse | null = null;
|
|
||||||
if (build == "enterprise") {
|
|
||||||
const licenseStatusRes = await cache(
|
|
||||||
async () =>
|
|
||||||
await priv.get<AxiosResponse<GetLicenseStatusResponse>>(
|
|
||||||
"/license/status"
|
|
||||||
)
|
|
||||||
)();
|
|
||||||
licenseStatus = licenseStatusRes.data.data;
|
|
||||||
if (
|
|
||||||
env.branding.hideAuthLayoutFooter &&
|
|
||||||
licenseStatusRes.data.data.isHostLicensed &&
|
|
||||||
licenseStatusRes.data.data.isLicenseValid &&
|
|
||||||
licenseStatusRes.data.data.tier !== "personal"
|
|
||||||
) {
|
|
||||||
hideFooter = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
<div className="hidden md:flex justify-end items-center p-3 space-x-2">
|
<div className="hidden md:flex justify-end items-center p-3 space-x-2">
|
||||||
@@ -55,89 +25,7 @@ export default async function AuthLayout({ children }: AuthLayoutProps) {
|
|||||||
<div className="w-full max-w-md p-3">{children}</div>
|
<div className="w-full max-w-md p-3">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!hideFooter && (
|
<AuthFooter />
|
||||||
<footer className="hidden md:block w-full mt-12 py-3 mb-6 px-4">
|
|
||||||
<div className="container mx-auto flex flex-wrap justify-center items-center h-3 space-x-4 text-xs text-neutral-400 dark:text-neutral-600">
|
|
||||||
<a
|
|
||||||
href="https://pangolin.net"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
aria-label="Built by Fossorial"
|
|
||||||
className="flex items-center space-x-2 whitespace-nowrap"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
© {new Date().getFullYear()} Fossorial, Inc.
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
{build !== "saas" && (
|
|
||||||
<>
|
|
||||||
<Separator orientation="vertical" />
|
|
||||||
<a
|
|
||||||
href="https://pangolin.net"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
aria-label="Built by Fossorial"
|
|
||||||
className="flex items-center space-x-2 whitespace-nowrap"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
{process.env.BRANDING_APP_NAME ||
|
|
||||||
"Pangolin"}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Separator orientation="vertical" />
|
|
||||||
<span>
|
|
||||||
{build === "oss"
|
|
||||||
? t("communityEdition")
|
|
||||||
: build === "enterprise"
|
|
||||||
? t("enterpriseEdition")
|
|
||||||
: t("pangolinCloud")}
|
|
||||||
</span>
|
|
||||||
{build === "enterprise" &&
|
|
||||||
licenseStatus?.isHostLicensed &&
|
|
||||||
licenseStatus?.isLicenseValid &&
|
|
||||||
licenseStatus?.tier === "personal" ? (
|
|
||||||
<>
|
|
||||||
<Separator orientation="vertical" />
|
|
||||||
<span>{t("personalUseOnly")}</span>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
{build === "enterprise" &&
|
|
||||||
(!licenseStatus?.isHostLicensed ||
|
|
||||||
!licenseStatus?.isLicenseValid) ? (
|
|
||||||
<>
|
|
||||||
<Separator orientation="vertical" />
|
|
||||||
<span>{t("unlicensed")}</span>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
{build === "saas" && (
|
|
||||||
<>
|
|
||||||
<Separator orientation="vertical" />
|
|
||||||
<a
|
|
||||||
href="https://pangolin.net/terms-of-service.html"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
aria-label="GitHub"
|
|
||||||
className="flex items-center space-x-2 whitespace-nowrap"
|
|
||||||
>
|
|
||||||
<span>{t("termsOfService")}</span>
|
|
||||||
</a>
|
|
||||||
<Separator orientation="vertical" />
|
|
||||||
<a
|
|
||||||
href="https://pangolin.net/privacy-policy.html"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
aria-label="GitHub"
|
|
||||||
className="flex items-center space-x-2 whitespace-nowrap"
|
|
||||||
>
|
|
||||||
<span>{t("privacyPolicy")}</span>
|
|
||||||
</a>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import type {
|
|||||||
FileInfo
|
FileInfo
|
||||||
} from "@devolutions/iron-remote-desktop-rdp/dist";
|
} from "@devolutions/iron-remote-desktop-rdp/dist";
|
||||||
import { GetBrowserTargetResponse } from "@server/routers/resource";
|
import { GetBrowserTargetResponse } from "@server/routers/resource";
|
||||||
|
import { Card, CardContent } from "@app/components/ui/card";
|
||||||
|
import LoginCardHeader from "@app/components/LoginCardHeader";
|
||||||
|
|
||||||
declare module "react" {
|
declare module "react" {
|
||||||
namespace JSX {
|
namespace JSX {
|
||||||
@@ -307,48 +309,51 @@ export default function RdpClient({
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
<Card className="w-full">
|
||||||
<p className="text-destructive">{error}</p>
|
<LoginCardHeader subtitle="RDP" />
|
||||||
</div>
|
<CardContent className="pt-6">
|
||||||
|
<p className="text-destructive text-sm">{error}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<>
|
||||||
{showLogin && (
|
{showLogin && (
|
||||||
<div className="mx-auto max-w-2xl p-6">
|
<Card className="w-full">
|
||||||
<h1 className="mb-4 text-2xl font-semibold">RDP</h1>
|
<LoginCardHeader subtitle="Connect via RDP" />
|
||||||
|
<CardContent className="pt-6">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Field label="Domain" id="domain">
|
<Field label="Domain" id="domain">
|
||||||
<Input
|
<Input
|
||||||
id="domain"
|
id="domain"
|
||||||
value={form.domain}
|
value={form.domain}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
update("domain", e.target.value)
|
update("domain", e.target.value)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Username" id="username">
|
<Field label="Username" id="username">
|
||||||
<Input
|
<Input
|
||||||
id="username"
|
id="username"
|
||||||
value={form.username}
|
value={form.username}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
update("username", e.target.value)
|
update("username", e.target.value)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Password" id="password">
|
<Field label="Password" id="password">
|
||||||
<Input
|
<Input
|
||||||
id="password"
|
id="password"
|
||||||
type="password"
|
type="password"
|
||||||
value={form.password}
|
value={form.password}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
update("password", e.target.value)
|
update("password", e.target.value)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
{/*
|
{/*
|
||||||
<Field label="Pre Connection Blob (optional)" id="pcb">
|
<Field label="Pre Connection Blob (optional)" id="pcb">
|
||||||
<Input
|
<Input
|
||||||
id="pcb"
|
id="pcb"
|
||||||
@@ -357,7 +362,7 @@ export default function RdpClient({
|
|||||||
/>
|
/>
|
||||||
</Field> */}
|
</Field> */}
|
||||||
|
|
||||||
{/* <Field
|
{/* <Field
|
||||||
label="KDC Proxy URL (optional)"
|
label="KDC Proxy URL (optional)"
|
||||||
id="kdcProxyUrl"
|
id="kdcProxyUrl"
|
||||||
>
|
>
|
||||||
@@ -369,7 +374,7 @@ export default function RdpClient({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Field> */}
|
</Field> */}
|
||||||
{/* <div className="flex items-center gap-2">
|
{/* <div className="flex items-center gap-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="enable_clipboard"
|
id="enable_clipboard"
|
||||||
checked={form.enableClipboard}
|
checked={form.enableClipboard}
|
||||||
@@ -381,20 +386,21 @@ export default function RdpClient({
|
|||||||
Enable Clipboard
|
Enable Clipboard
|
||||||
</Label>
|
</Label>
|
||||||
</div> */}
|
</div> */}
|
||||||
<Button
|
<Button
|
||||||
onClick={startSession}
|
onClick={startSession}
|
||||||
disabled={!moduleReady}
|
disabled={!moduleReady}
|
||||||
loading={connecting}
|
loading={connecting}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
{moduleReady ? "Connect" : "Loading module..."}
|
{moduleReady ? "Connect" : "Loading module..."}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CardContent>
|
||||||
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="flex h-screen flex-col bg-neutral-900"
|
className="fixed inset-0 z-50 flex flex-col bg-neutral-900"
|
||||||
style={{ display: showLogin ? "none" : "flex" }}
|
style={{ display: showLogin ? "none" : "flex" }}
|
||||||
>
|
>
|
||||||
<div className="flex flex-wrap items-center gap-2 bg-black p-2 text-white">
|
<div className="flex flex-wrap items-center gap-2 bg-black p-2 text-white">
|
||||||
@@ -500,7 +506,7 @@ export default function RdpClient({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { priv } from "@app/lib/api";
|
|||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { GetBrowserTargetResponse } from "@server/routers/resource";
|
import { GetBrowserTargetResponse } from "@server/routers/resource";
|
||||||
import RdpClient from "./RdpClient";
|
import RdpClient from "./RdpClient";
|
||||||
|
import AuthFooter from "@app/components/AuthFooter";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -29,5 +30,14 @@ export default async function RdpPage() {
|
|||||||
error = "No resource found for this domain";
|
error = "No resource found for this domain";
|
||||||
}
|
}
|
||||||
|
|
||||||
return <RdpClient target={target} error={error} />;
|
return (
|
||||||
|
<div className="h-full flex flex-col">
|
||||||
|
<div className="flex-1 flex md:items-center justify-center">
|
||||||
|
<div className="w-full max-w-md p-3">
|
||||||
|
<RdpClient target={target} error={error} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<AuthFooter />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import type { SignSshKeyResponse } from "@server/private/routers/ssh";
|
import type { SignSshKeyResponse } from "@server/private/routers/ssh";
|
||||||
import { GetBrowserTargetResponse } from "@server/routers/resource";
|
import { GetBrowserTargetResponse } from "@server/routers/resource";
|
||||||
|
import { Card, CardContent } from "@app/components/ui/card";
|
||||||
|
import LoginCardHeader from "@app/components/LoginCardHeader";
|
||||||
|
|
||||||
type FormState = {
|
type FormState = {
|
||||||
username: string;
|
username: string;
|
||||||
@@ -259,20 +261,12 @@ export default function SshClient({
|
|||||||
setConnected(false);
|
setConnected(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
|
||||||
<p className="text-destructive">{error}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// In push mode, show a connecting/connected state without the login form.
|
// In push mode, show a connecting/connected state without the login form.
|
||||||
if (signedKeyData && signedPrivateKey) {
|
if (signedKeyData && signedPrivateKey) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<>
|
||||||
{!connected && (
|
{!connected && (
|
||||||
<div className="flex min-h-screen items-center justify-center">
|
<div className="flex items-center justify-center py-12">
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
{connectError
|
{connectError
|
||||||
? connectError
|
? connectError
|
||||||
@@ -283,7 +277,7 @@ export default function SshClient({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{connected && (
|
{connected && (
|
||||||
<div className="flex h-screen flex-col bg-neutral-900">
|
<div className="fixed inset-0 z-50 flex flex-col bg-neutral-900">
|
||||||
<div className="flex flex-wrap items-center gap-2 bg-black p-2 text-white">
|
<div className="flex flex-wrap items-center gap-2 bg-black p-2 text-white">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -300,121 +294,136 @@ export default function SshClient({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Card className="w-full">
|
||||||
|
<LoginCardHeader subtitle="SSH" />
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<p className="text-destructive text-sm">{error}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<>
|
||||||
{!connected && (
|
{!connected && (
|
||||||
<div className="mx-auto max-w-2xl p-6">
|
<Card className="w-full">
|
||||||
<h1 className="mb-4 text-2xl font-semibold">SSH</h1>
|
<LoginCardHeader subtitle="Connect via SSH" />
|
||||||
|
<CardContent className="pt-6">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Field label="Username" id="username">
|
<Field label="Username" id="username">
|
||||||
<Input
|
<Input
|
||||||
id="username"
|
id="username"
|
||||||
value={form.username}
|
value={form.username}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setForm({
|
setForm({
|
||||||
...form,
|
...form,
|
||||||
username: e.target.value
|
username: e.target.value
|
||||||
})
|
})
|
||||||
}
|
|
||||||
placeholder="root"
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
<Field label="Password" id="password">
|
|
||||||
<Input
|
|
||||||
id="password"
|
|
||||||
type="password"
|
|
||||||
value={form.password}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm({
|
|
||||||
...form,
|
|
||||||
password: e.target.value
|
|
||||||
})
|
|
||||||
}
|
|
||||||
placeholder={
|
|
||||||
form.privateKey
|
|
||||||
? "Optional with key auth"
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field label="Private Key (optional)" id="privateKey">
|
|
||||||
<Textarea
|
|
||||||
id="privateKey"
|
|
||||||
value={form.privateKey}
|
|
||||||
onChange={(e) =>
|
|
||||||
setForm({
|
|
||||||
...form,
|
|
||||||
privateKey: e.target.value
|
|
||||||
})
|
|
||||||
}
|
|
||||||
placeholder="Paste your private key here (PEM format)…"
|
|
||||||
rows={5}
|
|
||||||
className="font-mono text-xs"
|
|
||||||
/>
|
|
||||||
<div className="mt-1.5 flex items-center gap-2">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() =>
|
|
||||||
fileInputRef.current?.click()
|
|
||||||
}
|
}
|
||||||
>
|
placeholder="root"
|
||||||
Upload key file
|
/>
|
||||||
</Button>
|
</Field>
|
||||||
{form.privateKey && (
|
<Field label="Password" id="password">
|
||||||
<button
|
<Input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
value={form.password}
|
||||||
|
onChange={(e) =>
|
||||||
|
setForm({
|
||||||
|
...form,
|
||||||
|
password: e.target.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
placeholder={
|
||||||
|
form.privateKey
|
||||||
|
? "Optional with key auth"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
label="Private Key (optional)"
|
||||||
|
id="privateKey"
|
||||||
|
>
|
||||||
|
<Textarea
|
||||||
|
id="privateKey"
|
||||||
|
value={form.privateKey}
|
||||||
|
onChange={(e) =>
|
||||||
|
setForm({
|
||||||
|
...form,
|
||||||
|
privateKey: e.target.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
placeholder="Paste your private key here (PEM format)…"
|
||||||
|
rows={5}
|
||||||
|
className="font-mono text-xs"
|
||||||
|
/>
|
||||||
|
<div className="mt-1.5 flex items-center gap-2">
|
||||||
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
className="text-xs text-muted-foreground underline"
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setForm((prev) => ({
|
fileInputRef.current?.click()
|
||||||
...prev,
|
|
||||||
privateKey: ""
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Clear
|
Upload key file
|
||||||
</button>
|
</Button>
|
||||||
)}
|
{form.privateKey && (
|
||||||
</div>
|
<button
|
||||||
<input
|
type="button"
|
||||||
ref={fileInputRef}
|
className="text-xs text-muted-foreground underline"
|
||||||
type="file"
|
onClick={() =>
|
||||||
className="hidden"
|
setForm((prev) => ({
|
||||||
accept=".pem,.key,.pub,*"
|
...prev,
|
||||||
onChange={handleKeyFile}
|
privateKey: ""
|
||||||
/>
|
}))
|
||||||
</Field>
|
}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
className="hidden"
|
||||||
|
accept=".pem,.key,.pub,*"
|
||||||
|
onChange={handleKeyFile}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
{connectError && (
|
{connectError && (
|
||||||
<p className="text-destructive text-sm">
|
<p className="text-destructive text-sm">
|
||||||
{connectError}
|
{connectError}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => connect()}
|
onClick={() => connect()}
|
||||||
loading={connecting}
|
loading={connecting}
|
||||||
disabled={
|
disabled={
|
||||||
!form.username ||
|
!form.username ||
|
||||||
(!form.password && !form.privateKey)
|
(!form.password && !form.privateKey)
|
||||||
}
|
}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
{connecting ? "Connecting..." : "Connect"}
|
{connecting ? "Connecting..." : "Connect"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CardContent>
|
||||||
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{connected && (
|
{connected && (
|
||||||
<div className="flex h-screen flex-col bg-neutral-900">
|
<div className="fixed inset-0 z-50 flex flex-col bg-neutral-900">
|
||||||
<div className="flex flex-wrap items-center gap-2 bg-black p-2 text-white">
|
<div className="flex flex-wrap items-center gap-2 bg-black p-2 text-white">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -431,7 +440,7 @@ export default function SshClient({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { GetBrowserTargetResponse } from "@server/routers/resource";
|
|||||||
import SshClient from "./SshClient";
|
import SshClient from "./SshClient";
|
||||||
import { SignSshKeyResponse } from "@server/private/routers/ssh";
|
import { SignSshKeyResponse } from "@server/private/routers/ssh";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
import AuthFooter from "@app/components/AuthFooter";
|
||||||
|
|
||||||
function generateEphemeralKeyPair(): {
|
function generateEphemeralKeyPair(): {
|
||||||
privateKeyPem: string;
|
privateKeyPem: string;
|
||||||
@@ -82,11 +83,18 @@ export default async function SshPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SshClient
|
<div className="h-full flex flex-col">
|
||||||
target={target}
|
<div className="flex-1 flex md:items-center justify-center">
|
||||||
error={error}
|
<div className="w-full max-w-md p-3">
|
||||||
signedKeyData={signedKeyData}
|
<SshClient
|
||||||
privateKey={privateKey}
|
target={target}
|
||||||
/>
|
error={error}
|
||||||
|
signedKeyData={signedKeyData}
|
||||||
|
privateKey={privateKey}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<AuthFooter />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { GetBrowserTargetResponse } from "@server/routers/resource";
|
import { GetBrowserTargetResponse } from "@server/routers/resource";
|
||||||
|
import { Card, CardContent } from "@app/components/ui/card";
|
||||||
|
import LoginCardHeader from "@app/components/LoginCardHeader";
|
||||||
|
|
||||||
type FormState = {
|
type FormState = {
|
||||||
password: string;
|
password: string;
|
||||||
@@ -146,39 +148,43 @@ export default function VncClient({
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background flex items-center justify-center">
|
<Card className="w-full">
|
||||||
<p className="text-destructive">{error}</p>
|
<LoginCardHeader subtitle="VNC" />
|
||||||
</div>
|
<CardContent className="pt-6">
|
||||||
|
<p className="text-destructive text-sm">{error}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<>
|
||||||
{!connected && (
|
{!connected && (
|
||||||
<div className="mx-auto max-w-2xl p-6">
|
<Card className="w-full">
|
||||||
<h1 className="mb-4 text-2xl font-semibold">VNC</h1>
|
<LoginCardHeader subtitle="Connect via VNC" />
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Field label="Password (optional)" id="password">
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
value={form.password}
|
||||||
|
onChange={(e) =>
|
||||||
|
update("password", e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<Button onClick={connect} className="w-full">
|
||||||
<Field label="Password (optional)" id="password">
|
Connect
|
||||||
<Input
|
</Button>
|
||||||
id="password"
|
</div>
|
||||||
type="password"
|
</CardContent>
|
||||||
value={form.password}
|
</Card>
|
||||||
onChange={(e) =>
|
|
||||||
update("password", e.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Button onClick={connect} className="w-full">
|
|
||||||
Connect
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="flex h-screen flex-col bg-neutral-900"
|
className="fixed inset-0 z-50 flex flex-col bg-neutral-900"
|
||||||
style={{ display: connected ? "flex" : "none" }}
|
style={{ display: connected ? "flex" : "none" }}
|
||||||
>
|
>
|
||||||
<div className="flex flex-wrap items-center gap-2 bg-black p-2 text-white">
|
<div className="flex flex-wrap items-center gap-2 bg-black p-2 text-white">
|
||||||
@@ -223,7 +229,7 @@ export default function VncClient({
|
|||||||
style={{ background: "#000" }}
|
style={{ background: "#000" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { priv } from "@app/lib/api";
|
|||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { GetBrowserTargetResponse } from "@server/routers/resource";
|
import { GetBrowserTargetResponse } from "@server/routers/resource";
|
||||||
import VncClient from "./VncClient";
|
import VncClient from "./VncClient";
|
||||||
|
import AuthFooter from "@app/components/AuthFooter";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -28,5 +29,14 @@ export default async function VncPage() {
|
|||||||
error = "No resource found for this domain";
|
error = "No resource found for this domain";
|
||||||
}
|
}
|
||||||
|
|
||||||
return <VncClient target={target} error={error} />;
|
return (
|
||||||
|
<div className="h-full flex flex-col">
|
||||||
|
<div className="flex-1 flex md:items-center justify-center">
|
||||||
|
<div className="w-full max-w-md p-3">
|
||||||
|
<VncClient target={target} error={error} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<AuthFooter />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
117
src/components/AuthFooter.tsx
Normal file
117
src/components/AuthFooter.tsx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { Separator } from "@app/components/ui/separator";
|
||||||
|
import { priv } from "@app/lib/api";
|
||||||
|
import { pullEnv } from "@app/lib/pullEnv";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
import { GetLicenseStatusResponse } from "@server/routers/license/types";
|
||||||
|
import { AxiosResponse } from "axios";
|
||||||
|
import { getTranslations } from "next-intl/server";
|
||||||
|
import { cache } from "react";
|
||||||
|
|
||||||
|
export default async function AuthFooter() {
|
||||||
|
const env = pullEnv();
|
||||||
|
const t = await getTranslations();
|
||||||
|
|
||||||
|
let hideFooter = false;
|
||||||
|
let licenseStatus: GetLicenseStatusResponse | null = null;
|
||||||
|
|
||||||
|
if (build === "enterprise") {
|
||||||
|
const licenseStatusRes = await cache(
|
||||||
|
async () =>
|
||||||
|
await priv.get<AxiosResponse<GetLicenseStatusResponse>>(
|
||||||
|
"/license/status"
|
||||||
|
)
|
||||||
|
)();
|
||||||
|
licenseStatus = licenseStatusRes.data.data;
|
||||||
|
if (
|
||||||
|
env.branding.hideAuthLayoutFooter &&
|
||||||
|
licenseStatusRes.data.data.isHostLicensed &&
|
||||||
|
licenseStatusRes.data.data.isLicenseValid &&
|
||||||
|
licenseStatusRes.data.data.tier !== "personal"
|
||||||
|
) {
|
||||||
|
hideFooter = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hideFooter) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer className="hidden md:block w-full mt-12 py-3 mb-6 px-4">
|
||||||
|
<div className="container mx-auto flex flex-wrap justify-center items-center h-3 space-x-4 text-xs text-neutral-400 dark:text-neutral-600">
|
||||||
|
<a
|
||||||
|
href="https://pangolin.net"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
aria-label="Built by Fossorial"
|
||||||
|
className="flex items-center space-x-2 whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<span>© {new Date().getFullYear()} Fossorial, Inc.</span>
|
||||||
|
</a>
|
||||||
|
{build !== "saas" && (
|
||||||
|
<>
|
||||||
|
<Separator orientation="vertical" />
|
||||||
|
<a
|
||||||
|
href="https://pangolin.net"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
aria-label="Built by Fossorial"
|
||||||
|
className="flex items-center space-x-2 whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{process.env.BRANDING_APP_NAME || "Pangolin"}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Separator orientation="vertical" />
|
||||||
|
<span>
|
||||||
|
{build === "oss"
|
||||||
|
? t("communityEdition")
|
||||||
|
: build === "enterprise"
|
||||||
|
? t("enterpriseEdition")
|
||||||
|
: t("pangolinCloud")}
|
||||||
|
</span>
|
||||||
|
{build === "enterprise" &&
|
||||||
|
licenseStatus?.isHostLicensed &&
|
||||||
|
licenseStatus?.isLicenseValid &&
|
||||||
|
licenseStatus?.tier === "personal" ? (
|
||||||
|
<>
|
||||||
|
<Separator orientation="vertical" />
|
||||||
|
<span>{t("personalUseOnly")}</span>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
{build === "enterprise" &&
|
||||||
|
(!licenseStatus?.isHostLicensed ||
|
||||||
|
!licenseStatus?.isLicenseValid) ? (
|
||||||
|
<>
|
||||||
|
<Separator orientation="vertical" />
|
||||||
|
<span>{t("unlicensed")}</span>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
{build === "saas" && (
|
||||||
|
<>
|
||||||
|
<Separator orientation="vertical" />
|
||||||
|
<a
|
||||||
|
href="https://pangolin.net/terms-of-service.html"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
aria-label="GitHub"
|
||||||
|
className="flex items-center space-x-2 whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<span>{t("termsOfService")}</span>
|
||||||
|
</a>
|
||||||
|
<Separator orientation="vertical" />
|
||||||
|
<a
|
||||||
|
href="https://pangolin.net/privacy-policy.html"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
aria-label="GitHub"
|
||||||
|
className="flex items-center space-x-2 whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<span>{t("privacyPolicy")}</span>
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user