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 { getContext } from '~/utils/config' 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 getContext() const issuer = context.oidcConfig?.issuer const id = context.oidcConfig?.client const secret = context.oidcConfig?.secret const normal = process.env.DISABLE_API_KEY_LOGIN if (issuer && (!id || !secret)) { throw new Error('An invalid OIDC configuration was provided') } const data = { oidc: issuer, apiKey: normal !== 'true', } if (!data.oidc && !data.apiKey) { throw new Error('No authentication method is enabled') } if (data.oidc && !data.apiKey) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return startOidc(data.oidc, id!, request) } return data } export async function action({ request }: ActionFunctionArgs) { const formData = await request.formData() const oidcStart = formData.get('oidc-start') if (oidcStart) { const context = await getContext() const issuer = context.oidcConfig?.issuer const id = context.oidcConfig?.client // We know it exists here because this action only happens on OIDC // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return startOidc(issuer!, id!, 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 (error) { console.error(error) 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}
) }