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 = { |
||||
useFileSystemPublicRoutes: false, |
||||
/* config options here */ |
||||
} |
||||
|
@ -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> |
||||
); |
||||
} |
||||
|
@ -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 } } |
||||
} |
||||
|
Loading…
Reference in new issue