Zones, Cache, Blocking, DNS Client, Settings, DHCP and Logs can now be changed on one chosen server through the hub, with that server's admin token. Operators manage cache and blocking; admins also zones, settings and DHCP. Under "All servers", or on a server without an admin token, the controls are disabled with a hint. The hub refuses settings/set changes to a server's web service fields and anything about users, SSO or LDAP; backup, restore, log and stats deletion and admin/* stay refused. Non-admins get settings with secrets blanked. Write params travel in the POST body, downloads pass through with a single-use token, and the audit log names no values. Co-Authored-By: Claude Opus 5.5 <[email protected]>
367 lines
12 KiB
TypeScript
367 lines
12 KiB
TypeScript
import { ClusterNodeSelect } from '../../ui/ClusterNodeSelect'
|
|
import { useContext, useState } from 'react'
|
|
import { HubMode } from '../../ui/ClusterNodeSelect'
|
|
import { useHubDefaultNode, useHubWrite } from '../../lib/hub-write'
|
|
import { HubWriteHint } from '../../ui/HubWriteHint'
|
|
import type { ResourceRecord, ZoneDetails } from '../../api/records'
|
|
import { Confirm } from '../../ui/Confirm'
|
|
import { ZoneList } from './ZoneList'
|
|
import { ZoneRecords } from './ZoneRecords'
|
|
import { AddEditRecord } from './modals/AddEditRecord'
|
|
import { AddZone } from './modals/AddZone'
|
|
import { CloneZone } from './modals/CloneZone'
|
|
import { ConvertZone } from './modals/ConvertZone'
|
|
import { UnsignZone } from './modals/UnsignZone'
|
|
import { SignZone } from './modals/SignZone'
|
|
import { ImportZone } from './modals/ImportZone'
|
|
import { EditZoneFile } from './modals/EditZoneFile'
|
|
import { ZoneOptions } from './modals/ZoneOptions'
|
|
import { ZonePermissions } from './modals/ZonePermissions'
|
|
import { DnssecProperties } from './modals/DnssecProperties'
|
|
import { ViewDs } from './modals/ViewDs'
|
|
import type { Notice, Confirmation } from './types'
|
|
import { Notifier } from '../../ui/Notifier'
|
|
|
|
/*
|
|
The whole Zones section: the list, a zone's view and the ten modals.
|
|
|
|
In upstream they are two `div` shown and hidden (`divViewZones` and
|
|
`divEditZone`); here it is a state, `open`. The rest of the structure is the
|
|
same on purpose: same modals, same texts, same steps.
|
|
|
|
**The alerts live here, not in each screen.** In upstream a `showAlert` with no
|
|
container draws them in the global bar, so an action fired from a modal that
|
|
closes the modal leaves the alert visible behind it. With the alert inside each
|
|
screen that case would be lost.
|
|
*/
|
|
|
|
type ModalId =
|
|
| 'add'
|
|
| 'editZoneFile'
|
|
| 'import'
|
|
| 'clone'
|
|
| 'convert'
|
|
| 'options'
|
|
| 'permissions'
|
|
| 'sign'
|
|
| 'unsign'
|
|
| 'viewds'
|
|
| 'dnssec'
|
|
| 'record'
|
|
|
|
export interface ZonesProps {
|
|
/** The cluster nodes, for the node selector. */
|
|
nodes?: { name: string; type: string }[]
|
|
clusterInitialised?: boolean
|
|
|
|
token: string | null
|
|
node?: string
|
|
canModify: boolean
|
|
canDelete: boolean
|
|
/** They inherit from Settings' global setting for the add modal. */
|
|
useSoaSerialDateScheme?: boolean
|
|
dnssecValidation?: boolean
|
|
/** The cluster's domain, when it is initialised: Add Zone preselects its catalog. */
|
|
clusterDomain?: string
|
|
/** The server's default TTLs, the record form's placeholders. */
|
|
recordTtls?: { defaultRecordTtl: number; defaultNsRecordTtl: number; defaultSoaRecordTtl: number }
|
|
}
|
|
|
|
const DEFAULT_TTLS = { defaultRecordTtl: 3600, defaultNsRecordTtl: 14400, defaultSoaRecordTtl: 900 }
|
|
|
|
export function Zones({
|
|
token,
|
|
nodes = [],
|
|
clusterInitialised = false,
|
|
canModify: mayModify,
|
|
canDelete: mayDelete,
|
|
useSoaSerialDateScheme = false,
|
|
dnssecValidation = false,
|
|
recordTtls = DEFAULT_TTLS,
|
|
clusterDomain,
|
|
}: ZonesProps) {
|
|
/* Upstream mounts a selector here (`optZonesClusterNode`) with no aggregate
|
|
and no persistence; the parameter was already travelling. */
|
|
const hub = useContext(HubMode)
|
|
/* Behind a hub the empty node is the hub, not a server: the first one is chosen, and a change
|
|
is allowed only on a server the hub can write to. */
|
|
const defaultNode = useHubDefaultNode(nodes)
|
|
const [node, setNode] = useState<string>(defaultNode)
|
|
const write = useHubWrite(node, mayModify)
|
|
const canModify = write.allowed
|
|
const canDelete = useHubWrite(node, mayDelete).allowed
|
|
const [open, setOpen] = useState<string | null>(null)
|
|
const [notice, setNotice] = useState<Notice | null>(null)
|
|
const [confirmation, setConfirmation] = useState<Confirmation | null>(null)
|
|
const [modal, setModal] = useState<ModalId | null>(null)
|
|
|
|
/** The zone the open modal acts on; it may not be the one on screen. */
|
|
const [modalZone, setModalZone] = useState('')
|
|
const [modalKind, setModalKind] = useState('')
|
|
|
|
const [recordMode, setRecordMode] = useState<'add' | 'update'>('add')
|
|
const [zoneDetails, setZoneDetails] = useState<ZoneDetails | null>(null)
|
|
const [records, setRecords] = useState<ResourceRecord[]>([])
|
|
const [originalRecord, setOriginalRecord] = useState<ResourceRecord | null>(null)
|
|
|
|
/*
|
|
The expiry TTL left over in the record modal. Upstream reads it from that
|
|
field when DISABLING a record from the row (`updateRecordState`,
|
|
zone.js:6236), so the value survives the modal's closing. It is a bug of
|
|
theirs; it is replicated because the rule is zero behaviour changes.
|
|
*/
|
|
const [modalExpiryTtl, setModalExpiryTtl] = useState('')
|
|
|
|
const [listRefresh, setListRefresh] = useState(0)
|
|
const [zoneRefresh, setZoneRefresh] = useState(0)
|
|
|
|
const refreshList = () => setListRefresh((n) => n + 1)
|
|
const refreshZone = () => setZoneRefresh((n) => n + 1)
|
|
|
|
function openModal(id: ModalId, zone: string, type = '') {
|
|
setModalZone(zone)
|
|
setModalKind(type)
|
|
setModal(id)
|
|
}
|
|
|
|
/** After a mutating modal: refreshes whatever is in front. */
|
|
function done(a: Notice) {
|
|
setNotice(a)
|
|
if (open == null) refreshList()
|
|
else refreshZone()
|
|
}
|
|
|
|
/*
|
|
After "Edit Zone File" and "Import Zone": upstream reloads the zone ONLY if its view is the one
|
|
showing (`if ($("#divEditZone").is(":visible")) showEditZone(zone)`,
|
|
zone.js:1302-1303 in v15.5.1). From the list nothing is reloaded, so this is
|
|
not `done`.
|
|
*/
|
|
function zoneFileSaved(a: Notice) {
|
|
setNotice(a)
|
|
if (open != null) refreshZone()
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Notifier notice={notice} onClose={() => setNotice(null)} />
|
|
|
|
{/* Draws nothing without a cluster. Upstream has one for the list and a
|
|
second for the records view (`optEditZoneClusterNode`); both read the
|
|
same state here, since it is the same question. The list's reloads
|
|
the list (index.html:436); the records view's is always disabled
|
|
(index.html:548). */}
|
|
<ClusterNodeSelect
|
|
nodes={nodes}
|
|
initialised={clusterInitialised}
|
|
value={node}
|
|
onChange={(n) => {
|
|
setNode(n)
|
|
refreshList()
|
|
}}
|
|
disabled={open != null}
|
|
label="Cluster Node"
|
|
/>
|
|
<HubWriteHint write={write} />
|
|
|
|
{open == null ? (
|
|
<ZoneList
|
|
token={token}
|
|
node={node}
|
|
canModify={canModify}
|
|
canDelete={canDelete}
|
|
onNotice={setNotice}
|
|
onConfirm={setConfirmation}
|
|
onAdd={() => openModal('add', '')}
|
|
onOpen={(z) => {
|
|
setNotice(null)
|
|
setOpen(z)
|
|
}}
|
|
onEditZoneFile={(z) => openModal('editZoneFile', z)}
|
|
onImport={(z) => openModal('import', z)}
|
|
onConvert={(z, t) => openModal('convert', z, t)}
|
|
onClone={(z) => openModal('clone', z)}
|
|
onPermissions={(z) => (hub ? setNotice({ type: 'info', title: 'Note!', text: "Zone permissions are changed in that server's own console." }) : openModal('permissions', z))}
|
|
onOptions={(z) => openModal('options', z)}
|
|
refresh={listRefresh}
|
|
/>
|
|
) : (
|
|
<ZoneRecords
|
|
zone={open}
|
|
token={token}
|
|
node={node}
|
|
canModify={canModify}
|
|
canDelete={canDelete}
|
|
onBack={() => {
|
|
setOpen(null)
|
|
refreshList()
|
|
}}
|
|
onNotice={setNotice}
|
|
onConfirm={setConfirmation}
|
|
onAddRecord={(zone, recs) => {
|
|
setRecordMode('add')
|
|
setZoneDetails(zone)
|
|
setRecords(recs)
|
|
setOriginalRecord(null)
|
|
openModal('record', open)
|
|
}}
|
|
onEditRecord={(zone, record, recs) => {
|
|
setRecordMode('update')
|
|
setZoneDetails(zone)
|
|
setRecords(recs)
|
|
setOriginalRecord(record)
|
|
openModal('record', open)
|
|
}}
|
|
onEditZoneFile={(z) => openModal('editZoneFile', z)}
|
|
onImport={(z) => openModal('import', z)}
|
|
onConvert={(z, t) => openModal('convert', z, t)}
|
|
onClone={(z) => openModal('clone', z)}
|
|
onPermissions={(z) => (hub ? setNotice({ type: 'info', title: 'Note!', text: "Zone permissions are changed in that server's own console." }) : openModal('permissions', z))}
|
|
onOptions={(z) => openModal('options', z)}
|
|
onSign={(z) => openModal('sign', z)}
|
|
onUnsign={(z) => openModal('unsign', z)}
|
|
onViewDs={(z) => openModal('viewds', z)}
|
|
onDnssecProperties={(z) => openModal('dnssec', z)}
|
|
refresh={zoneRefresh}
|
|
modalExpiryTtl={modalExpiryTtl}
|
|
/>
|
|
)}
|
|
|
|
<Confirm
|
|
open={confirmation !== null}
|
|
title={confirmation?.title ?? ''}
|
|
text={confirmation?.text}
|
|
label={confirmation?.label ?? ''}
|
|
variant={confirmation?.danger ? 'danger' : 'primary'}
|
|
onClose={() => setConfirmation(null)}
|
|
onConfirm={() => confirmation?.action()}
|
|
/>
|
|
|
|
<AddZone
|
|
open={modal === 'add'}
|
|
token={token}
|
|
node={node}
|
|
useSoaSerialDateScheme={useSoaSerialDateScheme}
|
|
dnssecValidation={dnssecValidation}
|
|
clusterDomain={clusterInitialised ? clusterDomain : undefined}
|
|
onClose={() => setModal(null)}
|
|
onCreated={(domain, a) => {
|
|
// Upstream opens the newly created zone, it does not go back to the list.
|
|
setOpen(domain === '' ? '.' : domain)
|
|
setNotice(a)
|
|
}}
|
|
/>
|
|
|
|
<EditZoneFile
|
|
zone={modalZone}
|
|
open={modal === 'editZoneFile'}
|
|
token={token}
|
|
node={node}
|
|
onClose={() => setModal(null)}
|
|
onSaved={zoneFileSaved}
|
|
/>
|
|
|
|
<ImportZone
|
|
zone={modalZone}
|
|
open={modal === 'import'}
|
|
token={token}
|
|
node={node}
|
|
onClose={() => setModal(null)}
|
|
// `importZone` (zone.js:1387-1393) reloads the zone only if its view is
|
|
// open, like the zone file editor; from the list it refreshes nothing.
|
|
onDone={zoneFileSaved}
|
|
/>
|
|
|
|
<CloneZone
|
|
zone={modalZone}
|
|
open={modal === 'clone'}
|
|
token={token}
|
|
node={node}
|
|
onClose={() => setModal(null)}
|
|
onDone={done}
|
|
/>
|
|
|
|
<ConvertZone
|
|
zone={modalZone}
|
|
sourceType={modalKind}
|
|
open={modal === 'convert'}
|
|
token={token}
|
|
node={node}
|
|
onClose={() => setModal(null)}
|
|
onDone={done}
|
|
/>
|
|
|
|
<ZoneOptions
|
|
zone={modalZone}
|
|
open={modal === 'options'}
|
|
token={token}
|
|
node={node}
|
|
onClose={() => setModal(null)}
|
|
onDone={done}
|
|
/>
|
|
|
|
<ZonePermissions
|
|
zone={modalZone}
|
|
open={modal === 'permissions'}
|
|
token={token}
|
|
node={node}
|
|
canModify={canModify}
|
|
onClose={() => setModal(null)}
|
|
onDone={done}
|
|
/>
|
|
|
|
<SignZone
|
|
zone={modalZone}
|
|
open={modal === 'sign'}
|
|
token={token}
|
|
node={node}
|
|
onClose={() => setModal(null)}
|
|
onDone={done}
|
|
/>
|
|
|
|
<UnsignZone
|
|
zone={modalZone}
|
|
open={modal === 'unsign'}
|
|
token={token}
|
|
node={node}
|
|
onClose={() => setModal(null)}
|
|
onDone={done}
|
|
/>
|
|
|
|
<ViewDs
|
|
zone={modalZone}
|
|
open={modal === 'viewds'}
|
|
token={token}
|
|
node={node}
|
|
onClose={() => setModal(null)}
|
|
/>
|
|
|
|
<DnssecProperties
|
|
zone={modalZone}
|
|
open={modal === 'dnssec'}
|
|
token={token}
|
|
node={node}
|
|
onClose={() => setModal(null)}
|
|
onConfirm={setConfirmation}
|
|
onChanged2={refreshZone}
|
|
/>
|
|
|
|
<AddEditRecord
|
|
open={modal === 'record'}
|
|
mode={recordMode}
|
|
zone={modalZone}
|
|
zoneInfo={zoneDetails}
|
|
records={records}
|
|
original={originalRecord}
|
|
token={token}
|
|
node={node}
|
|
defaults={{ ...recordTtls, dnssecValidation }}
|
|
onClose={() => setModal(null)}
|
|
onDone={done}
|
|
onExpiryTtl={setModalExpiryTtl}
|
|
/>
|
|
</>
|
|
)
|
|
}
|
|
|