import { type ActionFunctionArgs, json, type LoaderFunctionArgs, redirect } from '@remix-run/node' import { Form, useActionData, useLoaderData } from '@remix-run/react' import { useMemo } from 'react' import Button from '~/components/Button' import Card from '~/components/Card' import Code from '~/components/Code' import TextField from '~/components/TextField' import { type Key } from '~/types' import { loadContext } from '~/utils/config/headplane' import { pull } from '~/utils/headscale' import { startOidc } from '~/utils/oidc' import { commitSession, getSession } from '~/utils/sessions' export async function loader({ request }: LoaderFunctionArgs) { const session = await getSession(request.headers.get('Cookie')) if (session.has('hsApiKey')) { return redirect('/machines', { headers: { // eslint-disable-next-line @typescript-eslint/naming-convention 'Set-Cookie': await commitSession(session), }, }) } const context = await loadContext() // Only set if OIDC is properly enabled anyways if (context.oidc?.disableKeyLogin) { return startOidc(context.oidc, request) } return { oidc: context.oidc?.issuer, apiKey: !context.oidc?.disableKeyLogin, } } export async function action({ request }: ActionFunctionArgs) { const formData = await request.formData() const oidcStart = formData.get('oidc-start') if (oidcStart) { const context = await loadContext() if (!context.oidc) { throw new Error('An invalid OIDC configuration was provided') } // We know it exists here because this action only happens on OIDC return startOidc(context.oidc, request) } const apiKey = String(formData.get('api-key')) const session = await getSession(request.headers.get('Cookie')) // Test the API key try { const apiKeys = await pull<{ apiKeys: Key[] }>('v1/apikey', apiKey) const key = apiKeys.apiKeys.find(k => apiKey.startsWith(k.prefix)) if (!key) { throw new Error('Invalid API key') } const expiry = new Date(key.expiration) const expiresIn = expiry.getTime() - Date.now() const expiresDays = Math.round(expiresIn / 1000 / 60 / 60 / 24) session.set('hsApiKey', apiKey) session.set('user', { name: key.prefix, email: `${expiresDays.toString()} days`, }) return redirect('/machines', { headers: { // eslint-disable-next-line @typescript-eslint/naming-convention 'Set-Cookie': await commitSession(session, { maxAge: expiresIn, }), }, }) } catch { return json({ error: 'Invalid API key', }) } } export default function Page() { const data = useLoaderData() const actionData = useActionData() const showOr = useMemo(() => data.oidc && data.apiKey, [data]) return (
Welcome to Headplane {data.apiKey ? (
Enter an API key to authenticate with Headplane. You can generate one by running {' '} headscale apikeys create {' '} in your terminal. {actionData?.error ? (

{actionData.error}

) : undefined} ) : undefined} {showOr ? (

or
) : undefined} {data.oidc ? (
) : undefined}
) }