/* eslint-disable @typescript-eslint/no-non-null-assertion */ import { BeakerIcon, EyeIcon, IssueDraftIcon, PencilIcon } from '@primer/octicons-react' import { type 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 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 { loadAcl, loadContext, patchAcl } 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')!, ) console.log(policy) 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, isPolicyApi: true, currentAcl: policy, aclType: 'json', } as const } catch (error) { if (!(error instanceof HeadscaleError)) { throw error } if (error.status === 500) { return { hasAclWrite: false, isPolicyApi: true, currentAcl: policy, aclType: 'json', } as const } } } catch (error) { // Propagate our errors through normal error handling if (!(error instanceof HeadscaleError)) { throw error } // Not on 0.23-beta1 or later if (error.status === 404) { const { data, type, read, write } = await loadAcl() return { hasAclWrite: write, isPolicyApi: false, currentAcl: read ? data : '', aclType: type, } } throw error } return { hasAclWrite: true, isPolicyApi: 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 data = await request.json() as { acl: string, api: boolean } if (data.api) { try { await put('v1/policy', session.get('hsApiKey')!, { policy: data.acl, }) return json({ success: true }) } catch (error) { return json({ success: false }, { status: error instanceof HeadscaleError ? error.status : 500, }) } } const context = await loadContext() if (!context.acl.write) { return json({ success: false }, { status: 403, }) } await patchAcl(data.acl) if (context.integration?.onAclChange) { await context.integration.onAclChange(context.integration.context) } return json({ success: true }) } export function ErrorBoundary() { return (
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
.
If you are running an older version of Headscale, the
{' '}
ACL_FILE
{' '}
environment variable is not set. Refer to the
{' '}
Headplane Configuration
{' '}
documentation for more information on how to set the
ACL file and integrate it with Headscale.
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 .
Edit file
Preview changes
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.