import { LoaderFunctionArgs, ActionFunctionArgs } from '@remix-run/node' import { useLoaderData } from '@remix-run/react' import { useLiveData } from '~/utils/useLiveData' import { getSession } from '~/utils/sessions' import { Link as RemixLink } from '@remix-run/react' import { 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 => ( ))}
) }