Improved certs page and break it out into a separate page, hide internal backends map, setup button coming soon

develop
Thomas Lynch 1 year ago
parent cebe8478e7
commit 17ac71b38e
  1. 5
      acme.js
  2. 12
      api.js
  3. 36
      components/MenuLinks.js
  4. 2
      controllers/account.js
  5. 145
      controllers/certs.js
  6. 34
      controllers/domains.js
  7. 39
      pages/account.js
  8. 136
      pages/certs.js
  9. 4
      pages/clusters.js
  10. 8
      pages/domains.js
  11. 2
      pages/index.js
  12. 30
      router.js
  13. 18
      util.js

@ -80,10 +80,11 @@ module.exports = {
});
},
generate: async function(domain, email='tom@69420.me') {
generate: async function(domain, altnames, email='tom@69420.me') {
/* Create CSR */
const [key, csr] = await acme.crypto.createCsr({
commonName: domain
commonName: domain,
altNames: altnames,
});
/* Certificate */
const cert = await module.exports.client.auto({

@ -36,6 +36,17 @@ export async function deleteDomain(body, dispatch, errorCallback, router) {
return ApiCall('/forms/domain/delete', 'POST', body, dispatch, errorCallback, router, 0.5);
}
// Certs
export async function getCerts(dispatch, errorCallback, router) {
return ApiCall('/certs.json', 'GET', null, dispatch, errorCallback, router);
}
export async function addCert(body, dispatch, errorCallback, router) {
return ApiCall('/forms/cert/add', 'POST', body, dispatch, errorCallback, router, 0.5);
}
export async function deleteCert(body, dispatch, errorCallback, router) {
return ApiCall('/forms/cert/delete', 'POST', body, dispatch, errorCallback, router, 0.5);
}
// Maps
export async function getMap(mapName, dispatch, errorCallback, router) {
return ApiCall(`/map/${mapName}.json`, 'GET', null, dispatch, errorCallback, router);
@ -82,7 +93,6 @@ export async function ApiCall(route, method='get', body, dispatch, errorCallback
// Make request, catch errors, and finally{} to always end progress bar
let response;
try {
errorCallback(null);
response = await fetch(route, requestOptions);
} catch(e) {
console.error(e);

@ -37,21 +37,37 @@ export default withRouter(function MenuLinks({ router }) {
</Link>
</li>
<li className="nav-item">
<Link href="/map/hosts">
<a className={router.pathname === "/map/[name]" && router.query.name === "hosts" ? "nav-link active" : "nav-link"} aria-current="page">
<i className="bi-card-checklist pe-none me-2" width="16" height="16" />
Active Domains
<Link href="/clusters">
<a className={router.pathname === "/clusters" ? "nav-link active" : "nav-link"} aria-current="page">
<i className="bi-hdd-rack pe-none me-2" width="16" height="16" />
Clusters
</a>
</Link>
</li>
<li className="nav-item">
{/*process.env.NEXT_PUBLIC_CUSTOM_BACKENDS_ENABLED && <li className="nav-item">
<Link href="/map/backends">
<a className={router.pathname === "/map/[name]" && router.query.name === "backends" ? "nav-link active" : "nav-link"} aria-current="page">
<i className="bi-hdd-network pe-none me-2" width="16" height="16" />
Internal Backends
</a>
</Link>
</li>*/}
<li className="nav-item">
<Link href="/map/hosts">
<a className={router.pathname === "/map/[name]" && router.query.name === "hosts" ? "nav-link active" : "nav-link"} aria-current="page">
<i className="bi-hdd-network pe-none me-2" width="16" height="16" />
Backends
</a>
</Link>
</li>
<li className="nav-item">
<Link href="/certs">
<a className={router.pathname === "/certs" ? "nav-link active" : "nav-link"} aria-current="page">
<i className="bi-file-earmark-lock pe-none me-2" width="16" height="16" />
HTTPS Certificates
</a>
</Link>
</li>
<li className="nav-item">
<Link href="/map/ddos">
<a className={router.pathname === "/map/[name]" && router.query.name === "ddos" ? "nav-link active" : "nav-link"} aria-current="page">
@ -79,7 +95,7 @@ export default withRouter(function MenuLinks({ router }) {
<li className="nav-item">
<Link href="/map/maintenance">
<a className={router.pathname === "/map/[name]" && router.query.name === "maintenance" ? "nav-link active" : "nav-link"} aria-current="page">
<i className="bi-info-circle pe-none me-2" width="16" height="16" />
<i className="bi-info-square pe-none me-2" width="16" height="16" />
Maintenance Mode
</a>
</Link>
@ -87,6 +103,14 @@ export default withRouter(function MenuLinks({ router }) {
</ul>
<hr className="mt-auto" />
<ul className="nav nav-pills flex-column">
<li className="nav-item user-select-none">
<Link href="">
<a className={router.pathname === "/setup" ? "nav-link active" : "nav-link"} aria-current="page">
<i className="bi-rocket-takeoff pe-none me-2" width="16" height="16" />
Setup <small>(coming soon)</small>
</a>
</Link>
</li>
<li className="nav-item">
<Link href="/login">
<a className={router.pathname === "/login" ? "nav-link active" : "nav-link"} aria-current="page">

@ -1,6 +1,6 @@
const bcrypt = require('bcrypt');
const db = require('../db.js');
const { validClustersString, makeArrayIfSingle, extractMap, dynamicResponse, getMaps } = require('../util.js');
const { validClustersString, makeArrayIfSingle, extractMap, dynamicResponse } = require('../util.js');
/**
* account page data shared between html/json routes

@ -0,0 +1,145 @@
const db = require('../db.js');
const acme = require('../acme.js');
const url = require('url');
const { dynamicResponse } = require('../util.js');
/**
* GET /certs
* certs page
*/
exports.certsPage = async (app, req, res) => {
const certs = await db.db.collection('certs')
.find({
username: res.locals.user.username,
}, {
projection: {
_id: 1,
subject: 1,
altnames: 1,
date: 1,
storageName: 1,
}
})
.toArray();
certs.forEach(c => c.date = c.date.toISOString())
return app.render(req, res, '/certs', {
csrf: req.csrfToken(),
certs,
});
};
/**
* GET /certs.json
* certs json data
*/
exports.certsJson = async (req, res) => {
const certs = await db.db.collection('certs')
.find({
username: res.locals.user.username,
}, {
projection: {
_id: 1,
subject: 1,
altnames: 1,
date: 1,
storageName: 1,
}
})
.toArray();
certs.forEach(c => c.date = c.date.toISOString())
return res.json({
csrf: req.csrfToken(),
user: res.locals.user,
certs,
});
};
/**
* POST /cert/add
* add cert
*/
exports.addCert = async (req, res, next) => {
if (!req.body.subject || typeof req.body.subject !== 'string' || req.body.subject.length === 0
|| !res.locals.user.domains.includes(req.body.subject)) {
return dynamicResponse(req, res, 400, { error: 'Invalid subject' });
}
if (!req.body.altnames || typeof req.body.altnames !== 'object'
|| req.body.altnames.some(d => !res.locals.user.domains.includes(d))) {
return dynamicResponse(req, res, 400, { error: 'Invalid altname(s)' });
}
const existingCert = await db.db.collection('certs').findOne({ _id: req.body.subject });
if (existingCert) {
return dynamicResponse(req, res, 400, { error: 'Cert with this subject already exists' });
}
try {
url.parse(`https://${req.body.subject}`);
req.body.altnames.forEach(d => {
url.parse(`https://${d}`);
});
} catch (e) {
return dynamicResponse(req, res, 400, { error: 'Invalid input' });
}
try {
console.log(req.body.subject, req.body.altnames);
const { csr, key, cert, haproxyCert, date } = await acme.generate(req.body.subject, req.body.altnames);
const fd = new FormData();
fd.append('file_upload', new Blob([haproxyCert], { type: 'text/plain' }), `${req.body.subject}.pem`);
const { description, file, storage_name: storageName } = await res.locals.fetchAll('/v2/services/haproxy/storage/ssl_certificates?force_reload=true', {
method: 'POST',
headers: { 'authorization': res.locals.dataPlane.defaults.headers.authorization },
body: fd,
});
let update = {
_id: req.body.subject,
subject: req.body.subject,
altnames: req.body.altnames,
username: res.locals.user.username,
csr, key, cert, haproxyCert, // cert creation data
date,
}
if (description) {
//may be null due to "already exists", so we keep existing props
update = { ...update, description, file, storageName };
}
await db.db.collection('certs')
.updateOne({
_id: req.body.domain,
}, {
$set: update,
}, {
upsert: true,
});
} catch (e) {
return next(e);
}
return dynamicResponse(req, res, 302, { redirect: '/certs' });
};
//TODO: new route to sync ssl certs throughout cluster
/**
* POST /cert/delete
* delete cers
*/
exports.deleteCert = async (req, res) => {
if (!req.body.subject || typeof req.body.subject !== 'string' || req.body.subject.length === 0
//|| !res.locals.user.domains.includes(req.body.subject)
) {
return dynamicResponse(req, res, 400, { error: 'Invalid input' });
}
await db.db.collection('accounts')
.updateOne({ _id: res.locals.user.username }, { $pull: { domains: req.body.subject } });
await db.db.collection('certs')
.deleteOne({ _id: req.body.subject, username: res.locals.user.username });
return dynamicResponse(req, res, 302, { redirect: '/certs' });
};

@ -41,38 +41,6 @@ exports.addDomain = async (req, res, next) => {
}
try {
const { csr, key, cert, haproxyCert, date } = await acme.generate(req.body.domain);
const fd = new FormData();
fd.append('file_upload', new Blob([haproxyCert], { type: 'text/plain' }), `${req.body.domain}.pem`);
const { description, file, storage_name: storageName } = await res.locals.fetchAll('/v2/services/haproxy/storage/ssl_certificates?force_reload=true', {
method: 'POST',
headers: { 'authorization': res.locals.dataPlane.defaults.headers.authorization },
body: fd,
});
let update = {
_id: req.body.domain,
username: res.locals.user.username,
csr, key, cert, haproxyCert, // cert creation data
date,
}
if (description) {
//may be null due to "already exists", so we keep existing props
update = { ...update, description, file, storageName };
}
await db.db.collection('certs')
.updateOne({
_id: req.body.domain,
}, {
$set: update,
}, {
upsert: true,
});
//TODO: add scheduled task to aggregate domains and upload certs to clusters of that username through dataplane
//TODO: make scheduled task also run this again for certs close to expiry and repeat ^
//TODO: 90 day expiry on cert documents with index on date
//TODO: on domain removal, keep cert to use for re-adding if we still have the cert in DB
await db.db.collection('accounts')
.updateOne({_id: res.locals.user.username}, {$addToSet: {domains: req.body.domain }});
} catch (e) {
@ -82,8 +50,6 @@ exports.addDomain = async (req, res, next) => {
return dynamicResponse(req, res, 302, { redirect: '/domains' });
};
//TODO: new route to sync ssl cert names form haproxy so storage name and 🔐 can show for existing domains without removing/re-adding
/**
* POST /domain/delete
* delete domain

@ -65,11 +65,11 @@ const Account = (props) => {
<span className="fw-bold">
Global Override
</span>
<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>
</div>
<form onSubmit={toggleGlobal} action="/forms/global/toggle" method="post" className="me-2">
<input type="hidden" name="_csrf" value={csrf} />
<input className="btn btn-sm btn-primary" type="submit" value="Toggle" />
</form>
<div className={`badge rounded-pill bg-${globalAcl?'success':'dark'}`}>
{globalAcl?'ON':'OFF'}
</div>
@ -81,20 +81,20 @@ const Account = (props) => {
<div className="flex-row d-flex w-100">
<div className="ms-2 me-auto">
<div className="fw-bold">
Manage Servers
Manage Clusters
<span className="fw-normal">
{' '}- Add/Delete/Select server
{' '}- Add/Delete/Select cluster
</span>
</div>
</div>
<span className="ml-auto badge bg-info rounded-pill" style={{ maxHeight: "1.6em" }}>
Managing: 1
Cluster: {user.activeCluster}
</span>
</div>
<div className="d-flex w-100 justify-content-between mt-2">
<div className="ms-2 overflow-hidden">
<div className="fw-bold overflow-hidden text-truncate">
Servers ({user.clusters.length === 0 ? 0 : user.clusters[user.activeCluster].split(',').length})
Servers (n:{user.clusters.length === 0 ? 0 : user.clusters[user.activeCluster].split(',').length})
{user.clusters.length > 0 && (<span className="fw-normal">
: {user.clusters[user.activeCluster].split(',').map(x => {
const cUrl = new URL(x);
@ -119,12 +119,12 @@ const Account = (props) => {
</div>
</div>
{/* Available domains */}
{/* Domains */}
<Link href="/domains">
<a className="list-group-item list-group-item-action d-flex align-items-start">
<div className="ms-2 me-auto">
<div className="fw-bold">
Available Domains
Domains
<span className="fw-normal">
{' '}- Domains you have permission over
</span>
@ -136,6 +136,23 @@ const Account = (props) => {
</a>
</Link>
{/* HTTPS certificates */}
<Link href="/certs">
<a className="list-group-item list-group-item-action d-flex align-items-start">
<div className="ms-2 me-auto">
<div className="fw-bold">
HTTPS Certificates
<span className="fw-normal">
{' '}- Generated certs for your domains
</span>
</div>
</div>
<div className="badge bg-primary rounded-pill">
{user.numCerts}
</div>
</a>
</Link>
{/* Map links */}
{mapLinks}
@ -146,7 +163,7 @@ const Account = (props) => {
innerData = (
<>
{Array(9).fill(loadingSection)}
{Array(10).fill(loadingSection)}
</>
);

@ -0,0 +1,136 @@
import React, { useState, useEffect } from 'react';
import Head from 'next/head';
import BackButton from '../components/BackButton.js';
import ErrorAlert from '../components/ErrorAlert.js';
import * as API from '../api.js'
import { useRouter } from 'next/router';
export default function Certs(props) {
const router = useRouter();
const [state, dispatch] = useState(props);
const [error, setError] = useState();
useEffect(() => {
if (!state.user) {
API.getCerts(dispatch, setError, router);
}
}, [state.user, router]);
if (!state.user) {
return (
<>
Loading...
{error && <ErrorAlert error={error} />}
</>
);
}
const { user, csrf, certs } = state;
async function addCert(e) {
e.preventDefault();
await API.addCert({
_csrf: csrf,
subject: e.target.subject.value,
altnames: e.target.altnames.value.split(',').map(x => x.trim())
}, dispatch, setError, router);
await API.getCerts(dispatch, setError, router);
}
async function deleteCert(e) {
e.preventDefault();
await API.deleteCert({ _csrf: csrf, subject: e.target.subject.value }, dispatch, setError, router);
await API.getCerts(dispatch, setError, router);
}
const certList = certs.map((d, i) => {
//TODO: refactor, to component
return (
<tr key={i} className="align-middle">
<td className="col-1 text-center">
<form onSubmit={deleteCert} action="/forms/cert/delete" method="post">
<input type="hidden" name="_csrf" value={csrf} />
<input type="hidden" name="subject" value={d.subject || d._id} />
<input className="btn btn-danger" type="submit" value="×" />
</form>
</td>
<td>
{d.subject || '-'}
</td>
<td>
{d.altnames && d.altnames.join(', ') || '-'}
</td>
<td>
{d.date || '-'}
</td>
<td>
{d.storageName || '-'}
</td>
</tr>
);
})
return (
<>
<Head>
<title>Certificates</title>
</Head>
{error && <ErrorAlert error={error} />}
<h5 className="fw-bold">
HTTPS Certificates:
</h5>
{/* Certs table */}
<div className="table-responsive">
<table className="table table-bordered text-nowrap">
<tbody>
<tr className="align-middle">
<th className="col-1" />
<th>
Subject
</th>
<th>
Altname(s)
</th>
<th>
Creation Date
</th>
<th>
Storage Name
</th>
</tr>
{certList}
{/* Add new cert form */}
<tr className="align-middle">
<td className="col-1 text-center" colSpan="3">
<form className="d-flex" onSubmit={addCert} action="/forms/cert/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="subject" placeholder="domain.com" required />
<input className="form-control me-3" type="text" name="altnames" placeholder="www.domain.com,staging.domain.com,etc..." 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 } }
}

@ -60,13 +60,13 @@ export default function Clusters(props) {
return (
<>
<Head>
<title>Servers</title>
<title>Clusters</title>
</Head>
{error && <ErrorAlert error={error} />}
<h5 className="fw-bold">
Servers ({user.clusters.length}):
Clusters ({user.clusters.length}):
</h5>
{/* Clusters table */}

@ -54,9 +54,6 @@ export default function Domains(props) {
<td>
{d}
</td>
<td title={user.certsMap[d] ? user.certsMap[d].date : null}>
{user.certsMap[d] ? '🔐'+user.certsMap[d].storageName : '-'}
</td>
</tr>
);
})
@ -71,7 +68,7 @@ export default function Domains(props) {
{error && <ErrorAlert error={error} />}
<h5 className="fw-bold">
Available Domains:
Your Domains:
</h5>
{/* Domains table */}
@ -84,9 +81,6 @@ export default function Domains(props) {
<th>
Domain
</th>
<th>
HTTPS?
</th>
</tr>
{domainList}

@ -4,7 +4,7 @@ import Link from 'next/link';
const Index = () => (
<>
<Head>
<title>Homepage</title>
<title>BasedFlare</title>
</Head>
Welcome to BasedFlare.

@ -32,32 +32,14 @@ const testRouter = (server, app) => {
const account = await db.db.collection('accounts')
.findOne({ _id: req.session.user });
if (account) {
const certs = await db.db.collection('certs')
.find({
username: account._id
}, {
projection: {
_id: 1,
username: 1,
date: 1,
storageName: 1
}
})
.toArray();
const certsMap = (certs || [])
.reduce((acc, cert) => {
acc[cert._id] = {
date: cert.date.toISOString(),
storageName: cert.storageName,
}
return acc;
}, {});
const numCerts = await db.db.collection('certs')
.countDocuments({ username: account._id });
res.locals.user = {
username: account._id,
domains: account.domains,
clusters: account.clusters,
activeCluster: account.activeCluster,
certsMap,
numCerts,
};
return next();
}
@ -131,6 +113,7 @@ const testRouter = (server, app) => {
const accountController = require('./controllers/account')
, mapsController = require('./controllers/maps')
, clustersController = require('./controllers/clusters')
, certsController = require('./controllers/certs')
, domainsController = require('./controllers/domains');
//unauthed pages
@ -160,6 +143,9 @@ const testRouter = (server, app) => {
server.get('/domains', useSession, fetchSession, checkSession, csrfMiddleware, domainsController.domainsPage.bind(null, app));
server.get('/domains.json', useSession, fetchSession, checkSession, csrfMiddleware, domainsController.domainsJson);
server.get('/certs', useSession, fetchSession, checkSession, csrfMiddleware, certsController.certsPage.bind(null, app));
server.get('/certs.json', useSession, fetchSession, checkSession, csrfMiddleware, certsController.certsJson);
//authed pages that useHaproxy
const clusterRouter = express.Router({ caseSensitive: true });
clusterRouter.post('/global/toggle', useSession, fetchSession, checkSession, useHaproxy, hasCluster, csrfMiddleware, accountController.globalToggle);
@ -170,6 +156,8 @@ const testRouter = (server, app) => {
clusterRouter.post('/cluster/delete', useSession, fetchSession, checkSession, hasCluster, csrfMiddleware, clustersController.deleteClusters);
clusterRouter.post('/domain/add', useSession, fetchSession, checkSession, useHaproxy, hasCluster, csrfMiddleware, domainsController.addDomain);
clusterRouter.post('/domain/delete', useSession, fetchSession, checkSession, hasCluster, csrfMiddleware, domainsController.deleteDomain);
clusterRouter.post('/cert/add', useSession, fetchSession, checkSession, useHaproxy, hasCluster, csrfMiddleware, certsController.addCert);
clusterRouter.post('/cert/delete', useSession, fetchSession, checkSession, hasCluster, csrfMiddleware, certsController.deleteCert);
server.use('/forms', clusterRouter);
};

@ -3,8 +3,8 @@ const url = require('url');
const fMap = {
[process.env.HOSTS_MAP_NAME]: {
fname: 'Active Domains',
description: 'Domains that will be processed by the selected cluster',
fname: 'Backends',
description: 'Backend IP mappings for domains',
columnNames: ['Domain', 'Backend'],
},
@ -15,13 +15,13 @@ const fMap = {
},
[process.env.BLOCKED_MAP_NAME]: {
fname: 'Blocked IPs/Subnets',
fname: 'IP Blacklist',
description: 'IPs/subnets that are outright blocked',
columnNames: ['IP/Subnet', ''],
},
[process.env.WHITELIST_MAP_NAME]: {
fname: 'Whitelisted IPs/Subnets',
fname: 'IP Whitelist',
description: 'IPs/subnets that bypass protection rules',
columnNames: ['IP/Subnet', ''],
},
@ -32,11 +32,11 @@ const fMap = {
columnNames: ['Domain', ''],
},
[process.env.BACKENDS_MAP_NAME]: {
fname: 'Domain Backend Mappings',
description: 'Which internal server haproxy uses for domains',
columnNames: ['Domain', 'Server Name'],
},
// [process.env.BACKENDS_MAP_NAME]: {
// fname: 'Domain Backend Mappings',
// description: 'Which internal server haproxy uses for domains',
// columnNames: ['Domain', 'Server Name'],
// },
};

Loading…
Cancel
Save