+
+
@@ -66,7 +42,7 @@ export default function AddDNS({ records }: Props) {
isRequired
label="IP Address"
placeholder="101.101.101.101"
- name="ip"
+ name="record_value"
onChange={setIp}
isInvalid={isDuplicate}
/>
diff --git a/app/routes/dns/dns-actions.ts b/app/routes/dns/dns-actions.ts
new file mode 100644
index 0000000..98dfb44
--- /dev/null
+++ b/app/routes/dns/dns-actions.ts
@@ -0,0 +1,238 @@
+import { ActionFunctionArgs, data } from 'react-router';
+import { hs_patchConfig } from '~/utils/config/loader';
+import { auth } from '~/utils/sessions.server';
+import { hs_getConfig } from '~/utils/state';
+
+export async function dnsAction({ request }: ActionFunctionArgs) {
+ const session = await auth(request);
+ if (!session) {
+ return data({ success: false }, 401);
+ }
+
+ const { mode } = hs_getConfig();
+ if (mode !== 'rw') {
+ return data({ success: false }, 403);
+ }
+
+ const formData = await request.formData();
+ const action = formData.get('action_id')?.toString();
+ if (!action) {
+ return data({ success: false }, 400);
+ }
+
+ switch (action) {
+ case 'rename_tailnet':
+ return renameTailnet(formData);
+ case 'toggle_magic':
+ return toggleMagic(formData);
+ case 'remove_ns':
+ return removeNs(formData);
+ case 'add_ns':
+ return addNs(formData);
+ case 'remove_domain':
+ return removeDomain(formData);
+ case 'add_domain':
+ return addDomain(formData);
+ case 'remove_record':
+ return removeRecord(formData);
+ case 'add_record':
+ return addRecord(formData);
+ default:
+ return data({ success: false }, 400);
+ }
+
+ // TODO: Integration update
+}
+
+async function renameTailnet(formData: FormData) {
+ const newName = formData.get('new_name')?.toString();
+ if (!newName) {
+ return data({ success: false }, 400);
+ }
+
+ await hs_patchConfig([
+ {
+ path: 'dns.base_domain',
+ value: newName,
+ },
+ ]);
+}
+
+async function toggleMagic(formData: FormData) {
+ const newState = formData.get('new_state')?.toString();
+ if (!newState) {
+ return data({ success: false }, 400);
+ }
+
+ await hs_patchConfig([
+ {
+ path: 'dns.magic_dns',
+ value: newState === 'enabled',
+ },
+ ]);
+}
+
+async function removeNs(formData: FormData) {
+ const ns = formData.get('ns')?.toString();
+ const splitName = formData.get('split_name')?.toString();
+
+ if (!ns || !splitName) {
+ return data({ success: false }, 400);
+ }
+
+ const { config, mode } = hs_getConfig();
+ if (mode !== 'rw') {
+ return data({ success: false }, 403);
+ }
+
+ if (splitName === 'global') {
+ const servers = config.dns.nameservers.global.filter((i) => i !== ns);
+
+ await hs_patchConfig([
+ {
+ path: 'dns.nameservers.global',
+ value: servers,
+ },
+ ]);
+ } else {
+ const splits = config.dns.nameservers.split;
+ const servers = splits[splitName].filter((i) => i !== ns);
+
+ await hs_patchConfig([
+ {
+ path: `dns.nameservers.split."${splitName}"`,
+ value: servers,
+ },
+ ]);
+ }
+}
+
+async function addNs(formData: FormData) {
+ const ns = formData.get('ns')?.toString();
+ const splitName = formData.get('split_name')?.toString();
+
+ if (!ns || !splitName) {
+ return data({ success: false }, 400);
+ }
+
+ const { config, mode } = hs_getConfig();
+ if (mode !== 'rw') {
+ return data({ success: false }, 403);
+ }
+
+ if (splitName === 'global') {
+ const servers = config.dns.nameservers.global;
+ servers.push(ns);
+
+ await hs_patchConfig([
+ {
+ path: 'dns.nameservers.global',
+ value: servers,
+ },
+ ]);
+ } else {
+ const splits = config.dns.nameservers.split;
+ const servers = splits[splitName] ?? [];
+ servers.push(ns);
+
+ await hs_patchConfig([
+ {
+ path: `dns.nameservers.split."${splitName}"`,
+ value: servers,
+ },
+ ]);
+ }
+}
+
+async function removeDomain(formData: FormData) {
+ const domain = formData.get('domain')?.toString();
+ if (!domain) {
+ return data({ success: false }, 400);
+ }
+
+ const { config, mode } = hs_getConfig();
+ if (mode !== 'rw') {
+ return data({ success: false }, 403);
+ }
+
+ const domains = config.dns.search_domains.filter((i) => i !== domain);
+
+ await hs_patchConfig([
+ {
+ path: 'dns.search_domains',
+ value: domains,
+ },
+ ]);
+}
+
+async function addDomain(formData: FormData) {
+ const domain = formData.get('domain')?.toString();
+ if (!domain) {
+ return data({ success: false }, 400);
+ }
+
+ const { config, mode } = hs_getConfig();
+ if (mode !== 'rw') {
+ return data({ success: false }, 403);
+ }
+
+ const domains = config.dns.search_domains;
+ domains.push(domain);
+
+ await hs_patchConfig([
+ {
+ path: 'dns.search_domains',
+ value: domains,
+ },
+ ]);
+}
+
+async function removeRecord(formData: FormData) {
+ const recordName = formData.get('record_name')?.toString();
+ const recordType = formData.get('record_type')?.toString();
+
+ if (!recordName || !recordType) {
+ return data({ success: false }, 400);
+ }
+
+ const { config, mode } = hs_getConfig();
+ if (mode !== 'rw') {
+ return data({ success: false }, 403);
+ }
+
+ const records = config.dns.extra_records.filter(
+ (i) => i.name !== recordName || i.type !== recordType,
+ );
+
+ await hs_patchConfig([
+ {
+ path: 'dns.extra_records',
+ value: records,
+ },
+ ]);
+}
+
+async function addRecord(formData: FormData) {
+ const recordName = formData.get('record_name')?.toString();
+ const recordType = formData.get('record_type')?.toString();
+ const recordValue = formData.get('record_value')?.toString();
+
+ if (!recordName || !recordType || !recordValue) {
+ return data({ success: false }, 400);
+ }
+
+ const { config, mode } = hs_getConfig();
+ if (mode !== 'rw') {
+ return data({ success: false }, 403);
+ }
+
+ const records = config.dns.extra_records;
+ records.push({ name: recordName, type: recordType, value: recordValue });
+
+ await hs_patchConfig([
+ {
+ path: 'dns.extra_records',
+ value: records,
+ },
+ ]);
+}
diff --git a/app/routes/dns/overview.tsx b/app/routes/dns/overview.tsx
index 317f1a1..2048980 100644
--- a/app/routes/dns/overview.tsx
+++ b/app/routes/dns/overview.tsx
@@ -1,26 +1,22 @@
import type { ActionFunctionArgs } from 'react-router';
-import { data, useLoaderData } from 'react-router';
-
+import { useLoaderData } from 'react-router';
import Code from '~/components/Code';
import Notice from '~/components/Notice';
-import { loadContext } from '~/utils/config/headplane';
-import { loadConfig, patchConfig } from '~/utils/config/headscale';
-import { getSession } from '~/utils/sessions.server';
-
-import DNS from './components/dns';
-import Domains from './components/domains';
-import MagicModal from './components/magic';
-import Nameservers from './components/nameservers';
-import RenameModal from './components/rename';
+import { hs_getConfig } from '~/utils/state';
+import ManageDomains from './components/manage-domains';
+import ManageNS from './components/manage-ns';
+import ManageRecords from './components/manage-records';
+import RenameTailnet from './components/rename-tailnet';
+import ToggleMagic from './components/toggle-magic';
+import { dnsAction } from './dns-actions';
// We do not want to expose every config value
export async function loader() {
- const context = await loadContext();
- if (!context.config.read) {
+ const { config, mode } = hs_getConfig();
+ if (mode === 'no') {
throw new Error('No configuration is available');
}
- const config = await loadConfig();
const dns = {
prefixes: config.prefixes,
magicDns: config.dns.magic_dns,
@@ -33,34 +29,12 @@ export async function loader() {
return {
...dns,
- ...context,
+ mode,
};
}
-export async function action({ request }: ActionFunctionArgs) {
- const session = await getSession(request.headers.get('Cookie'));
- if (!session.has('hsApiKey')) {
- return data({ success: false }, { status: 401 });
- }
-
- const context = await loadContext();
- if (!context.config.write) {
- return data({ success: false }, { status: 403 });
- }
-
- const textData = await request.text();
- if (!textData) {
- return data({ success: true });
- }
-
- const patch = JSON.parse(textData) as Record
;
- await patchConfig(patch);
-
- if (context.integration?.onConfigChange) {
- await context.integration.onConfigChange(context.integration.context);
- }
-
- return data({ success: true });
+export async function action(data: ActionFunctionArgs) {
+ return dnsAction(data);
}
export default function Page() {
@@ -72,24 +46,23 @@ export default function Page() {
}
allNs.global = data.nameservers;
+ const isDisabled = data.mode !== 'rw';
return (
- {data.config.write ? undefined : (
+ {data.mode === 'rw' ? undefined : (
The Headscale configuration is read-only. You cannot make changes to
the configuration
)}
-
-
-
-
-
-
+
+
+
@@ -103,7 +76,7 @@ export default function Page() {
{' '}
when Magic DNS is enabled.
-
+
);