Next.js 13, new bootstrap, ESM everything

update-next
Thomas Lynch 5 months ago
parent b15783fe6a
commit 516d2df2e1
  1. 35
      .eslintrc.json
  2. 78
      acme.js
  3. 11
      agent.js
  4. 3
      api.js
  5. 4
      autorenew/main.js
  6. 79
      ca.js
  7. 8
      components/BackButton.js
  8. 12
      components/ClusterRow.js
  9. 4
      components/ErrorAlert.js
  10. 38
      components/Layout.js
  11. 19
      components/LoadingPlaceholder.js
  12. 20
      components/MapLink.js
  13. 18
      components/MapRow.js
  14. 201
      components/MenuLinks.js
  15. 40
      components/RecordSetRow.js
  16. 10
      components/SearchFilter.js
  17. 76
      controllers/account.js
  18. 60
      controllers/certs.js
  19. 28
      controllers/clusters.js
  20. 56
      controllers/dns.js
  21. 46
      controllers/domains.js
  22. 54
      controllers/maps.js
  23. 20
      db.js
  24. 0
      ecosystem.config.cjs
  25. 2
      healthcheck/worker.js
  26. 2
      loki/main.js
  27. 4
      next.config.js
  28. 7304
      package-lock.json
  29. 28
      package.json
  30. 159
      pages/_app.js
  31. 24
      pages/_document.js
  32. 124
      pages/account.js
  33. 165
      pages/certs.js
  34. 42
      pages/changepassword.js
  35. 38
      pages/clusters.js
  36. 50
      pages/csr.js
  37. 390
      pages/dns/[domain]/[zone]/[type].js
  38. 40
      pages/dns/[domain]/index.js
  39. 144
      pages/domains.js
  40. 149
      pages/global.css
  41. 33
      pages/index.js
  42. 46
      pages/login.js
  43. 159
      pages/map/[name].js
  44. 2
      pages/menu.js
  45. 162
      pages/onboarding.js
  46. 50
      pages/register.js
  47. 2
      pages/tos.js
  48. 123
      redis.js
  49. 6
      redlock.js
  50. 27
      reset.js
  51. 786
      router.js
  52. 34
      server.js
  53. 3
      specification_openapiv3.js
  54. 48
      templates.js
  55. 54
      update.js
  56. 172
      util.js

@ -1,3 +1,36 @@
{
"extends": "next/core-web-vitals"
"extends": "next/core-web-vitals",
"rules": {
"brace-style": [
"error",
"1tbs",
{ "allowSingleLine": true }
],
"indent": [
"error",
"tab",
{ "SwitchCase": 1, "ignoreComments": true, "MemberExpression": 1 }
],
"linebreak-style": [
"error",
"unix"
],
"jsx-quotes": [
"error",
"prefer-single"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
],
"curly": ["error"],
"no-multiple-empty-lines": ["error", { "max": 1 }],
"no-template-curly-in-string": [
"error"
]
}
}

@ -1,12 +1,12 @@
'use strict';
const fs = require('fs').promises;
const acme = require('acme-client');
const dev = process.env.NODE_ENV !== 'production';
const redis = require('./redis.js');
const redlock = require('./redlock.js');
const psl = require('psl');
import fs from 'fs';
import acme from 'acme-client';
import * as redis from './redis.js';
import * as redlock from './redlock.js';
import psl from 'psl';
const dev = process.env.NODE_ENV !== 'production';
/**
* Function used to satisfy an ACME challenge
@ -35,7 +35,7 @@ async function challengeCreateFn(authz, challenge, keyAuthorization) {
else if (challenge.type === 'dns-01') {
const parsed = psl.parse(authz.identifier.value);
const domain = parsed.domain;
let subdomain = `_acme-challenge`;
let subdomain = '_acme-challenge';
if (parsed.subdomain && parsed.subdomain.length > 0) {
subdomain += `.${parsed.subdomain}`;
}
@ -59,7 +59,6 @@ async function challengeCreateFn(authz, challenge, keyAuthorization) {
}
}
/**
* Function used to remove an ACME challenge response
*
@ -83,7 +82,7 @@ async function challengeRemoveFn(authz, challenge, keyAuthorization) {
else if (challenge.type === 'dns-01') {
const parsed = psl.parse(authz.identifier.value);
const domain = parsed.domain;
let subdomain = `_acme-challenge`;
let subdomain = '_acme-challenge';
if (parsed.subdomain && parsed.subdomain.length > 0) {
subdomain += `.${parsed.subdomain}`;
}
@ -110,39 +109,32 @@ async function challengeRemoveFn(authz, challenge, keyAuthorization) {
}
}
let _client;
export async function init() {
_client = new acme.Client({
directoryUrl: dev ? acme.directory.letsencrypt.staging : acme.directory.letsencrypt.production,
accountKey: await acme.crypto.createPrivateKey()
});
}
module.exports = {
client: null,
init: async function() {
/* Init client */
module.exports.client = new acme.Client({
directoryUrl: dev ? acme.directory.letsencrypt.staging : acme.directory.letsencrypt.production,
accountKey: await acme.crypto.createPrivateKey()
});
},
generate: async function(domain, altnames, email, challengePriority=['http-01', 'dns-01']) {
/* Create CSR */
const [key, csr] = await acme.crypto.createCsr({
commonName: domain,
altNames: altnames,
});
/* Certificate */
const cert = await module.exports.client.auto({
csr,
email,
termsOfServiceAgreed: true,
skipChallengeVerification: true,
challengeCreateFn,
challengeRemoveFn,
challengePriority,
});
/* Done */
const haproxyCert = `${cert.toString()}\n${key.toString()}`;
return { key, csr, cert, haproxyCert, date: new Date() };
},
};
export async function generate(domain, altnames, email, challengePriority=['http-01', 'dns-01']) {
/* Create CSR */
const [key, csr] = await acme.crypto.createCsr({
commonName: domain,
altNames: altnames,
});
/* Certificate */
const cert = await _client.auto({
csr,
email,
termsOfServiceAgreed: true,
skipChallengeVerification: true,
challengeCreateFn,
challengeRemoveFn,
challengePriority,
});
/* Done */
const haproxyCert = `${cert.toString()}\n${key.toString()}`;
return { key, csr, cert, haproxyCert, date: new Date() };
}

