mirror of
https://github.com/tale/headplane.git
synced 2026-08-28 07:57:03 +00:00
22a521dff5
Headscale's RegisterNodeRequest.user proto field is a string that the RegisterNode handler resolves with GetUserByName (a strict WHERE name=? SQL match) — there is no numeric-id fallback. The Owner select in the Register Machine Key dialog was sending user.id, so registration failed with ErrUserNotFound whenever the display name and the numeric id disagreed (which is always for unlinked Headscale users). Send user.name instead. Closes #532 Amp-Thread-ID: https://ampcode.com/threads/T-019e7ae4-6862-760c-a3e7-239350eab71d Co-authored-by: Amp <amp@ampcode.com>
91 lines
3.1 KiB
TypeScript
91 lines
3.1 KiB
TypeScript
import { type } from "arktype";
|
|
import { Computer, FileKey2 } from "lucide-react";
|
|
import { useState } from "react";
|
|
import { useNavigate } from "react-router";
|
|
|
|
import CodeBlock from "~/components/code-block";
|
|
import Dialog, { DialogPanel } from "~/components/dialog";
|
|
import Input from "~/components/input";
|
|
import { Menu, MenuContent, MenuItem, MenuTrigger } from "~/components/menu";
|
|
import Select from "~/components/select";
|
|
import Text from "~/components/text";
|
|
import Title from "~/components/title";
|
|
import { useForm } from "~/hooks/use-form";
|
|
import type { User } from "~/types";
|
|
import { getUserDisplayName } from "~/utils/user";
|
|
|
|
const registerSchema = type({
|
|
register_key: "string == 24",
|
|
user: "string > 0",
|
|
});
|
|
|
|
export interface NewMachineProps {
|
|
server: string;
|
|
users: User[];
|
|
isDisabled?: boolean;
|
|
disabledKeys?: string[];
|
|
}
|
|
|
|
export default function NewMachine(data: NewMachineProps) {
|
|
const [pushDialog, setPushDialog] = useState(false);
|
|
const form = useForm({ schema: registerSchema });
|
|
const navigate = useNavigate();
|
|
|
|
return (
|
|
<>
|
|
<Dialog isOpen={pushDialog} onOpenChange={setPushDialog}>
|
|
<DialogPanel isDisabled={!form.canSubmit}>
|
|
<Title>Register Machine Key</Title>
|
|
<Text>The machine key is given when you run the following command on your device:</Text>
|
|
<CodeBlock className="mb-4">{`tailscale up --login-server=${data.server}`}</CodeBlock>
|
|
<input name="action_id" type="hidden" value="register" />
|
|
<Input
|
|
{...form.field("register_key")}
|
|
required
|
|
label="Machine Key"
|
|
placeholder="AbCd..."
|
|
/>
|
|
<Select
|
|
required
|
|
label="Owner"
|
|
name="user"
|
|
onValueChange={(v) => form.setValue("user", v)}
|
|
placeholder="Select a user"
|
|
items={data.users.map((user) => ({
|
|
// Headscale's v1/node/register endpoint resolves the owner by
|
|
// username via GetUserByName, so we must pass user.name (not id).
|
|
value: user.name,
|
|
label: getUserDisplayName(user),
|
|
}))}
|
|
/>
|
|
</DialogPanel>
|
|
</Dialog>
|
|
<Menu disabled={data.isDisabled}>
|
|
<MenuTrigger className="rounded-md bg-indigo-500 px-3.5 py-2 text-sm font-semibold text-white hover:bg-indigo-500/90 dark:bg-indigo-500/90 dark:hover:bg-indigo-500/80">
|
|
Add Device
|
|
</MenuTrigger>
|
|
<MenuContent>
|
|
<MenuItem
|
|
disabled={data.disabledKeys?.includes("register")}
|
|
onClick={() => setPushDialog(true)}
|
|
>
|
|
<div className="flex items-center gap-x-3">
|
|
<Computer className="w-4" />
|
|
Register Machine Key
|
|
</div>
|
|
</MenuItem>
|
|
<MenuItem
|
|
disabled={data.disabledKeys?.includes("pre-auth")}
|
|
onClick={() => navigate("/settings/auth-keys")}
|
|
>
|
|
<div className="flex items-center gap-x-3">
|
|
<FileKey2 className="w-4" />
|
|
Generate Pre-auth Key
|
|
</div>
|
|
</MenuItem>
|
|
</MenuContent>
|
|
</Menu>
|
|
</>
|
|
);
|
|
}
|