implement add/delete domain implement add/delete cluster implement add/delete map entries implement NProgress for when loading from api all works without js toodevelop
parent
b57deff025
commit
4e25bf8cd7
18 changed files with 386 additions and 252 deletions
@ -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; |
||||||
|
} |
||||||
|
} |
@ -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,3 +1,3 @@ |
|||||||
module.exports = { |
module.exports = { |
||||||
useFileSystemPublicRoutes: false, |
/* config options here */ |
||||||
} |
} |
||||||
|
@ -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> |
||||||
|
); |
||||||
} |
} |
||||||
|
@ -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 } } |
||||||
|
} |
||||||
|
Loading…
Reference in new issue