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';
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(' ');
return (
<tr className="align-middle">
<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="key" value={key} />
<input className="btn btn-danger" type="submit" value="×" />

@ -3,7 +3,7 @@ const db = require('../db.js');
const { validClustersString, makeArrayIfSingle, extractMap } = require('../util.js');
/**
* account data
* account page data shared between html/json routes
*/
exports.accountData = async (req, res, next) => {
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) => {
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) => {
const data = await exports.accountData(req, res, next);
return res.json({ ...data, user: res.locals.user });
}
/**
* POST /global/toggle
* POST /forms/global/toggle
* toggle global ACL
*/
exports.globalToggle = async (req, res, next) => {
@ -85,11 +91,11 @@ exports.globalToggle = async (req, res, next) => {
};
/**
* POST /login
* POST /forms/login
* login
*/
exports.login = async (req, res) => {
const username = req.body.username; //.toLowerCase();
const username = req.body.username.toLowerCase();
const password = req.body.password;
const account = await db.db.collection('accounts').findOne({_id:username});
if (!account) {
@ -104,18 +110,18 @@ exports.login = async (req, res) => {
};
/**
* POST /register
* POST /forms/register
* regiser
*/
exports.register = async (req, res) => {
const username = req.body.username; //.toLowerCase();
const username = req.body.username.toLowerCase();
const password = req.body.password;
const rPassword = req.body.repeat_password;
if (!username || typeof username !== "string" || username.length === 0
|| !password || typeof password !== "string" || password.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');
}
@ -123,7 +129,7 @@ exports.register = async (req, res) => {
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) {
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')
.insertOne({
_id: req.body.username,
_id: username,
displayName: req.body.username,
passwordHash: passwordHash,
domains: [],
clusters: [],
@ -144,7 +151,7 @@ exports.register = async (req, res) => {
};
/**
* POST /logout
* POST /forms/logout
* logout
*/
exports.logout = (req, res) => {

@ -2,13 +2,16 @@ const db = require('../db.js');
const { validClustersString, makeArrayIfSingle, extractMap } = require('../util.js');
exports.clustersPage = async (app, req, res, next) => {
return res.render('clusters', {
return app.render(req, res, '/clusters', {
csrf: req.csrfToken(),
});
};
exports.clustersJson = async (app, req, res, next) => {
return res.json({ user: res.locals.user });
exports.clustersJson = async (req, res, next) => {
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") {
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');
}
req.body.cluster = parseInt(req.body.cluster, 10) || 0;

@ -6,12 +6,23 @@ const { deleteFromMap } = require('../util.js');
* GET /domains
* domains page
*/
exports.domainsPage = async (req, res) => {
return res.render('domains', {
exports.domainsPage = async (app, req, res) => {
return app.render(req, res, '/domains', {
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
* 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)
//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 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);
return res.redirect(`/maps/${req.params.name}`);
return res.redirect(`/map/${req.params.name}`);
} catch (e) {
return next(e);
}
@ -254,7 +254,7 @@ exports.patchMapForm = async (req, res, next) => {
const mapId = await getMapId(res.locals.haproxy, req.params.name);
await res.locals.haproxy
.addMap(mapId.index, req.body.key, value);
return res.redirect(`/maps/${req.params.name}`);
return res.redirect(`/map/${req.params.name}`);
} catch (e) {
return next(e);
}

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

11
package-lock.json generated

@ -23,6 +23,7 @@
"gulp": "^4.0.2",
"ip6addr": "^0.2.5",
"next": "^12.1.6",
"nprogress": "^0.2.0",
"react": "^18.1.0",
"react-dom": "^18.1.0"
}
@ -3548,6 +3549,11 @@
"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": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
@ -8324,6 +8330,11 @@
"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": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",

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

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

@ -2,6 +2,7 @@ import React, { useState } from 'react';
import Head from 'next/head';
import Link from 'next/link';
import MapLink from '../components/MapLink.js';
import ApiCall from '../api.js';
const Account = (props) => {
@ -9,17 +10,12 @@ const Account = (props) => {
React.useEffect(() => {
if (!accountData.user) {
async function getAccount() {
const response = await fetch('/account.json')
.then(res => res.json());
setAccountData(response);
}
getAccount();
ApiCall('/account.json', 'GET', null, setAccountData);
}
}, []);
if (!accountData.user) {
return <>Loading...</>;
return <>Loading...</>; //todo: page with animated css placeholder boxes
}
const { user, maps, acls, globalAcl, csrf } = accountData;
@ -33,9 +29,21 @@ const Account = (props) => {
// Links to each map and bubble/pill for map counts
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 (
<>
<Head>
<title>Account</title>
</Head>
@ -53,7 +61,7 @@ const Account = (props) => {
<span className="fw-bold">
Global Override
</span>
<form action="/global/toggle" method="post">
<form onSubmit={toggleGlobal} action="/forms/global/toggle" method="post">
<input type="hidden" name="_csrf" value={csrf} />
<input className="btn btn-sm btn-primary" type="submit" value="Toggle" />
</form>
@ -84,12 +92,12 @@ const Account = (props) => {
<div className="fw-bold">
Servers ({user.clusters[user.activeCluster].split(',').length})
<span className="fw-normal">
: {user.clusters[user.activeCluster]}
: {user.clusters[user.activeCluster].split(',').map(x => x.substring(0, x.length/2)+'...').join(', ')}
</span>
</div>
</div>
<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="cluster" value={nextCluster}/>
<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 Link from 'next/link';
import BackButton from '../components/BackButton.js'
import ApiCall from '../api.js'
const Domains = () => (
<>
<Head>
<title>Domains</title>
</Head>
export default function Domains(props) {
<p>TODO: domains</p>
const [accountData, setAccountData] = useState(props);
{/* back to account */}
<BackButton to="/account" />
React.useEffect(() => {
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';
const Login = ({ isLoggedIn }) => (
const Login = () => (
<>
<Head>
<title>Login</title>
</Head>
<h5 className="fw-bold">Login</h5>
<form action="/login" method="POST">
<form action="/forms/login" method="POST">
<div className="mb-2">
<label className="form-label">Username
<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;

@ -4,6 +4,7 @@ import Head from 'next/head';
import Link from 'next/link';
import MapRow from '../../components/MapRow.js';
import BackButton from '../../components/BackButton.js';
import ApiCall from '../../api.js';
const MapPage = (props) => {
@ -14,24 +15,31 @@ const MapPage = (props) => {
React.useEffect(() => {
if (!mapData.user) {
async function getAccount() {
const response = await fetch(`/map/${mapName}.json`)
.then(res => res.json());
console.log(response)
setMapData(response);
}
getAccount();
ApiCall(`/map/${mapName}.json`, 'GET', null, setMapData);
}
}, []);
if (!mapData.user) {
return <>Loading...</>;
return <>Loading...</>; //todo: page with animated css placeholder boxes
}
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) => {
//todo: address prop drilling
//TODO: address prop drilling
return (
<MapRow
key={i}
@ -40,25 +48,48 @@ const MapPage = (props) => {
csrf={csrf}
showValues={showValues}
mapValueNames={mapValueNames}
onDeleteSubmit={deleteFromMap}
/>
)
});
let formElements;
//todo: env var case map names
//TODO: env var case map names
switch (mapId.name) {
case "ddos":
case "ddos": {
const mapValueOptions = Object.entries(mapValueNames)
.map(entry => (<option value={entry[0]}>{entry[1]}</option>))
formElements = (
<>
<input type="hidden" name="_csrf" value={csrf} />
<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;
}
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;
}
case "blocked":
case "whitelist":
formElements = (
@ -91,24 +122,26 @@ const MapPage = (props) => {
<tbody>
{/* header row */}
<tr>
<th />
<th>
{mapId.columnNames[0]}
</th>
{showValues === true && (
{mapRows.length > 0 && (
<tr>
<th />
<th>
{mapId.columnNames[1]}
{mapId.columnNames[0]}
</th>
)}
</tr>
{showValues === true && (
<th>
{mapId.columnNames[1]}
</th>
)}
</tr>
)}
{mapRows}
{/* Add new row form */}
<tr className="align-middle">
<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}
</form>
</td>

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

@ -16,7 +16,7 @@ const testRouter = (server, app) => {
rolling: true,
cookie: {
httpOnly: true,
secure: !dev,
secure: false, //!dev, //TODO: check https
sameSite: 'strict',
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') });
//register/login/logout forms
server.post('/login', useSession, accountController.login);
server.post('/logout', useSession, accountController.logout);
server.post('/register', useSession, accountController.register);
server.post('/forms/login', useSession, accountController.login);
server.post('/forms/logout', useSession, accountController.logout);
server.post('/forms/register', useSession, accountController.register);
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]
@ -108,15 +108,15 @@ const testRouter = (server, app) => {
//authed pages that useHaproxy
const clusterRouter = express.Router({ caseSensitive: true });
server.post('/global/toggle', accountController.globalToggle);
server.post('/cluster', clustersController.setCluster);
server.post('/cluster/add', clustersController.addCluster);
server.post('/cluster/delete', clustersController.deleteClusters);
server.post('/domain/add', domainsController.addDomain);
server.post('/domain/delete', domainsController.deleteDomain);
server.post(`/map/:name(${mapNamesOrString})/add`, mapsController.patchMapForm); //add to MAP
server.post(`/map/:name(${mapNamesOrString})/delete`, mapsController.deleteMapForm); //delete from MAP
server.post('/forms', useSession, fetchSession, checkSession, useHaproxy, hasCluster, csrfMiddleware, clusterRouter);
clusterRouter.post('/global/toggle', accountController.globalToggle);
clusterRouter.post('/cluster', clustersController.setCluster);
clusterRouter.post('/cluster/add', clustersController.addCluster);
clusterRouter.post('/cluster/delete', clustersController.deleteClusters);
clusterRouter.post('/domain/add', domainsController.addDomain);
clusterRouter.post('/domain/delete', domainsController.deleteDomain);
clusterRouter.post(`/map/:name(${mapNamesOrString})/add`, mapsController.patchMapForm); //add to MAP
clusterRouter.post(`/map/:name(${mapNamesOrString})/delete`, mapsController.deleteMapForm); //delete from MAP
server.use('/forms', useSession, fetchSession, checkSession, useHaproxy, hasCluster, csrfMiddleware, clusterRouter);
};

Loading…
Cancel
Save