implement global toggle

implement add/delete domain
implement add/delete cluster
implement add/delete map entries
implement NProgress for when loading from api
all works without js too
develop
Thomas Lynch 2 years ago
parent b57deff025
commit 4e25bf8cd7
  1. 28
      api.js
  2. 140
      app.js
  3. 4
      components/MapRow.js
  4. 33
      controllers/account.js
  5. 11
      controllers/clusters.js
  6. 16
      controllers/domains.js
  7. 4
      controllers/maps.js
  8. 2
      next.config.js
  9. 11
      package-lock.json
  10. 4
      package.json
  11. 44
      pages/_app.js
  12. 30
      pages/account.js
  13. 96
      pages/clusters.js
  14. 99
      pages/domains.js
  15. 9
      pages/login.js
  16. 79
      pages/map/[name].js
  17. 2
      pages/register.js
  18. 26
      router.js

@ -0,0 +1,28 @@
import NProgress from 'nprogress';
export default async function ApiCall(route, method, body, stateCallback, finishProgress) {
try {
const options = {
method,
};
if (body != null) {
options.body = body;
options.headers = { 'Content-Type': 'application/json' };
}
console.log(options)
NProgress.start();
let response = await fetch(route, options)
.then(res => res.json());
console.log(response)
stateCallback && stateCallback(response);
} catch(e) {
console.error(e);
} finally {
if (finishProgress != null) {
NProgress.set(finishProgress);
} else {
NProgress.done(true);
}
return null;
}
}

140
app.js

@ -1,140 +0,0 @@
process
.on('uncaughtException', console.error)
.on('unhandledRejection', console.error);
//Dependencies
const express = require('express');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser')
const dotenv = require('dotenv');
dotenv.config({ path: '.env' });
const HAProxy = require('@fatchan/haproxy-sdk');
const session = require('express-session');
const MongoStore = require('connect-mongo');
const csrf = require('csurf');
const db = require('./db.js');
//Controllers
const mapsController = require('./controllers/maps');
const accountsController = require('./controllers/accounts');
const domainsController = require('./controllers/domains');
//Express setup
const app = express();
app.set('query parser', 'simple');
app.use(bodyParser.json({ extended: false })); // for parsing application/json
app.use(bodyParser.urlencoded({ extended: false })); // for parsing application/x-www-form-urlencoded
app.use(cookieParser(process.env.COOKIE_SECRET));
app.disable('x-powered-by');
app.set('view engine', 'pug');
app.set('views', './views/pages');
app.enable('view cache');
app.set('trust proxy', 1);
//template locals
app.locals.mapValueNames = { '0': 'None', '1': 'Proof-of-work', '2': 'hCaptcha' };
app.locals.fMap = require('./util.js').fMap;
async function run() {
//Session & auth
await db.connect();
const sessionStore = session({
secret: process.env.COOKIE_SECRET,
store: MongoStore.create({ mongoUrl: process.env.DB_URL }),
resave: false,
saveUninitialized: false,
rolling: true,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 1000 * 60 * 60 * 24 * 7, //week
}
});
const useSession = (req, res, next) => {
sessionStore(req, res, next);
}
const fetchSession = async (req, res, next) => {
if (req.session.user) {
const account = await db.db.collection('accounts').findOne({_id:req.session.user});
if (account) {
res.locals.user = {
username: account._id,
domains: account.domains,
clusters: account.clusters,
activeCluster: account.activeCluster,
};
return next();
}
req.session.destroy();
}
next();
};
const checkSession = (req, res, next) => {
if (!res.locals.user) {
return res.redirect('/login');
}
next();
};
const csrfMiddleware = csrf();
//HAProxy-sdk middleware
const useHaproxy = (req, res, next) => {
if (res.locals.user.clusters.length === 0) {
return next();
}
try {
//uses cluster from account
res.locals.haproxy = new HAProxy(res.locals.user.clusters[res.locals.user.activeCluster]);
next();
} catch (e) {
res.status(500).send(e);
}
};
const hasCluster = (req, res, next) => {
if (res.locals.user.clusters.length > 0) {
return next();
}
res.redirect('/clusters');
}
//static
app.use('/static', express.static('static'));
app.use('/static/css', express.static('node_modules/bootstrap/dist/css'));
//unauthed pages
app.get('/', useSession, fetchSession, accountsController.homePage);
app.get('/login', useSession, fetchSession, accountsController.loginPage);
app.get('/register', useSession, fetchSession, accountsController.registerPage);
//register/login/logout
app.post('/login', useSession, accountsController.login);
app.post('/logout', useSession, accountsController.logout);
app.post('/register', useSession, accountsController.register);
//authed pages that dont require a cluster
app.get('/account', useSession, fetchSession, checkSession, useHaproxy, csrfMiddleware, accountsController.accountPage);
app.get('/domains', useSession, fetchSession, checkSession, useHaproxy, csrfMiddleware, domainsController.domainsPage);
app.get('/clusters', useSession, fetchSession, checkSession, useHaproxy, csrfMiddleware, accountsController.clustersPage);
//authed pages that useHaproxy
const clusterRouter = express.Router({ caseSensitive: true });
clusterRouter.post('/global/toggle', accountsController.globalToggle);
clusterRouter.post('/cluster', accountsController.setCluster);
clusterRouter.post('/cluster/add', accountsController.addCluster);
clusterRouter.post('/cluster/delete', accountsController.deleteClusters);
clusterRouter.post('/domain/add', domainsController.addDomain);
clusterRouter.post('/domain/delete', domainsController.deleteDomain);
clusterRouter.get(`/maps/:name(${process.env.BLOCKED_MAP_NAME}|${process.env.MAINTENANCE_MAP_NAME}|${process.env.WHITELIST_MAP_NAME}|${process.env.BLOCKED_MAP_NAME}|${process.env.DDOS_MAP_NAME}|${process.env.HOSTS_MAP_NAME})`, mapsController.getMapHtml);
clusterRouter.post(`/maps/:name(${process.env.BLOCKED_MAP_NAME}|${process.env.MAINTENANCE_MAP_NAME}|${process.env.WHITELIST_MAP_NAME}|${process.env.DDOS_MAP_NAME}|${process.env.HOSTS_MAP_NAME})/add`, mapsController.patchMapForm); //add to MAP
clusterRouter.post(`/maps/:name(${process.env.BLOCKED_MAP_NAME}|${process.env.MAINTENANCE_MAP_NAME}|${process.env.WHITELIST_MAP_NAME}|${process.env.DDOS_MAP_NAME}|${process.env.HOSTS_MAP_NAME})/delete`, mapsController.deleteMapForm); //delete from MAP
app.use('/', useSession, fetchSession, checkSession, useHaproxy, hasCluster, csrfMiddleware, clusterRouter);
app.listen(8080, () => {
console.log('Running at http://localhost:8080 in %s mode', process.env.NODE_ENV);
});
}
run();

