import type { Key } from "react"; import { useEffect, useRef, useState } from "react"; import { useFetcher } from "react-router"; import Button from "~/components/Button"; import Code from "~/components/Code"; import Dialog from "~/components/Dialog"; import Input from "~/components/Input"; import Link from "~/components/link"; import NumberInput from "~/components/NumberInput"; import Select from "~/components/Select"; import Switch from "~/components/Switch"; import type { User } from "~/types"; import toast from "~/utils/toast"; import { getUserDisplayName } from "~/utils/user"; interface AddAuthKeyProps { users: User[]; url: string; selfServiceOnly: boolean; currentSubject?: string; } function findCurrentUser(users: User[], subject: string | undefined): User | undefined { if (!subject) { return undefined; } return users.find((u) => { if (u.provider !== "oidc" || !u.providerId) { return false; } return u.providerId.split("/").pop() === subject; }); } export default function AddAuthKey({ users, url, selfServiceOnly, currentSubject, }: AddAuthKeyProps) { const fetcher = useFetcher(); const submittingRef = useRef(false); const [isOpen, setIsOpen] = useState(false); const [reusable, setReusable] = useState(false); const [ephemeral, setEphemeral] = useState(false); const [tagOnly, setTagOnly] = useState(false); const currentUser = selfServiceOnly ? findCurrentUser(users, currentSubject) : null; const availableUsers = selfServiceOnly && currentUser ? [currentUser] : users; const [userId, setUserId] = useState(availableUsers[0]?.id); const [tags, setTags] = useState(""); const createdKey = fetcher.data?.success ? fetcher.data.key : null; useEffect(() => { if (fetcher.state === "idle" && fetcher.data) { submittingRef.current = false; } }, [fetcher.data, fetcher.state]); useEffect(() => { if (!isOpen) { setReusable(false); setEphemeral(false); setTagOnly(false); setUserId(availableUsers[0]?.id); setTags(""); fetcher.data = undefined; } }, [isOpen]); const parsedTags = tags .split(",") .map((t) => t.trim()) .filter((t) => t.length > 0) .map((t) => (t.startsWith("tag:") ? t : `tag:${t}`)); const canSubmit = tagOnly ? parsedTags.length > 0 : userId != null; return ( { if (!open && submittingRef.current) { return; } setIsOpen(open); }} > {createdKey ? ( Pre-auth key created Copy this key now. You will not be able to see the full key again.
{createdKey}
To register a device with this key: {`tailscale up --login-server=${url} --authkey ${createdKey}`}
) : ( { event.preventDefault(); submittingRef.current = true; const form = new FormData(event.currentTarget as HTMLFormElement); form.set("action_id", "add_preauthkey"); form.set("user_id", tagOnly ? "" : (userId?.toString() ?? "")); form.set("reusable", reusable ? "on" : "off"); form.set("ephemeral", ephemeral ? "on" : "off"); form.set("acl_tags", parsedTags.join(",")); fetcher.submit(form, { method: "POST" }); }} isDisabled={fetcher.state !== "idle" || !canSubmit} > Generate auth key {!selfServiceOnly && (
Tag-only key Create a key owned by ACL tags instead of a user.
setTagOnly(!tagOnly)} />
)} {!tagOnly && ( )} setTags(value)} placeholder="server, prod" value={tags} />
Reusable Use this key to authenticate more than one device.
setReusable(!reusable)} />
Ephemeral Devices authenticated with this key will be automatically removed once they go offline.{" "} Learn more
setEphemeral(!ephemeral)} />
)}
); }