import type { LoaderFunctionArgs, ActionFunctionArgs } from 'react-router'; import { useLoaderData } from 'react-router'; import { useLiveData } from '~/utils/useLiveData'; import { getSession } from '~/utils/sessions'; import { Link as RemixLink } from 'react-router'; import type { PreAuthKey, User } from '~/types'; import { pull, post } from '~/utils/headscale'; import { loadContext } from '~/utils/config/headplane'; import { useState } from 'react'; import { send } from '~/utils/res'; import Link from '~/components/Link'; import TableList from '~/components/TableList'; import Select from '~/components/Select'; import Switch from '~/components/Switch'; import AddPreAuthKey from './dialogs/new'; import AuthKeyRow from './components/key'; export async function action({ request }: ActionFunctionArgs) { const session = await getSession(request.headers.get('Cookie')); if (!session.has('hsApiKey')) { return send( { message: 'Unauthorized' }, { status: 401, }, ); } const data = await request.formData(); // Expiring a pre-auth key if (request.method === 'DELETE') { const key = data.get('key'); const user = data.get('user'); if (!key || !user) { return send( { message: 'Missing parameters' }, { status: 400, }, ); } await post<{ preAuthKey: PreAuthKey }>( 'v1/preauthkey/expire', session.get('hsApiKey')!, { user: user, key: key, }, ); return { message: 'Pre-auth key expired' }; } // Creating a new pre-auth key if (request.method === 'POST') { const user = data.get('user'); const expiry = data.get('expiry'); const reusable = data.get('reusable'); const ephemeral = data.get('ephemeral'); if (!user || !expiry || !reusable || !ephemeral) { return send( { message: 'Missing parameters' }, { status: 400, }, ); } // Extract the first "word" from expiry which is the day number // Calculate the date X days from now using the day number const day = Number(expiry.toString().split(' ')[0]); const date = new Date(); date.setDate(date.getDate() + day); const key = await post<{ preAuthKey: PreAuthKey }>( 'v1/preauthkey', session.get('hsApiKey')!, { user: user, ephemeral: ephemeral === 'on', reusable: reusable === 'on', expiration: date.toISOString(), aclTags: [], // TODO }, ); return { message: 'Pre-auth key created', key }; } } export async function loader({ request }: LoaderFunctionArgs) { const context = await loadContext(); const session = await getSession(request.headers.get('Cookie')); const users = await pull<{ users: User[] }>( 'v1/user', session.get('hsApiKey')!, ); const preAuthKeys = await Promise.all( users.users.map((user) => { const qp = new URLSearchParams(); qp.set('user', user.name); return pull<{ preAuthKeys: PreAuthKey[] }>( `v1/preauthkey?${qp.toString()}`, session.get('hsApiKey')!, ); }), ); return { keys: preAuthKeys.flatMap((keys) => keys.preAuthKeys), users: users.users, server: context.headscalePublicUrl ?? context.headscaleUrl, }; } export default function Page() { const { keys, users, server } = useLoaderData(); const [user, setUser] = useState('All'); const [status, setStatus] = useState('Active'); useLiveData({ interval: 3000 }); const filteredKeys = keys.filter((key) => { if (user !== 'All' && key.user !== user) { return false; } if (status !== 'All') { const now = new Date(); const expiry = new Date(key.expiration); if (status === 'Active') { return !(expiry < now) && !key.used; } if (status === 'Used/Expired') { return key.used || expiry < now; } if (status === 'Reusable') { return key.reusable; } if (status === 'Ephemeral') { return key.ephemeral; } } return true; }); return (

Settings / Pre-Auth Keys

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

Filter by user

Filter by status

{filteredKeys.length === 0 ? (

No pre-auth keys

) : ( filteredKeys.map((key) => ( )) )}
); }