@ -1,13 +1,13 @@
import Link from 'next/link'; import Link from 'next/link';
export default function MapRow({ name, row, csrf, showValues, mapValueNames }) { export default function MapRow({ name, row, csrf, showValues, mapValueNames, onDeleteSubmit }) {
const [id, key, value] = row.split(' '); const [id, key, value] = row.split(' ');
return ( return (
<tr className="align-middle"> <tr className="align-middle">
<td className="col-1 text-center"> <td className="col-1 text-center">
<form action={`/maps/${name}/delete`} method="post"> <form onSubmit={onDeleteSubmit} action={`/forms/map/${name}/delete`} method="post">
<input type="hidden" name="_csrf" value={csrf} /> <input type="hidden" name="_csrf" value={csrf} />
<input type="hidden" name="key" value={key} /> <input type="hidden" name="key" value={key} />
<input className="btn btn-danger" type="submit" value="×" /> <input className="btn btn-danger" type="submit" value="×" />

@ -3,7 +3,7 @@ const db = require('../db.js');
const { validClustersString, makeArrayIfSingle, extractMap } = require('../util.js'); const { validClustersString, makeArrayIfSingle, extractMap } = require('../util.js');
/** /**
* account data * account page data shared between html/json routes
*/ */
exports.accountData = async (req, res, next) => { exports.accountData = async (req, res, next) => {
let maps = [] let maps = []
@ -39,20 +39,26 @@ exports.accountData = async (req, res, next) => {
} }
}; };
// SSR page / first loac /**
* GET /account
* account page html
*/
exports.accountPage = async (app, req, res, next) => { exports.accountPage = async (app, req, res, next) => {
const data = await exports.accountData(req, res, next); const data = await exports.accountData(req, res, next);
return app.render(req, res, '/account', data); return app.render(req, res, '/account', { ...data, user: res.locals.user });
} }
// JSON for CSR / later page switching /**
* GET /account.json
* account page json data
*/
exports.accountJson = async (req, res, next) => { exports.accountJson = async (req, res, next) => {
const data = await exports.accountData(req, res, next); const data = await exports.accountData(req, res, next);
return res.json({ ...data, user: res.locals.user }); return res.json({ ...data, user: res.locals.user });
} }
/** /**
* POST /global/toggle * POST /forms/global/toggle
* toggle global ACL * toggle global ACL
*/ */
exports.globalToggle = async (req, res, next) => { exports.globalToggle = async (req, res, next) => {
@ -85,11 +91,11 @@ exports.globalToggle = async (req, res, next) => {
}; };
/** /**
* POST /login * POST /forms/login
* login * login
*/ */
exports.login = async (req, res) => { exports.login = async (req, res) => {
const username = req.body.username; //.toLowerCase(); const username = req.body.username.toLowerCase();
const password = req.body.password; const password = req.body.password;
const account = await db.db.collection('accounts').findOne({_id:username}); const account = await db.db.collection('accounts').findOne({_id:username});
if (!account) { if (!account) {
@ -104,18 +110,18 @@ exports.login = async (req, res) => {
}; };
/** /**
* POST /register * POST /forms/register
* regiser * regiser
*/ */
exports.register = async (req, res) => { exports.register = async (req, res) => {
const username = req.body.username; //.toLowerCase(); const username = req.body.username.toLowerCase();
const password = req.body.password; const password = req.body.password;
const rPassword = req.body.repeat_password; const rPassword = req.body.repeat_password;
if (!username || typeof username !== "string" || username.length === 0 if (!username || typeof username !== "string" || username.length === 0
|| !password || typeof password !== "string" || password.length === 0 || !password || typeof password !== "string" || password.length === 0
|| !rPassword || typeof rPassword !== "string" || rPassword.length === 0) { || !rPassword || typeof rPassword !== "string" || rPassword.length === 0) {
//todo: length limits, copy jschan input validator //todo: length limits, make jschan input validator LGPL lib and use here
return res.status(400).send('Invalid inputs'); return res.status(400).send('Invalid inputs');
} }
@ -123,7 +129,7 @@ exports.register = async (req, res) => {
return res.status(400).send('Passwords did not match'); return res.status(400).send('Passwords did not match');
} }
const existingAccount = await db.db.collection('accounts').findOne({ _id: req.body.username }); const existingAccount = await db.db.collection('accounts').findOne({ _id: username });
if (existingAccount) { if (existingAccount) {
return res.status(409).send('Account already exists with that username'); return res.status(409).send('Account already exists with that username');
} }
@ -132,7 +138,8 @@ exports.register = async (req, res) => {
await db.db.collection('accounts') await db.db.collection('accounts')
.insertOne({ .insertOne({
_id: req.body.username, _id: username,
displayName: req.body.username,
passwordHash: passwordHash, passwordHash: passwordHash,
domains: [], domains: [],
clusters: [], clusters: [],
@ -144,7 +151,7 @@ exports.register = async (req, res) => {
}; };
/** /**
* POST /logout * POST /forms/logout
* logout * logout
*/ */
exports.logout = (req, res) => { exports.logout = (req, res) => {

@ -2,13 +2,16 @@ const db = require('../db.js');
const { validClustersString, makeArrayIfSingle, extractMap } = require('../util.js'); const { validClustersString, makeArrayIfSingle, extractMap } = require('../util.js');
exports.clustersPage = async (app, req, res, next) => { exports.clustersPage = async (app, req, res, next) => {
return res.render('clusters', { return app.render(req, res, '/clusters', {
csrf: req.csrfToken(), csrf: req.csrfToken(),
}); });
}; };
exports.clustersJson = async (app, req, res, next) => { exports.clustersJson = async (req, res, next) => {
return res.json({ user: res.locals.user }); return res.json({
csrf: req.csrfToken(),
user: res.locals.user,
});
} }
/** /**
@ -52,7 +55,7 @@ exports.setCluster = async (req, res, next) => {
if (res.locals.user.username !== "admin") { if (res.locals.user.username !== "admin") {
return res.status(403).send('only admin can change cluster'); return res.status(403).send('only admin can change cluster');
} }
if (!req.body || !req.body.cluster) { if (req.body == null || req.body.cluster == null) {
return res.status(400).send('invalid cluster'); return res.status(400).send('invalid cluster');
} }
req.body.cluster = parseInt(req.body.cluster, 10) || 0; req.body.cluster = parseInt(req.body.cluster, 10) || 0;

@ -6,12 +6,23 @@ const { deleteFromMap } = require('../util.js');
* GET /domains * GET /domains
* domains page * domains page
*/ */
exports.domainsPage = async (req, res) => { exports.domainsPage = async (app, req, res) => {
return res.render('domains', { return app.render(req, res, '/domains', {
csrf: req.csrfToken(), csrf: req.csrfToken(),
}); });
}; };
/**
* GET /domains.json
* domains json data
*/
exports.domainsJSON = async (req, res) => {
return res.json({
csrf: req.csrfToken(),
user: res.locals.user,
});
};
/** /**
* POST /domain/add * POST /domain/add
* add domain * add domain
@ -48,6 +59,7 @@ exports.deleteDomain = async (req, res) => {
} }
//will fail if domain is only in the hosts map for a different cluster, so we wont do it (for now) //will fail if domain is only in the hosts map for a different cluster, so we wont do it (for now)
//but will cause permission problems "invalid input" when trying to delete it from the other cluster later... hmmm...
//await deleteFromMap(res.locals.haproxy, process.env.HOSTS_MAP_NAME, [req.body.domain]); //await deleteFromMap(res.locals.haproxy, process.env.HOSTS_MAP_NAME, [req.body.domain]);
await db.db.collection('accounts') await db.db.collection('accounts')

@ -114,7 +114,7 @@ exports.deleteMapForm = async (req, res, next) => {
} }
await deleteFromMap(res.locals.haproxy, req.params.name, req.body.key); await deleteFromMap(res.locals.haproxy, req.params.name, req.body.key);
return res.redirect(`/maps/${req.params.name}`); return res.redirect(`/map/${req.params.name}`);
} catch (e) { } catch (e) {
return next(e); return next(e);
} }
@ -254,7 +254,7 @@ exports.patchMapForm = async (req, res, next) => {
const mapId = await getMapId(res.locals.haproxy, req.params.name); const mapId = await getMapId(res.locals.haproxy, req.params.name);
await res.locals.haproxy await res.locals.haproxy
.addMap(mapId.index, req.body.key, value); .addMap(mapId.index, req.body.key, value);
return res.redirect(`/maps/${req.params.name}`); return res.redirect(`/map/${req.params.name}`);
} catch (e) { } catch (e) {
return next(e); return next(e);
} }

@ -1,3 +1,3 @@
module.exports = { module.exports = {
useFileSystemPublicRoutes: false, /* config options here */
} }

11
package-lock.json generated

@ -23,6 +23,7 @@
"gulp": "^4.0.2", "gulp": "^4.0.2",
"ip6addr": "^0.2.5", "ip6addr": "^0.2.5",
"next": "^12.1.6", "next": "^12.1.6",
"nprogress": "^0.2.0",
"react": "^18.1.0", "react": "^18.1.0",
"react-dom": "^18.1.0" "react-dom": "^18.1.0"
} }
@ -3548,6 +3549,11 @@
"set-blocking": "^2.0.0" "set-blocking": "^2.0.0"
} }
}, },
"node_modules/nprogress": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz",
"integrity": "sha1-y480xTIT2JVyP8urkH6UIq28r7E="
},
"node_modules/number-is-nan": { "node_modules/number-is-nan": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
@ -8324,6 +8330,11 @@
"set-blocking": "^2.0.0" "set-blocking": "^2.0.0"
} }
}, },
"nprogress": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz",
"integrity": "sha1-y480xTIT2JVyP8urkH6UIq28r7E="
},
"number-is-nan": { "number-is-nan": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",

@ -6,8 +6,7 @@
"scripts": { "scripts": {
"dev": "node server.js", "dev": "node server.js",
"build": "next build", "build": "next build",
"start": "next start", "start": "NODE_ENV=production node server.js"
"export": "next export"
}, },
"keywords": [], "keywords": [],
"author": "Thomas Lynch (fatchan) <tom@69420.me>", "author": "Thomas Lynch (fatchan) <tom@69420.me>",
@ -27,6 +26,7 @@
"gulp": "^4.0.2", "gulp": "^4.0.2",
"ip6addr": "^0.2.5", "ip6addr": "^0.2.5",
"next": "^12.1.6", "next": "^12.1.6",
"nprogress": "^0.2.0",
"react": "^18.1.0", "react": "^18.1.0",
"react-dom": "^18.1.0" "react-dom": "^18.1.0"
} }

