import { FileKey2 } from "lucide-react"; import { useMemo, useState } from "react"; import { Link as RemixLink } from "react-router"; import Code from "~/components/Code"; import Link from "~/components/Link"; import Notice from "~/components/Notice"; import Select from "~/components/Select"; import TableList from "~/components/TableList"; import { Capabilities } from "~/server/web/roles"; import log from "~/utils/log"; import { filterUsersWithValidIds, getUserDisplayName } from "~/utils/user"; import type { Route } from "./+types/overview"; import { authKeysAction } from "./actions"; import AuthKeyRow from "./auth-key-row"; import AddAuthKey from "./dialogs/add-auth-key"; export async function loader({ request, context }: Route.LoaderArgs) { const session = await context.sessions.auth(request); const api = context.hsApi.getRuntimeClient(session.api_key); const users = await api.getUsers(); const preAuthKeys = await Promise.all( filterUsersWithValidIds(users).map(async (user) => { try { const preAuthKeys = await api.getPreAuthKeys(user.id); return { success: true, user, preAuthKeys, }; } catch (error) { log.error("api", "GET /v1/preauthkey for %s: %o", user.name, error); return { success: false, user, error, preAuthKeys: [], }; } }), ); const keys = preAuthKeys .filter(({ success }) => success) .map(({ user, preAuthKeys }) => ({ user, preAuthKeys, })); const missing = preAuthKeys .filter(({ success }) => !success) .map(({ user, error }) => ({ user, error, })); return { keys, missing, users, access: await context.sessions.check(request, Capabilities.generate_authkeys), url: context.config.headscale.public_url ?? context.config.headscale.url, }; } export const action = authKeysAction; type Status = "all" | "active" | "expired" | "reusable" | "ephemeral"; export default function Page({ loaderData: { keys, missing, users, url, access }, }: Route.ComponentProps) { const [selectedUser, setSelectedUser] = useState("__headplane_all"); const [status, setStatus] = useState("active"); const isDisabled = !access || keys.flatMap(({ preAuthKeys }) => preAuthKeys).length === 0; const filteredKeys = useMemo(() => { const now = new Date(); return keys .filter(({ user }) => { if (selectedUser === "__headplane_all") { return true; } return user.id === selectedUser; }) .flatMap(({ preAuthKeys }) => preAuthKeys) .filter((key) => { if (status === "all") { return true; } if (status === "ephemeral") { return key.ephemeral; } if (status === "reusable") { return key.reusable; } const expiry = new Date(key.expiration); if (status === "expired") { // Expired keys are either used or expired // BUT only used if they are not reusable if (key.used && !key.reusable) { return true; } return expiry < now; } if (status === "active") { // Active keys are either not expired or reusable if (expiry < now) { return false; } if (!key.used) { return true; } return key.reusable; } return false; }); }, [keys, selectedUser, status]); return (

Settings / Pre-Auth Keys

{!access ? ( You do not have the necessary permissions to generate pre-auth keys. Please contact your administrator to request access or to generate a pre-auth key for you. ) : missing.length > 0 ? ( An error occurred while fetching the authentication keys for the following users:{" "} {missing.map(({ user }, index) => ( <> {getUserDisplayName(user)} {index < missing.length - 1 ? ", " : ". "} ))} Their keys may not be listed correctly. Please check the server logs for more information. ) : undefined}

Pre-Auth Keys

Headscale fully supports pre-authentication keys in order to easily add devices to your Tailnet. To learn more about using pre-authentication keys, visit the{" "} Tailscale documentation

{keys.flatMap(({ preAuthKeys }) => preAuthKeys).length === 0 ? (

No pre-auth keys have been created yet.

) : filteredKeys.length === 0 ? (

No pre-auth keys match the selected filters.

) : ( filteredKeys.map((key) => { // TODO: Why is Headscale using email as the user ID here? // https://github.com/juanfont/headscale/issues/2520 const user = users.find((user) => user.id === key.user.id); if (!user) { return null; } return ( ); }) )}
); }