mirror of
https://github.com/fosrl/pangolin.git
synced 2026-06-10 17:43:15 +00:00
Replace tab component
This commit is contained in:
@@ -17,7 +17,7 @@ import {
|
||||
import Link from "next/link";
|
||||
import { ExternalLink, Loader2, AlertCircle } from "lucide-react";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { cn } from "@app/lib/cn";
|
||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||
import type { SignSshKeyResponse } from "@server/routers/ssh/types";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
@@ -61,8 +61,6 @@ export default function SshClient({
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
const [authTab, setAuthTab] = useState<AuthTab>("password");
|
||||
|
||||
function handleKeyFile(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
@@ -182,7 +180,10 @@ export default function SshClient({
|
||||
}
|
||||
}, []);
|
||||
|
||||
function connect(override?: ConnectCredentials) {
|
||||
function connect(
|
||||
override?: ConnectCredentials,
|
||||
authMethod: AuthTab = "password"
|
||||
) {
|
||||
setConnectError(null);
|
||||
setConnecting(true);
|
||||
|
||||
@@ -194,10 +195,11 @@ export default function SshClient({
|
||||
|
||||
const username = override?.username ?? form.username;
|
||||
const password =
|
||||
override?.password ?? (authTab === "password" ? form.password : "");
|
||||
override?.password ??
|
||||
(authMethod === "password" ? form.password : "");
|
||||
const privateKey =
|
||||
override?.privateKey ??
|
||||
(authTab === "privateKey" ? form.privateKey : "");
|
||||
(authMethod === "privateKey" ? form.privateKey : "");
|
||||
const certificate = override?.certificate;
|
||||
|
||||
const proxyAddress = `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/gateway/ssh`;
|
||||
@@ -224,7 +226,7 @@ export default function SshClient({
|
||||
ws.onopen = () => {
|
||||
// Send credentials as the first frame so the proxy can complete
|
||||
// SSH authentication before piping pty data. Stay in "connecting"
|
||||
// state until the server responds — this prevents the flash to the
|
||||
// state until the server responds - this prevents the flash to the
|
||||
// terminal page that would occur if we set connected=true here.
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
@@ -260,7 +262,7 @@ export default function SshClient({
|
||||
xtermRef.current?.write(msg.data);
|
||||
} else if (msg.type === "error") {
|
||||
if (!authConfirmed) {
|
||||
// Auth-phase error — show in the login form.
|
||||
// Auth-phase error - show in the login form.
|
||||
authErrorShown = true;
|
||||
setConnecting(false);
|
||||
setConnectError(
|
||||
@@ -281,13 +283,13 @@ export default function SshClient({
|
||||
xtermRef.current?.write(evt.data);
|
||||
}
|
||||
} else if (evt.data instanceof Blob) {
|
||||
evt.data.text().then((t) => {
|
||||
evt.data.text().then((text) => {
|
||||
if (!authConfirmed) {
|
||||
authConfirmed = true;
|
||||
setConnecting(false);
|
||||
setConnected(true);
|
||||
}
|
||||
xtermRef.current?.write(t);
|
||||
xtermRef.current?.write(text);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -426,31 +428,15 @@ export default function SshClient({
|
||||
</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"
|
||||
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"
|
||||
)}
|
||||
>
|
||||
{tab === "password"
|
||||
? t("sshPasswordTab")
|
||||
: t("sshPrivateKeyTab")}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{authTab === "password" && (
|
||||
<div className="space-y-4">
|
||||
<HorizontalTabs
|
||||
clientSide
|
||||
defaultTab={0}
|
||||
items={[
|
||||
{ title: t("sshPasswordTab"), href: "#" },
|
||||
{ title: t("sshPrivateKeyTab"), href: "#" }
|
||||
]}
|
||||
>
|
||||
<div className="space-y-4 mt-4 p-1">
|
||||
<Field
|
||||
label={t("username")}
|
||||
id="username-pw"
|
||||
@@ -464,7 +450,6 @@ export default function SshClient({
|
||||
username: e.target.value
|
||||
})
|
||||
}
|
||||
placeholder="root"
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t("password")} id="password">
|
||||
@@ -480,11 +465,31 @@ export default function SshClient({
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-4 space-y-3">
|
||||
{connectError && (
|
||||
<p className="text-destructive text-sm">
|
||||
{connectError}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{authTab === "privateKey" && (
|
||||
<div className="space-y-4">
|
||||
<Button
|
||||
onClick={() =>
|
||||
connect(undefined, "password")
|
||||
}
|
||||
loading={connecting}
|
||||
disabled={
|
||||
!form.username || !form.password
|
||||
}
|
||||
className="w-full"
|
||||
>
|
||||
{connecting
|
||||
? t("sshConnecting")
|
||||
: t("sshAuthenticate")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 mt-4 p-1">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("sshPrivateKeyDisclaimer")}{" "}
|
||||
<Link
|
||||
@@ -510,7 +515,6 @@ export default function SshClient({
|
||||
username: e.target.value
|
||||
})
|
||||
}
|
||||
placeholder="root"
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
@@ -542,32 +546,31 @@ export default function SshClient({
|
||||
onChange={handleKeyFile}
|
||||
/>
|
||||
</Field>
|
||||
<div className="mt-4 space-y-3">
|
||||
{connectError && (
|
||||
<p className="text-destructive text-sm">
|
||||
{connectError}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={() =>
|
||||
connect(undefined, "privateKey")
|
||||
}
|
||||
loading={connecting}
|
||||
disabled={
|
||||
!form.username ||
|
||||
!form.privateKey
|
||||
}
|
||||
className="w-full"
|
||||
>
|
||||
{connecting
|
||||
? t("sshConnecting")
|
||||
: t("sshAuthenticate")}
|
||||
</Button>
|
||||
</div>
|
||||
</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
|
||||
? t("sshConnecting")
|
||||
: t("sshAuthenticate")}
|
||||
</Button>
|
||||
</div>
|
||||
</HorizontalTabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -43,11 +43,14 @@ export function NewtSiteInstallCommands({
|
||||
const t = useTranslations();
|
||||
|
||||
const [acceptClients, setAcceptClients] = useState(true);
|
||||
const [allowPangolinSsh, setAllowPangolinSsh] = useState(true);
|
||||
const [platform, setPlatform] = useState<Platform>("linux");
|
||||
const [architecture, setArchitecture] = useState(
|
||||
() => getArchitectures(platform)[0]
|
||||
);
|
||||
|
||||
const supportsSshOption = platform === "linux" || platform === "nixos";
|
||||
|
||||
const acceptClientsFlag = !acceptClients ? " --disable-clients" : "";
|
||||
const acceptClientsEnv = !acceptClients
|
||||
? "\n - DISABLE_CLIENTS=true"
|
||||
@@ -57,6 +60,11 @@ export function NewtSiteInstallCommands({
|
||||
--set newtInstances[0].acceptClients=true`
|
||||
: "";
|
||||
|
||||
const disableSshFlag =
|
||||
supportsSshOption && !allowPangolinSsh ? " --disable-ssh" : "";
|
||||
const runAsRootPrefix =
|
||||
supportsSshOption && allowPangolinSsh ? "sudo " : "";
|
||||
|
||||
const commandList: Record<Platform, Record<string, CommandItem[]>> = {
|
||||
linux: {
|
||||
Run: [
|
||||
@@ -66,7 +74,7 @@ export function NewtSiteInstallCommands({
|
||||
},
|
||||
{
|
||||
title: t("run"),
|
||||
command: `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
||||
command: `${runAsRootPrefix}newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}${disableSshFlag}`
|
||||
}
|
||||
],
|
||||
"Systemd Service": [
|
||||
@@ -86,6 +94,11 @@ PANGOLIN_ENDPOINT=${endpoint}${
|
||||
? `
|
||||
DISABLE_CLIENTS=true`
|
||||
: ""
|
||||
}${
|
||||
!allowPangolinSsh
|
||||
? `
|
||||
DISABLE_SSH=true`
|
||||
: ""
|
||||
}
|
||||
EOF
|
||||
sudo chmod 600 /etc/newt/newt.env`
|
||||
@@ -205,7 +218,7 @@ WantedBy=default.target`
|
||||
},
|
||||
nixos: {
|
||||
Flake: [
|
||||
`nix run 'nixpkgs#fosrl-newt' -- --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
||||
`${runAsRootPrefix}nix run 'nixpkgs#fosrl-newt' -- --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}${disableSshFlag}`
|
||||
]
|
||||
}
|
||||
};
|
||||
@@ -273,6 +286,19 @@ WantedBy=default.target`
|
||||
label={t("siteAcceptClientConnections")}
|
||||
/>
|
||||
</div>
|
||||
{supportsSshOption && (
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<CheckboxWithLabel
|
||||
id="allowPangolinSsh"
|
||||
checked={allowPangolinSsh}
|
||||
onCheckedChange={(checked) => {
|
||||
const value = checked as boolean;
|
||||
setAllowPangolinSsh(value);
|
||||
}}
|
||||
label="Allow Pangolin SSH"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<p
|
||||
id="acceptClients-desc"
|
||||
className="text-sm text-muted-foreground"
|
||||
|
||||
Reference in New Issue
Block a user