@ -1,25 +1,29 @@
import 'bootstrap/dist/css/bootstrap.css'; import 'bootstrap/dist/css/bootstrap.css';
import NProgress from 'nprogress';
import Layout from '../components/Layout.js'; import Layout from '../components/Layout.js';
import "nprogress/nprogress.css";
export default function App({ Component, pageProps }) { export default function App({ Component, pageProps }) {
return (<Layout> return (
<style> <Layout>
{` <style>
html, body { font-family: arial,helvetica,sans-serif; height: 100%; } {`
.green { color: green; } html, body { font-family: arial,helvetica,sans-serif; height: 100%; }
.red { color: red; } .green { color: green; }
footer { margin-top: auto; } .red { color: red; }
.btn { font-weight: bold; } footer { margin-top: auto; }
@media (prefers-color-scheme: dark) { .btn { font-weight: bold; }
:root { --bs-body-color: #fff; --bs-body-bg: #000000; } @media (prefers-color-scheme: dark) {
.text-muted, a, a:visited, a:hover, .nav-link, .nav-link:hover { color:#fff!important; } :root { --bs-body-color: #fff; --bs-body-bg: #000000; }
.list-group-item { color: #fff; background-color: #111111; } .text-muted, a, a:visited, a:hover, .nav-link, .nav-link:hover { color:#fff!important; }
input:not(.btn), option, select { color: #fff!important; background-color: #111111!important; } .list-group-item { color: #fff; background-color: #111111; }
.list-group-item-action:focus, .list-group-item-action:hover { color: #fff; background-color: #1F1F1F; } input:not(.btn), option, select { color: #fff!important; background-color: #111111!important; }
.table { color: #fff; border-color: transparent !important; } .list-group-item-action:focus, .list-group-item-action:hover { color: #fff; background-color: #1F1F1F; }
} .table { color: #fff; border-color: transparent !important; }
`} }
</style> `}
<Component {...pageProps} /> </style>
</Layout>); <Component {...pageProps} />
</Layout>
);
} }

@ -2,6 +2,7 @@ import React, { useState } from 'react';
import Head from 'next/head'; import Head from 'next/head';
import Link from 'next/link'; import Link from 'next/link';
import MapLink from '../components/MapLink.js'; import MapLink from '../components/MapLink.js';
import ApiCall from '../api.js';
const Account = (props) => { const Account = (props) => {
@ -9,17 +10,12 @@ const Account = (props) => {
React.useEffect(() => { React.useEffect(() => {
if (!accountData.user) { if (!accountData.user) {
async function getAccount() { ApiCall('/account.json', 'GET', null, setAccountData);
const response = await fetch('/account.json')
.then(res => res.json());
setAccountData(response);
}
getAccount();
} }
}, []); }, []);
if (!accountData.user) { if (!accountData.user) {
return <>Loading...</>; return <>Loading...</>; //todo: page with animated css placeholder boxes
} }
const { user, maps, acls, globalAcl, csrf } = accountData; const { user, maps, acls, globalAcl, csrf } = accountData;
@ -33,9 +29,21 @@ const Account = (props) => {
// Links to each map and bubble/pill for map counts // Links to each map and bubble/pill for map counts
const mapLinks = maps.map((map, i) => <MapLink key={i} map={map} />); const mapLinks = maps.map((map, i) => <MapLink key={i} map={map} />);
async function switchCluster(e) {
e.preventDefault();
await ApiCall('/forms/cluster', 'POST', JSON.stringify({ _csrf: csrf, cluster: nextCluster }), null, 0.5);
await ApiCall('/account.json', 'GET', null, setAccountData);
}
async function toggleGlobal(e) {
e.preventDefault();
await ApiCall('/forms/global/toggle', 'POST', JSON.stringify({ _csrf: csrf }), null, 0.5);
await ApiCall('/account.json', 'GET', null, setAccountData);
}
return ( return (
<> <>
<Head> <Head>
<title>Account</title> <title>Account</title>
</Head> </Head>
@ -53,7 +61,7 @@ const Account = (props) => {
<span className="fw-bold"> <span className="fw-bold">
Global Override Global Override
</span> </span>
<form action="/global/toggle" method="post"> <form onSubmit={toggleGlobal} action="/forms/global/toggle" method="post">
<input type="hidden" name="_csrf" value={csrf} /> <input type="hidden" name="_csrf" value={csrf} />
<input className="btn btn-sm btn-primary" type="submit" value="Toggle" /> <input className="btn btn-sm btn-primary" type="submit" value="Toggle" />
</form> </form>
@ -84,12 +92,12 @@ const Account = (props) => {
<div className="fw-bold"> <div className="fw-bold">
Servers ({user.clusters[user.activeCluster].split(',').length}) Servers ({user.clusters[user.activeCluster].split(',').length})
<span className="fw-normal"> <span className="fw-normal">
: {user.clusters[user.activeCluster]} : {user.clusters[user.activeCluster].split(',').map(x => x.substring(0, x.length/2)+'...').join(', ')}
</span> </span>
</div> </div>
</div> </div>
<span className="ml-auto d-flex flex-row"> <span className="ml-auto d-flex flex-row">
<form action="/cluster" method="post"> <form onSubmit={switchCluster} action="/forms/cluster" method="post">
<input type="hidden" name="_csrf" value={csrf}/> <input type="hidden" name="_csrf" value={csrf}/>
<input type="hidden" name="cluster" value={nextCluster}/> <input type="hidden" name="cluster" value={nextCluster}/>
<input className="btn btn-primary px-2 py-0" type="submit" value="&gt;"/> <input className="btn btn-primary px-2 py-0" type="submit" value="&gt;"/>

@ -0,0 +1,96 @@
import React, { useState } from 'react';
import Head from 'next/head';
import Link from 'next/link';
import BackButton from '../components/BackButton.js'
import ApiCall from '../api.js'
export default function Clusters(props) {
const [accountData, setAccountData] = useState(props);
React.useEffect(() => {
if (!accountData.user) {
ApiCall('/account.json', 'GET', null, setAccountData);
}
}, []);
if (!accountData.user) {
return <>Loading...</>; //TODO: page with animated css placeholder boxes
}
const { user, maps, acls, globalAcl, csrf } = accountData;
async function addCluster(e) {
e.preventDefault();
await ApiCall('/forms/cluster/add', 'POST', JSON.stringify({ _csrf: csrf, cluster: e.target.cluster.value }), null, 0.5);
await ApiCall('/account.json', 'GET', null, setAccountData);
}
async function deleteCluster(e) {
e.preventDefault();
await ApiCall('/forms/cluster/delete', 'POST', JSON.stringify({ _csrf: csrf, cluster: e.target.cluster.value }), null, 0.5);
await ApiCall('/account.json', 'GET', null, setAccountData);
}
const domainList = user.clusters.map(c => {
//TODO: refactor, to component
return (
<tr className="align-middle">
<td className="col-1 text-center">
<form onSubmit={deleteCluster} action="/forms/cluster/delete" method="post">
<input type="hidden" name="_csrf" value={csrf} />
<input type="hidden" name="cluster" value={c} />
<input className="btn btn-danger" type="submit" value="×" />
</form>
</td>
<td>
{c}
</td>
</tr>
);
})
return (
<>
<Head>
<title>Clusters</title>
</Head>
<h5 className="fw-bold">
Clusters ({user.clusters.length}):
</h5>
{/* Clusters table */}
<div className="table-responsive">
<table className="table table-bordered text-nowrap">
<tbody>
{domainList}
{/* Add new domain form */}
<tr className="align-middle">
<td className="col-1 text-center" colSpan="3">
<form className="d-flex" onSubmit={addCluster} action="/forms/cluster/add" method="post">
<input type="hidden" name="_csrf" value={csrf} />
<input className="btn btn-success" type="submit" value="+" />
<input className="form-control mx-3" type="text" name="cluster" placeholder="tcp://host1:port,tcp://host2:port,..." required />
</form>
</td>
</tr>
</tbody>
</table>
</div>
{/* back to account */}
<BackButton to="/account" />
</>
);
};
export async function getServerSideProps({ req, res, query, resolvedUrl, locale, locales, defaultLocale}) {
return { props: { user: res.locals.user || null, ...query } }
}

@ -1,19 +1,96 @@
import React, { useState } from 'react';
import Head from 'next/head'; import Head from 'next/head';
import Link from 'next/link'; import Link from 'next/link';
import BackButton from '../components/BackButton.js' import BackButton from '../components/BackButton.js'
import ApiCall from '../api.js'
const Domains = () => ( export default function Domains(props) {
<>
<Head>
<title>Domains</title>
</Head>
<p>TODO: domains</p> const [accountData, setAccountData] = useState(props);
{/* back to account */} React.useEffect(() => {
<BackButton to="/account" /> if (!accountData.user) {
ApiCall('/account.json', 'GET', null, setAccountData);
}
}, []);
</> if (!accountData.user) {
); return <>Loading...</>; //TODO: page with animated css placeholder boxes
}
export default Domains; const { user, maps, acls, globalAcl, csrf } = accountData;
async function addDomain(e) {
e.preventDefault();
await ApiCall('/forms/domain/add', 'POST', JSON.stringify({ _csrf: csrf, domain: e.target.domain.value }), null, 0.5);
await ApiCall('/account.json', 'GET', null, setAccountData);
}
async function deleteDomain(e) {
e.preventDefault();
await ApiCall('/forms/domain/delete', 'POST', JSON.stringify({ _csrf: csrf, domain: e.target.domain.value }), null, 0.5);
await ApiCall('/account.json', 'GET', null, setAccountData);
}
const domainList = user.domains.map(d => {
//TODO: refactor, to component
return (
<tr className="align-middle">
<td className="col-1 text-center">
<form onSubmit={deleteDomain} action="/forms/domain/delete" method="post">
<input type="hidden" name="_csrf" value={csrf} />
<input type="hidden" name="domain" value={d} />
<input className="btn btn-danger" type="submit" value="×" />
</form>
</td>
<td>
{d}
</td>
</tr>
);
})
return (
<>
<Head>
<title>Domains</title>
</Head>
<h5 className="fw-bold">
Your Domains ({user.domains.length}):
</h5>
{/* Domains table */}
<div className="table-responsive">
<table className="table table-bordered text-nowrap">
<tbody>
{domainList}
{/* Add new domain form */}
<tr className="align-middle">
<td className="col-1 text-center" colSpan="3">
<form className="d-flex" onSubmit={addDomain} action="/forms/domain/add" method="post">
<input type="hidden" name="_csrf" value={csrf} />
<input className="btn btn-success" type="submit" value="+" />
<input className="form-control mx-3" type="text" name="domain" placeholder="domain" required />
</form>
</td>
</tr>
</tbody>
</table>
</div>
{/* back to account */}
<BackButton to="/account" />
</>
);
};
export async function getServerSideProps({ req, res, query, resolvedUrl, locale, locales, defaultLocale}) {
return { props: { user: res.locals.user || null, ...query } }
}

@ -1,13 +1,13 @@
import Head from 'next/head'; import Head from 'next/head';
const Login = ({ isLoggedIn }) => ( const Login = () => (
<> <>
<Head> <Head>
<title>Login</title> <title>Login</title>
</Head> </Head>
<h5 className="fw-bold">Login</h5> <h5 className="fw-bold">Login</h5>
<form action="/login" method="POST"> <form action="/forms/login" method="POST">
<div className="mb-2"> <div className="mb-2">
<label className="form-label">Username <label className="form-label">Username
<input className="form-control" type="text" name="username" maxLength="50" required="required"/> <input className="form-control" type="text" name="username" maxLength="50" required="required"/>
@ -24,9 +24,4 @@ const Login = ({ isLoggedIn }) => (
</> </>
); );
// This gets called on every request
export async function getServerSideProps({ req, res, query, resolvedUrl, locale, locales, defaultLocale}) {
return { props: { isLoggedIn: res.locals.user != null } }
}
export default Login; export default Login;

@ -4,6 +4,7 @@ import Head from 'next/head';
import Link from 'next/link'; import Link from 'next/link';
import MapRow from '../../components/MapRow.js'; import MapRow from '../../components/MapRow.js';
import BackButton from '../../components/BackButton.js'; import BackButton from '../../components/BackButton.js';
import ApiCall from '../../api.js';
const MapPage = (props) => { const MapPage = (props) => {
@ -14,24 +15,31 @@ const MapPage = (props) => {
React.useEffect(() => { React.useEffect(() => {
if (!mapData.user) { if (!mapData.user) {
async function getAccount() { ApiCall(`/map/${mapName}.json`, 'GET', null, setMapData);
const response = await fetch(`/map/${mapName}.json`)
.then(res => res.json());
console.log(response)
setMapData(response);
}
getAccount();
} }
}, []); }, []);
if (!mapData.user) { if (!mapData.user) {
return <>Loading...</>; return <>Loading...</>; //todo: page with animated css placeholder boxes
} }
const { user, mapValueNames, mapId, map, csrf, name, showValues } = mapData; const { user, mapValueNames, mapId, map, csrf, name, showValues } = mapData;
async function addToMap(e) {
e.preventDefault();
await ApiCall(`/forms/map/${mapId.name}/add`, 'POST', JSON.stringify({ _csrf: csrf, key: e.target.key.value, value: e.target.value?.value }), null, 0.5);
await ApiCall(`/map/${mapId.name}.json`, 'GET', null, setMapData);
e.target.reset();
}
async function deleteFromMap(e) {
e.preventDefault();
await ApiCall(`/forms/map/${mapId.name}/delete`, 'POST', JSON.stringify({ _csrf: csrf, key: e.target.key.value }), null, 0.5);
await ApiCall(`/map/${mapId.name}.json`, 'GET', null, setMapData);
}
const mapRows = map.map((row, i) => { const mapRows = map.map((row, i) => {
//todo: address prop drilling //TODO: address prop drilling
return ( return (
<MapRow <MapRow
key={i} key={i}
@ -40,25 +48,48 @@ const MapPage = (props) => {
csrf={csrf} csrf={csrf}
showValues={showValues} showValues={showValues}
mapValueNames={mapValueNames} mapValueNames={mapValueNames}
onDeleteSubmit={deleteFromMap}
/> />
) )
}); });
let formElements; let formElements;
//todo: env var case map names //TODO: env var case map names
switch (mapId.name) { switch (mapId.name) {
case "ddos": case "ddos": {
const mapValueOptions = Object.entries(mapValueNames)
.map(entry => (<option value={entry[0]}>{entry[1]}</option>))
formElements = ( formElements = (
<> <>
<input type="hidden" name="_csrf" value={csrf} /> <input type="hidden" name="_csrf" value={csrf} />
<input className="btn btn-success" type="submit" value="+" /> <input className="btn btn-success" type="submit" value="+" />
<input className="form-control mx-3" type="text" name="key" placeholder="ip or subnet" required /> <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>
</> </>
); );
break; break;
}
case "hosts": case "hosts":
case "maintenance": case "maintenance": {
const activeDomains = map.map(e => e.split(' ')[1]);
const inactiveDomains = user.domains.filter(d => !activeDomains.includes(d));
const domainSelectOptions = inactiveDomains.map(d => (<option value={d}>{d}</option>));
formElements = (
<>
<input type="hidden" name="_csrf" value={csrf} />
<input className="btn btn-success" type="submit" value="+" />
<select className="form-select mx-3" name="key" required>
<option selected />
{domainSelectOptions}
</select>
</>
);
break; break;
}
case "blocked": case "blocked":
case "whitelist": case "whitelist":
formElements = ( formElements = (
@ -91,24 +122,26 @@ const MapPage = (props) => {
<tbody> <tbody>
{/* header row */} {/* header row */}
<tr> {mapRows.length > 0 && (
<th /> <tr>
<th> <th />
{mapId.columnNames[0]}
</th>
{showValues === true && (
<th> <th>
{mapId.columnNames[1]} {mapId.columnNames[0]}
</th> </th>
)} {showValues === true && (
</tr> <th>
{mapId.columnNames[1]}
</th>
)}
</tr>
)}
{mapRows} {mapRows}
{/* Add new row form */} {/* Add new row form */}
<tr className="align-middle"> <tr className="align-middle">
<td className="col-1 text-center" colSpan="3"> <td className="col-1 text-center" colSpan="3">
<form className="d-flex" action={`/map/${mapId.name}/add`} method="post"> <form onSubmit={addToMap} className="d-flex" action={`/forms/map/${mapId.name}/add`} method="post">
{formElements} {formElements}
</form> </form>
</td> </td>

@ -7,7 +7,7 @@ const Register = () => (
</Head> </Head>
<h5 className="fw-bold">Register</h5> <h5 className="fw-bold">Register</h5>
<form action="/register" method="POST"> <form action="/forms/register" method="POST">
<div className="mb-2"> <div className="mb-2">
<label className="form-label">Username <label className="form-label">Username
<input className="form-control" type="text" name="username" maxLength="50" required="required"/> <input className="form-control" type="text" name="username" maxLength="50" required="required"/>

@ -16,7 +16,7 @@ const testRouter = (server, app) => {
rolling: true, rolling: true,
cookie: { cookie: {
httpOnly: true, httpOnly: true,
secure: !dev, secure: false, //!dev, //TODO: check https
sameSite: 'strict', sameSite: 'strict',
maxAge: 1000 * 60 * 60 * 24 * 7, //week maxAge: 1000 * 60 * 60 * 24 * 7, //week
} }
@ -87,9 +87,9 @@ const testRouter = (server, app) => {
server.get('/register', useSession, fetchSession, (req, res, next) => { return app.render(req, res, '/register') }); server.get('/register', useSession, fetchSession, (req, res, next) => { return app.render(req, res, '/register') });
//register/login/logout forms //register/login/logout forms
server.post('/login', useSession, accountController.login); server.post('/forms/login', useSession, accountController.login);
server.post('/logout', useSession, accountController.logout); server.post('/forms/logout', useSession, accountController.logout);
server.post('/register', useSession, accountController.register); server.post('/forms/register', useSession, accountController.register);
const mapNames = [process.env.BLOCKED_MAP_NAME, process.env.MAINTENANCE_MAP_NAME, process.env.WHITELIST_MAP_NAME, const mapNames = [process.env.BLOCKED_MAP_NAME, process.env.MAINTENANCE_MAP_NAME, process.env.WHITELIST_MAP_NAME,
process.env.BLOCKED_MAP_NAME, process.env.DDOS_MAP_NAME, process.env.HOSTS_MAP_NAME] process.env.BLOCKED_MAP_NAME, process.env.DDOS_MAP_NAME, process.env.HOSTS_MAP_NAME]
@ -108,15 +108,15 @@ const testRouter = (server, app) => {
//authed pages that useHaproxy //authed pages that useHaproxy
const clusterRouter = express.Router({ caseSensitive: true }); const clusterRouter = express.Router({ caseSensitive: true });
server.post('/global/toggle', accountController.globalToggle); clusterRouter.post('/global/toggle', accountController.globalToggle);
server.post('/cluster', clustersController.setCluster); clusterRouter.post('/cluster', clustersController.setCluster);
server.post('/cluster/add', clustersController.addCluster); clusterRouter.post('/cluster/add', clustersController.addCluster);
server.post('/cluster/delete', clustersController.deleteClusters); clusterRouter.post('/cluster/delete', clustersController.deleteClusters);
server.post('/domain/add', domainsController.addDomain); clusterRouter.post('/domain/add', domainsController.addDomain);
server.post('/domain/delete', domainsController.deleteDomain); clusterRouter.post('/domain/delete', domainsController.deleteDomain);
server.post(`/map/:name(${mapNamesOrString})/add`, mapsController.patchMapForm); //add to MAP clusterRouter.post(`/map/:name(${mapNamesOrString})/add`, mapsController.patchMapForm); //add to MAP
server.post(`/map/:name(${mapNamesOrString})/delete`, mapsController.deleteMapForm); //delete from MAP clusterRouter.post(`/map/:name(${mapNamesOrString})/delete`, mapsController.deleteMapForm); //delete from MAP
server.post('/forms', useSession, fetchSession, checkSession, useHaproxy, hasCluster, csrfMiddleware, clusterRouter); server.use('/forms', useSession, fetchSession, checkSession, useHaproxy, hasCluster, csrfMiddleware, clusterRouter);
}; };

Loading…
Cancel
Save