mirror of
https://github.com/fosrl/pangolin.git
synced 2026-09-08 20:16:50 +02:00
Compare commits
3 Commits
c1ef5b4fbe
...
71756812b6
| Author | SHA1 | Date | |
|---|---|---|---|
| 71756812b6 | |||
| 76cd716caa | |||
| 9617eb2bd7 |
@@ -61,6 +61,7 @@ export type VerifyResourceSessionSchema = z.infer<
|
||||
>;
|
||||
|
||||
type BasicUserData = {
|
||||
userId: string;
|
||||
username: string;
|
||||
email: string | null;
|
||||
name: string | null;
|
||||
@@ -945,6 +946,7 @@ async function isUserAllowedToAccessResource(
|
||||
);
|
||||
if (roleResourceAccess && roleResourceAccess.length > 0) {
|
||||
return {
|
||||
userId: user.userId,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
@@ -959,6 +961,7 @@ async function isUserAllowedToAccessResource(
|
||||
|
||||
if (userResourceAccess) {
|
||||
return {
|
||||
userId: user.userId,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
|
||||
+99
-54
@@ -15,8 +15,14 @@ import type {
|
||||
FileInfo
|
||||
} from "@devolutions/iron-remote-desktop-rdp/dist";
|
||||
import { GetBrowserTargetResponse } from "@server/routers/resource";
|
||||
import { Card, CardContent } from "@app/components/ui/card";
|
||||
import LoginCardHeader from "@app/components/LoginCardHeader";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription
|
||||
} from "@app/components/ui/card";
|
||||
import Link from "next/link";
|
||||
|
||||
declare module "react" {
|
||||
namespace JSX {
|
||||
@@ -309,51 +315,87 @@ export default function RdpClient({
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<LoginCardHeader subtitle="RDP" />
|
||||
<CardContent className="pt-6">
|
||||
<p className="text-destructive text-sm">{error}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div>
|
||||
<div className="text-center mb-2">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Powered by{" "}
|
||||
<Link
|
||||
href="https://pangolin.net/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
Pangolin
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>RDP</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-destructive text-sm">{error}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{showLogin && (
|
||||
<Card className="w-full">
|
||||
<LoginCardHeader subtitle="Connect via RDP" />
|
||||
<CardContent className="pt-6">
|
||||
<div className="space-y-4">
|
||||
<Field label="Domain" id="domain">
|
||||
<Input
|
||||
id="domain"
|
||||
value={form.domain}
|
||||
onChange={(e) =>
|
||||
update("domain", e.target.value)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Username" id="username">
|
||||
<Input
|
||||
id="username"
|
||||
value={form.username}
|
||||
onChange={(e) =>
|
||||
update("username", e.target.value)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Password" id="password">
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={form.password}
|
||||
onChange={(e) =>
|
||||
update("password", e.target.value)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
{/*
|
||||
<div>
|
||||
<div className="text-center mb-2">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Powered by{" "}
|
||||
<Link
|
||||
href="https://pangolin.net/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
Pangolin
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Sign in to Remote Desktop</CardTitle>
|
||||
<CardDescription>
|
||||
Enter Windows credentials to access xxxx
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<Field label="Domain" id="domain">
|
||||
<Input
|
||||
id="domain"
|
||||
value={form.domain}
|
||||
onChange={(e) =>
|
||||
update("domain", e.target.value)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Username" id="username">
|
||||
<Input
|
||||
id="username"
|
||||
value={form.username}
|
||||
onChange={(e) =>
|
||||
update("username", e.target.value)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Password" id="password">
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={form.password}
|
||||
onChange={(e) =>
|
||||
update("password", e.target.value)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
{/*
|
||||
<Field label="Pre Connection Blob (optional)" id="pcb">
|
||||
<Input
|
||||
id="pcb"
|
||||
@@ -362,7 +404,7 @@ export default function RdpClient({
|
||||
/>
|
||||
</Field> */}
|
||||
|
||||
{/* <Field
|
||||
{/* <Field
|
||||
label="KDC Proxy URL (optional)"
|
||||
id="kdcProxyUrl"
|
||||
>
|
||||
@@ -374,7 +416,7 @@ export default function RdpClient({
|
||||
}
|
||||
/>
|
||||
</Field> */}
|
||||
{/* <div className="flex items-center gap-2">
|
||||
{/* <div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="enable_clipboard"
|
||||
checked={form.enableClipboard}
|
||||
@@ -386,17 +428,20 @@ export default function RdpClient({
|
||||
Enable Clipboard
|
||||
</Label>
|
||||
</div> */}
|
||||
<Button
|
||||
onClick={startSession}
|
||||
disabled={!moduleReady}
|
||||
loading={connecting}
|
||||
className="w-full"
|
||||
>
|
||||
{moduleReady ? "Connect" : "Loading module..."}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Button
|
||||
onClick={startSession}
|
||||
disabled={!moduleReady}
|
||||
loading={connecting}
|
||||
className="w-full"
|
||||
>
|
||||
{moduleReady
|
||||
? "Connect"
|
||||
: "Loading module..."}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
|
||||
+195
-111
@@ -8,8 +8,18 @@ import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import type { SignSshKeyResponse } from "@server/private/routers/ssh";
|
||||
import { GetBrowserTargetResponse } from "@server/routers/resource";
|
||||
import { Card, CardContent } from "@app/components/ui/card";
|
||||
import LoginCardHeader from "@app/components/LoginCardHeader";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription
|
||||
} from "@app/components/ui/card";
|
||||
import Link from "next/link";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import { cn } from "@app/lib/cn";
|
||||
|
||||
type AuthTab = "password" | "privateKey";
|
||||
|
||||
type FormState = {
|
||||
username: string;
|
||||
@@ -47,7 +57,7 @@ export default function SshClient({
|
||||
return { username: "", password: "", privateKey: "" };
|
||||
});
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [authTab, setAuthTab] = useState<AuthTab>("password");
|
||||
|
||||
function handleKeyFile(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const file = e.target.files?.[0];
|
||||
@@ -180,8 +190,11 @@ export default function SshClient({
|
||||
}
|
||||
|
||||
const username = override?.username ?? form.username;
|
||||
const password = override?.password ?? form.password;
|
||||
const privateKey = override?.privateKey ?? form.privateKey;
|
||||
const password =
|
||||
override?.password ?? (authTab === "password" ? form.password : "");
|
||||
const privateKey =
|
||||
override?.privateKey ??
|
||||
(authTab === "privateKey" ? form.privateKey : "");
|
||||
const certificate = override?.certificate;
|
||||
|
||||
const proxyAddress = `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/gateway/ssh`;
|
||||
@@ -300,126 +313,197 @@ export default function SshClient({
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<LoginCardHeader subtitle="SSH" />
|
||||
<CardContent className="pt-6">
|
||||
<p className="text-destructive text-sm">{error}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div>
|
||||
<div className="text-center mb-2">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Powered by{" "}
|
||||
<Link
|
||||
href="https://pangolin.net/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
Pangolin
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>SSH</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-destructive text-sm">{error}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{!connected && (
|
||||
<Card className="w-full">
|
||||
<LoginCardHeader subtitle="Connect via SSH" />
|
||||
<CardContent className="pt-6">
|
||||
<div className="space-y-4">
|
||||
<Field label="Username" id="username">
|
||||
<Input
|
||||
id="username"
|
||||
value={form.username}
|
||||
onChange={(e) =>
|
||||
setForm({
|
||||
...form,
|
||||
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"
|
||||
<div>
|
||||
<div className="text-center mb-2">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Powered by{" "}
|
||||
<Link
|
||||
href="https://pangolin.net/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
<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()
|
||||
}
|
||||
>
|
||||
Upload key file
|
||||
</Button>
|
||||
{form.privateKey && (
|
||||
Pangolin
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>Sign in to SSH</CardTitle>
|
||||
<CardDescription>
|
||||
Enter credentials to access xxxx
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{/* Tab row */}
|
||||
<div className="flex space-x-4 border-b mb-4">
|
||||
{(["password", "privateKey"] as const).map(
|
||||
(tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
type="button"
|
||||
className="text-xs text-muted-foreground underline"
|
||||
onClick={() =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
privateKey: ""
|
||||
}))
|
||||
}
|
||||
onClick={() => setAuthTab(tab)}
|
||||
className={cn(
|
||||
"px-4 py-2 text-sm font-medium transition-colors whitespace-nowrap relative",
|
||||
authTab === tab
|
||||
? "text-primary after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:bg-primary after:rounded-full"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
Clear
|
||||
{tab === "password"
|
||||
? "Password"
|
||||
: "Private Key"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
className="hidden"
|
||||
accept=".pem,.key,.pub,*"
|
||||
onChange={handleKeyFile}
|
||||
/>
|
||||
</Field>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{connectError && (
|
||||
<p className="text-destructive text-sm">
|
||||
{connectError}
|
||||
</p>
|
||||
{authTab === "password" && (
|
||||
<div className="space-y-4">
|
||||
<Field label="Username" id="username-pw">
|
||||
<Input
|
||||
id="username-pw"
|
||||
value={form.username}
|
||||
onChange={(e) =>
|
||||
setForm({
|
||||
...form,
|
||||
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
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={() => connect()}
|
||||
loading={connecting}
|
||||
disabled={
|
||||
!form.username ||
|
||||
(!form.password && !form.privateKey)
|
||||
}
|
||||
className="w-full"
|
||||
>
|
||||
{connecting ? "Connecting..." : "Connect"}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{authTab === "privateKey" && (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Your private key is not stored or
|
||||
visible to Pangolin. Alternatively, you
|
||||
can use short-lived certificates for
|
||||
seamless authentication using your
|
||||
existing Pangolin identity.{" "}
|
||||
<Link
|
||||
href="https://docs.pangolin.net/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline inline-flex items-center gap-1"
|
||||
>
|
||||
Learn more
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</Link>
|
||||
</p>
|
||||
<Field label="Username" id="username-key">
|
||||
<Input
|
||||
id="username-key"
|
||||
value={form.username}
|
||||
onChange={(e) =>
|
||||
setForm({
|
||||
...form,
|
||||
username: e.target.value
|
||||
})
|
||||
}
|
||||
placeholder="root"
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Private Key" id="privateKey">
|
||||
<Textarea
|
||||
id="privateKey"
|
||||
value={form.privateKey}
|
||||
onChange={(e) =>
|
||||
setForm({
|
||||
...form,
|
||||
privateKey: e.target.value
|
||||
})
|
||||
}
|
||||
placeholder="-----BEGIN OPENSSH PRIVATE KEY-----"
|
||||
rows={5}
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Private Key File"
|
||||
id="privateKeyFile"
|
||||
>
|
||||
<Input
|
||||
id="privateKeyFile"
|
||||
type="file"
|
||||
accept=".pem,.key,.pub,*"
|
||||
onChange={handleKeyFile}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 space-y-3">
|
||||
{connectError && (
|
||||
<p className="text-destructive text-sm">
|
||||
{connectError}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={() => connect()}
|
||||
loading={connecting}
|
||||
disabled={
|
||||
!form.username ||
|
||||
(authTab === "password"
|
||||
? !form.password
|
||||
: !form.privateKey)
|
||||
}
|
||||
className="w-full"
|
||||
>
|
||||
{connecting
|
||||
? "Connecting..."
|
||||
: "Authenticate"}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{connected && (
|
||||
|
||||
+73
-27
@@ -6,8 +6,14 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { GetBrowserTargetResponse } from "@server/routers/resource";
|
||||
import { Card, CardContent } from "@app/components/ui/card";
|
||||
import LoginCardHeader from "@app/components/LoginCardHeader";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription
|
||||
} from "@app/components/ui/card";
|
||||
import Link from "next/link";
|
||||
|
||||
type FormState = {
|
||||
password: string;
|
||||
@@ -148,39 +154,79 @@ export default function VncClient({
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<LoginCardHeader subtitle="VNC" />
|
||||
<CardContent className="pt-6">
|
||||
<p className="text-destructive text-sm">{error}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div>
|
||||
<div className="text-center mb-2">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Powered by{" "}
|
||||
<Link
|
||||
href="https://pangolin.net/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
Pangolin
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>VNC</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-destructive text-sm">{error}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{!connected && (
|
||||
<Card className="w-full">
|
||||
<LoginCardHeader subtitle="Connect via VNC" />
|
||||
<CardContent className="pt-6">
|
||||
<div className="space-y-4">
|
||||
<Field label="Password (optional)" id="password">
|
||||
<Input
|
||||
<div>
|
||||
<div className="text-center mb-2">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Powered by{" "}
|
||||
<Link
|
||||
href="https://pangolin.net/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
Pangolin
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>VNC</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your credentials to access xxxx
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<Field
|
||||
label="Password (optional)"
|
||||
id="password"
|
||||
type="password"
|
||||
value={form.password}
|
||||
onChange={(e) =>
|
||||
update("password", e.target.value)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={form.password}
|
||||
onChange={(e) =>
|
||||
update("password", e.target.value)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Button onClick={connect} className="w-full">
|
||||
Connect
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Button onClick={connect} className="w-full">
|
||||
Connect
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user