feat: switch to normal form actions for dns
This commit is contained in:
parent
2a1c795d46
commit
5be3cb345e
@ -1,44 +0,0 @@
|
|||||||
import { useFetcher } from 'react-router';
|
|
||||||
import Dialog from '~/components/Dialog';
|
|
||||||
import Spinner from '~/components/Spinner';
|
|
||||||
|
|
||||||
type Properties = {
|
|
||||||
readonly isEnabled: boolean;
|
|
||||||
readonly disabled?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: Use form action instead of JSON patching
|
|
||||||
// AND FIX JSON END OF UNEXPECTED INPUT
|
|
||||||
export default function Modal({ isEnabled, disabled }: Properties) {
|
|
||||||
const fetcher = useFetcher();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog>
|
|
||||||
<Dialog.Button isDisabled={disabled}>
|
|
||||||
{fetcher.state === 'idle' ? undefined : <Spinner className="w-3 h-3" />}
|
|
||||||
{isEnabled ? 'Disable' : 'Enable'} Magic DNS
|
|
||||||
</Dialog.Button>
|
|
||||||
<Dialog.Panel
|
|
||||||
onSubmit={() => {
|
|
||||||
fetcher.submit(
|
|
||||||
{
|
|
||||||
'dns.magic_dns': !isEnabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'PATCH',
|
|
||||||
encType: 'application/json',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Dialog.Title>
|
|
||||||
{isEnabled ? 'Disable' : 'Enable'} Magic DNS
|
|
||||||
</Dialog.Title>
|
|
||||||
<Dialog.Text>
|
|
||||||
Devices will no longer be accessible via your tailnet domain. The
|
|
||||||
search domain will also be disabled.
|
|
||||||
</Dialog.Text>
|
|
||||||
</Dialog.Panel>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable unicorn/no-keyword-prefix */
|
|
||||||
import { DndContext, DragOverlay, closestCorners } from '@dnd-kit/core';
|
import { DndContext, DragOverlay, closestCorners } from '@dnd-kit/core';
|
||||||
import {
|
import {
|
||||||
restrictToParentElement,
|
restrictToParentElement,
|
||||||
@ -13,29 +12,25 @@ import {
|
|||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import { GripVertical, Lock } from 'lucide-react';
|
import { GripVertical, Lock } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { type FetcherWithComponents, useFetcher } from 'react-router';
|
import { type FetcherWithComponents, Form, useFetcher } from 'react-router';
|
||||||
import Button from '~/components/Button';
|
import Button from '~/components/Button';
|
||||||
import Input from '~/components/Input';
|
import Input from '~/components/Input';
|
||||||
|
|
||||||
import Spinner from '~/components/Spinner';
|
|
||||||
import TableList from '~/components/TableList';
|
import TableList from '~/components/TableList';
|
||||||
import cn from '~/utils/cn';
|
import cn from '~/utils/cn';
|
||||||
|
|
||||||
type Properties = {
|
interface Props {
|
||||||
readonly baseDomain?: string;
|
searchDomains: string[];
|
||||||
readonly searchDomains: string[];
|
isDisabled: boolean;
|
||||||
readonly disabled?: boolean; // TODO: isDisabled
|
magic?: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
export default function Domains({
|
export default function ManageDomains({
|
||||||
baseDomain,
|
|
||||||
searchDomains,
|
searchDomains,
|
||||||
disabled,
|
isDisabled,
|
||||||
}: Properties) {
|
magic,
|
||||||
|
}: Props) {
|
||||||
const [activeId, setActiveId] = useState<number | string | null>(null);
|
const [activeId, setActiveId] = useState<number | string | null>(null);
|
||||||
const [localDomains, setLocalDomains] = useState(searchDomains);
|
const [localDomains, setLocalDomains] = useState(searchDomains);
|
||||||
const [newDomain, setNewDomain] = useState('');
|
|
||||||
const fetcher = useFetcher();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLocalDomains(searchDomains);
|
setLocalDomains(searchDomains);
|
||||||
@ -77,16 +72,16 @@ export default function Domains({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TableList>
|
<TableList>
|
||||||
{baseDomain ? (
|
{magic ? (
|
||||||
<TableList.Item key="magic-dns-sd">
|
<TableList.Item key="magic-dns-sd">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-4',
|
'flex items-center gap-4',
|
||||||
disabled ? 'flex-row-reverse justify-between w-full' : '',
|
isDisabled ? 'flex-row-reverse justify-between w-full' : '',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Lock className="p-0.5" />
|
<Lock className="p-0.5" />
|
||||||
<p className="font-mono text-sm py-0.5">{baseDomain}</p>
|
<p className="font-mono text-sm py-0.5">{magic}</p>
|
||||||
</div>
|
</div>
|
||||||
</TableList.Item>
|
</TableList.Item>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
@ -99,63 +94,49 @@ export default function Domains({
|
|||||||
key={sd}
|
key={sd}
|
||||||
domain={sd}
|
domain={sd}
|
||||||
id={index + 1}
|
id={index + 1}
|
||||||
localDomains={localDomains}
|
isDisabled={isDisabled}
|
||||||
disabled={disabled}
|
|
||||||
fetcher={fetcher}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<DragOverlay adjustScale>
|
<DragOverlay adjustScale>
|
||||||
{activeId ? (
|
{activeId ? (
|
||||||
<Domain
|
<Domain
|
||||||
isDrag
|
isDragging
|
||||||
domain={localDomains[(activeId as number) - 1]}
|
domain={localDomains[(activeId as number) - 1]}
|
||||||
localDomains={localDomains}
|
|
||||||
id={(activeId as number) - 1}
|
id={(activeId as number) - 1}
|
||||||
disabled={disabled}
|
isDisabled={isDisabled}
|
||||||
fetcher={fetcher}
|
|
||||||
/>
|
/>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
</DragOverlay>
|
</DragOverlay>
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
{disabled ? undefined : (
|
{isDisabled ? undefined : (
|
||||||
<TableList.Item key="add-sd">
|
<TableList.Item key="add-sd">
|
||||||
<Input
|
<Form
|
||||||
type="text"
|
method="POST"
|
||||||
className={cn(
|
className="flex items-center justify-between w-full"
|
||||||
'border-none font-mono p-0',
|
>
|
||||||
'rounded-none focus:ring-0 w-full',
|
<input type="hidden" name="action_id" value="add_domain" />
|
||||||
)}
|
<Input
|
||||||
placeholder="Search Domain"
|
type="text"
|
||||||
onChange={setNewDomain}
|
className={cn(
|
||||||
label="Search Domain"
|
'border-none font-mono p-0 text-sm',
|
||||||
labelHidden
|
'rounded-none focus:ring-0 w-full ml-1',
|
||||||
/>
|
)}
|
||||||
{fetcher.state === 'idle' ? (
|
placeholder="Search Domain"
|
||||||
|
label="Search Domain"
|
||||||
|
name="domain"
|
||||||
|
labelHidden
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
type="submit"
|
||||||
className={cn(
|
className={cn(
|
||||||
'px-2 py-1 rounded-md',
|
'px-2 py-1 rounded-md',
|
||||||
'text-blue-500 dark:text-blue-400',
|
'text-blue-500 dark:text-blue-400',
|
||||||
)}
|
)}
|
||||||
isDisabled={newDomain.length === 0}
|
|
||||||
onPress={() => {
|
|
||||||
fetcher.submit(
|
|
||||||
{
|
|
||||||
'dns.search_domains': [...localDomains, newDomain],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'PATCH',
|
|
||||||
encType: 'application/json',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
setNewDomain('');
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
</Form>
|
||||||
<Spinner className="w-3 h-3 mr-0" />
|
|
||||||
)}
|
|
||||||
</TableList.Item>
|
</TableList.Item>
|
||||||
)}
|
)}
|
||||||
</TableList>
|
</TableList>
|
||||||
@ -164,38 +145,29 @@ export default function Domains({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type DomainProperties = {
|
interface DomainProps {
|
||||||
readonly domain: string;
|
domain: string;
|
||||||
readonly id: number;
|
id: number;
|
||||||
readonly isDrag?: boolean;
|
isDragging?: boolean;
|
||||||
readonly localDomains: string[];
|
isDisabled: boolean;
|
||||||
readonly disabled?: boolean; // TODO: isDisabled
|
}
|
||||||
readonly fetcher: FetcherWithComponents<unknown>;
|
|
||||||
};
|
|
||||||
|
|
||||||
function Domain({
|
function Domain({ domain, id, isDragging, isDisabled }: DomainProps) {
|
||||||
domain,
|
|
||||||
id,
|
|
||||||
localDomains,
|
|
||||||
isDrag,
|
|
||||||
disabled,
|
|
||||||
fetcher,
|
|
||||||
}: DomainProperties) {
|
|
||||||
const {
|
const {
|
||||||
attributes,
|
attributes,
|
||||||
listeners,
|
listeners,
|
||||||
setNodeRef,
|
setNodeRef,
|
||||||
transform,
|
transform,
|
||||||
transition,
|
transition,
|
||||||
isDragging,
|
isDragging: isSortableDragging,
|
||||||
} = useSortable({ id });
|
} = useSortable({ id });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableList.Item
|
<TableList.Item
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
isDragging ? 'opacity-50' : '',
|
isSortableDragging ? 'opacity-50' : '',
|
||||||
isDrag ? 'ring bg-white dark:bg-headplane-900' : '',
|
isDragging ? 'ring bg-white dark:bg-headplane-900' : '',
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
transform: CSS.Transform.toString(transform),
|
transform: CSS.Transform.toString(transform),
|
||||||
@ -203,34 +175,30 @@ function Domain({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p className="font-mono text-sm flex items-center gap-4">
|
<p className="font-mono text-sm flex items-center gap-4">
|
||||||
{disabled ? undefined : (
|
{isDisabled ? undefined : (
|
||||||
<GripVertical {...attributes} {...listeners} className="p-0.5" />
|
<GripVertical
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
className="p-0.5 focus:ring outline-none rounded-md"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{domain}
|
{domain}
|
||||||
</p>
|
</p>
|
||||||
{isDrag ? undefined : (
|
{isDragging ? undefined : (
|
||||||
<Button
|
<Form method="POST">
|
||||||
className={cn(
|
<input type="hidden" name="action_id" value="remove_domain" />
|
||||||
'px-2 py-1 rounded-md',
|
<input type="hidden" name="domain" value={domain} />
|
||||||
'text-red-500 dark:text-red-400',
|
<Button
|
||||||
)}
|
type="submit"
|
||||||
isDisabled={disabled}
|
isDisabled={isDisabled}
|
||||||
onPress={() => {
|
className={cn(
|
||||||
fetcher.submit(
|
'px-2 py-1 rounded-md',
|
||||||
{
|
'text-red-500 dark:text-red-400',
|
||||||
'dns.search_domains': localDomains.filter(
|
)}
|
||||||
(_, index) => index !== id - 1,
|
>
|
||||||
),
|
Remove
|
||||||
},
|
</Button>
|
||||||
{
|
</Form>
|
||||||
method: 'PATCH',
|
|
||||||
encType: 'application/json',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
</TableList.Item>
|
</TableList.Item>
|
||||||
);
|
);
|
||||||
@ -1,16 +1,16 @@
|
|||||||
import { useSubmit } from 'react-router';
|
import { Form } from 'react-router';
|
||||||
import Button from '~/components/Button';
|
import Button from '~/components/Button';
|
||||||
import Link from '~/components/Link';
|
import Link from '~/components/Link';
|
||||||
import TableList from '~/components/TableList';
|
import TableList from '~/components/TableList';
|
||||||
import cn from '~/utils/cn';
|
import cn from '~/utils/cn';
|
||||||
import AddNameserver from '../dialogs/nameserver';
|
import AddNS from '../dialogs/add-ns';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
nameservers: Record<string, string[]>;
|
nameservers: Record<string, string[]>;
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Nameservers({ nameservers, isDisabled }: Props) {
|
export default function ManageNS({ nameservers, isDisabled }: Props) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-2/3">
|
<div className="flex flex-col w-2/3">
|
||||||
<h1 className="text-2xl font-medium mb-4">Nameservers</h1>
|
<h1 className="text-2xl font-medium mb-4">Nameservers</h1>
|
||||||
@ -35,7 +35,7 @@ export default function Nameservers({ nameservers, isDisabled }: Props) {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{isDisabled ? undefined : <AddNameserver nameservers={nameservers} />}
|
{isDisabled ? undefined : <AddNS nameservers={nameservers} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -54,7 +54,6 @@ function NameserverList({
|
|||||||
nameservers,
|
nameservers,
|
||||||
name,
|
name,
|
||||||
}: ListProps) {
|
}: ListProps) {
|
||||||
const submit = useSubmit();
|
|
||||||
const list = isGlobal ? nameservers.global : nameservers[name];
|
const list = isGlobal ? nameservers.global : nameservers[name];
|
||||||
if (list.length === 0) {
|
if (list.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
@ -69,46 +68,28 @@ function NameserverList({
|
|||||||
</div>
|
</div>
|
||||||
<TableList>
|
<TableList>
|
||||||
{list.length > 0
|
{list.length > 0
|
||||||
? list.map((ns, index) => (
|
? list.map((ns) => (
|
||||||
<TableList.Item key={ns}>
|
<TableList.Item key={ns}>
|
||||||
<p className="font-mono text-sm">{ns}</p>
|
<p className="font-mono text-sm">{ns}</p>
|
||||||
<Button
|
<Form method="POST">
|
||||||
className={cn(
|
<input type="hidden" name="action_id" value="remove_ns" />
|
||||||
'px-2 py-1 rounded-md',
|
<input type="hidden" name="ns" value={ns} />
|
||||||
'text-red-500 dark:text-red-400',
|
<input
|
||||||
)}
|
type="hidden"
|
||||||
isDisabled={isDisabled}
|
name="split_name"
|
||||||
onPress={() => {
|
value={isGlobal ? 'global' : name}
|
||||||
if (isGlobal) {
|
/>
|
||||||
submit(
|
<Button
|
||||||
{
|
isDisabled={isDisabled}
|
||||||
'dns.nameservers.global': list.filter(
|
type="submit"
|
||||||
(_, i) => i !== index,
|
className={cn(
|
||||||
),
|
'px-2 py-1 rounded-md',
|
||||||
},
|
'text-red-500 dark:text-red-400',
|
||||||
{
|
)}
|
||||||
method: 'PATCH',
|
>
|
||||||
encType: 'application/json',
|
Remove
|
||||||
},
|
</Button>
|
||||||
);
|
</Form>
|
||||||
} else {
|
|
||||||
submit(
|
|
||||||
{
|
|
||||||
'dns.nameservers.split': {
|
|
||||||
...nameservers,
|
|
||||||
[name]: list.filter((_, i) => i !== index),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'PATCH',
|
|
||||||
encType: 'application/json',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</Button>
|
|
||||||
</TableList.Item>
|
</TableList.Item>
|
||||||
))
|
))
|
||||||
: undefined}
|
: undefined}
|
||||||
@ -1,19 +1,17 @@
|
|||||||
import { useSubmit } from 'react-router';
|
import { Form } from 'react-router';
|
||||||
import Button from '~/components/Button';
|
import Button from '~/components/Button';
|
||||||
import Code from '~/components/Code';
|
import Code from '~/components/Code';
|
||||||
import Link from '~/components/Link';
|
import Link from '~/components/Link';
|
||||||
import TableList from '~/components/TableList';
|
import TableList from '~/components/TableList';
|
||||||
import cn from '~/utils/cn';
|
import cn from '~/utils/cn';
|
||||||
import AddDNS from '../dialogs/dns';
|
import AddRecord from '../dialogs/add-record';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
records: { name: string; type: 'A'; value: string }[];
|
records: { name: string; type: 'A' | string; value: string }[];
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DNS({ records, isDisabled }: Props) {
|
export default function ManageRecords({ records, isDisabled }: Props) {
|
||||||
const submit = useSubmit();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-2/3">
|
<div className="flex flex-col w-2/3">
|
||||||
<h1 className="text-2xl font-medium mb-4">DNS Records</h1>
|
<h1 className="text-2xl font-medium mb-4">DNS Records</h1>
|
||||||
@ -50,34 +48,27 @@ export default function DNS({ records, isDisabled }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
<p className="font-mono text-sm">{record.value}</p>
|
<p className="font-mono text-sm">{record.value}</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Form method="POST">
|
||||||
className={cn(
|
<input type="hidden" name="action_id" value="remove_record" />
|
||||||
'px-2 py-1 rounded-md',
|
<input type="hidden" name="record_name" value={record.name} />
|
||||||
'text-red-500 dark:text-red-400',
|
<input type="hidden" name="record_type" value={record.type} />
|
||||||
)}
|
<Button
|
||||||
isDisabled={isDisabled}
|
type="submit"
|
||||||
onPress={() => {
|
isDisabled={isDisabled}
|
||||||
submit(
|
className={cn(
|
||||||
{
|
'px-2 py-1 rounded-md',
|
||||||
'dns.extra_records': records.filter(
|
'text-red-500 dark:text-red-400',
|
||||||
(_, i) => i !== index,
|
)}
|
||||||
),
|
>
|
||||||
},
|
Remove
|
||||||
{
|
</Button>
|
||||||
method: 'PATCH',
|
</Form>
|
||||||
encType: 'application/json',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</Button>
|
|
||||||
</TableList.Item>
|
</TableList.Item>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</TableList>
|
</TableList>
|
||||||
|
|
||||||
{isDisabled ? undefined : <AddDNS records={records} />}
|
{isDisabled ? undefined : <AddRecord records={records} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -1,22 +1,13 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { useFetcher } from 'react-router';
|
|
||||||
|
|
||||||
import Code from '~/components/Code';
|
import Code from '~/components/Code';
|
||||||
import Dialog from '~/components/Dialog';
|
import Dialog from '~/components/Dialog';
|
||||||
import Input from '~/components/Input';
|
import Input from '~/components/Input';
|
||||||
import Spinner from '~/components/Spinner';
|
|
||||||
import cn from '~/utils/cn';
|
|
||||||
|
|
||||||
type Properties = {
|
interface Props {
|
||||||
readonly name: string;
|
name: string;
|
||||||
readonly disabled?: boolean;
|
isDisabled: boolean;
|
||||||
};
|
}
|
||||||
|
|
||||||
// TODO: Switch to form submit instead of JSON patch
|
|
||||||
export default function Modal({ name, disabled }: Properties) {
|
|
||||||
const [newName, setNewName] = useState(name);
|
|
||||||
const fetcher = useFetcher();
|
|
||||||
|
|
||||||
|
export default function RenameTailnet({ name, isDisabled }: Props) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-2/3 gap-y-4">
|
<div className="flex flex-col w-2/3 gap-y-4">
|
||||||
<h1 className="text-2xl font-medium mb-2">Tailnet Name</h1>
|
<h1 className="text-2xl font-medium mb-2">Tailnet Name</h1>
|
||||||
@ -35,34 +26,20 @@ export default function Modal({ name, disabled }: Properties) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<Dialog.Button isDisabled={disabled}>
|
<Dialog.Button isDisabled={isDisabled}>Rename Tailnet</Dialog.Button>
|
||||||
{fetcher.state === 'idle' ? undefined : (
|
<Dialog.Panel isDisabled={isDisabled}>
|
||||||
<Spinner className="w-3 h-3" />
|
|
||||||
)}
|
|
||||||
Rename Tailnet
|
|
||||||
</Dialog.Button>
|
|
||||||
<Dialog.Panel
|
|
||||||
onSubmit={() => {
|
|
||||||
fetcher.submit(
|
|
||||||
{
|
|
||||||
'dns.base_domain': newName,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'PATCH',
|
|
||||||
encType: 'application/json',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Dialog.Title>Rename Tailnet</Dialog.Title>
|
<Dialog.Title>Rename Tailnet</Dialog.Title>
|
||||||
<Dialog.Text>
|
<Dialog.Text className="mb-8">
|
||||||
Keep in mind that changing this can lead to all sorts of unexpected
|
Keep in mind that changing this can lead to all sorts of unexpected
|
||||||
behavior and may break existing devices in your tailnet.
|
behavior and may break existing devices in your tailnet.
|
||||||
</Dialog.Text>
|
</Dialog.Text>
|
||||||
|
<input type="hidden" name="action_id" value="rename_tailnet" />
|
||||||
<Input
|
<Input
|
||||||
|
isRequired
|
||||||
label="Tailnet name"
|
label="Tailnet name"
|
||||||
placeholder="ts.net"
|
placeholder="ts.net"
|
||||||
onChange={setNewName}
|
defaultValue={name}
|
||||||
|
name="new_name"
|
||||||
/>
|
/>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
31
app/routes/dns/components/toggle-magic.tsx
Normal file
31
app/routes/dns/components/toggle-magic.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import Dialog from '~/components/Dialog';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isEnabled: boolean;
|
||||||
|
isDisabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Modal({ isEnabled, isDisabled }: Props) {
|
||||||
|
return (
|
||||||
|
<Dialog>
|
||||||
|
<Dialog.Button isDisabled={isDisabled}>
|
||||||
|
{isEnabled ? 'Disable' : 'Enable'} Magic DNS
|
||||||
|
</Dialog.Button>
|
||||||
|
<Dialog.Panel isDisabled={isDisabled}>
|
||||||
|
<Dialog.Title>
|
||||||
|
{isEnabled ? 'Disable' : 'Enable'} Magic DNS
|
||||||
|
</Dialog.Title>
|
||||||
|
<Dialog.Text>
|
||||||
|
Devices will no longer be accessible via your tailnet domain. The
|
||||||
|
search domain will also be disabled.
|
||||||
|
</Dialog.Text>
|
||||||
|
<input type="hidden" name="action_id" value="toggle_magic" />
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="new_state"
|
||||||
|
value={isEnabled ? 'disabled' : 'enabled'}
|
||||||
|
/>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,8 +1,6 @@
|
|||||||
import { RepoForkedIcon } from '@primer/octicons-react';
|
import { RepoForkedIcon } from '@primer/octicons-react';
|
||||||
import { useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useSubmit } from 'react-router';
|
|
||||||
import Chip from '~/components/Chip';
|
import Chip from '~/components/Chip';
|
||||||
|
|
||||||
import Dialog from '~/components/Dialog';
|
import Dialog from '~/components/Dialog';
|
||||||
import Input from '~/components/Input';
|
import Input from '~/components/Input';
|
||||||
import Switch from '~/components/Switch';
|
import Switch from '~/components/Switch';
|
||||||
@ -14,69 +12,40 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function AddNameserver({ nameservers }: Props) {
|
export default function AddNameserver({ nameservers }: Props) {
|
||||||
const submit = useSubmit();
|
|
||||||
const [split, setSplit] = useState(false);
|
const [split, setSplit] = useState(false);
|
||||||
const [ns, setNs] = useState('');
|
const [ns, setNs] = useState('');
|
||||||
const [domain, setDomain] = useState('');
|
const [domain, setDomain] = useState('');
|
||||||
|
|
||||||
|
const isInvalid = useMemo(() => {
|
||||||
|
if (ns === '') return false;
|
||||||
|
// Test if it's a valid IPv4 or IPv6 address
|
||||||
|
const ipv4 = /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/;
|
||||||
|
const ipv6 = /^([0-9a-fA-F:]+:+)+[0-9a-fA-F]+$/;
|
||||||
|
if (!ipv4.test(ns) && !ipv6.test(ns)) return true;
|
||||||
|
|
||||||
|
if (split) {
|
||||||
|
return nameservers[domain]?.includes(ns);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.values(nameservers).some((nsList) => nsList.includes(ns));
|
||||||
|
}, [nameservers, ns]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<Dialog.Button>Add nameserver</Dialog.Button>
|
<Dialog.Button>Add nameserver</Dialog.Button>
|
||||||
<Dialog.Panel
|
<Dialog.Panel>
|
||||||
onSubmit={(event) => {
|
<Dialog.Title className="mb-4">Add nameserver</Dialog.Title>
|
||||||
event.preventDefault();
|
<input type="hidden" name="action_id" value="add_ns" />
|
||||||
if (!ns) return;
|
|
||||||
if (split) {
|
|
||||||
const splitNs: Record<string, string[]> = {};
|
|
||||||
for (const [key, value] of Object.entries(nameservers)) {
|
|
||||||
if (key === 'global') continue;
|
|
||||||
splitNs[key] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(splitNs).includes(domain)) {
|
|
||||||
splitNs[domain].push(ns);
|
|
||||||
} else {
|
|
||||||
splitNs[domain] = [ns];
|
|
||||||
}
|
|
||||||
|
|
||||||
submit(
|
|
||||||
{
|
|
||||||
'dns.nameservers.split': splitNs,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'PATCH',
|
|
||||||
encType: 'application/json',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const globalNs = nameservers.global;
|
|
||||||
globalNs.push(ns);
|
|
||||||
|
|
||||||
submit(
|
|
||||||
{
|
|
||||||
'dns.nameservers.global': globalNs,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'PATCH',
|
|
||||||
encType: 'application/json',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setNs('');
|
|
||||||
setDomain('');
|
|
||||||
setSplit(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Dialog.Title>Add nameserver</Dialog.Title>
|
|
||||||
<Input
|
<Input
|
||||||
|
isRequired
|
||||||
label="Nameserver"
|
label="Nameserver"
|
||||||
description="Use this IPv4 or IPv6 address to resolve names."
|
description="Use this IPv4 or IPv6 address to resolve names."
|
||||||
placeholder="1.2.3.4"
|
placeholder="1.2.3.4"
|
||||||
name="ns"
|
name="ns"
|
||||||
onChange={setNs}
|
onChange={setNs}
|
||||||
|
isInvalid={isInvalid}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between mt-8">
|
||||||
<div className="block">
|
<div className="block">
|
||||||
<div className="inline-flex items-center gap-2">
|
<div className="inline-flex items-center gap-2">
|
||||||
<Dialog.Text className="font-semibold">
|
<Dialog.Text className="font-semibold">
|
||||||
@ -99,21 +68,16 @@ export default function AddNameserver({ nameservers }: Props) {
|
|||||||
This nameserver will only be used for some domains.
|
This nameserver will only be used for some domains.
|
||||||
</Dialog.Text>
|
</Dialog.Text>
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch label="Split DNS" onChange={setSplit} />
|
||||||
label="Split DNS"
|
|
||||||
defaultSelected={split}
|
|
||||||
onChange={() => {
|
|
||||||
setSplit(!split);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{split ? (
|
{split ? (
|
||||||
<>
|
<>
|
||||||
<Dialog.Text className="font-semibold mt-8">Domain</Dialog.Text>
|
<Dialog.Text className="font-semibold mt-8">Domain</Dialog.Text>
|
||||||
<Input
|
<Input
|
||||||
|
isRequired={split === true}
|
||||||
label="Domain"
|
label="Domain"
|
||||||
placeholder="example.com"
|
placeholder="example.com"
|
||||||
name="domain"
|
name="split_name"
|
||||||
onChange={setDomain}
|
onChange={setDomain}
|
||||||
/>
|
/>
|
||||||
<Dialog.Text className="text-sm">
|
<Dialog.Text className="text-sm">
|
||||||
@ -121,7 +85,9 @@ export default function AddNameserver({ nameservers }: Props) {
|
|||||||
should use the nameserver.
|
should use the nameserver.
|
||||||
</Dialog.Text>
|
</Dialog.Text>
|
||||||
</>
|
</>
|
||||||
) : undefined}
|
) : (
|
||||||
|
<input type="hidden" name="split_name" value="global" />
|
||||||
|
)}
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
@ -1,15 +1,13 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useSubmit } from 'react-router';
|
|
||||||
import Code from '~/components/Code';
|
import Code from '~/components/Code';
|
||||||
import Dialog from '~/components/Dialog';
|
import Dialog from '~/components/Dialog';
|
||||||
import Input from '~/components/Input';
|
import Input from '~/components/Input';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
records: { name: string; type: 'A'; value: string }[];
|
records: { name: string; type: 'A' | string; value: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AddDNS({ records }: Props) {
|
export default function AddRecord({ records }: Props) {
|
||||||
const submit = useSubmit();
|
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [ip, setIp] = useState('');
|
const [ip, setIp] = useState('');
|
||||||
|
|
||||||
@ -21,44 +19,22 @@ export default function AddDNS({ records }: Props) {
|
|||||||
return lookup.value === ip;
|
return lookup.value === ip;
|
||||||
}, [records, name, ip]);
|
}, [records, name, ip]);
|
||||||
|
|
||||||
// TODO: Ditch useSubmit here (non JSON form)
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<Dialog.Button>Add DNS record</Dialog.Button>
|
<Dialog.Button>Add DNS record</Dialog.Button>
|
||||||
<Dialog.Panel
|
<Dialog.Panel>
|
||||||
onSubmit={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
if (!name || !ip) return;
|
|
||||||
|
|
||||||
setName('');
|
|
||||||
setIp('');
|
|
||||||
submit(
|
|
||||||
{
|
|
||||||
'dns.extra_records': [
|
|
||||||
...records,
|
|
||||||
{
|
|
||||||
name,
|
|
||||||
type: 'A',
|
|
||||||
value: ip,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: 'PATCH',
|
|
||||||
encType: 'application/json',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Dialog.Title>Add DNS record</Dialog.Title>
|
<Dialog.Title>Add DNS record</Dialog.Title>
|
||||||
<Dialog.Text>
|
<Dialog.Text>
|
||||||
Enter the domain and IP address for the new DNS record.
|
Enter the domain and IP address for the new DNS record.
|
||||||
</Dialog.Text>
|
</Dialog.Text>
|
||||||
<div className="flex flex-col gap-2 mt-4">
|
<div className="flex flex-col gap-2 mt-4">
|
||||||
|
<input type="hidden" name="action_id" value="add_record" />
|
||||||
|
<input type="hidden" name="record_type" value="A" />
|
||||||
<Input
|
<Input
|
||||||
isRequired
|
isRequired
|
||||||
label="Domain"
|
label="Domain"
|
||||||
placeholder="test.example.com"
|
placeholder="test.example.com"
|
||||||
|
name="record_name"
|
||||||
onChange={setName}
|
onChange={setName}
|
||||||
isInvalid={isDuplicate}
|
isInvalid={isDuplicate}
|
||||||
/>
|
/>
|
||||||
@ -66,7 +42,7 @@ export default function AddDNS({ records }: Props) {
|
|||||||
isRequired
|
isRequired
|
||||||
label="IP Address"
|
label="IP Address"
|
||||||
placeholder="101.101.101.101"
|
placeholder="101.101.101.101"
|
||||||
name="ip"
|
name="record_value"
|
||||||
onChange={setIp}
|
onChange={setIp}
|
||||||
isInvalid={isDuplicate}
|
isInvalid={isDuplicate}
|
||||||
/>
|
/>
|
||||||
238
app/routes/dns/dns-actions.ts
Normal file
238
app/routes/dns/dns-actions.ts
Normal file
@ -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,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
@ -1,26 +1,22 @@
|
|||||||
import type { ActionFunctionArgs } from 'react-router';
|
import type { ActionFunctionArgs } from 'react-router';
|
||||||
import { data, useLoaderData } from 'react-router';
|
import { useLoaderData } from 'react-router';
|
||||||
|
|
||||||
import Code from '~/components/Code';
|
import Code from '~/components/Code';
|
||||||
import Notice from '~/components/Notice';
|
import Notice from '~/components/Notice';
|
||||||
import { loadContext } from '~/utils/config/headplane';
|
import { hs_getConfig } from '~/utils/state';
|
||||||
import { loadConfig, patchConfig } from '~/utils/config/headscale';
|
import ManageDomains from './components/manage-domains';
|
||||||
import { getSession } from '~/utils/sessions.server';
|
import ManageNS from './components/manage-ns';
|
||||||
|
import ManageRecords from './components/manage-records';
|
||||||
import DNS from './components/dns';
|
import RenameTailnet from './components/rename-tailnet';
|
||||||
import Domains from './components/domains';
|
import ToggleMagic from './components/toggle-magic';
|
||||||
import MagicModal from './components/magic';
|
import { dnsAction } from './dns-actions';
|
||||||
import Nameservers from './components/nameservers';
|
|
||||||
import RenameModal from './components/rename';
|
|
||||||
|
|
||||||
// We do not want to expose every config value
|
// We do not want to expose every config value
|
||||||
export async function loader() {
|
export async function loader() {
|
||||||
const context = await loadContext();
|
const { config, mode } = hs_getConfig();
|
||||||
if (!context.config.read) {
|
if (mode === 'no') {
|
||||||
throw new Error('No configuration is available');
|
throw new Error('No configuration is available');
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await loadConfig();
|
|
||||||
const dns = {
|
const dns = {
|
||||||
prefixes: config.prefixes,
|
prefixes: config.prefixes,
|
||||||
magicDns: config.dns.magic_dns,
|
magicDns: config.dns.magic_dns,
|
||||||
@ -33,34 +29,12 @@ export async function loader() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...dns,
|
...dns,
|
||||||
...context,
|
mode,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function action({ request }: ActionFunctionArgs) {
|
export async function action(data: ActionFunctionArgs) {
|
||||||
const session = await getSession(request.headers.get('Cookie'));
|
return dnsAction(data);
|
||||||
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<string, unknown>;
|
|
||||||
await patchConfig(patch);
|
|
||||||
|
|
||||||
if (context.integration?.onConfigChange) {
|
|
||||||
await context.integration.onConfigChange(context.integration.context);
|
|
||||||
}
|
|
||||||
|
|
||||||
return data({ success: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
@ -72,24 +46,23 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
allNs.global = data.nameservers;
|
allNs.global = data.nameservers;
|
||||||
|
const isDisabled = data.mode !== 'rw';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-16 max-w-screen-lg">
|
<div className="flex flex-col gap-16 max-w-screen-lg">
|
||||||
{data.config.write ? undefined : (
|
{data.mode === 'rw' ? undefined : (
|
||||||
<Notice>
|
<Notice>
|
||||||
The Headscale configuration is read-only. You cannot make changes to
|
The Headscale configuration is read-only. You cannot make changes to
|
||||||
the configuration
|
the configuration
|
||||||
</Notice>
|
</Notice>
|
||||||
)}
|
)}
|
||||||
<RenameModal name={data.baseDomain} disabled={!data.config.write} />
|
<RenameTailnet name={data.baseDomain} isDisabled={isDisabled} />
|
||||||
<Nameservers nameservers={allNs} isDisabled={!data.config.write} />
|
<ManageNS nameservers={allNs} isDisabled={isDisabled} />
|
||||||
|
<ManageRecords records={data.extraRecords} isDisabled={isDisabled} />
|
||||||
<DNS records={data.extraRecords} isDisabled={!data.config.write} />
|
<ManageDomains
|
||||||
|
|
||||||
<Domains
|
|
||||||
baseDomain={data.magicDns ? data.baseDomain : undefined}
|
|
||||||
searchDomains={data.searchDomains}
|
searchDomains={data.searchDomains}
|
||||||
disabled={!data.config.write}
|
isDisabled={isDisabled}
|
||||||
|
magic={data.magicDns ? data.baseDomain : undefined}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-col w-2/3">
|
<div className="flex flex-col w-2/3">
|
||||||
@ -103,7 +76,7 @@ export default function Page() {
|
|||||||
</Code>{' '}
|
</Code>{' '}
|
||||||
when Magic DNS is enabled.
|
when Magic DNS is enabled.
|
||||||
</p>
|
</p>
|
||||||
<MagicModal isEnabled={data.magicDns} disabled={!data.config.write} />
|
<ToggleMagic isEnabled={data.magicDns} isDisabled={isDisabled} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user