Start using data plane api, and will potentially move away from the single backend model once i have a way to fix register_init adding all the servers without server templates...

develop
Thomas Lynch 1 year ago
parent 3a33a0d2a6
commit 01b4ce71cc
  1. 1
      .env.example
  2. 2
      components/MapRow.js
  3. 42
      controllers/account.js
  4. 2
      controllers/domains.js
  5. 157
      controllers/maps.js
  6. 430
      package-lock.json
  7. 1
      package.json
  8. 27
      pages/map/[name].js
  9. 13
      router.js
  10. 5
      server.js
  11. 33
      util.js

@ -1,5 +1,6 @@
COOKIE_SECRET="changeme"
DB_URL="mongodb://localhost:27017"
NEXT_PUBLIC_CUSTOM_BACKENDS_ENABLED="true"
CUSTOM_BACKENDS_ENABLED="true"
BACKEND_NAME="servers"
SERVER_PREFIX="websrv"

@ -1,6 +1,6 @@
export default function MapRow({ row, onDeleteSubmit, name, csrf, showValues, mapValueNames }) {
const [_id, key, value] = row.split(' ');
const { _id, key, value } = row;
return (
<tr className="align-middle">

@ -1,41 +1,35 @@
const bcrypt = require('bcrypt');
const db = require('../db.js');
const { validClustersString, makeArrayIfSingle, extractMap, dynamicResponse } = require('../util.js');
const { validClustersString, makeArrayIfSingle, extractMap, dynamicResponse, getMaps } = require('../util.js');
/**
* account page data shared between html/json routes
*/
exports.accountData = async (req, res, next) => {
let maps = []
, acls = []
, globalAcl;
if (res.locals.user.clusters.length > 0) {
maps = await res.locals.haproxy
.showMap()
.then(list => {
return list.map(extractMap)
.filter(i => i && i.fname)
.sort((a, b) => a.fname.localeCompare(b.fname));
});
let globalIndex;
acls = await res.locals.haproxy
.showAcl()
.then(list => {
const hdrCntAcl = list.find(x => x.includes("acl 'hdr_cnt'"));
if (hdrCntAcl != null) {
globalIndex = hdrCntAcl.split(' ')[0];
}
return list.map(extractMap)
.filter(i => i);
});
globalAcl = await res.locals.haproxy
.showAcl(globalIndex);
maps = await res.locals.dataPlane.getAllRuntimeMapFiles()
.then(res => res.data)
.then(data => data.map(extractMap))
.then(maps => maps.sort((a, b) => a.fname.localeCompare(b.fname)));
const globalIndex = await res.locals.dataPlane.getAcls({
parent_name: 'http-in',
parent_type:'frontend'
})
.then(res => res.data.data)
.then(acls => acls.find(a => a.acl_name === 'ddos_mode_enabled_override').index)
globalAcl = await res.locals.dataPlane.getAcl({
index: globalIndex,
parent_name: 'http-in',
parent_type:'frontend'
})
.then(res => res.data.data)
}
return {
csrf: req.csrfToken(),
maps,
acls,
globalAcl: globalAcl && globalAcl.length === 1 && globalAcl[0].endsWith(0),
globalAcl: globalAcl && globalAcl.value.endsWith(0),
}
};

@ -1,6 +1,6 @@
const db = require('../db.js');
const url = require('url');
const { deleteFromMap, dynamicResponse } = require('../util.js');
const { dynamicResponse } = require('../util.js');
/**
* GET /domains

@ -1,4 +1,4 @@
const { deleteFromMap, getMapId, dynamicResponse } = require('../util.js');
const { extractMap, dynamicResponse } = require('../util.js');
const { createCIDR, parse } = require('ip6addr');
const url = require('url');
/**
@ -7,15 +7,21 @@ const url = require('url');
*/
exports.mapData = async (req, res, next) => {
let map,
mapId,
mapInfo,
showValues = false;
try {
mapId = await getMapId(res.locals.haproxy, req.params.name);
if (!mapId) {
mapInfo = await res.locals
.dataPlane.getOneRuntimeMap(req.params.name)
.then(res => res.data)
.then(extractMap);
if (!mapInfo) {
return dynamicResponse(req, res, 400, { error: 'Invalid map' });
}
map = await res.locals.haproxy
.showMap(mapId.index);
map = await res.locals
.dataPlane.showRuntimeMap({
map: req.params.name
})
.then(res => res.data);
} catch (e) {
return next(e);
}
@ -30,16 +36,14 @@ exports.mapData = async (req, res, next) => {
}
case process.env.MAINTENANCE_MAP_NAME:
map = map.filter(a => {
const [id, key, value] = a.split(' ');
const { hostname, pathname } = url.parse(`https://${key}`);
const { hostname, pathname } = url.parse(`https://${a.key}`);
return res.locals.user.domains.includes(hostname);
});
break;
case process.env.BLOCKED_MAP_NAME:
case process.env.WHITELIST_MAP_NAME:
map = map.filter(a => {
const [id, key, value] = a.split(' ');
return res.locals.user.username === value;
return res.locals.user.username === a.value;
});
break;
default:
@ -48,7 +52,7 @@ exports.mapData = async (req, res, next) => {
return {
mapValueNames: { '0': 'None', '1': 'Proof-of-work', '2': 'hCaptcha' },
mapId,
mapInfo,
map,
csrf: req.csrfToken(),
name: req.params.name,
@ -91,29 +95,35 @@ exports.deleteMapForm = async (req, res, next) => {
try {
if (process.env.CUSTOM_BACKENDS_ENABLED && req.params.name === process.env.HOSTS_MAP_NAME) {
//refactor -> getServer(hostname)
const backendMapId = await getMapId(res.locals.haproxy, process.env.BACKENDS_MAP_NAME);
const backendMapEntry = await res.locals.haproxy
.showMap(backendMapId.index)
.then(map => map.find(m => m.split(' ')[1] === req.body.key));
const backendMapEntry = await res.locals
.dataPlane.getRuntimeMapEntry({
map: process.env.BACKENDS_MAP_NAME,
id: req.body.key,
})
.then(res => res.data)
.catch(() => {});
if (backendMapEntry) {
const serverName = backendMapEntry.split(' ')[2];
const server = await res.locals.haproxy
.backend(process.env.BACKEND_NAME)
.then(backend => backend.server(serverName));
await Promise.all([
server.setState('disable'),
//server.setAddress(),
//server.setPort(),
deleteFromMap(res.locals.haproxy, process.env.BACKENDS_MAP_NAME, req.body.key),
]);
await res.locals
.dataPlane.deleteRuntimeServer({
backend: 'servers',
name: backendMapEntry.value,
});
await res.locals
.dataPlane.deleteRuntimeMapEntry({
map: process.env.BACKENDS_MAP_NAME, //'backends'
id: req.body.key, //'example.com'
});
} else {
console.warn('no backend found to remove');
//dont return because otherwise they will have a domain stuck in the hosts map
}
}
await deleteFromMap(res.locals.haproxy, req.params.name, req.body.key);
await res.locals
.dataPlane.deleteRuntimeMapEntry({
map: req.params.name, //'ddos'
id: req.body.key, //'example.com'
});
return dynamicResponse(req, res, 302, { redirect: `/map/${req.params.name}` });
} catch (e) {
return next(e);
@ -202,58 +212,65 @@ exports.patchMapForm = async (req, res, next) => {
try {
if (process.env.CUSTOM_BACKENDS_ENABLED && req.params.name === process.env.HOSTS_MAP_NAME) {
//refactor -> getServer(hostname)
const backendMapId = await getMapId(res.locals.haproxy, process.env.BACKENDS_MAP_NAME);
let backendMapSize;
const backendMapEntry = await res.locals.haproxy
.showMap(backendMapId.index)
.then(map => {
backendMapSize = map.length;
return map.find(m => m.split(' ')[1] === req.body.key)
});
const backend = await res.locals.haproxy
.backend(process.env.BACKEND_NAME);
const backendMapEntry = await res.locals
.dataPlane.getRuntimeMapEntry({
map: process.env.BACKENDS_MAP_NAME,
id: req.body.key,
})
.then(res => res.data)
.catch(() => {});
let server;
if (backendMapEntry) {
return dynamicResponse(req, res, 400, { error: `this domain is active already and has a backend server mapping: "${backendMapEntry}"` });
//TODO: allow multiple backends (but requires reworking haproxy.cfg)
return dynamicResponse(req, res, 400, { error: 'Domain already has a backend server mapping' });
} else {
//no existing backend map entry (i.e. didnt exist at startup to get constructed in the lua script)
let backendCounter = 0;
let backendMapCheckId = 1;
const maxServers = (await backend.servers()).length;
if (backendMapSize > 0 && backendMapSize < maxServers) {
//try and skip to an empty index for speed improvement.
//will depend if any early servers are removed, but probably will be faster overall.
backendMapCheckId = backendMapSize;
}
while (backendCounter < maxServers) {
try {
server = await backend.server(`${process.env.SERVER_PREFIX}${backendMapCheckId}`);
const status = await server.status();
if (status === 'MAINT') { //would atively used servers ever enter this state?
break;
}
} catch (e) {
server = null; //probably out of servers
}
backendMapCheckId = (backendMapCheckId+1) % maxServers;
backendCounter++;
}
if (!server) {
const freeSlotId = await res.locals.dataPlane
.getRuntimeServers({
backend: 'servers'
})
.then(res => res.data)
.then(servers => {
const server = servers.find(s => s.admin_state === 'maint' && s.operational_state === 'down');
return server ? parseInt(server.id) : servers.length+1;
});
if (!freeSlotId) {
return dynamicResponse(req, res, 400, { error: 'No server slots available' });
}
const backendsMapId = await getMapId(res.locals.haproxy, process.env.BACKENDS_MAP_NAME);
await res.locals.haproxy
.addMap(backendsMapId.index, req.body.key, server.name);
const [address, port] = value.split(':');
//delete, add because apparently "replace" cant update the fucking ADDRESS???
// await res.locals
// .dataPlane.deleteRuntimeServer({
// backend: 'servers',
// name: `websrv${freeSlotId}`,
// })
// .catch(e => console.error(e));
await res.locals
.dataPlane.addRuntimeServer({
backend: 'servers',
}, {
address,
port: parseInt(port),
name: `websrv${freeSlotId}`,
id: `${freeSlotId}`,
});
await res.locals.dataPlane
.addPayloadRuntimeMap({
name: process.env.BACKENDS_MAP_NAME,
}, [{
key: req.body.key,
value: `websrv${freeSlotId}`,
}]);
}
await server.setState('enable');
await server.setAddress(value.split(':')[0]);
await server.setPort(value.split(':')[1]);
}
const mapId = await getMapId(res.locals.haproxy, req.params.name);
await res.locals.haproxy
.addMap(mapId.index, req.body.key, value);
await res.locals.dataPlane
.addPayloadRuntimeMap({
name: req.params.name
}, [{
key: req.body.key,
value: value,
}]);
return dynamicResponse(req, res, 302, { redirect: `/map/${req.params.name}` });
} catch (e) {
return next(e);

430
package-lock.json generated

@ -24,6 +24,7 @@
"ip6addr": "^0.2.5",
"next": "^12.3.4",
"nprogress": "^0.2.0",
"openapi-client-axios": "^7.1.1",
"react": "^18.2.0",
"react-content-loader": "^6.2.0",
"react-dom": "^18.2.0"
@ -1310,6 +1311,126 @@
"glob": "7.1.7"
}
},
"node_modules/@next/swc-android-arm-eabi": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.4.tgz",
"integrity": "sha512-cM42Cw6V4Bz/2+j/xIzO8nK/Q3Ly+VSlZJTa1vHzsocJRYz8KT6MrreXaci2++SIZCF1rVRCDgAg5PpqRibdIA==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-android-arm64": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.3.4.tgz",
"integrity": "sha512-5jf0dTBjL+rabWjGj3eghpLUxCukRhBcEJgwLedewEA/LJk2HyqCvGIwj5rH+iwmq1llCWbOky2dO3pVljrapg==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.3.4.tgz",
"integrity": "sha512-DqsSTd3FRjQUR6ao0E1e2OlOcrF5br+uegcEGPVonKYJpcr0MJrtYmPxd4v5T6UCJZ+XzydF7eQo5wdGvSZAyA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.4.tgz",
"integrity": "sha512-PPF7tbWD4k0dJ2EcUSnOsaOJ5rhT3rlEt/3LhZUGiYNL8KvoqczFrETlUx0cUYaXe11dRA3F80Hpt727QIwByQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-freebsd-x64": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.4.tgz",
"integrity": "sha512-KM9JXRXi/U2PUM928z7l4tnfQ9u8bTco/jb939pdFUHqc28V43Ohd31MmZD1QzEK4aFlMRaIBQOWQZh4D/E5lQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm-gnueabihf": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.3.4.tgz",
"integrity": "sha512-3zqD3pO+z5CZyxtKDTnOJ2XgFFRUBciOox6EWkoZvJfc9zcidNAQxuwonUeNts6Xbm8Wtm5YGIRC0x+12YH7kw==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.4.tgz",
"integrity": "sha512-kiX0vgJGMZVv+oo1QuObaYulXNvdH/IINmvdZnVzMO/jic/B8EEIGlZ8Bgvw8LCjH3zNVPO3mGrdMvnEEPEhKA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.4.tgz",
"integrity": "sha512-EETZPa1juczrKLWk5okoW2hv7D7WvonU+Cf2CgsSoxgsYbUCZ1voOpL4JZTOb6IbKMDo6ja+SbY0vzXZBUMvkQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.3.4.tgz",
@ -1340,6 +1461,51 @@
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.4.tgz",
"integrity": "sha512-Sd0qFUJv8Tj0PukAYbCCDbmXcMkbIuhnTeHm9m4ZGjCf6kt7E/RMs55Pd3R5ePjOkN7dJEuxYBehawTR/aPDSQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.4.tgz",
"integrity": "sha512-rt/vv/vg/ZGGkrkKcuJ0LyliRdbskQU+91bje+PgoYmxTZf/tYs6IfbmgudBJk6gH3QnjHWbkphDdRQrseRefQ==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.4.tgz",
"integrity": "sha512-DQ20JEfTBZAgF8QCjYfJhv2/279M6onxFjdG/+5B0Cyj00/EdBxiWb2eGGFgQhrBbNv/lsvzFbbi0Ptf8Vw/bg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -1894,8 +2060,7 @@
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"node_modules/aria-query": {
"version": "5.1.3",
@ -2179,6 +2344,12 @@
"node": ">= 0.10"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"peer": true
},
"node_modules/atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
@ -2211,6 +2382,17 @@
"node": ">=4"
}
},
"node_modules/axios": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.3.tgz",
"integrity": "sha512-eYq77dYIFS77AQlhzEL937yUBSepBfPIe8FcgEDN35vMNZKMrs81pgnyrQpwfy4NF4b4XWX1Zgx7yX+25w8QJA==",
"peer": true,
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axobject-query": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz",
@ -2335,6 +2517,11 @@
],
"peer": true
},
"node_modules/bath-es5": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/bath-es5/-/bath-es5-3.0.3.tgz",
"integrity": "sha512-PdCioDToH3t84lP40kUFCKWCOCH389Dl1kbC8FGoqOwamxsmqxxnJSXdkTOsPoNHXjem4+sJ+bbNoQm5zeCqxg=="
},
"node_modules/bcrypt": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.0.tgz",
@ -2784,6 +2971,18 @@
"color-support": "bin.js"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"peer": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
@ -3132,6 +3331,15 @@
"node": ">=0.10.0"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"peer": true,
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
@ -3145,6 +3353,11 @@
"node": ">= 0.8"
}
},
"node_modules/dereference-json-schema": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/dereference-json-schema/-/dereference-json-schema-0.2.1.tgz",
"integrity": "sha512-uzJsrg225owJyRQ8FNTPHIuBOdSzIZlHhss9u6W8mp7jJldHqGuLv9cULagP/E26QVJDnjtG8U7Dw139mM1ydA=="
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
@ -4499,6 +4712,26 @@
"readable-stream": "^2.3.6"
}
},
"node_modules/follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"peer": true,
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@ -4527,6 +4760,20 @@
"node": ">=0.10.0"
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"peer": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@ -5775,7 +6022,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"dependencies": {
"argparse": "^2.0.1"
},
@ -7047,6 +7293,28 @@
"wrappy": "1"
}
},
"node_modules/openapi-client-axios": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/openapi-client-axios/-/openapi-client-axios-7.1.1.tgz",
"integrity": "sha512-ebqW8bacdRAOidH+O7D03AcoIG/4lpgWjnE0Ma+pjUarDBvJXyqXqktMPpUSlkX/sh3Rq6v3zXZ5lQipTAFMHw==",
"dependencies": {
"bath-es5": "^3.0.3",
"dereference-json-schema": "^0.2.1",
"openapi-types": "^12.0.2"
},
"funding": {
"url": "https://github.com/sponsors/anttiviljami"
},
"peerDependencies": {
"axios": "^0.25.0 || ^1.1.3",
"js-yaml": "^4.1.0"
}
},
"node_modules/openapi-types": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.0.tgz",
"integrity": "sha512-XpeCy01X6L5EpP+6Hc3jWN7rMZJ+/k1lwki/kTmWzbVhdPie3jd5O2ZtedEx8Yp58icJ0osVldLMrTB/zslQXA=="
},
"node_modules/optionator": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
@ -7342,6 +7610,12 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"peer": true
},
"node_modules/pump": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
@ -10683,6 +10957,54 @@
"glob": "7.1.7"
}
},
"@next/swc-android-arm-eabi": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.4.tgz",
"integrity": "sha512-cM42Cw6V4Bz/2+j/xIzO8nK/Q3Ly+VSlZJTa1vHzsocJRYz8KT6MrreXaci2++SIZCF1rVRCDgAg5PpqRibdIA==",
"optional": true
},
"@next/swc-android-arm64": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-android-arm64/-/swc-android-arm64-12.3.4.tgz",
"integrity": "sha512-5jf0dTBjL+rabWjGj3eghpLUxCukRhBcEJgwLedewEA/LJk2HyqCvGIwj5rH+iwmq1llCWbOky2dO3pVljrapg==",
"optional": true
},
"@next/swc-darwin-arm64": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.3.4.tgz",
"integrity": "sha512-DqsSTd3FRjQUR6ao0E1e2OlOcrF5br+uegcEGPVonKYJpcr0MJrtYmPxd4v5T6UCJZ+XzydF7eQo5wdGvSZAyA==",
"optional": true
},
"@next/swc-darwin-x64": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.4.tgz",
"integrity": "sha512-PPF7tbWD4k0dJ2EcUSnOsaOJ5rhT3rlEt/3LhZUGiYNL8KvoqczFrETlUx0cUYaXe11dRA3F80Hpt727QIwByQ==",
"optional": true
},
"@next/swc-freebsd-x64": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.4.tgz",
"integrity": "sha512-KM9JXRXi/U2PUM928z7l4tnfQ9u8bTco/jb939pdFUHqc28V43Ohd31MmZD1QzEK4aFlMRaIBQOWQZh4D/E5lQ==",
"optional": true
},
"@next/swc-linux-arm-gnueabihf": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.3.4.tgz",
"integrity": "sha512-3zqD3pO+z5CZyxtKDTnOJ2XgFFRUBciOox6EWkoZvJfc9zcidNAQxuwonUeNts6Xbm8Wtm5YGIRC0x+12YH7kw==",
"optional": true
},
"@next/swc-linux-arm64-gnu": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.4.tgz",
"integrity": "sha512-kiX0vgJGMZVv+oo1QuObaYulXNvdH/IINmvdZnVzMO/jic/B8EEIGlZ8Bgvw8LCjH3zNVPO3mGrdMvnEEPEhKA==",
"optional": true
},
"@next/swc-linux-arm64-musl": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.4.tgz",
"integrity": "sha512-EETZPa1juczrKLWk5okoW2hv7D7WvonU+Cf2CgsSoxgsYbUCZ1voOpL4JZTOb6IbKMDo6ja+SbY0vzXZBUMvkQ==",
"optional": true
},
"@next/swc-linux-x64-gnu": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.3.4.tgz",
@ -10695,6 +11017,24 @@
"integrity": "sha512-YeBmI+63Ro75SUiL/QXEVXQ19T++58aI/IINOyhpsRL1LKdyfK/35iilraZEFz9bLQrwy1LYAR5lK200A9Gjbg==",
"optional": true
},
"@next/swc-win32-arm64-msvc": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.4.tgz",
"integrity": "sha512-Sd0qFUJv8Tj0PukAYbCCDbmXcMkbIuhnTeHm9m4ZGjCf6kt7E/RMs55Pd3R5ePjOkN7dJEuxYBehawTR/aPDSQ==",
"optional": true
},
"@next/swc-win32-ia32-msvc": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.4.tgz",
"integrity": "sha512-rt/vv/vg/ZGGkrkKcuJ0LyliRdbskQU+91bje+PgoYmxTZf/tYs6IfbmgudBJk6gH3QnjHWbkphDdRQrseRefQ==",
"optional": true
},
"@next/swc-win32-x64-msvc": {
"version": "12.3.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.4.tgz",
"integrity": "sha512-DQ20JEfTBZAgF8QCjYfJhv2/279M6onxFjdG/+5B0Cyj00/EdBxiWb2eGGFgQhrBbNv/lsvzFbbi0Ptf8Vw/bg==",
"optional": true
},
"@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -11099,8 +11439,7 @@
"argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"aria-query": {
"version": "5.1.3",
@ -11310,6 +11649,12 @@
"async-done": "^1.2.2"
}
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"peer": true
},
"atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
@ -11327,6 +11672,17 @@
"integrity": "sha512-/BQzOX780JhsxDnPpH4ZiyrJAzcd8AfzFPkv+89veFSr1rcMjuq2JDCwypKaPeB6ljHp9KjXhPpjgCvQlWYuqg==",
"dev": true
},
"axios": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.3.3.tgz",
"integrity": "sha512-eYq77dYIFS77AQlhzEL937yUBSepBfPIe8FcgEDN35vMNZKMrs81pgnyrQpwfy4NF4b4XWX1Zgx7yX+25w8QJA==",
"peer": true,
"requires": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"axobject-query": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz",
@ -11418,6 +11774,11 @@
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"peer": true
},
"bath-es5": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/bath-es5/-/bath-es5-3.0.3.tgz",
"integrity": "sha512-PdCioDToH3t84lP40kUFCKWCOCH389Dl1kbC8FGoqOwamxsmqxxnJSXdkTOsPoNHXjem4+sJ+bbNoQm5zeCqxg=="
},
"bcrypt": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.0.tgz",
@ -11748,6 +12109,15 @@
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="
},
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"peer": true,
"requires": {
"delayed-stream": "~1.0.0"
}
},
"component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
@ -12019,6 +12389,12 @@
"is-descriptor": "^0.1.0"
}
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"peer": true
},
"delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
@ -12029,6 +12405,11 @@
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
},
"dereference-json-schema": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/dereference-json-schema/-/dereference-json-schema-0.2.1.tgz",
"integrity": "sha512-uzJsrg225owJyRQ8FNTPHIuBOdSzIZlHhss9u6W8mp7jJldHqGuLv9cULagP/E26QVJDnjtG8U7Dw139mM1ydA=="
},
"destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
@ -13111,6 +13492,12 @@
"readable-stream": "^2.3.6"
}
},
"follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"peer": true
},
"for-each": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@ -13133,6 +13520,17 @@
"for-in": "^1.0.1"
}
},
"form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"peer": true,
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
},
"forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@ -14042,7 +14440,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"requires": {
"argparse": "^2.0.1"
}
@ -14997,6 +15394,21 @@
"wrappy": "1"
}
},
"openapi-client-axios": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/openapi-client-axios/-/openapi-client-axios-7.1.1.tgz",
"integrity": "sha512-ebqW8bacdRAOidH+O7D03AcoIG/4lpgWjnE0Ma+pjUarDBvJXyqXqktMPpUSlkX/sh3Rq6v3zXZ5lQipTAFMHw==",
"requires": {
"bath-es5": "^3.0.3",
"dereference-json-schema": "^0.2.1",
"openapi-types": "^12.0.2"
}
},
"openapi-types": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.0.tgz",
"integrity": "sha512-XpeCy01X6L5EpP+6Hc3jWN7rMZJ+/k1lwki/kTmWzbVhdPie3jd5O2ZtedEx8Yp58icJ0osVldLMrTB/zslQXA=="
},
"optionator": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
@ -15207,6 +15619,12 @@
"ipaddr.js": "1.9.1"
}
},
"proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"peer": true
},
"pump": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",

