/* eslint-disable @typescript-eslint/no-non-null-assertion */ import { BeakerIcon, EyeIcon, IssueDraftIcon, PencilIcon } from '@primer/octicons-react' import { ActionFunctionArgs, json, LoaderFunctionArgs } from '@remix-run/node' import { useFetcher, useLoaderData } from '@remix-run/react' import { useEffect, useState } from 'react' import { Tab, TabList, TabPanel, Tabs } from 'react-aria-components' import { setTimeout } from 'node:timers/promises' import Button from '~/components/Button' import Code from '~/components/Code' import Link from '~/components/Link' import Notice from '~/components/Notice' import Spinner from '~/components/Spinner' import { toast } from '~/components/Toaster' import { cn } from '~/utils/cn' import { loadContext } from '~/utils/config/headplane' import { HeadscaleError, pull, put } from '~/utils/headscale' import { getSession } from '~/utils/sessions' import Monaco from './editor' export async function loader({ request }: LoaderFunctionArgs) { const session = await getSession(request.headers.get('Cookie')) try { const { policy } = await pull<{ policy: string }>( 'v1/policy', session.get('hsApiKey')!, ) try { // We have read access, now do we have write access? // Attempt to set the policy to what we just got await put('v1/policy', session.get('hsApiKey')!, { policy, }) return { hasAclWrite: true, currentAcl: policy, aclType: 'json', } as const } catch (error) { if (!(error instanceof HeadscaleError)) { throw error } if (error.status === 500) { return { hasAclWrite: false, currentAcl: policy, aclType: 'json', } as const } } } catch {} return { hasAclWrite: true, currentAcl: '', aclType: 'json', } as const } export async function action({ request }: ActionFunctionArgs) { const session = await getSession(request.headers.get('Cookie')) if (!session.has('hsApiKey')) { return json({ success: false }, { status: 401, }) } const { acl } = await request.json() as { acl: string, api: boolean } try { await put('v1/policy', session.get('hsApiKey')!, { policy: acl, }) await setTimeout(250) return json({ success: true }) } catch (error) { return json({ success: false }, { status: error instanceof HeadscaleError ? error.status : 500, }) } return json({ success: true }) } export function ErrorBoundary() { return (
An ACL policy is not available or an error occurred while trying to fetch it.

Access Control List (ACL)

The ACL file is used to define the access control rules for your network. You can find more information about the ACL file in the {' '} Tailscale ACL guide {' '} and the {' '} Headscale docs .

If you are running Headscale 0.23-beta1 or later, the ACL configuration is most likely set to {' '} file {' '} mode but the ACL file is not available. In order to resolve this you will either need to correctly set {' '} policy.path {' '} in your Headscale configuration or set the {' '} policy.mode {' '} to {' '} database .

) } export default function Page() { const data = useLoaderData() const fetcher = useFetcher() const [acl, setAcl] = useState(data.currentAcl) const [toasted, setToasted] = useState(false) useEffect(() => { if (!fetcher.data || toasted) { return } if (fetcher.data.success) { toast('Updated tailnet ACL policy') } else { toast('Failed to update tailnet ACL policy') } setToasted(true) setAcl(data.currentAcl) }, [fetcher.data, toasted, data.currentAcl]) return (
{data.hasAclWrite ? undefined : (
The ACL policy is read-only. You can view the current policy but you cannot make changes to it.
To resolve this, you need to set the ACL policy mode to database in your Headscale configuration.
)}

Access Control List (ACL)

The ACL file is used to define the access control rules for your network. You can find more information about the ACL file in the {' '} Tailscale ACL guide {' '} and the {' '} Headscale docs .

cn( 'px-4 py-2 rounded-tl-lg', 'focus:outline-none flex items-center gap-2', 'border-x border-gray-200 dark:border-gray-700', isSelected ? 'text-gray-900 dark:text-gray-100' : '', )} >

Edit file

cn( 'px-4 py-2', 'focus:outline-none flex items-center gap-2', 'border-x border-gray-200 dark:border-gray-700', isSelected ? 'text-gray-900 dark:text-gray-100' : '', )} >

Preview changes

cn( 'px-4 py-2 rounded-tr-lg', 'focus:outline-none flex items-center gap-2', 'border-x border-gray-200 dark:border-gray-700', isSelected ? 'text-gray-900 dark:text-gray-100' : '', )} >

Preview rules

The Preview rules is very much still a work in progress. It is a bit complicated to implement right now but hopefully it will be available soon.

) }