Early iteration of actual submitting new/update/delete DNS records, with some scuffed form validation

develop
Thomas Lynch 1 year ago
parent 8657f3afaf
commit b6ad9ed655
  1. 6
      api.js
  2. 16
      components/RecordSetRow.js
  3. 131
      controllers/dns.js
  4. 330
      pages/dns/[domain]/[zone]/[type].js
  5. 6
      pages/dns/[domain]/index.js
  6. 5
      redis.js
  7. 1
      router.js

@ -43,6 +43,12 @@ export async function getDnsDomain(domain, dispatch, errorCallback, router) {
export async function getDnsRecords(domain, zone, type, dispatch, errorCallback, router) {
return ApiCall(`/dns/${domain}/${zone}/${type}.json`, 'GET', null, dispatch, errorCallback, router);
}
export async function addUpdateDnsRecord(domain, zone, type, body, dispatch, errorCallback, router) {
return ApiCall(`/forms/dns/${domain}/${zone}/${type}`, 'POST', body, dispatch, errorCallback, router);
}
export async function deleteDnsRecord(domain, zone, type, body, dispatch, errorCallback, router) {
return ApiCall(`/forms/dns/${domain}/${zone}/${type}/delete`, 'POST', body, dispatch, errorCallback, router);
}
// Certs
export async function getCerts(dispatch, errorCallback, router) {

@ -1,8 +1,14 @@
import Link from 'next/link';
import * as API from '../api.js';
export default function RecordSetRow({ domain, name, recordSet }) {
export default function RecordSetRow({ dispatch, setError, router, domain, name, recordSet, csrf }) {
const type = recordSet[0]
const recordSetArray = Array.isArray(recordSet[1]) ? recordSet[1] : [recordSet[1]];
async function deleteDnsRecord(e) {
e.preventDefault();
await API.deleteDnsRecord(domain, name, type, Object.fromEntries(new FormData(e.target)), dispatch, setError, router);
await API.getDnsDomain(domain, dispatch, setError, router);
}
return (
<tr className="align-middle">
<td>
@ -18,7 +24,7 @@ export default function RecordSetRow({ domain, name, recordSet }) {
: "";
return (<div key={i}>
<strong>{r.id ? r.id+': ' : ''}</strong>
<span className={healthClass}>{r.ip || r.host || r.value || r.ns || r.text}</span>
<span className={healthClass}>{r.ip || r.host || r.value || r.ns || r.text || r.target}</span>
</div>)
})}
</td>
@ -39,6 +45,12 @@ export default function RecordSetRow({ domain, name, recordSet }) {
</a>
</Link>
</td>
<td>
<form onSubmit={deleteDnsRecord} action={`/forms/dns/${domain}/${name}/${type}/delete`} method="post">
<input type="hidden" name="_csrf" value={csrf} />
<input className="btn btn-danger" type="submit" value="×" />
</form>
</td>
</tr>
);
}

@ -1,15 +1,16 @@
const db = require('../db.js');
const redis = require('../redis.js');
const url = require('url');
const { isIPv4, isIPv6 } = require('net');
const { dynamicResponse } = require('../util.js');
/**
* GET /dns/:domain
* domains page
* domains records page
*/
exports.dnsDomainPage = async (app, req, res) => {
if (!res.locals.user.domains.includes(req.params.domain)) {
return res.redirect('/domains');
return dynamicResponse(req, res, 302, { redirect: '/domains' });
}
const recordSetsRaw = await redis.hgetall(`${req.params.domain}.`);
const recordSets = recordSetsRaw && Object.keys(recordSetsRaw)
@ -24,11 +25,11 @@ exports.dnsDomainPage = async (app, req, res) => {
/**
* GET /dns/:domain/:zone/:type
* domains page
* record set page
*/
exports.dnsRecordPage = async (app, req, res) => {
if (!res.locals.user.domains.includes(req.params.domain)) {
return res.redirect('/domains');
return dynamicResponse(req, res, 302, { redirect: '/domains' });
}
let recordSet = [{}];
if (req.params.zone && req.params.type) {
@ -44,7 +45,7 @@ exports.dnsRecordPage = async (app, req, res) => {
/**
* GET /dns/:domain.json
* domains json data
* domain record json
*/
exports.dnsDomainJson = async (req, res) => {
if (!res.locals.user.domains.includes(req.params.domain)) {
@ -64,7 +65,7 @@ exports.dnsDomainJson = async (req, res) => {
/**
* GET /dns/:domain/:zone/:type.json
* domains json data
* record set json
*/
exports.dnsRecordJson = async (req, res) => {
if (!res.locals.user.domains.includes(req.params.domain)) {
@ -84,34 +85,111 @@ exports.dnsRecordJson = async (req, res) => {
};
/**
* POST /post/:domain/:zone/:type/delete
* delete record
*/
exports.dnsRecordDelete = async (req, res) => {
if (!res.locals.user.domains.includes(req.params.domain)) {
return dynamicResponse(req, res, 302, { redirect: '/domains' });
}
if (req.params.zone && req.params.type) {
const recordSetRaw = await redis.hget(`${req.params.domain}.`, req.params.zone);
if (!recordSetRaw) {
recordSetRaw = {};
}
delete recordSetRaw[req.params.type];
await redis.hset(`${req.params.domain}.`, req.params.zone, recordSetRaw);
}
return dynamicResponse(req, res, 302, { redirect: `/dns/${req.params.domain}` });
};
/**
* POST /post/:domain/:zone/:type
* domains json data
* add/update record
*/
exports.dnsRecordUpdate = async (req, res) => {
if (!res.locals.user.domains.includes(req.params.domain)) {
return dynamicResponse(req, res, 403, { error: 'No permission for this domain' });
}
const { ttl, selection } = req.body;
if (Object.values(req.body).some(v => typeof v !== "string")) {
return dynamicResponse(req, res, 400, { error: 'Invalid input' });
}
let { ttl, selection } = req.body;
const { domain, zone, type } = req.params;
const records = [];
for (let i = 0; i < 100; i++) {
const {
[`id_${i}`]: id,
for (let i = 0; i < (type == "soa" ? 1 : 100); i++) {
let {
[`value_${i}`]: value,
[`health_${i}`]: h,
//geo
[`geok_${i}`]: geok,
[`geov_${i}`]: geov,
[`pref_${i}`]: preference,
[`port_${i}`]: port,
//health
[`id_${i}`]: id,
[`health_${i}`]: h,
[`fallbacks_${i}`]: fb,
[`sel_${i}`]: sel,
[`bsel_${i}`]: bsel,
[`pref_${i}`]: preference,
//other (numbers)
[`weight_${i}`]: weight,
[`priority_${i}`]: priority,
[`flag_${i}`]: flag,
[`refresh_${i}`]: refresh,
[`retry_${i}`]: retry,
[`expire_${i}`]: expire,
//other
[`tag_${i}`]: tag,
[`mbox_${i}`]: MBox,
} = req.body;
if (!value) { break; }
try {
if (geok && !["cn", "cc"].includes(geok)
|| sel && !["0", "1", "2", "3"].includes(sel)
|| bsel && !["0", "1", "2", "3"].includes(bsel)
|| flag && (isNaN(flag) || parseInt(flag) !== +flag)
|| ttl && (isNaN(ttl) || parseInt(ttl) !== +ttl)
|| preference && (isNaN(preference) || parseInt(preference) !== +preference)
|| port && (isNaN(port) || parseInt(port) !== +port)
|| weight && (isNaN(weight) || parseInt(weight) !== +weight)
|| priority && (isNaN(priority) || parseInt(priority) !== +priority)
|| refresh && (isNaN(refresh) || parseInt(refresh) !== +refresh)
|| retry && (isNaN(retry) || parseInt(retry) !== +retry)
|| expire && (isNaN(expire) || parseInt(expire) !== +expire)) {
return dynamicResponse(req, res, 400, { error: 'Invalid input' });
}
flag && (flag = parseInt(flag));
ttl && (ttl = parseInt(ttl));
preference && (preference = parseInt(preference));
port && (port = parseInt(port));
weight && (weight = parseInt(weight));
priority && (priority = parseInt(priority));
refresh && (refresh = parseInt(refresh));
retry && (retry = parseInt(retry));
expire && (expire = parseInt(expire));
sel && (sel = parseInt(sel));
bsel && (bsel = parseInt(bsel));
h && (h = (h != null ? true : false));
geov && (geov = geov.split(',').map(x => x.trim()));
fb && (fb = fb.split(',').map(x => x.trim()));
} catch(e) {
console.error(e);
return dynamicResponse(req, res, 400, { error: 'Invalid input' });
}
let record;
switch(type) {
case "a":
if (!isIPv4(value)) {
return dynamicResponse(req, res, 400, { error: 'Invalid input' });
}
record = { ttl, id, ip: value, geok, geov, h, sel, bsel, fb };
break;
case "aaaa":
record = { ttl, id, ip: value, geok, geov, h, geok, geov, sel, bsel };
if (!isIPv6(value)) {
return dynamicResponse(req, res, 400, { error: 'Invalid input' });
}
record = { ttl, id, ip: value, geok, geov, h, sel, bsel, fb };
break;
case "txt":
record = { ttl, text: value };
@ -121,23 +199,34 @@ exports.dnsRecordUpdate = async (req, res) => {
record = { ttl, host: value };
break;
case "mx":
record = { ttl, host: value, preference }; //TODO: preference
record = { ttl, host: value, preference };
break;
case "srv":
record = { ttl, target: value, preference, port, weight, priority }; //TODO: preference, port, weight, prio
record = { ttl, target: value, port, weight, priority };
break;
case "caa":
record = { ttl, value, flag, tag }; //TODO: flag, tag
record = { ttl, value, flag, tag };
break;
case "soa":
record = { ttl, ns, MBox, refresh, retry, expire, minttl }; //TODO: MBox, refresh, retry, expire, minttl
record = { ttl, ns: value, MBox, refresh, retry, expire };
break;
default:
return dynamicResponse(req, res, 400, { error: 'Invalid input' });
}
//TODO: filter
records.push(record);
}
console.log(JSON.stringify({ [type]: records }, null, 4))
return dynamicResponse(req, res, 403, { error: 'Not implemented' });
if (records.lencth === 0) {
return dynamicResponse(req, res, 400, { error: 'Invalid input' });
}
let recordSetRaw = await redis.hget(`${req.params.domain}.`, req.params.zone);
if (!recordSetRaw) {
recordSetRaw = {};
}
if (type == "soa") {
recordSetRaw[type] = records[0];
} else {
recordSetRaw[type] = records;
}
await redis.hset(`${domain}.`, zone, recordSetRaw);
return dynamicResponse(req, res, 302, { redirect: `/dns/${domain}` });
};

@ -14,7 +14,7 @@ const DnsEditRecordPage = (props) => {
const [recordSet, setRecordSet] = useState(state.recordSet)
const [zone, setZone] = useState(routerZone || "name");
const [type, setType] = useState(routerType || "a");
const [recordSelection, setRecordSelection] = useState(newRecord ? "geo" : (recordSet[0].h ? "geo" : "roundrobin"));
const [recordSelection, setRecordSelection] = useState(recordSet && recordSet.length > 0 ? (recordSet[0].geok ? "geo" : "roundrobin") : "roundrobin");
const [error, setError] = useState();
useEffect(() => {
@ -23,6 +23,7 @@ const DnsEditRecordPage = (props) => {
.then(res => {
if (res && res.recordSet) {
setRecordSet([...res.recordSet]);
setRecordSelection(res.recordSet[0].geok ? "geo" : "roundrobin");
}
});
@ -42,6 +43,11 @@ const DnsEditRecordPage = (props) => {
);
}
async function addUpdateRecord(e) {
e.preventDefault();
await API.addUpdateDnsRecord(domain, zone, type, Object.fromEntries(new FormData(e.target)), dispatch, setError, router);
}
const { csrf } = state;
const supportsGeo = ["a", "aaaa"].includes(type) && recordSelection === "geo";
const supportsHealth = ["a", "aaaa"].includes(type);
@ -65,10 +71,7 @@ const DnsEditRecordPage = (props) => {
<form
method="POST"
action={`/forms/dns/${domain}/${zone}/${type}`}
onSubmit={e => {
// e.preventDefault();
// console.log(e)
}}
onSubmit={addUpdateRecord}
>
<input type="hidden" name="_csrf" value={csrf} />
<div className="card text-bg-dark col p-3 border-0 shadow-sm">
@ -136,6 +139,7 @@ const DnsEditRecordPage = (props) => {
name="selection"
id="roundrobin"
value="roundrobin"
checked={recordSelection === "roundrobin"}
onChange={e => setRecordSelection(e.target.value)}
/>
<label
@ -167,7 +171,7 @@ const DnsEditRecordPage = (props) => {
value="geo"
id="geo"
onChange={e => setRecordSelection(e.target.value)}
defaultChecked
checked={recordSelection === "geo"}
/>
<label
className="form-check-label"
@ -183,119 +187,215 @@ const DnsEditRecordPage = (props) => {
Records:
</div>
</div>
{recordSet.map((rec, i) => (<>
<div className="row" key={`row1_${i}`}>
{supportsHealth && <div className="col-sm-4 col-md-2">
ID:
<input className="form-control" type="text" name={`id_${i}`} defaultValue={rec.id} required />
</div>}
<div className="col">
<label className="w-100">
Value
<input className="form-control" type="text" name={`value_${i}`} defaultValue={rec.ip || rec.host || rec.value || rec.ns || rec.text} required />
</label>
</div>
<div className="col-auto ms-auto">
<button
className="btn btn-danger mt-4"
onClick={(e) =>{
e.preventDefault();
recordSet.splice(i, 1);
setRecordSet([...recordSet]);
}}
disabled={i === 0}
>
×
</button>
</div>
</div>
{supportsHealth && <div className="row" key={`row2_${i}`}>
<div className="col-sm-12 col-md-2 align-self-end mb-2">
<div className="form-check form-switch">
<input
className="form-check-input"
type="checkbox"
name={`health_${i}`}
value="1"
id="flexCheckDefault"
checked={rec.h === true}
onChange={(e) =>{
recordSet[i].h = e.target.checked;
setRecordSet([...recordSet]);
}}
/>
<label className="form-check-label" htmlFor="flexCheckDefault">
Health Check
{recordSet.map((rec, i) => {
let typeFields;
switch (type) {
case "mx":
typeFields = <div className="row">
<div className="col-sm-12 col-md-3">
<label className="w-100">
Preference
<input className="form-control" type="number" name={`preference_${i}`} defaultValue={rec.preference} required />
</label>
</div>
</div>;
break;
case "srv":
typeFields = <div className="row">
<div className="col-sm-12 col-md-3">
<label className="w-100">
Preference
<input className="form-control" type="number" name={`preference_${i}`} defaultValue={rec.preference} required />
</label>
</div>
<div className="col-sm-12 col-md-3">
<label className="w-100">
Port
<input className="form-control" type="number" name={`port_${i}`} defaultValue={rec.port} required />
</label>
</div>
<div className="col-sm-12 col-md-3">
<label className="w-100">
Weight
<input className="form-control" type="number" name={`weight_${i}`} defaultValue={rec.weight} required />
</label>
</div>
<div className="col-sm-12 col-md-3">
<label className="w-100">
Priority
<input className="form-control" type="number" name={`priority_${i}`} defaultValue={rec.priority} required />
</label>
</div>
</div>;
break;
case "caa":
typeFields = <div className="row">
<div className="col-sm-12 col-md-3">
<label className="w-100">
Flag
<input className="form-control" type="number" name={`flag_${i}`} defaultValue={rec.flag} required />
</label>
</div>
<div className="col-sm-12 col-md-3">
<label className="w-100">
Tag
<input className="form-control" type="text" name={`tag_${i}`} defaultValue={rec.tag} required />
</label>
</div>
</div>;
break;
case "soa":
typeFields = <div className="row">
<div className="col-sm-12 col-md-3">
<label className="w-100">
MBox
<input className="form-control" type="text" name={`mbox_${i}`} defaultValue={rec.MBox} required />
</label>
</div>
<div className="col-sm-12 col-md-3">
<label className="w-100">
Refresh
<input className="form-control" type="number" name={`refresh_${i}`} defaultValue={rec.refresh} required />
</label>
</div>
<div className="col-sm-12 col-md-3">
<label className="w-100">
Retry
<input className="form-control" type="number" name={`retry_${i}`} defaultValue={rec.retry} required />
</label>
</div>
<div className="col-sm-12 col-md-3">
<label className="w-100">
Expire
<input className="form-control" type="number" name={`expire_${i}`} defaultValue={rec.expire} required />
</label>
</div>
{/*<div className="col-sm-12 col-md-3">
<label className="w-100">
MinTTL
<input className="form-control" type="number" name={`minttl_${i}`} defaultValue={rec.refresh} required />
</label>
</div>*/}
</div>;
break;
default:
break;
}
return (<>
<div className="row" key={`row1_${i}`}>
{supportsHealth && <div className="col-sm-4 col-md-2">
ID:
<input className="form-control" type="text" name={`id_${i}`} defaultValue={rec.id} required />
</div>}
<div className="col">
<label className="w-100">
Value
<input className="form-control" type="text" name={`value_${i}`} defaultValue={rec.ip || rec.host || rec.value || rec.ns || rec.text || rec.target} required />
</label>
</div>
</div>
<div className="col-sm-12 col-md">
<label className="w-100">
Fallback IDs
<input
className="form-control"
type="text"
name={`fallbacks_${i}`}
defaultValue={(rec.fb||[]).join(', ')}
disabled={!rec.h}
required
/>
</label>
</div>
<div className="col-sm-12 col-md-3">
<label className="w-100">
Backup Selector
<select
className="form-select"
name={`sel_${i}`}
defaultValue={rec.sel}
disabled={!rec.h}
required
>
<option value="0">None</option>
<option value="1">First</option>
<option value="2">Random</option>
<option value="3">All</option>
</select>
</label>
</div>
<div className="col-sm-12 col-md-3">
<label className="w-100">
Backup Selector
<select
className="form-select"
name={`bsel_${i}`}
defaultValue={rec.bsel}
disabled={!rec.h}
required
<div className="col-auto ms-auto">
<button
className="btn btn-danger mt-4"
onClick={(e) =>{
e.preventDefault();
recordSet.splice(i, 1);
setRecordSet([...recordSet]);
}}
disabled={i === 0}
>
<option value="0">None</option>
<option value="1">First</option>
<option value="2">Random</option>
<option value="3">All</option>
</select>
</label>
</div>
</div>}
{supportsGeo && <div className="row" key={`row3_${i}`}>
<div className="col-sm-12 col-md-2">
<label className="w-100">
Geo Key
<select className="form-select" name={`geok_${i}`} defaultValue={rec.geok} required>
<option value="cn">Continent</option>
<option value="cc">Country</option>
</select>
</label>
</div>
<div className="col">
<label className="w-100">
Geo Value(s)
<input className="form-control" type="text" name={`geov_${i}`} defaultValue={(rec.geov||[]).join(', ')} required />
</label>
×
</button>
</div>
</div>
</div>}
{i < recordSet.length-1 && <hr className="mb-2 mt-3" />}
</>))}
{typeFields}
{supportsHealth && <div className="row" key={`row2_${i}`}>
<div className="col-sm-12 col-md-2 align-self-end mb-2">
<div className="form-check form-switch">
<input
className="form-check-input"
type="checkbox"
name={`health_${i}`}
value="1"
id="flexCheckDefault"
checked={rec.h === true}
onChange={(e) =>{
recordSet[i].h = e.target.checked;
setRecordSet([...recordSet]);
}}
/>
<label className="form-check-label" htmlFor="flexCheckDefault">
Health Check
</label>
</div>
</div>
<div className="col-sm-12 col-md">
<label className="w-100">
Fallback IDs
<input
className="form-control"
type="text"
name={`fallbacks_${i}`}
defaultValue={(rec.fb||[]).join(', ')}
disabled={!rec.h}
required
/>
</label>
</div>
<div className="col-sm-12 col-md-3">
<label className="w-100">
Backup Selector
<select
className="form-select"
name={`sel_${i}`}
defaultValue={rec.sel}
disabled={!rec.h}
required
>
<option value="0">None</option>
<option value="1">First</option>
<option value="2">Random</option>
<option value="3">All</option>
</select>
</label>
</div>
<div className="col-sm-12 col-md-3">
<label className="w-100">
Backup Selector
<select
className="form-select"
name={`bsel_${i}`}
defaultValue={rec.bsel}
disabled={!rec.h}
required
>
<option value="0">None</option>
<option value="1">First</option>
<option value="2">Random</option>
<option value="3">All</option>
</select>
</label>
</div>
</div>}
{supportsGeo && <div className="row" key={`row3_${i}`}>
<div className="col-sm-12 col-md-2">
<label className="w-100">
Geo Key
<select className="form-select" name={`geok_${i}`} defaultValue={rec.geok} required>
<option value="cn">Continent</option>
<option value="cc">Country</option>
</select>
</label>
</div>
<div className="col">
<label className="w-100">
Geo Value(s)
<input className="form-control" type="text" name={`geov_${i}`} defaultValue={(rec.geov||[]).join(', ')} required />
</label>
</div>
</div>}
{i < recordSet.length-1 && <hr className="mb-2 mt-3" />}
</>);
})}
<div className="row mt-2">
<div className="col-auto ms-auto">
<button className="ms-auto btn btn-success mt-2" onClick={(e) =>{

@ -44,10 +44,14 @@ const DnsDomainIndexPage = (props) => {
return Object.entries(e[1])
.map((recordSet, i) => (
<RecordSetRow
csrf={csrf}
domain={domain}
key={`${e[0]}_${i}`}
name={e[0]}
recordSet={recordSet}
dispatch={dispatch}
setError={setError}
router={router}
/>
));
});
@ -91,7 +95,7 @@ const DnsDomainIndexPage = (props) => {
<th>
Details
</th>
<th>
<th colSpan="2">
Actions
</th>
</tr>

@ -30,6 +30,11 @@ module.exports = {
return client.hget(key, hash).then(res => { return JSON.parse(res); });
},
//set a hash value
hset: (key, hash, value) => {
return client.hset(key, hash, JSON.stringify(value));
},
//set a value on key
set: (key, value) => {
return client.set(key, JSON.stringify(value));

@ -179,6 +179,7 @@ const testRouter = (server, app) => {
clusterRouter.post('/global/toggle', useSession, fetchSession, checkSession, useHaproxy, hasCluster, csrfMiddleware, accountController.globalToggle);
clusterRouter.post(`/map/:name(${mapNamesOrString})/add`, useSession, fetchSession, checkSession, useHaproxy, hasCluster, csrfMiddleware, mapsController.patchMapForm);
clusterRouter.post(`/map/:name(${mapNamesOrString})/delete`, useSession, fetchSession, checkSession, useHaproxy, hasCluster, csrfMiddleware, mapsController.deleteMapForm);
clusterRouter.post('/dns/:domain([a-zA-Z0-9-\.]+)/:zone([a-zA-Z0-9-\.@]+)/:type([a-z]+)/delete', useSession, fetchSession, checkSession, csrfMiddleware, dnsController.dnsRecordDelete);
clusterRouter.post('/dns/:domain([a-zA-Z0-9-\.]+)/:zone([a-zA-Z0-9-\.@]+)/:type([a-z]+)', useSession, fetchSession, checkSession, csrfMiddleware, dnsController.dnsRecordUpdate);
clusterRouter.post('/cluster', useSession, fetchSession, checkSession, hasCluster, csrfMiddleware, clustersController.setCluster);
clusterRouter.post('/cluster/add', useSession, fetchSession, checkSession, hasCluster, csrfMiddleware, clustersController.addCluster);

Loading…
Cancel
Save