import { FileKey2 } from 'lucide-react'; import { useMemo, useState } from 'react'; import type { ActionFunctionArgs, LoaderFunctionArgs } from 'react-router'; import { useLoaderData } from 'react-router'; 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 type { LoadContext } from '~/server'; import { Capabilities } from '~/server/web/roles'; import type { PreAuthKey, User } from '~/types'; import log from '~/utils/log'; import { authKeysAction } from './actions'; import AuthKeyRow from './auth-key-row'; import AddAuthKey from './dialogs/add-auth-key'; export async function loader({ request, context, }: LoaderFunctionArgs) { const session = await context.sessions.auth(request); const { users } = await context.client.get<{ users: User[] }>( 'v1/user', session.get('api_key')!, ); const preAuthKeys = await Promise.all( users .filter((user) => user.name?.length > 0) // Filter out any invalid users .map(async (user) => { const qp = new URLSearchParams(); qp.set('user', user.name); try { const { preAuthKeys } = await context.client.get<{ preAuthKeys: PreAuthKey[]; }>(`v1/preauthkey?${qp.toString()}`, session.get('api_key')!); 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: [] as PreAuthKey[], }; } }), ); 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 async function action(request: ActionFunctionArgs) { return authKeysAction(request); } type Status = 'all' | 'active' | 'expired' | 'reusable' | 'ephemeral'; export default function Page() { const { keys, missing, users, url, access } = useLoaderData(); 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; } }); }, [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) => ( <> {user.name} {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.email === key.user); if (!user) { return null; } return ( ); }) )}
); }