@ -1,6 +1,6 @@
'use strict';
const https = require('https')
import https from 'https';
const agentOptions = {
rejectUnauthorized: !process.env.ALLOW_SELF_SIGNED_SSL,
@ -8,17 +8,20 @@ const agentOptions = {
if (process.env.PINNED_FP) {
// console.log('Pinned fingerprint:', process.env.PINNED_FP);
agentOptions.checkServerIdentity = (host, cert) => {
agentOptions.checkServerIdentity = (_host, cert) => {
//TODO: host verification? e.g. tls.checkServerIdentity(host, cert);
// console.log('Checking:', cert.fingerprint256);
if (process.env.PINNED_FP !== cert.fingerprint256) {
return new Error('Certificate not pinned');
}
}
};
}
if (process.env.CUSTOM_CA_PATH) {
// console.log('Private CA file path:', process.env.CUSTOM_CA_PATH);
agentOptions.ca = require('fs').readFileSync(process.env.CUSTOM_CA_PATH);
}
module.exports = new https.Agent(agentOptions);
const agent = new https.Agent(agentOptions);
export default agent;

@ -91,13 +91,12 @@ export async function globalToggle(body, dispatch, errorCallback, router) {
return ApiCall('/forms/global/toggle', 'POST', body, dispatch, errorCallback, router, 0.5);
}
function buildOptions(route, method, body) {
// Convert method uppercase
method = method.toUpperCase();
const options = {
redirect: "manual",
redirect: 'manual',
method,
headers: {
'Content-Type': 'application/json',

@ -22,7 +22,7 @@ async function main() {
}
function getCertsOlderThan(days=60) {
return db.db.collection('certs')
return db.db().collection('certs')
.find({
// _id: '*.zeroddos.net',
date: {
@ -74,7 +74,7 @@ async function updateCert(dbCert) {
//may be null due to "already exists", so we keep existing props
update = { ...update, description, file, storageName };
}
await db.db.collection('certs')
await db.db().collection('certs')
.updateOne({
'_id': subject,
}, {

79
ca.js

@ -1,36 +1,37 @@
'use strict';
const { generateKeyPairSync } = require('crypto')
, { wildcardAllowed } = require('./util.js')
, fs = require('fs')
, forge = require('node-forge')
, pki = forge.pki
, CAAttrs = [
// {
// name: "commonName",
// value: "cp.basedflare.com",
// },
{
name: "countryName",
value: "XX",
},
{
shortName: "ST",
value: "BASEDFLARE",
},
{
name: "localityName",
value: "BASEDFLARE",
},
{
name: "organizationName",
value: "BASEDFLARE",
},
{
shortName: "OU",
value: "BASEDFLARE",
},
];
import { generateKeyPairSync } from 'node:crypto';
import { wildcardAllowed } from './util.js';
import fs from 'node:fs';
import forge from 'node-forge';
const pki = forge.pki;
const CAAttrs = [
// {
// name: "commonName",
// value: "cp.basedflare.com",
// },
{
name: 'countryName',
value: 'XX',
},
{
shortName: 'ST',
value: 'BASEDFLARE',
},
{
name: 'localityName',
value: 'BASEDFLARE',
},
{
name: 'organizationName',
value: 'BASEDFLARE',
},
{
shortName: 'OU',
value: 'BASEDFLARE',
},
];
let RootCAPrivateKey = null
, RootCAPublicKey = null
@ -66,11 +67,11 @@ function generateCertificate(privateKey, publicKey) {
cert.setIssuer(CAAttrs);
cert.setExtensions([
{
name: "basicConstraints",
name: 'basicConstraints',
cA: true,
},
{
name: "keyUsage",
name: 'keyUsage',
keyCertSign: true,
digitalSignature: true,
nonRepudiation: true,
@ -82,7 +83,7 @@ function generateCertificate(privateKey, publicKey) {
return pki.certificateToPem(cert);
}
function verifyCSR(csrPem, allowedDomains, serialNumber) {
export function verifyCSR(csrPem, allowedDomains, serialNumber) {
const csr = pki.certificationRequestFromPem(csrPem);
const subject = csr.subject.getField('CN').value;
const isWildcard = subject.startsWith('*.');
@ -124,11 +125,11 @@ function verifyCSR(csrPem, allowedDomains, serialNumber) {
cert.setIssuer(caCert.subject.attributes); //CA issuer
const certExtensions = [
{
name: "basicConstraints",
name: 'basicConstraints',
cA: false,
},
{
name: "keyUsage",
name: 'keyUsage',
digitalSignature: true,
nonRepudiation: true,
keyEncipherment: true,
@ -170,9 +171,3 @@ if (!RootCAPrivateKey || !RootCAPublicKey || !RootCACertificate) {
RootCACertificate = pki.certificateFromPem(CACert);
fs.writeFileSync('./ca/ca-cert.pem', CACert, { encoding: 'utf-8' });
}
module.exports = {
// generateCAKeyPair,
// generateCertificate,
verifyCSR,
};

@ -2,11 +2,9 @@ import Link from 'next/link';
export default function BackButton({ to }) {
return (
<Link href={to}>
<a className="btn btn-sm btn-primary mt-3 ms-1 ps-2">
<i className="bi-chevron-left pe-2" width="16" height="16" />
<Link href={to} className='btn btn-sm btn-primary mt-3 ms-1 ps-2'>
<i className='bi-chevron-left pe-2' width='16' height='16' />
Back
</a>
</Link>
)
);
}

@ -3,14 +3,14 @@ export default function ClusterRow({ i, cluster, setCluster, deleteCluster, csrf
const splitCluster = cluster.split(',');
return (
<tr className="align-middle">
<td className="text-left" style={{width:0}}>
<a className="btn btn-sm btn-danger" onClick={() => deleteCluster(csrf, key)}>
<i className="bi-trash-fill pe-none" width="16" height="16" />
<tr className='align-middle'>
<td className='text-left' style={{width:0}}>
<a className='btn btn-sm btn-danger' onClick={() => deleteCluster(csrf, key)}>
<i className='bi-trash-fill pe-none' width='16' height='16' />
</a>
</td>
<td className="col-1 text-center">
<input onSubmit={() => setCluster(csrf, i)} className="btn btn-sm btn-primary" type="button" value="Select" disabled={(i === user.activeCluster ? 'disabled' : null)} />
<td className='col-1 text-center'>
<input onSubmit={() => setCluster(csrf, i)} className='btn btn-sm btn-primary' type='button' value='Select' disabled={(i === user.activeCluster ? 'disabled' : null)} />
</td>
<td>
({splitCluster.length}): {splitCluster.map(c => new URL(c).hostname).join(', ')}

@ -1,7 +1,7 @@
export default function ErrorAlert({ error }) {
return error && (
<div className="alert alert-danger" role="alert">
<div className='alert alert-danger' role='alert'>
{error}
</div>
)
);
}

@ -9,37 +9,35 @@ export default withRouter(function Layout({ children, router }) {
<>
<Head>
<meta charSet="utf-8"/>
<meta name="viewport" content="width=device-width initial-scale=1"/>
<link rel="shortcut icon" href="/favicon.ico" />
<meta charSet='utf-8'/>
<meta name='viewport' content='width=device-width initial-scale=1'/>
<link rel='shortcut icon' href='/favicon.ico' />
</Head>
<div className="row h-100 p-0 m-0">
<div className='row h-100 p-0 m-0'>
{showMenu && <div className="col-auto sidebar h-100 m-0 px-0">
<div className="d-flex flex-column flex-shrink-0 p-3 h-100 overflow-auto" style={{ width: '250px' }}>
{showMenu && <div className='col-auto sidebar h-100 m-0 px-0'>
<div className='d-flex flex-column flex-shrink-0 p-3 h-100 overflow-auto' style={{ width: '250px' }}>
<MenuLinks />
</div>
</div>}
<div className="col-1 flex-fill m-0 px-0 h-100 overflow-auto">
<div className="p-3 h-100 d-flex flex-column">
<span className="corner-ribbon">Beta</span>
<main className="mx-auto col col-12 col-xl-8">
{showMenu && <Link href="/menu">
<a className="btn btn-sm btn-primary mobile-btn mb-4 d-inline-block">
<i className="bi-list pe-none me-2" width="16" height="16" />
<div className='col-1 flex-fill m-0 px-0 h-100 overflow-auto'>
<div className='p-3 h-100 d-flex flex-column'>
<span className='corner-ribbon'>Beta</span>
<main className='mx-auto col col-12 col-xl-8'>
{showMenu && <Link href='/menu' className='btn btn-sm btn-primary mobile-btn mb-4 d-inline-block'>
<i className='bi-list pe-none me-2' width='16' height='16' />
Menu
</a>
</Link>}
{children}
</main>
<footer className="mt-auto text-center text-muted small">
<footer className='mt-auto text-center text-muted small'>
<hr />
<a className="pb-3 fs-xs" href="https://gitgud.io/fatchan/haproxy-panel-next/">source code</a>
{" "}&bull;{" "}
<a className="pb-3 fs-xs" target="_blank" rel="noreferrer" href="https://basedstatus.online">status page</a>
<a className='pb-3 fs-xs' href='https://gitgud.io/fatchan/haproxy-panel-next/'>source code</a>
{' '}&bull;{' '}
<a className='pb-3 fs-xs' target='_blank' rel='noreferrer' href='https://basedstatus.online'>status page</a>
</footer>
</div>
@ -47,5 +45,5 @@ export default withRouter(function Layout({ children, router }) {
</div>
</>
)
})
);
});

@ -1,19 +0,0 @@
import React from "react"
import ContentLoader from "react-content-loader"
const LoadingPlaceholder = (props) => (
<ContentLoader
speed={2}
width={"100%"}
height={30}
viewBox="0 0 100% 10"
backgroundColor="#2e2e2e"
foregroundColor="#5a5a5a"
{...props}
>
<rect x="0" y="10" rx="5" width="100%" height="10" />
</ContentLoader>
);
export default LoadingPlaceholder;

@ -2,20 +2,18 @@ import Link from 'next/link';
export default function MapLink({ map }) {
return (
<Link href={`/map/[name]?name=${map.name}`} as={`/map/${map.name}`}>
<a className="list-group-item list-group-item-action d-flex align-items-start">
<span className="ms-2 me-auto">
<span className="fw-bold">
{map.fname}
<span className="fw-normal">
{' '}- {map.description}
</span>
<Link href={`/map/[name]?name=${map.name}`} as={`/map/${map.name}`} className='list-group-item list-group-item-action d-flex align-items-start'>
<span className='ms-2 me-auto'>
<span className='fw-bold'>
{map.fname}
<span className='fw-normal'>
{' '}- {map.description}
</span>
</span>
{/*<span className="badge bg-primary rounded-pill">
</span>
{/*<span className="badge bg-primary rounded-pill">
{map.count}
</span>*/}
</a>
</Link>
)
);
}

@ -3,10 +3,10 @@ export default function MapRow({ row, onDeleteSubmit, name, csrf, showValues, ma
const { _id, key, value } = row;
return (
<tr className="align-middle">
<td className="text-left">
<a className="btn btn-sm btn-danger" onClick={() => onDeleteSubmit(csrf, key)}>
<i className="bi-trash-fill pe-none" width="16" height="16" />
<tr className='align-middle'>
<td className='text-left'>
<a className='btn btn-sm btn-danger' onClick={() => onDeleteSubmit(csrf, key)}>
<i className='bi-trash-fill pe-none' width='16' height='16' />
</a>
</td>
<td>
@ -21,16 +21,16 @@ export default function MapRow({ row, onDeleteSubmit, name, csrf, showValues, ma
let displayValue = value[ck] && (mapValueNames[value[ck].toString()] || value[ck].toString());
if (typeof value[ck] === 'boolean') {
displayValue = value[ck] === true
? <span className="text-success">
<i className="bi-check-lg pe-none me-2" width="16" height="16" />
? <span className='text-success'>
<i className='bi-check-lg pe-none me-2' width='16' height='16' />
</span>
: <span className="text-secondary">
<i className="bi-dash-lg pe-none me-2" width="16" height="16" />
: <span className='text-secondary'>
<i className='bi-dash-lg pe-none me-2' width='16' height='16' />
</span>;
}
return <td key={`mvi_${mvi}`}>
{displayValue}
</td>
</td>;
})}
</tr>
);

@ -1,202 +1,119 @@
import Image from 'next/image';
// TODO: Remove once https://github.com/vercel/next.js/issues/52216 is resolved.
// next/image` seems to be affected by a default + named export bundling bug.
let ResolvedImage = Image;
if ('default' in ResolvedImage) {
ResolvedImage = ResolvedImage.default;
}
import Link from 'next/link';
import { withRouter } from 'next/router';
export default withRouter(function MenuLinks({ router }) {
return (<>
<Link href="/">
<a className="d-flex align-items-center mb-3 mb-md-0 text-body text-decoration-none">
<Image src="/favicon.ico" width="32" height="32" alt=" " />
<span className="mx-2 fs-4 text-decoration-none">BasedFlare</span>
</a>
<Link href='/' className='d-flex align-items-center mb-3 mb-md-0 text-body text-decoration-none'>
<ResolvedImage src='/favicon.ico' width='32' height='32' alt=' ' />
<span className='mx-2 fs-4 text-decoration-none'>BasedFlare</span>
</Link>
<hr />
<ul className="nav nav-pills flex-column mb-auto">
{/*<li className="nav-item">
<Link href="/">
<a className={router.pathname === "/" ? "nav-link active" : "nav-link"} aria-current="page">
<i className="bi-house-door pe-none me-2" width="16" height="16" />
Home
</a>
</Link>
</li>*/}
<li className="nav-item">
<Link href="/account">
<a className={router.pathname === "/account" ? "nav-link active" : "nav-link text-body"} aria-current="page">
<i className="bi-person-square pe-none me-2" width="16" height="16" />
<ul className='nav nav-pills flex-column mb-auto'>
<li className='nav-item'>
<Link href='/account' className={router.pathname === '/account' ? 'nav-link active' : 'nav-link text-body'} aria-current='page'>
<i className='bi-person-square pe-none me-2' width='16' height='16' />
Account
</a>
</Link>
</li>
<li className="nav-item">
<Link href="/domains">
<a className={router.pathname === "/domains" ? "nav-link active" : "nav-link text-body"} aria-current="page">
<i className="bi-layers pe-none me-2" width="16" height="16" />
<li className='nav-item'>
<Link href='/domains' className={router.pathname === '/domains' ? 'nav-link active' : 'nav-link text-body'} aria-current='page'>
<i className='bi-layers pe-none me-2' width='16' height='16' />
Domains
</a>
</Link>
</li>
{/*<li className="nav-item">
<Link href="/dns">
<a className={router.pathname === "/dns" ? "nav-link active" : "nav-link text-body"} aria-current="page">
<i className="bi-card-list pe-none me-2" width="16" height="16" />
DNS
</a>
</Link>
</li>*/}
{/*<li className="nav-item">
<Link href="/clusters">
<a className={router.pathname === "/clusters" ? "nav-link active" : "nav-link text-body"} aria-current="page">
<i className="bi-clouds pe-none me-2" width="16" height="16" />
Clusters
</a>
</Link>
</li>*/}
{/*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 text-body"} aria-current="page">
<i className="bi-hdd-network pe-none me-2" width="16" height="16" />
<li className='nav-item'>
<Link href='/map/hosts' className={router.pathname === '/map/[name]' && router.query.name === 'hosts' ? 'nav-link active' : 'nav-link text-body'} 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 text-body"} aria-current="page">
<i className="bi-file-earmark-lock pe-none me-2" width="16" height="16" />
<li className='nav-item'>
<Link href='/certs' className={router.pathname === '/certs' ? 'nav-link active' : 'nav-link text-body'} 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="/csr">
<a className={router.pathname === "/csr" ? "nav-link active" : "nav-link text-body"} aria-current="page">
<i className="bi-building-lock pe-none me-2" width="16" height="16" />
<li className='nav-item'>
<Link href='/csr' className={router.pathname === '/csr' ? 'nav-link active' : 'nav-link text-body'} aria-current='page'>
<i className='bi-building-lock pe-none me-2' width='16' height='16' />
Origin CSR
</a>
</Link>
</li>
<li className="nav-item">
<Link href="/map/ddos_config">
<a className={router.pathname === "/map/[name]" && router.query.name === "ddos_config" ? "nav-link active" : "nav-link text-body"} aria-current="page">
<i className="bi-sliders2 pe-none me-2" width="16" height="16" />
<li className='nav-item'>
<Link href='/map/ddos_config' className={router.pathname === '/map/[name]' && router.query.name === 'ddos_config' ? 'nav-link active' : 'nav-link text-body'} aria-current='page'>
<i className='bi-sliders2 pe-none me-2' width='16' height='16' />
Protection Settings
</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 text-body"} aria-current="page">
<i className="bi-shield-check pe-none me-2" width="16" height="16" />
<li className='nav-item'>
<Link href='/map/ddos' className={router.pathname === '/map/[name]' && router.query.name === 'ddos' ? 'nav-link active' : 'nav-link text-body'} aria-current='page'>
<i className='bi-shield-check pe-none me-2' width='16' height='16' />
Protection Rules
</a>
</Link>
</li>
<li className="nav-item">
<Link href="/map/rewrite">
<a className={router.pathname === "/map/[name]" && router.query.name === "rewrite" ? "nav-link active" : "nav-link text-body"} aria-current="page">
<i className="bi-pencil pe-none me-2" width="16" height="16" />
<li className='nav-item'>
<Link href='/map/rewrite' className={router.pathname === '/map/[name]' && router.query.name === 'rewrite' ? 'nav-link active' : 'nav-link text-body'} aria-current='page'>
<i className='bi-pencil pe-none me-2' width='16' height='16' />
Rewrites
</a>
</Link>
</li>
<li className="nav-item">
<Link href="/map/redirect">
<a className={router.pathname === "/map/[name]" && router.query.name === "redirect" ? "nav-link active" : "nav-link text-body"} aria-current="page">
<i className="bi-signpost pe-none me-2" width="16" height="16" />
<li className='nav-item'>
<Link href='/map/redirect' className={router.pathname === '/map/[name]' && router.query.name === 'redirect' ? 'nav-link active' : 'nav-link text-body'} aria-current='page'>
<i className='bi-signpost pe-none me-2' width='16' height='16' />
Redirects
</a>
</Link>
</li>
<li className="nav-item">
<Link href="/map/whitelist">
<a className={router.pathname === "/map/[name]" && router.query.name === "whitelist" ? "nav-link active" : "nav-link text-body"} aria-current="page">
<i className="bi-person-check pe-none me-2" width="16" height="16" />
<li className='nav-item'>
<Link href='/map/whitelist' className={router.pathname === '/map/[name]' && router.query.name === 'whitelist' ? 'nav-link active' : 'nav-link text-body'} aria-current='page'>
<i className='bi-person-check pe-none me-2' width='16' height='16' />
IP Whitelist
</a>
</Link>
</li>
<li className="nav-item">
<Link href="/map/blockedip">
<a className={router.pathname === "/map/[name]" && router.query.name === "blockedip" ? "nav-link active" : "nav-link text-body"} aria-current="page">
<i className="bi-person-slash pe-none me-2" width="16" height="16" />
<li className='nav-item'>
<Link href='/map/blockedip' className={router.pathname === '/map/[name]' && router.query.name === 'blockedip' ? 'nav-link active' : 'nav-link text-body'} aria-current='page'>
<i className='bi-person-slash pe-none me-2' width='16' height='16' />
IP Blacklist
</a>
</Link>
</li>
<li className="nav-item">
<Link href="/map/blockedasn">
<a className={router.pathname === "/map/[name]" && router.query.name === "blockedasn" ? "nav-link active" : "nav-link text-body"} aria-current="page">
<i className="bi-building-slash pe-none me-2" width="16" height="16" />
<li className='nav-item'>
<Link href='/map/blockedasn' className={router.pathname === '/map/[name]' && router.query.name === 'blockedasn' ? 'nav-link active' : 'nav-link text-body'} aria-current='page'>
<i className='bi-building-slash pe-none me-2' width='16' height='16' />
ASN Blacklist
</a>
</Link>
</li>
<li className="nav-item">
<Link href="/map/maintenance">
<a className={router.pathname === "/map/[name]" && router.query.name === "maintenance" ? "nav-link active" : "nav-link text-body"} aria-current="page">
<i className="bi-info-square pe-none me-2" width="16" height="16" />
<li className='nav-item'>
<Link href='/map/maintenance' className={router.pathname === '/map/[name]' && router.query.name === 'maintenance' ? 'nav-link active' : 'nav-link text-body'} aria-current='page'>
<i className='bi-info-square pe-none me-2' width='16' height='16' />
Maintenance Mode
</a>
</Link>
</li>
{/*<li className="nav-item">
<Link href="/stats">
<a className={router.pathname === "/stats" ? "nav-link active" : "nav-link text-body"} aria-current="page">
<i className="bi-table pe-none me-2" width="16" height="16" />
Statistics
</a>
</Link>
</li>*/}
</ul>
<hr />
<ul className="nav nav-pills flex-column">
<li className="nav-item user-select-none">
<Link href="/onboarding">
<a className={router.pathname === "/onboarding" ? "nav-link active" : "nav-link text-body"} aria-current="page">
<i className="bi-rocket-takeoff pe-none me-2" width="16" height="16" />
<ul className='nav nav-pills flex-column'>
<li className='nav-item user-select-none'>
<Link href='/onboarding' className={router.pathname === '/onboarding' ? 'nav-link active' : 'nav-link text-body'} aria-current='page'>
<i className='bi-rocket-takeoff pe-none me-2' width='16' height='16' />
Onboarding
</a>
</Link>
</li>
</ul>
{/*<hr />
<ul className="nav nav-pills flex-column">
<li className="nav-item">
<Link href="/login">
<a className={router.pathname === "/login" ? "nav-link active" : "nav-link text-body"} aria-current="page">
<i className="bi-door-closed pe-none me-2" width="16" height="16" />
Login
</a>
</Link>
</li>
<li className="nav-item">
<Link href="/register">
<a className={router.pathname === "/register" ? "nav-link active" : "nav-link text-body"} aria-current="page">
<i className="bi-person-plus pe-none me-2" width="16" height="16" />
Register
</a>
</Link>
</li>
</ul>*/}
<hr />
<ul className="nav nav-pills flex-column">
<li className="nav-item">
<form action="/forms/logout" method="POST">
<button className="nav-link text-body" type="submit">
<i className="bi-door-open pe-none me-2" width="16" height="16" />
<ul className='nav nav-pills flex-column'>
<li className='nav-item'>
<form action='/forms/logout' method='POST'>
<button className='nav-link text-body' type='submit'>
<i className='bi-door-open pe-none me-2' width='16' height='16' />
Logout
</button>
</form>
</li>
</ul>
</>);
})
});

@ -2,7 +2,7 @@ import Link from 'next/link';
import * as API from '../api.js';
export default function RecordSetRow({ dispatch, setError, router, domain, name, recordSet, csrf }) {
const type = recordSet[0]
const type = recordSet[0];
const recordSetArray = Array.isArray(recordSet[1]) ? recordSet[1] : [recordSet[1]];
async function deleteDnsRecord(e) {
e.preventDefault();
@ -12,37 +12,35 @@ export default function RecordSetRow({ dispatch, setError, router, domain, name,
const recordSetContent = recordSetArray.map((r, i) => {
const healthClass = r.h != null
? (r.u === true
? "text-success"
? 'text-success'
: (r.fb
? "text-warning"
: "text-danger"))
: "";
? 'text-warning'
: 'text-danger'))
: '';
//todo: make fbrecord correctly calculate multiple fallbacks, 3 mode, etc
const fbRecord = healthClass === "text-warning"
const fbRecord = healthClass === 'text-warning'
&& r.sel === 1
&& recordSetArray.find(fbr => fbr.id === r.fb[0])
&& recordSetArray.find(fbr => fbr.id === r.fb[0]);
return (<div key={i}>
<strong>{r.id ? r.id+': ' : ''}</strong>
<span className={healthClass}>{r.ip || r.host || r.value || r.ns || r.text || r.target}</span>
{fbRecord && <>{' -> '}<span className="text-success">{fbRecord.ip}</span></>}
{fbRecord && <>{' -> '}<span className='text-success'>{fbRecord.ip}</span></>}
{r.geok ? `${r.geok === 'cn' ? ' Continents' : ' Countries'}: ` : ''}{(r.geov||[]).join(', ')}
</div>)
</div>);
});
return (
<tr className="align-middle">
<tr className='align-middle'>
<td>
<span className="d-inline-block">
<form onSubmit={deleteDnsRecord} action={`/forms/dns/${domain}/${name}/${type}/delete`} method="post">
<input type="hidden" name="_csrf" value={csrf} />
<button className="btn btn-sm btn-danger" type="submit">
<i className="bi-trash-fill pe-none" width="16" height="16" />
<span className='d-inline-block'>
<form onSubmit={deleteDnsRecord} action={`/forms/dns/${domain}/${name}/${type}/delete`} method='post'>
<input type='hidden' name='_csrf' value={csrf} />
<button className='btn btn-sm btn-danger' type='submit'>
<i className='bi-trash-fill pe-none' width='16' height='16' />
</button>
</form>
</span>
{recordSetArray[0].l !== true && <Link href={`/dns/${domain}/${name}/${type}`}>
<a className="btn btn-sm btn-primary ms-2">
<i className="bi-pencil-fill pe-none" width="16" height="16" />
</a>
{recordSetArray[0].l !== true && <Link href={`/dns/${domain}/${name}/${type}`} className='btn btn-sm btn-primary ms-2'>
<i className='bi-pencil-fill pe-none' width='16' height='16' />
</Link>}
</td>
<td>
@ -60,8 +58,8 @@ export default function RecordSetRow({ dispatch, setError, router, domain, name,
{recordSetArray && recordSetArray.length > 0 ? recordSetArray[0].ttl : '-'}
</td>
<td>
{recordSetArray[0].t && <div className="text-warning">Template</div>}
{recordSetArray[0].l && <div className="text-danger">Locked</div>}
{recordSetArray[0].t && <div className='text-warning'>Template</div>}
{recordSetArray[0].l && <div className='text-danger'>Locked</div>}
</td>
</tr>
);

@ -1,13 +1,13 @@
export default function SearchFilter({ filter, setFilter }) {
return (
<div className="input-group mb-3">
<div className="input-group-prepend">
<span className="input-group-text" style={{ borderRadius: '5px 0 0 5px' }}>
<i className="bi bi-search" />
<div className='input-group mb-3'>
<div className='input-group-prepend'>
<span className='input-group-text' style={{ borderRadius: '5px 0 0 5px' }}>
<i className='bi bi-search' />
</span>
</div>
<input onChange={e => setFilter(e.target.value||'')} type="text" className="form-control" placeholder="Search" />
<input onChange={e => setFilter(e.target.value||'')} type='text' className='form-control' placeholder='Search' />
</div>
);

@ -1,14 +1,15 @@
const bcrypt = require('bcrypt');
const db = require('../db.js');
const { extractMap, dynamicResponse } = require('../util.js');
const { Resolver } = require('node:dns').promises;
import bcrypt from 'bcrypt';
import * as db from '../db.js';
import { extractMap, dynamicResponse } from '../util.js';
import { Resolver } from 'node:dns/promises';
const resolver = new Resolver();
resolver.setServers(process.env.NAMESERVERS.split(','));
/**
* account page data shared between html/json routes
*/
exports.accountData = async (req, res, _next) => {
export async function accountData(req, res, _next) {
let maps = []
, globalAcl
, txtRecords = [];
@ -37,8 +38,8 @@ exports.accountData = async (req, res, _next) => {
* GET /account
* account page html
*/
exports.accountPage = async (app, req, res, next) => {
const data = await exports.accountData(req, res, next);
export async function accountPage(app, req, res, next) {
const data = await accountData(req, res, next);
return app.render(req, res, '/account', { ...data, user: res.locals.user });
}
@ -46,8 +47,8 @@ exports.accountPage = async (app, req, res, next) => {
* GET /onboarding
* account page html
*/
exports.onboardingPage = async (app, req, res, next) => {
const data = await exports.accountData(req, res, next);
export async function onboardingPage(app, req, res, next) {
const data = await accountData(req, res, next);
return app.render(req, res, '/onboarding', { ...data, user: res.locals.user });
}
@ -55,27 +56,8 @@ exports.onboardingPage = async (app, req, res, next) => {
* 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 });
}
/**
* GET /stats
* stats page html
*/
exports.statsPage = async (app, req, res, next) => {
const data = await exports.statsData(req, res, next);
return app.render(req, res, '/stats', { ...data, user: res.locals.user });
}
/**
* GET /stats.json
* stats json
*/
exports.statsJson = async (req, res, next) => {
const data = await exports.statsData(req, res, next);
export async function accountJson(req, res, next) {
const data = await accountData(req, res, next);
return res.json({ ...data, user: res.locals.user });
}
@ -83,14 +65,14 @@ exports.statsJson = async (req, res, next) => {
* POST /forms/global/toggle
* toggle global ACL
*/
exports.globalToggle = async (req, res, next) => {
if (res.locals.user.username !== "admin") {
export async function globalToggle(req, res, next) {
if (res.locals.user.username !== 'admin') {
return dynamicResponse(req, res, 403, { error: 'Global ACL can only be toggled by an administrator' });
}
try {
const globalAcl = await res.locals
.dataPlaneRetry('getOneRuntimeMap', 'ddos_global')
.then(res => res.data.description.split('').reverse()[0])
.then(res => res.data.description.split('').reverse()[0]);
if (globalAcl === '1') {
await res.locals
.dataPlaneAll('deleteRuntimeMapEntry', {
@ -110,16 +92,16 @@ exports.globalToggle = async (req, res, next) => {
return next(e);
}
return dynamicResponse(req, res, 302, { redirect: '/account' });
};
}
/**
* POST /forms/login
* login
*/
exports.login = async (req, res) => {
export async function login(req, res) {
const username = req.body.username.toLowerCase();
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) {
return dynamicResponse(req, res, 403, { error: 'Incorrect username or password' });
}
@ -129,15 +111,15 @@ exports.login = async (req, res) => {
return dynamicResponse(req, res, 302, { redirect: '/account' });
}
return dynamicResponse(req, res, 403, { error: 'Incorrect username or password' });
};
}
/**
* POST /forms/register
* regiser
*/
exports.register = async (req, res) => {
export async function register(req, res) {
if (!res.locals.user || res.locals.user.username !== "admin") {
if (!res.locals.user || res.locals.user.username !== 'admin') {
return dynamicResponse(req, res, 400, { error: 'Registration is currently disabled, please try again later.' });
}
@ -145,9 +127,9 @@ exports.register = async (req, res) => {
const password = req.body.password;
const rPassword = req.body.repeat_password;
if (!username || typeof username !== "string" || username.length === 0 || !/^[a-zA-Z0-9]+$/.test(username)
|| !password || typeof password !== "string" || password.length === 0
|| !rPassword || typeof rPassword !== "string" || rPassword.length === 0) {
if (!username || typeof username !== 'string' || username.length === 0 || !/^[a-zA-Z0-9]+$/.test(username)
|| !password || typeof password !== 'string' || password.length === 0
|| !rPassword || typeof rPassword !== 'string' || rPassword.length === 0) {
//todo: length limits, make jschan input validator LGPL lib and use here
return dynamicResponse(req, res, 400, { error: 'Invalid inputs' });
}
@ -156,14 +138,14 @@ exports.register = async (req, res) => {
return dynamicResponse(req, res, 400, { error: 'Passwords did not match' });
}
const existingAccount = await db.db.collection('accounts').findOne({ _id: username });
const existingAccount = await db.db().collection('accounts').findOne({ _id: username });
if (existingAccount) {
return dynamicResponse(req, res, 409, { error: 'Account already exists with this username' });
}
const passwordHash = await bcrypt.hash(req.body.password, 12);
await db.db.collection('accounts')
await db.db().collection('accounts')
.insertOne({
_id: username,
displayName: req.body.username,
@ -182,7 +164,7 @@ exports.register = async (req, res) => {
* POST /forms/logout
* logout
*/
exports.logout = (req, res) => {
export function logout(req, res) {
req.session.destroy();
return dynamicResponse(req, res, 302, { redirect: '/login' });
};
@ -191,11 +173,11 @@ exports.logout = (req, res) => {
* POST /forms/onboarding
* finish/skip onboarding
*/
exports.finishOnboarding = async (req, res) => {
export async function finishOnboarding(req, res) {
if (!res.locals.user) {
return dynamicResponse(req, res, 400, { error: 'Bad request' });
}
await db.db.collection('accounts')
await db.db().collection('accounts')
.updateOne({
_id: res.locals.user.username
}, {

@ -1,15 +1,15 @@
const db = require('../db.js');
const acme = require('../acme.js');
const url = require('url');
const { dynamicResponse, wildcardAllowed, filterCertsByDomain } = require('../util.js');
const { verifyCSR } = require('../ca.js');
import * as db from '../db.js';
import * as acme from '../acme.js';
import url from 'node:url';
import { dynamicResponse, wildcardAllowed, filterCertsByDomain } from '../util.js';
import { verifyCSR } from '../ca.js';
/**
* GET /certs
* certs page
*/
exports.certsPage = async (app, req, res) => {
const dbCerts = await db.db.collection('certs')
export async function certsPage(app, req, res) {
const dbCerts = await db.db().collection('certs')
.find({
username: res.locals.user.username,
}, {
@ -21,15 +21,15 @@ exports.certsPage = async (app, req, res) => {
storageName: 1,
}
})
.toArray()
.toArray();
dbCerts.forEach(c => c.date = c.date.toISOString());
const clusterCerts = await res.locals
.dataPlaneRetry('getAllStorageSSLCertificates')
.then(certs => filterCertsByDomain(certs.data, res.locals.user.domains));
return app.render(req, res, '/certs', {
csrf: req.csrfToken(),
dbCerts,
clusterCerts,
dbCerts: dbCerts || [],
clusterCerts: clusterCerts || [],
});
};
@ -37,8 +37,8 @@ exports.certsPage = async (app, req, res) => {
* GET /certs.json
* certs json data
*/
exports.certsJson = async (req, res) => {
const dbCerts = await db.db.collection('certs')
export async function certsJson(req, res) {
const dbCerts = await db.db().collection('certs')
.find({
username: res.locals.user.username,
}, {
@ -50,7 +50,7 @@ exports.certsJson = async (req, res) => {
storageName: 1,
}
})
.toArray()
.toArray();
dbCerts.forEach(c => c.date = c.date.toISOString());
const clusterCerts = await res.locals
.dataPlaneRetry('getAllStorageSSLCertificates')
@ -58,8 +58,8 @@ exports.certsJson = async (req, res) => {
return res.json({
csrf: req.csrfToken(),
user: res.locals.user,
dbCerts,
clusterCerts,
dbCerts: dbCerts || [],
clusterCerts: clusterCerts || [],