@ -29,6 +29,7 @@
"ip6addr": "^0.2.5",
"next": "^12.3.4",
"nprogress": "^0.2.0",
"openapi-client-axios": "^7.1.1",
"react": "^18.2.0",
"react-content-loader": "^6.2.0",
"react-dom": "^18.2.0"

@ -12,7 +12,7 @@ const MapPage = (props) => {
const { name: mapName } = router.query;
const [state, dispatch] = useState(props);
const [error, setError] = useState();
const changedMap = state.mapId?.name != mapName;
const changedMap = state.mapInfo?.name != mapName;
useEffect(() => {
if (!state.map) {
@ -29,18 +29,18 @@ const MapPage = (props) => {
);
}
const { user, mapValueNames, mapId, map, csrf, showValues } = state;
const { user, mapValueNames, mapInfo, map, csrf, showValues } = state;
async function addToMap(e) {
e.preventDefault();
await API.addToMap(mapId.name, { _csrf: csrf, key: e.target.key.value, value: e.target.value?.value }, dispatch, setError, router);
await API.addToMap(mapInfo.name, { _csrf: csrf, key: e.target.key.value, value: e.target.value?.value }, dispatch, setError, router);
await API.getMap(mapName, dispatch, setError, router);
e.target.reset();
}
async function deleteFromMap(e) {
e.preventDefault();
await API.deleteFromMap(mapId.name, { _csrf: csrf, key: e.target.key.value }, dispatch, setError, router);
await API.deleteFromMap(mapInfo.name, { _csrf: csrf, key: e.target.key.value }, dispatch, setError, router);
await API.getMap(mapName, dispatch, setError, router);
}
@ -49,7 +49,7 @@ const MapPage = (props) => {
<MapRow
key={i}
row={row}
name={mapId.name}
name={mapInfo.name}
csrf={csrf}
showValues={showValues}
mapValueNames={mapValueNames}
@ -61,7 +61,7 @@ const MapPage = (props) => {
let formElements;
//TODO: env var case map names
switch (mapId.name) {
switch (mapInfo.name) {
case "ddos": {
const mapValueOptions = Object.entries(mapValueNames)
.map((entry, i) => (<option key={'option'+i} value={entry[0]}>{entry[1]}</option>))
@ -71,7 +71,6 @@ const MapPage = (props) => {
<input className="btn btn-success" type="submit" value="+" />
<input className="form-control mx-3" type="text" name="key" placeholder="domain/path" required />
<select className="form-select mx-3" name="value" required>
<option selected />
{mapValueOptions}
</select>
</>
@ -80,7 +79,7 @@ const MapPage = (props) => {
}
case "hosts":
case "maintenance": {
const activeDomains = map.map(e => e.split(' ')[1]);
const activeDomains = map.map(e => e.key);
const inactiveDomains = user.domains.filter(d => !activeDomains.includes(d));
const domainSelectOptions = inactiveDomains.map((d, i) => (<option key={'option'+i} value={d}>{d}</option>));
formElements = (
@ -92,7 +91,7 @@ const MapPage = (props) => {
{domainSelectOptions}
</select>
{
(process.env.NEXT_PUBLIC_CUSTOM_BACKENDS_ENABLED && mapId.name === "hosts") &&
(process.env.NEXT_PUBLIC_CUSTOM_BACKENDS_ENABLED && mapInfo.name === "hosts") &&
<input
className="form-control ml-2"
type="text"
@ -122,7 +121,7 @@ const MapPage = (props) => {
<Head>
<title>
{mapId.fname}
{mapInfo.fname}
</title>
</Head>
@ -130,7 +129,7 @@ const MapPage = (props) => {
{/* Map friendly name (same as shown on acc page) */}
<h5 className="fw-bold">
{mapId.fname}:
{mapInfo.fname}:
</h5>
{/* Map table */}
@ -143,11 +142,11 @@ const MapPage = (props) => {
<tr>
<th />
<th>
{mapId.columnNames[0]}
{mapInfo.columnNames[0]}
</th>
{showValues === true && (
<th>
{mapId.columnNames[1]}
{mapInfo.columnNames[1]}
</th>
)}
</tr>
@ -158,7 +157,7 @@ const MapPage = (props) => {
{/* Add new row form */}
<tr className="align-middle">
<td className="col-1 text-center" colSpan="3">
<form onSubmit={addToMap} className="d-flex" action={`/forms/map/${mapId.name}/add`} method="post">
<form onSubmit={addToMap} className="d-flex" action={`/forms/map/${mapInfo.name}/add`} method="post">
{formElements}
</form>
</td>

@ -5,6 +5,7 @@ const HAProxy = require('@fatchan/haproxy-sdk')
, MongoStore = require('connect-mongo')
, db = require('./db.js')
, csrf = require('csurf')
, OpenAPIClientAxios = require('openapi-client-axios').default
, { dynamicResponse } = require('./util.js');
const testRouter = (server, app) => {
@ -54,7 +55,7 @@ const testRouter = (server, app) => {
const csrfMiddleware = csrf();
//HAProxy-sdk middleware
const useHaproxy = (req, res, next) => {
const useHaproxy = async (req, res, next) => {
if (res.locals.user.clusters.length === 0) {
return next();
}
@ -63,6 +64,16 @@ const testRouter = (server, app) => {
res.locals.haproxy = new HAProxy(res.locals.user.clusters[res.locals.user.activeCluster]);
res.locals.fMap = server.locals.fMap;
res.locals.mapValueNames = server.locals.mapValueNames;
const api = new OpenAPIClientAxios({
definition: 'http://127.0.0.1:2001/v2/specification_openapiv3',
axiosConfigDefaults: {
headers: {
'authorization': 'Basic YWRtaW46YWRtaW4=', // admin:admin for testing,
}
}
});
res.locals.dataPlane = await api.init();
res.locals.dataPlane.defaults.baseURL = 'http://127.0.0.1:2001/v2';
next();
} catch (e) {
return dynamicResponse(req, res, 500, { error: e });

@ -39,6 +39,11 @@ app.prepare()
return handle(req, res);
});
server.use((err, req, res, next) => {
console.error(err)
return res.end();
});
server.listen(3000, (err) => {
if (err) {
throw err;

@ -54,31 +54,14 @@ module.exports = {
},
extractMap: (item) => {
const match = item.match(/(?<index>\d+) \(\/etc\/haproxy\/map\/(?<name>.+).map\)(?:.+entry_cnt=(?<count>\d+)$)?/);
if (match && match.groups) {
return {
...match.groups,
...fMap[match.groups.name],
}
}
},
getMapId: (haproxy, name) => {
return haproxy.showMap()
.then(list => {
return list
.map(module.exports.extractMap)
.find(l => l && l.name == name);
});
},
deleteFromMap: async (haproxy, name, value) => {
//maybe in future, we should cache the map # or ID because they dont(afaik) change during runtime
const mapId = await module.exports.getMapId(haproxy, name);
if (!mapId) {
throw 'Invalid map';
}
return haproxy.delMap(mapId.index, value);
const name = item.file && item.file.match(/\/etc\/haproxy\/map\/(?<name>.+).map/).groups.name;
const count = item.description && item.description.match(/(?:.+entry_cnt=(?<count>\d+)$)?/).groups.count;
return {
name,
count,
id: item.id,
...fMap[name],
};
},
dynamicResponse: (req, res, code, data) => {

Loading…
Cancel
Save