Compare commits

...

3 Commits

Author SHA1 Message Date
Owen 71756812b6 Adjusting the ui 2026-05-29 10:57:28 -07:00
Owen 76cd716caa Add user id 2026-05-29 10:57:16 -07:00
Owen 9617eb2bd7 Add login formatting 2026-05-28 21:38:40 -07:00
4 changed files with 370 additions and 192 deletions
+3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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