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