fix: support user input when not specified for ssh

This commit is contained in:
Aarnav Tale
2025-06-20 20:09:28 -04:00
parent dd287c0444
commit 1150d1616e
3 changed files with 70 additions and 6 deletions
+21 -4
View File
@@ -9,7 +9,6 @@ import {
useLoaderData,
useSubmit,
} from 'react-router';
import { useEventSource } from 'remix-utils/sse/react';
import { LoadContext } from '~/server';
import { Machine, PreAuthKey, User } from '~/types';
import { useLiveData } from '~/utils/live-data';
@@ -19,6 +18,7 @@ import { eq } from 'drizzle-orm';
import wasm from '~/hp_ssh.wasm?url';
import { EphemeralNodeInsert, ephemeralNodes } from '~/server/db/schema';
import '~/wasm_exec';
import UserPrompt from './user-prompt';
export const shouldRevalidate: ShouldRevalidateFunction = () => {
return false;
@@ -88,8 +88,18 @@ export async function loader({
const qp = new URL(request.url).searchParams;
const username = qp.get('username') || undefined;
const hostname = qp.get('hostname') || undefined;
if (!username || !hostname) {
throw data('Missing required parameters: username, hostname', 400);
if (!hostname) {
throw data('Missing required parameter: hostname', 400);
}
if (!username) {
return {
ipnDetails: undefined,
sshDetails: {
username,
hostname,
},
};
}
// We're making a request to <url>/key?v=116 to check the CORS headers
@@ -202,9 +212,12 @@ export default function Page() {
const [ipn, setIpn] = useState<TsWasmNet | null>(null);
const [nodeKey, setNodeKey] = useState<string | null>(null);
const { ipnDetails, sshDetails } = useLoaderData<typeof loader>();
const ping = useEventSource('/ssh/ping', { event: 'ping' });
useEffect(() => {
if (!ipnDetails) {
return;
}
pause();
const go = new Go(); // Go is defined by wasm_exec.js
WebAssembly.instantiateStreaming(fetch(wasm), go.importObject).then(
@@ -249,6 +262,10 @@ export default function Page() {
);
}, []);
if (!sshDetails.username) {
return <UserPrompt hostname={sshDetails.hostname} />;
}
return (
<div className="w-screen h-screen bg-headplane-900">
{ipn === null ? (
+47
View File
@@ -0,0 +1,47 @@
import { useState } from 'react';
import Button from '~/components/Button';
import Card from '~/components/Card';
import Code from '~/components/Code';
import Input from '~/components/Input';
interface UserPromptProps {
hostname: string;
}
export default function UserPrompt({ hostname }: UserPromptProps) {
const [username, setUsername] = useState('');
return (
<div className="flex items-center justify-center h-screen">
<Card>
<Card.Title>Enter Username</Card.Title>
<Card.Text className="mb-4">
Enter the username you want to use to connect to{' '}
<Code>{hostname}</Code>
{'. '}
WebSSH follows the Headscale ACLs, so only permitted usernames will be
able to connect.
</Card.Text>
<Input
labelHidden
type="text"
label="Username"
placeholder="Username"
className="mb-2"
onChange={setUsername}
/>
<Button
variant="heavy"
className="w-full"
onPress={() => {
// We can't use the navigate hook here as we need to do a
// full page reload to ensure the SSH connection is established
window.location.href = `${__PREFIX__}/ssh?hostname=${hostname}&username=${username}`;
}}
>
Connect
</Button>
</Card>
</div>
);
}
+2 -2
View File
@@ -41,7 +41,7 @@ export async function pruneEphemeralNodes({
// Delete from the Headscale nodes list and then from the database
const promises = toPrune.map((node) => {
return async () => {
log.info('api', `Pruning node ${node.name}`);
log.debug('api', `Pruning node ${node.name}`);
await context.client.delete(
`v1/node/${node.id}`,
session.get('api_key')!,
@@ -50,7 +50,7 @@ export async function pruneEphemeralNodes({
await context.db
.delete(ephemeralNodes)
.where(eq(ephemeralNodes.node_key, node.nodeKey));
log.info('api', `Node ${node.name} pruned successfully`);
log.debug('api', `Node ${node.name} pruned successfully`);
};
});