Files
headplane/app/routes/acls/overview.tsx
T
2026-03-14 13:52:08 -04:00

191 lines
6.8 KiB
TypeScript

import { AlertCircle, Construction, Eye, FlaskConical, Pencil } from "lucide-react";
import { useEffect, useState } from "react";
import { isRouteErrorResponse, useFetcher, useRevalidator } from "react-router";
import Button from "~/components/Button";
import Card from "~/components/Card";
import Code from "~/components/Code";
import Link from "~/components/link";
import Notice from "~/components/Notice";
import PageError from "~/components/page-error";
import Tabs from "~/components/Tabs";
import { isApiError } from "~/server/headscale/api/error-client";
import toast from "~/utils/toast";
import type { Route } from "./+types/overview";
import { aclAction } from "./acl-action";
import { aclLoader } from "./acl-loader";
import { Differ, Editor } from "./components/cm.client";
export const loader = aclLoader;
export const action = aclAction;
export default function Page({ loaderData: { access, writable, policy } }: Route.ComponentProps) {
const [codePolicy, setCodePolicy] = useState(policy);
const fetcher = useFetcher<typeof action>();
const { revalidate } = useRevalidator();
const disabled = !access || !writable; // Disable if no permission or not writable
useEffect(() => {
// Update the codePolicy when the loader data changes
if (policy !== codePolicy) {
setCodePolicy(policy);
}
}, [policy]);
useEffect(() => {
if (!fetcher.data) {
// No data yet, return
return;
}
if (fetcher.data.success === true) {
toast("Updated policy");
revalidate();
}
}, [fetcher.data]);
return (
<div>
{!access ? (
<Notice title="ACL Policy restricted" variant="warning">
You do not have the necessary permissions to edit the Access Control List policy. Please
contact your administrator to request access or to make changes to the ACL policy.
</Notice>
) : !writable ? (
<Notice title="Read-only ACL Policy" variant="error">
The ACL policy mode is most likely set to <Code>file</Code> in your Headscale
configuration. This means that the ACL file cannot be edited through the web interface. In
order to resolve this, you'll need to set <Code>policy.mode</Code> to{" "}
<Code>database</Code> in your Headscale configuration.
</Notice>
) : undefined}
<h1 className="mb-4 text-2xl font-medium">Access Control List (ACL)</h1>
<p className="mb-4 max-w-prose">
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{" "}
<Link external styled to="https://tailscale.com/kb/1018/acls">
Tailscale ACL guide
</Link>{" "}
and the{" "}
<Link external styled to="https://headscale.net/stable/ref/acls/">
Headscale docs
</Link>
.
</p>
{fetcher.data?.error !== undefined ? (
<Notice title={fetcher.data.error.split(":")[0] ?? "Error"} variant="error">
{fetcher.data.error.split(":").slice(1).join(": ") ??
"An unknown error occurred while trying to update the ACL policy."}
</Notice>
) : undefined}
<Tabs className="mb-4" label="ACL Editor">
<Tabs.Item
key="edit"
title={
<div className="flex items-center gap-2">
<Pencil className="p-1" />
<span>Edit file</span>
</div>
}
>
<Editor isDisabled={disabled} onChange={setCodePolicy} value={codePolicy} />
</Tabs.Item>
<Tabs.Item
key="diff"
title={
<div className="flex items-center gap-2">
<Eye className="p-1" />
<span>Preview changes</span>
</div>
}
>
<Differ left={policy} right={codePolicy} />
</Tabs.Item>
<Tabs.Item
key="preview"
title={
<div className="flex items-center gap-2">
<FlaskConical className="p-1" />
<span>Preview rules</span>
</div>
}
>
<div className="flex flex-col items-center py-8">
<Construction />
<p className="mt-4 w-1/2 text-center">
Previewing rules is not available yet. This feature is still in development and is
pretty complicated to implement. Hopefully I will be able to get to it soon.
</p>
</div>
</Tabs.Item>
</Tabs>
<Button
className="mr-2"
isDisabled={
disabled || fetcher.state !== "idle" || codePolicy.length === 0 || codePolicy === policy
}
onPress={() => {
const formData = new FormData();
formData.append("policy", codePolicy);
fetcher.submit(formData, { method: "PATCH" });
}}
variant="heavy"
>
Save
</Button>
<Button
isDisabled={disabled || fetcher.state !== "idle" || codePolicy === policy}
onPress={() => {
// Reset the editor to the original policy
setCodePolicy(policy);
}}
>
Discard Changes
</Button>
</div>
);
}
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
if (
isRouteErrorResponse(error) &&
isApiError(error.data) &&
error.data.rawData.includes("reading policy from path") &&
error.data.rawData.includes("no such file or directory")
) {
return (
<div className="flex flex-col gap-4">
<Card className="max-w-2xl" variant="flat">
<div className="flex items-center justify-between gap-4">
<Card.Title>ACL Policy Unavailable</Card.Title>
<AlertCircle className="mb-2 h-6 w-6 text-red-500" />
</div>
<Card.Text>
The ACL policy is currently unavailable because the policy file does not exist on the
server. This usually indicates that Headscale is running in <Code>file</Code> mode for
ACLs, and the specified policy file is missing.
</Card.Text>
</Card>
<Card className="max-w-2xl" variant="flat">
<Card.Text>
In order to resolve this issue, there are two possible actions you can take:
</Card.Text>
<ul className="mt-2 ml-4 list-outside list-disc space-y-1 text-sm">
<li>
Create the ACL policy file at the specified path in your Headscale configuration.
</li>
<li>
Alternatively, you can switch Headscale to use <Code>database</Code> mode for ACLs by
updating your Headscale configuration. This will allow Headplane to manage the ACL
policy directly through the web interface.
</li>
</ul>
</Card>
</div>
);
}
return <PageError error={error} page="Access Control" />;
}