Next.js 13, new bootstrap, ESM everything

dev
Thomas Lynch 10 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 || [],
});
};
@ -67,7 +67,7 @@ exports.certsJson = async (req, res) => {
* POST /cert/add
* add cert
*/
exports.addCert = async (req, res, next) => {
export async function addCert(req, res, next) {
if (!req.body.subject || typeof req.body.subject !== 'string' || req.body.subject.length === 0) {
return dynamicResponse(req, res, 400, { error: 'Missing subject' });
}
@ -83,7 +83,7 @@ exports.addCert = async (req, res, next) => {
}
if (!req.body.altnames.every(altName => {
return res.locals.user.domains.includes(altName)
|| (altName.startsWith('*.') && wildcardAllowed(req.body.subject, res.locals.user.domains))
|| (altName.startsWith('*.') && wildcardAllowed(req.body.subject, res.locals.user.domains));
})) {
return dynamicResponse(req, res, 400, { error: `You don't have permission to generate a certificate with altname(s) ${req.body.altnames}` });
}
@ -97,7 +97,7 @@ exports.addCert = async (req, res, next) => {
const altnames = req.body.altnames.map(a => a.toLowerCase());
const email = req.body.email;
const existingCert = await db.db.collection('certs').findOne({ _id: subject });
const existingCert = await db.db().collection('certs').findOne({ _id: subject });
if (existingCert) {
return dynamicResponse(req, res, 400, { error: 'Cert with this subject already exists' });
}
@ -137,12 +137,12 @@ exports.addCert = async (req, res, next) => {
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')
}
await db.db().collection('certs')
.updateOne({
_id: subject,
}, {
@ -163,7 +163,7 @@ exports.addCert = async (req, res, next) => {
* POST /cert/upload
* push existing db cert to cluster
*/
exports.uploadCert = async (req, res, next) => {
export async function uploadCert(req, res, next) {
if (!req.body.domain || typeof req.body.domain !== 'string' || req.body.domain.length === 0
|| !res.locals.user.domains.includes(req.body.domain)) {
@ -172,7 +172,7 @@ exports.uploadCert = async (req, res, next) => {
const domain = req.body.domain.toLowerCase();
const existingCert = await db.db.collection('certs').findOne({ _id: domain, username: res.locals.user.username });
const existingCert = await db.db().collection('certs').findOne({ _id: domain, username: res.locals.user.username });
if (!existingCert || !existingCert.haproxyCert) {
return dynamicResponse(req, res, 400, { error: 'Invalid input' });
}
@ -206,11 +206,11 @@ exports.uploadCert = async (req, res, next) => {
* POST /cert/delete
* delete cers
*/
exports.deleteCert = async (req, res) => {
export async function deleteCert(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' });
}
@ -238,7 +238,7 @@ exports.deleteCert = async (req, res) => {
}
//otherwise completely delete from db
await db.db.collection('certs')
await db.db().collection('certs')
.deleteOne({ _id: subject, username: res.locals.user.username });
return dynamicResponse(req, res, 302, { redirect: '/certs' });
@ -249,12 +249,12 @@ exports.deleteCert = async (req, res) => {
* POST /csr/verify
* Delete the map entries of the body 'domain'
*/
exports.verifyUserCSR = async (req, res, next) => {
export async function verifyUserCSR(req, res, next) {
if(!req.body || !req.body.csr || typeof req.body.csr !== 'string' || req.body.csr.length === 0) {
return dynamicResponse(req, res, 400, { error: 'Invalid CSR' });
}
try {
const serial = await db.db.collection('certs')
const serial = await db.db().collection('certs')
.findOneAndUpdate({
_id: 'serial',
}, {
@ -264,7 +264,7 @@ exports.verifyUserCSR = async (req, res, next) => {
}, {
upsert: true,
});
await db.db.collection('accounts')
await db.db().collection('accounts')
.updateOne({
_id: res.locals.user.username
}, {
@ -273,7 +273,7 @@ exports.verifyUserCSR = async (req, res, next) => {
}
});
const serialNumber = serial && serial.value && serial.value.number || 1;
console.log('Attempting to sign CSR, serial', serialNumber)
console.log('Attempting to sign CSR, serial', serialNumber);
const signedCert = verifyCSR(req.body.csr, res.locals.user.domains, serialNumber);
if (req.body.json) {
return res.json({

@ -1,25 +1,25 @@
const db = require('../db.js');
const { validClustersString, makeArrayIfSingle, extractMap, dynamicResponse } = require('../util.js');
import * as db from '../db.js';
import { validClustersString, makeArrayIfSingle, extractMap, dynamicResponse } from '../util.js';
exports.clustersPage = async (app, req, res, next) => {
export async function clustersPage(app, req, res, next) {
return app.render(req, res, '/clusters', {
csrf: req.csrfToken(),
});
};
exports.clustersJson = async (req, res, next) => {
export async function clustersJson(req, res, next) {
return res.json({
csrf: req.csrfToken(),
user: res.locals.user,
});
}
};
/**
* POST /cluster
* set active cluster
*/
exports.setCluster = async (req, res, next) => {
if (res.locals.user.username !== "admin") {
export async function setCluster(req, res, next) {
if (res.locals.user.username !== 'admin') {
return dynamicResponse(req, res, 403, { error: 'Changing cluster is only supported on enterprise plans' });
}
if (req.body == null || req.body.cluster == null) {
@ -31,7 +31,7 @@ exports.setCluster = async (req, res, next) => {
return dynamicResponse(req, res, 404, { error: 'Invalid cluster' });
}
try {
await db.db.collection('accounts')
await db.db().collection('accounts')
.updateOne({_id: res.locals.user.username}, {$set: {activeCluster: req.body.cluster }});
} catch (e) {
return next(e);
@ -43,8 +43,8 @@ exports.setCluster = async (req, res, next) => {
* POST /cluster/add
* add cluster
*/
exports.addCluster = async (req, res, next) => {
if (res.locals.user.username !== "admin") {
export async function addCluster(req, res, next) {
if (res.locals.user.username !== 'admin') {
return dynamicResponse(req, res, 403, { error: 'Adding clusters is only supported on enterprise plans' });
}
if (!req.body || !req.body.cluster
@ -53,7 +53,7 @@ exports.addCluster = async (req, res, next) => {
return dynamicResponse(req, res, 400, { error: 'Invalid cluster' });
}
try {
await db.db.collection('accounts')
await db.db().collection('accounts')
.updateOne({_id: res.locals.user.username}, {$addToSet: {clusters: req.body.cluster }});
} catch (e) {
return next(e);
@ -65,8 +65,8 @@ exports.addCluster = async (req, res, next) => {
* POST /cluster/delete
* delete cluster
*/
exports.deleteClusters = async (req, res, next) => {
if (res.locals.user.username !== "admin") {
export async function deleteClusters(req, res, next) {
if (res.locals.user.username !== 'admin') {
return dynamicResponse(req, res, 403, { error: 'Removing clusters is only supported on enterprise plans' });
}
//TODO: warning modal and extra "confirm" param before deleting cluster
@ -85,7 +85,7 @@ exports.deleteClusters = async (req, res, next) => {
newActiveCluster = 0;
}
try {
await db.db.collection('accounts')
await db.db().collection('accounts')
.updateOne({_id: res.locals.user.username}, {$set: {clusters: filteredClusters, activeCluster: newActiveCluster }});
} catch (e) {
return next(e);

@ -1,15 +1,14 @@
const db = require('../db.js');
const redis = require('../redis.js');
const url = require('url');
const { isIPv4, isIPv6 } = require('net');
const { dynamicResponse } = require('../util.js');
const { nsTemplate, soaTemplate, aTemplate, aaaaTemplate } = require('../templates.js');
import * as db from '../db.js';
import * as redis from '../redis.js';
import { isIPv4, isIPv6 } from 'node:net';
import { dynamicResponse } from '../util.js';
import { nsTemplate, soaTemplate, aTemplate, aaaaTemplate } from '../templates.js';
/**
* GET /dns/:domain
* domains records page
*/
exports.dnsDomainPage = async (app, req, res) => {
export async function dnsDomainPage(app, req, res) {
if (!res.locals.user.domains.includes(req.params.domain)) {
return dynamicResponse(req, res, 302, { redirect: '/domains' });
}
@ -28,7 +27,7 @@ exports.dnsDomainPage = async (app, req, res) => {
* GET /dns/:domain/:zone/:type
* record set page
*/
exports.dnsRecordPage = async (app, req, res) => {
export async function dnsRecordPage(app, req, res) {
if (!res.locals.user.domains.includes(req.params.domain)) {
return dynamicResponse(req, res, 302, { redirect: '/domains' });
}
@ -41,7 +40,7 @@ exports.dnsRecordPage = async (app, req, res) => {
recordSet = recordSetRaw[req.params.type];
recordSet = Array.isArray(recordSet) ? recordSet : (recordSet ? [recordSet] : []);
}
return app.render(req, res, `/dns/${req.params.domain}/${req.params.zone||"name"}/${req.params.type||"a"}`, {
return app.render(req, res, `/dns/${req.params.domain}/${req.params.zone||'name'}/${req.params.type||'a'}`, {
csrf: req.csrfToken(),
recordSet,
});
@ -51,7 +50,7 @@ exports.dnsRecordPage = async (app, req, res) => {
* GET /dns/:domain.json
* domain record json
*/
exports.dnsDomainJson = async (req, res) => {
export async function dnsDomainJson(req, res) {
if (!res.locals.user.domains.includes(req.params.domain)) {
return dynamicResponse(req, res, 403, { error: 'No permission for this domain' });
}
@ -71,7 +70,7 @@ exports.dnsDomainJson = async (req, res) => {
* GET /dns/:domain/:zone/:type.json
* record set json
*/
exports.dnsRecordJson = async (req, res) => {
export async function dnsRecordJson(req, res) {
if (!res.locals.user.domains.includes(req.params.domain)) {
return dynamicResponse(req, res, 403, { error: 'No permission for this domain' });
}
@ -91,12 +90,11 @@ exports.dnsRecordJson = async (req, res) => {
});
};
/**
* POST /post/:domain/:zone/:type/delete
* delete record
*/
exports.dnsRecordDelete = async (req, res) => {
export async function dnsRecordDelete(req, res) {
if (!res.locals.user.domains.includes(req.params.domain)) {
return dynamicResponse(req, res, 302, { redirect: '/domains' });
}
@ -119,7 +117,7 @@ exports.dnsRecordDelete = async (req, res) => {
* POST /post/:domain/:zone/:type
* add/update record
*/
exports.dnsRecordUpdate = async (req, res) => {
export async function dnsRecordUpdate(req, res) {
if (!res.locals.user.domains.includes(req.params.domain)) {
return dynamicResponse(req, res, 403, { error: 'No permission for this domain' });
}
@ -153,7 +151,7 @@ exports.dnsRecordUpdate = async (req, res) => {
type = 'aaaa';
break;
default: {
for (let i = 0; i < (type == "soa" ? 1 : 100); i++) {
for (let i = 0; i < (type == 'soa' ? 1 : 100); i++) {
let {
[`value_${i}`]: value,
//geo
@ -180,9 +178,9 @@ exports.dnsRecordUpdate = async (req, res) => {
} = req.body;
if (!value) { break; }
try {
if ((geok && !["cn", "cc"].includes(geok))
|| (sel && !["0", "1", "2", "3"].includes(sel))
|| (bsel && !["0", "1", "2", "3", "4", "5", "6"].includes(bsel))
if ((geok && !['cn', 'cc'].includes(geok))
|| (sel && !['0', '1', '2', '3'].includes(sel))
|| (bsel && !['0', '1', '2', '3', '4', '5', '6'].includes(bsel))
|| (flag && (isNaN(flag) || parseInt(flag) !== +flag))
|| (ttl && (isNaN(ttl) || parseInt(ttl) !== +ttl))
|| (preference && (isNaN(preference) || parseInt(preference) !== +preference))
@ -216,35 +214,35 @@ exports.dnsRecordUpdate = async (req, res) => {
}
let record;
switch(type) {
case "a":
case 'a':
if (!isIPv4(value)) {
return dynamicResponse(req, res, 400, { error: 'Invalid input' });
}
record = { ttl, id, ip: value, geok, geov, h, sel, bsel, fb, u: true };
break;
case "aaaa":
case 'aaaa':
if (!isIPv6(value)) {
return dynamicResponse(req, res, 400, { error: 'Invalid input' });
}
record = { ttl, id, ip: value, geok, geov, h, sel, bsel, fb, u: true };
break;
case "txt":
case 'txt':
record = { ttl, text: value };
break;
case "cname":
case "ns":
case 'cname':
case 'ns':
record = { ttl, host: value };
break;
case "mx":
case 'mx':
record = { ttl, host: value, preference };
break;
case "srv":
case 'srv':
record = { ttl, target: value, port, weight, priority };
break;
case "caa":
case 'caa':
record = { ttl, value, flag, tag };
break;
case "soa":
case 'soa':
record = { ttl, ns: value, MBox, refresh, retry, expire, minttl: 180 };
break;
default:
@ -262,9 +260,9 @@ exports.dnsRecordUpdate = async (req, res) => {
recordSetRaw = {};
} else if (recordSetRaw[type] && recordSetRaw[type].l === true
|| (Array.isArray(recordSetRaw[type]) && recordSetRaw[type][0].l === true)) {
return dynamicResponse(req, res, 400, { error: "You can't edit or overwrite locked records" });
return dynamicResponse(req, res, 400, { error: 'You can\'t edit or overwrite locked records' });
}
if (type == "soa") {
if (type == 'soa') {
template = template || (recordSetRaw[type] && recordSetRaw[type]['t'] === true);
recordSetRaw[type] = records[0];
recordSetRaw[type]['t'] = template;

@ -1,17 +1,17 @@
const db = require('../db.js');
const acme = require('../acme.js');
const url = require('url');
const { dynamicResponse } = require('../util.js');
const redis = require('../redis.js');
const psl = require('psl');
const { nsTemplate, soaTemplate } = require('../templates.js');
import * as db from '../db.js';
import * as acme from '../acme.js';
import url from 'node:url';
import { dynamicResponse } from '../util.js';
import * as redis from '../redis.js';
import psl from 'psl';
import { nsTemplate, soaTemplate } from '../templates.js';
/**
* GET /domains
* domains page
*/
exports.domainsPage = async (app, req, res) => {
const certs = await db.db.collection('certs')
export async function domainsPage(app, req, res) {
const certs = await db.db().collection('certs')
.find({
username: res.locals.user.username,
}, {
@ -24,10 +24,10 @@ exports.domainsPage = async (app, req, res) => {
}
})
.toArray();
certs.forEach(c => c.date = c.date.toISOString())
certs.forEach(c => c.date = c.date.toISOString());
return app.render(req, res, '/domains', {
csrf: req.csrfToken(),
certs,
certs: certs || [],
});
};
@ -35,8 +35,8 @@ exports.domainsPage = async (app, req, res) => {
* GET /domains.json
* domains json data
*/
exports.domainsJson = async (req, res) => {
const certs = await db.db.collection('certs')
export async function domainsJson(req, res) {
const certs = await db.db().collection('certs')
.find({
username: res.locals.user.username,
}, {
@ -49,11 +49,11 @@ exports.domainsJson = async (req, res) => {
}
})
.toArray();
certs.forEach(c => c.date = c.date.toISOString())
certs.forEach(c => c.date = c.date.toISOString());
return res.json({
csrf: req.csrfToken(),
user: res.locals.user,
certs,
certs: certs || [],
});
};
@ -61,13 +61,13 @@ exports.domainsJson = async (req, res) => {
* POST /domain/add
* add domain
*/
exports.addDomain = async (req, res, next) => {
export async function addDomain(req, res, next) {
if (!req.body.domain || typeof req.body.domain !== 'string' || req.body.domain.length === 0) {
return dynamicResponse(req, res, 400, { error: 'Invalid input' });
}
if (res.locals.user.username !== "admin" && res.locals.user.domains && res.locals.user.domains.length >= 20) {
if (res.locals.user.username !== 'admin' && res.locals.user.domains && res.locals.user.domains.length >= 20) {
return dynamicResponse(req, res, 403, { error: 'Domain limit reached' });
}
@ -87,13 +87,13 @@ exports.addDomain = async (req, res, next) => {
try {
const parsed = psl.parse(domain);
if (!parsed || !parsed.domain) {
return dynamicResponse(req, res, 400, { error: 'Invalid input' })
return dynamicResponse(req, res, 400, { error: 'Invalid input' });
}
if (parsed.domain !== domain && !res.locals.user.domains.includes(parsed.domain)) {
return dynamicResponse(req, res, 403, { error: 'Add the root domain before adding subdomains' });
}
const domains = [domain, parsed.domain];
const existing = await db.db.collection('accounts')
const existing = await db.db().collection('accounts')
.findOne({
'$or': [
{ domains: domain },
@ -103,7 +103,7 @@ exports.addDomain = async (req, res, next) => {
if (existing) {
return dynamicResponse(req, res, 400, { error: 'This domain is already in use or belongs to another user' });
}
await db.db.collection('accounts')
await db.db().collection('accounts')
.updateOne({
_id: res.locals.user.username
}, {
@ -152,7 +152,7 @@ exports.addDomain = async (req, res, next) => {
* POST /domain/delete
* delete domain
*/
exports.deleteDomain = async (req, res) => {
export async function deleteDomain(req, res) {
if (!req.body.domain || typeof req.body.domain !== 'string' || req.body.domain.length === 0
|| !res.locals.user.domains.includes(req.body.domain)) {
@ -177,10 +177,10 @@ exports.deleteDomain = async (req, res) => {
]);
if (existingHost || existingMaintenance || existingRewrite || existingDdos) {
return dynamicResponse(req, res, 400, { error: "Cannot remove domain while still in use. Remove it from backends/maintenance/rewrites/protection first." });
return dynamicResponse(req, res, 400, { error: 'Cannot remove domain while still in use. Remove it from backends/maintenance/rewrites/protection first.' });
}
await db.db.collection('accounts')
await db.db().collection('accounts')
.updateOne({_id: res.locals.user.username}, {$pull: {domains: domain }});
await res.locals
.dataPlaneAll('deleteRuntimeMapEntry', {

@ -1,12 +1,12 @@
const { extractMap, dynamicResponse } = require('../util.js');
const { createCIDR, parse } = require('ip6addr');
const url = require('url');
import { extractMap, dynamicResponse } from '../util.js';
import { createCIDR, parse } from 'ip6addr';
import url from 'url';
/**
* GET /maps/:name
* Show map filtering to users domains
*/
exports.mapData = async (req, res, next) => {
export async function mapData(req, res, next) {
let map,
mapInfo,
showValues = false;
@ -24,6 +24,7 @@ exports.mapData = async (req, res, next) => {
})
.then(res => res.data);
} catch (e) {
console.error(e);
return next(e);
}
@ -61,7 +62,7 @@ exports.mapData = async (req, res, next) => {
.map(x => {
x.value = res.locals.user.username;
return x;
})
});
break;
default:
return dynamicResponse(req, res, 400, { error: 'Invalid map' });
@ -75,23 +76,23 @@ exports.mapData = async (req, res, next) => {
name: req.params.name,
showValues,
};
};
}
exports.mapPage = async (app, req, res, next) => {
const data = await exports.mapData(req, res, next);
export async function mapPage(app, req, res, next) {
const data = await mapData(req, res, next);
return app.render(req, res, `/map/${data.name}`, data);
};
}
exports.mapJson = async (req, res, next) => {
const data = await exports.mapData(req, res, next);
export async function mapJson(req, res, next) {
const data = await mapData(req, res, next);
return res.json({ ...data, user: res.locals.user });
};
}
/**
* POST /maps/:name/delete
* Delete the map entries of the body 'domain'
*/
exports.deleteMapForm = async (req, res, next) => {
export async function deleteMapForm(req, res, next) {
if(!req.body || !req.body.key || typeof req.body.key !== 'string' || req.body.key.length === 0) {
return dynamicResponse(req, res, 400, { error: 'Invalid value' });
}
@ -101,7 +102,7 @@ exports.deleteMapForm = async (req, res, next) => {
|| req.params.name === process.env.WHITELIST_MAP_NAME) {
let value;
const existingEntry = await res.locals
.dataPlaneRetry("getRuntimeMapEntry", {
.dataPlaneRetry('getRuntimeMapEntry', {
map: req.params.name,
id: req.body.key,
})
@ -183,14 +184,13 @@ exports.deleteMapForm = async (req, res, next) => {
}
}
return dynamicResponse(req, res, 302, { redirect: `/map/${req.params.name}` });
};
}
/**
* POST /maps/:name/add
* Add map entries of the body 'domain'
*/
exports.patchMapForm = async (req, res, next) => {
export async function patchMapForm(req, res, next) {
if(req.body && req.body.key && typeof req.body.key === 'string') {
//validate key is domain
@ -267,7 +267,7 @@ exports.patchMapForm = async (req, res, next) => {
if (process.env.CUSTOM_BACKENDS_ENABLED && req.params.name === process.env.HOSTS_MAP_NAME) {
let parsedValue;
try {
parsedValue = url.parse(`https://${req.body.value}`)
parsedValue = url.parse(`https://${req.body.value}`);
if (!parsedValue.host || !parsedValue.port) {
return dynamicResponse(req, res, 400, { error: 'Invalid input' });
}
@ -295,7 +295,7 @@ exports.patchMapForm = async (req, res, next) => {
case process.env.BLOCKED_ASN_MAP_NAME:
case process.env.WHITELIST_MAP_NAME: {
const existingEntry = await res.locals
.dataPlaneRetry("getRuntimeMapEntry", {
.dataPlaneRetry('getRuntimeMapEntry', {
map: req.params.name,
id: req.body.key,
})
@ -323,7 +323,7 @@ exports.patchMapForm = async (req, res, next) => {
case process.env.DDOS_CONFIG_MAP_NAME:
value = JSON.stringify({
pd: parseInt(req.body.pd || 24),
pt: req.body.pt === "argon2" ? "argon2" : "sha256",
pt: req.body.pt === 'argon2' ? 'argon2' : 'sha256',
cex: parseInt(req.body.cex || 21600),
cip: req.body.cip ? true : false,
});
@ -406,12 +406,12 @@ exports.patchMapForm = async (req, res, next) => {
const existingEntry = req.params.name === process.env.HOSTS_MAP_NAME
? null
: (await res.locals
.dataPlaneRetry('getRuntimeMapEntry', {
map: req.params.name,
id: req.body.key,
})
.then(res => res.data)
.catch(() => {}));
.dataPlaneRetry('getRuntimeMapEntry', {
map: req.params.name,
id: req.body.key,
})
.then(res => res.data)
.catch(() => {}));
if (existingEntry) {
await res.locals
.dataPlaneAll('replaceRuntimeMapEntry', {
@ -435,4 +435,4 @@ exports.patchMapForm = async (req, res, next) => {
}
}
return dynamicResponse(req, res, 400, { error: 'Invalid value' });
};
}

20
db.js

@ -1,12 +1,16 @@
const { MongoClient } = require('mongodb');
import { MongoClient } from 'mongodb';
module.exports = {
let _client;
connect: async () => {
const client = new MongoClient(process.env.DB_URL);
await client.connect();
module.exports.client = client;
module.exports.db = client.db();
}
export async function connect() {
_client = new MongoClient(process.env.DB_URL);
await _client.connect();
}
export function client() {
return _client;
}
export function db() {
return _client && _client.db();
}

@ -132,7 +132,7 @@ async function handleJob(job, done) { //job.id, job.data
async function updateDowned() {
try {
downedIps = await db.db.collection('down')
downedIps = await db.db().collection('down')
.findOne({
_id: 'down',
})

@ -36,7 +36,7 @@ const deleteLokiLabel = (value, label="hh") => fetch(`${process.env.LOKI_HOST}lo
async function loop() {
try {
const lokiDomains = await getLokiDomains();
let userDomains = await db.db.collection('accounts')
let userDomains = await db.db().collection('accounts')
.find({}, { projection: { domains: 1 } })
.toArray();
userDomains = userDomains.reduce((acc, account) => {

@ -1,3 +1,3 @@
module.exports = {
export default {
/* config options here */
}
};

7304
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -3,13 +3,14 @@
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"dev": "node server.js",
"build": "next build",
"lint": "next lint",
"next": "next build",
"start": "node_modules/gulp/bin/gulp.js; NODE_ENV=production node server.js",
"start-dev": "node_modules/gulp/bin/gulp.js; NODE_ENV=development node server.js"
"start": "NODE_ENV=production node server.js",
"start-dev": "NODE_ENV=development node server.js"
},
"keywords": [],
"author": "Thomas Lynch (fatchan) <tom@69420.me>",
@ -18,31 +19,30 @@
"@fontsource/inter": "^5.0.8",
"@influxdata/influxdb-client": "^1.33.2",
"acme-client": "^5.0.0",
"bcrypt": "^5.1.0",
"bcrypt": "^5.1.1",
"body-parser": "^1.20.2",
"bootstrap": "^5.2.3",
"bootstrap-icons": "^1.10.3",
"bull": "^4.10.4",
"bootstrap": "^5.3.2",
"bootstrap-icons": "^1.11.1",
"bull": "^4.11.3",
"connect-mongo": "^4.6.0",
"cookie-parser": "^1.4.6",
"csurf": "^1.11.0",
"dotenv": "^16.0.3",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-session": "^1.17.3",
"fs-extra": "^10.1.0",
"gulp": "^4.0.2",
"i18n-iso-countries": "^7.6.0",
"i18n-iso-countries": "^7.7.0",
"ioredis": "^5.3.2",
"ip6addr": "^0.2.5",
"next": "^12.3.4",
"node-fetch": "^2.6.9",
"next": "^13.5.2",
"node-fetch": "^2.7.0",
"nprogress": "^0.2.0",
"openapi-client-axios": "^7.1.1",
"openapi-client-axios": "^7.3.3",
"psl": "^1.9.0",
"react": "^18.2.0",
"react-content-loader": "^6.2.0",
"react-content-loader": "^6.2.1",
"react-dom": "^18.2.0",
"react-select": "^5.7.3",
"react-select": "^5.7.5",
"redlock": "^5.0.0-beta.2"
},
"devDependencies": {

@ -2,163 +2,20 @@ import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap-icons/font/bootstrap-icons.css';
import Layout from '../components/Layout.js';
import 'nprogress/nprogress.css';
import NProgress from "nprogress";
import Router from "next/router";
import "@fontsource/inter";
import NProgress from 'nprogress';
import Router from 'next/router';
import '@fontsource/inter';
import './global.css';
const loadRoutes = ['/login', '/register', '/changepassword', '/']
const loadRoutes = ['/login', '/register', '/changepassword', '/'];
NProgress.configure({ showSpinner: false });
Router.events.on("routeChangeStart", (url) => loadRoutes.includes(url) && NProgress.start());
Router.events.on("routeChangeComplete", (url) => loadRoutes.includes(url) && NProgress.done());
Router.events.on("routeChangeError", (url) => NProgress.done());
Router.events.on('routeChangeStart', (url) => loadRoutes.includes(url) && NProgress.start());
Router.events.on('routeChangeComplete', (url) => loadRoutes.includes(url) && NProgress.done());
Router.events.on('routeChangeError', (url) => NProgress.done());
export default function App({ Component, pageProps }) {
return (
<Layout>
<style>
{`
:root { font-family: 'Inter',arial,Helvetica,sans-serif; }
@supports (font-variation-settings: normal) {
:root { font-family: 'Inter var',arial,Helvetica,sans-serif; }
}
html, body { font-family: 'Inter',arial,Helvetica,sans-serif; height: 100%; overflow: hidden; background: #F4F5F7; }
.sidebar { background: var(--bs-body-bg); }
.corner-ribbon {z-index:9999; width: 180px;top: 8px;left: auto;text-align: center;line-height: 30px;letter-spacing: 1px;color: white;background: darkorange;box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);text-shadow: 0 0 3px rgba(0, 0, 0, 0.5);right: -70px;transform: rotate(45deg);-webkit-transform: rotate(46deg);position: fixed;overflow: hidden;}
.green { color: green; }
.red { color: red; }
footer { margin-top: auto; }
.btn { font-weight: bold; }
.nav-item:not(:first-child) { margin-top: 10px; }
.nav-link { color: white; }
.nav-link:hover { color: #6aa6fd; }
.mobile-menu { margin: 0 -16px; }
.fs-xs { font-size: small; }
.table, .list-group { box-shadow: 0 0px 3px rgba(0,0,0,.1); max-width: 100%; min-width: 600px; background-color: var(--bs-body-bg); }
.text-decoration-none { color: var(--bs-body-color); }
.sidebar { box-shadow: 0 0px 3px rgba(0,0,0,0.2); }
.card { background: var(--bs-body-bg) !important; color: var(--bs-body-color) !important; }
.table { margin-bottom: 0; }
.table-responsive { box-shadow: 0 0px 3px rgba(0,0,0,0.2); }
a.text-success:visited, a.text-success:hover { color: rgba(var(--bs-success-rgb),var(--bs-text-opacity)) !important }
.select__control {
transition: none;
}
input:autofill, input:-webkit-autofill {
background-color: initial!important;
background-image: initial!important;
color: initial!important;
}
.select__control:hover:not(.select__control--is-focused) {
border-color: #ced4da;
}
.select__control--menu-is-open, .select__control--is-focused {
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
border-color: #86b7fe!important;
}
tr:target, tr:target textarea {
background: var(--bs-highlight-bg);
}
table.notaborder textarea {
border: none!important;
}
@media (max-width: 650px) {
.table, .list-group { min-width: unset; }
}
@media (min-width: 800px) {
.mobile-btn { display: none!important; }
}
@media (max-width: 800px) {
.sidebar { display: none; }
}
@media (prefers-color-scheme: dark) {
:root {
--bs-body-color: #fff;
--bs-body-bg: #23272a;
}
html, body { background: var(--bs-body-bg); }
.nav-pills {
--bs-nav-pills-link-active-bg: #7289da;
--bs-btn-hover-bg: #7289da;
--bs-btn-hover-border-color: #7289da;
}
.btn-primary {
--bs-btn-bg: #7289da;
--bs-btn-border-color: #7289da;
--bs-btn-hover-bg: #6481e7;
--bs-btn-hover-border-color: #6481e7;
--bs-btn-active-bg: #6481e7;
--bs-btn-active-border-color: #6481e7;
--bs-btn-disabled-bg: #7289da;
--bs-btn-disabled-border-color: #7289da;
}
.badge.bg-primary {
background-color: #7289da;
}
.text-muted, a, a:visited, a:hover, .nav-link, .nav-link:hover { color:#fff!important; }
.list-group-item { color: #fff; background-color: #2c2f33; }
input:not(.btn):not(.select__input), option, select.form-select, textarea, .input-group-text { color: #fff!important; background-color: #2c2f33!important; border: 1px solid black!important; }
.list-group-item-action:focus, .list-group-item-action:hover { color: #fff; background-color: #1F1F1F; }
.sidebar, .table { background-color: #2c2f33; }
.table { color: #fff; border-color: var(--bs-gray-900)!important; }
tr:target {
background: #ffc10720!important;
}
tr:target textarea {
background: transparent!important;
}
.select__control {
background-color: #393939;
border-color: var(--bs-gray-900);
transition: none;
}
.select__input {
color: white!important;
}
.select__control:hover:not(.select__control--is-focused) {
border-color: black;
}
.select__control--is-focused,
.select__control--menu-is-open {
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
border-color: black!important;
}
.select__multi-value {
background-color: var(--bs-dark);
border-radius: 5px;
}
.select__multi-value__label {
padding: 2px 10px;
color: #fff;
}
.select__menu {
background-color: #393939;
border: 1px solid var(--bs-gray-900);
}
.select__option {
background-color: #393939;
}
.select__option:hover {
background-color: var(--bs-dark);
}
.select__indicator-separator {
background-color: #393939;
}
.select__clear-indicator:hover {
color: #fff;
}
.select__multi-value__remove:hover {
background-color: #5D2F24;
color: #DE350B;
border-radius: 0 5px 5px 0;
}
.select__placeholder,
.select__single-value {
color: #fff;
}
}
`}
</style>
<Component {...pageProps} />
</Layout>
);

@ -1,14 +1,14 @@
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
return (
<Html>
<Head />
<body>
<Main />
<NextScript className="d-flex flex-column" />
<style>
{`
return (
<Html>
<Head />
<body>
<Main />
<NextScript className='d-flex flex-column' />
<style>
{`
html, body, #__next {
min-height: 100vh;
display: flex;
@ -16,8 +16,8 @@ export default function Document() {
overflow: hidden;
}
`}
</style>
</body>
</Html>
)
</style>
</body>
</Html>
);
}

@ -2,7 +2,6 @@ import React, { useState, useEffect, useMemo } from 'react';
import Head from 'next/head';
import Link from 'next/link';
import MapLink from '../components/MapLink.js';
import LoadingPlaceholder from '../components/LoadingPlaceholder.js';
import ErrorAlert from '../components/ErrorAlert.js';
import * as API from '../api.js';
import { useRouter } from 'next/router';
@ -17,14 +16,6 @@ export default function Account(props) {
API.getAccount(dispatch, setError, router);
}, []);
const loadingSection = useMemo(() => {
return (
<div className="list-group-item list-group-item-action d-flex align-items-start">
<LoadingPlaceholder />
</div>
);
}, []);
let innerData;
if (state && state.user && state.maps != null) {
@ -38,7 +29,7 @@ export default function Account(props) {
const isAdmin = user.username === 'admin';
// Next cluster number for > browse button
const nextCluster = user.clusters[user.activeCluster+1] ? user.activeCluster+1 : 0
const nextCluster = user.clusters[user.activeCluster+1] ? user.activeCluster+1 : 0;
// Links to each map and bubble/pill for map counts
const mapLinks = maps.map((map, i) => <MapLink key={i} map={map} />);
@ -59,15 +50,15 @@ export default function Account(props) {
<>
{/* Global overide */}
<div className="list-group-item d-flex align-items-center">
<div className="ms-2 me-auto d-flex align-items-center gap-2">
<span className="fw-bold">
<div className='list-group-item d-flex align-items-center'>
<div className='ms-2 me-auto d-flex align-items-center gap-2'>
<span className='fw-bold'>
Global Override
</span>
</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 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'}
@ -107,77 +98,55 @@ export default function Account(props) {
<input type="hidden" name="cluster" value={nextCluster}/>
<input className="btn btn-primary px-2 py-0" type="submit" value="&gt;" />
</form>
<Link href="/clusters">
<a className="btn btn-success px-2 py-0 ms-2" style={{ maxHeight: "1.6em" }}>
+
</a>
<Link href="/clusters" className="btn btn-success px-2 py-0 ms-2" style={{ maxHeight: "1.6em" }}>
+
</Link>
</span>
</div>
</div>*/}
{/* 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">
<Link href='/domains' className='list-group-item list-group-item-action d-flex align-items-start'>
<div className='ms-2 me-auto'>
<div className='fw-bold'>
Domains
<span className="fw-normal">
{' '}- Domains you have permission over
</span>
</div>
</div>
<div className="badge bg-primary rounded-pill">
{user.domains.filter(x => x.split('.').length <= 2).length}
<span className='fw-normal'>
{' '}- Domains you have permission over
</span>
</div>
</a>
</div>
<div className='badge bg-primary rounded-pill'>
{user.domains.filter(x => x.split('.').length <= 2).length}
</div>
</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">
<Link href='/certs' 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>
<span className='fw-normal'>
{' '}- Generated certs for your domains
</span>
</div>
<div className="badge bg-primary rounded-pill">
{user.numCerts}
</div>
</a>
</div>
<div className='badge bg-primary rounded-pill'>
{user.numCerts}
</div>
</Link>
{/* Origin CSR */}
<Link href="/csr">
<a className="list-group-item list-group-item-action d-flex align-items-start">
<div className="ms-2 me-auto">
<div className="fw-bold">
<Link href='/csr' className='list-group-item list-group-item-action d-flex align-items-start'>
<div className='ms-2 me-auto'>
<div className='fw-bold'>
Origin CSR
<span className="fw-normal">
{' '}- Sign CSR to get certs for your origins
</span>
</div>
<span className='fw-normal'>
{' '}- Sign CSR to get certs for your origins
</span>
</div>
</a>
</div>
</Link>
{/* Statistics
<Link href="/stats">
<a className="list-group-item list-group-item-action d-flex align-items-start">
<div className="ms-2 me-auto">
<div className="fw-bold">
Statistics
<span className="fw-normal">
{' '}- Statistics from cluster servers
</span>
</div>
</div>
</a>
</Link>*/}
{/* Map links */}
{mapLinks}
@ -187,9 +156,14 @@ export default function Account(props) {
} else {
innerData = (
<>
{Array(12).fill(loadingSection)}
</>
<div className='d-flex flex-column'>
{error && <ErrorAlert error={error} />}
<div className='text-center mb-4'>
<div className='spinner-border mt-5' role='status'>
<span className='visually-hidden'>Loading...</span>
</div>
</div>
</div>
);
}
@ -203,20 +177,20 @@ export default function Account(props) {
{error && <ErrorAlert error={error} />}
<h5 className="fw-bold">
<h5 className='fw-bold'>
Account:
</h5>
<div className="list-group col-sm-12 col-xl-8 mx-auto">
<div className='list-group col-sm-12 col-xl-8 mx-auto'>
{innerData}
{innerData}
</div>
</>
)
);
};
export async function getServerSideProps({ req, res, query, resolvedUrl, locale, locales, defaultLocale}) {
return { props: { user: res.locals.user || null, ...query } }
return { props: { user: res.locals.user || null, ...query } };
};

@ -3,8 +3,8 @@ import Head from 'next/head';
import BackButton from '../components/BackButton.js';
import ErrorAlert from '../components/ErrorAlert.js';
import SearchFilter from '../components/SearchFilter.js';
import * as API from '../api.js'
import { getApproxSubject } from '../util.js'
import * as API from '../api.js';
import { getApproxSubject } from '../util.js';
import { useRouter } from 'next/router';
export default function Certs(props) {
@ -15,7 +15,7 @@ export default function Certs(props) {
const [filter, setFilter] = useState('');
useEffect(() => {
if (!state.user) {
if (!state.user || state.dbCerts == null) {
API.getCerts(dispatch, setError, router)
.then(() => {
setTimeout(() => {
@ -23,15 +23,15 @@ export default function Certs(props) {
}, 10);
});
}
}, [state.user, router]);
}, [state.user, state.dbCerts, router]);
if (!state.user) {
if (!state.user || state.dbCerts == null) {
return (
<div className="d-flex flex-column">
<div className='d-flex flex-column'>
{error && <ErrorAlert error={error} />}
<div className="text-center mb-4">
<div className="spinner-border mt-5" role="status">
<span className="visually-hidden">Loading...</span>
<div className='text-center mb-4'>
<div className='spinner-border mt-5' role='status'>
<span className='visually-hidden'>Loading...</span>
</div>
</div>
</div>
@ -39,6 +39,7 @@ export default function Certs(props) {
}
const { user, csrf, dbCerts, clusterCerts } = state;
console.log(user, csrf, dbCerts, clusterCerts);
if (user && !user.onboarding) {
router.push('/onboarding');
@ -101,10 +102,10 @@ export default function Certs(props) {
const clusterOnlyCertList = clusterOnlyCerts.map((c, i) => {
const approxSubject = getApproxSubject(c.storage_name);
return (
<tr id={c.storage_name} key={'clusterOnlyCertList'+i} className="align-middle">
<td className="col-1 text-center">
<a className="btn btn-sm btn-danger" onClick={() => deleteCert(csrf, approxSubject, c.storage_name)}>
<i className="bi-trash-fill pe-none" width="16" height="16" />
<tr id={c.storage_name} key={'clusterOnlyCertList'+i} className='align-middle'>
<td className='col-1 text-center'>
<a className='btn btn-sm btn-danger' onClick={() => deleteCert(csrf, approxSubject, c.storage_name)}>
<i className='bi-trash-fill pe-none' width='16' height='16' />
</a>
</td>
<td>
@ -127,49 +128,49 @@ export default function Certs(props) {
.filter(d => d.subject.includes(filter) || d.altnames.some(an => an.includes(filter)))
.map((d, i) => {
//TODO: refactor, to component
let creation = new Date(d.date);
const expiry = creation.setDate(creation.getDate()+90);
const daysRemaining = (Math.floor(expiry - Date.now()) / 86400000).toFixed(1);
const inCluster = clusterCerts.some(c => c.storage_name === d.storageName);
return (
<tr id={d.storageName} key={'certList'+i} className="align-middle">
<td className="text-left" style={{width:0}}>
{inCluster
? <a className="btn btn-sm btn-danger" onClick={() => deleteCert(csrf, (d.subject || d._id), d.storageName)}>
<i className="bi-trash-fill pe-none" width="16" height="16" />
</a>
: (<>
<a className="btn btn-sm btn-warning" onClick={() => uploadCert(csrf, (d.subject || d._id))}>
<i className="bi-cloud-upload pe-none" width="16" height="16" />
</a>
<a className="btn btn-sm btn-warning ms-2" onClick={() => deleteCert(csrf, (d.subject || d._id))}>
<i className="bi-trash-fill pe-none" width="16" height="16" />
let creation = new Date(d.date);
const expiry = creation.setDate(creation.getDate()+90);
const daysRemaining = (Math.floor(expiry - Date.now()) / 86400000).toFixed(1);
const inCluster = clusterCerts.some(c => c.storage_name === d.storageName);
return (
<tr id={d.storageName} key={'certList'+i} className='align-middle'>
<td className='text-left' style={{width:0}}>
{inCluster
? <a className='btn btn-sm btn-danger' onClick={() => deleteCert(csrf, (d.subject || d._id), d.storageName)}>
<i className='bi-trash-fill pe-none' width='16' height='16' />
</a>
</>)
}
</td>
<td>
{d.subject || '-'}
</td>
<td>
<textarea
className="w-100"
style={{border:'none'}}
readOnly
cols={20}
rows={Math.min(3, d.altnames.length)}
defaultValue={d.altnames && d.altnames.join('\n') || '-'}
/>
</td>
<td suppressHydrationWarning={true}>
{expiry ? `${daysRemaining} days` : '-'}
</td>
<td>
{d.storageName || '-'}
</td>
</tr>
);
});
: (<>
<a className='btn btn-sm btn-warning' onClick={() => uploadCert(csrf, (d.subject || d._id))}>
<i className='bi-cloud-upload pe-none' width='16' height='16' />
</a>
<a className='btn btn-sm btn-warning ms-2' onClick={() => deleteCert(csrf, (d.subject || d._id))}>
<i className='bi-trash-fill pe-none' width='16' height='16' />
</a>
</>)
}
</td>
<td>
{d.subject || '-'}
</td>
<td>
<textarea
className='w-100'
style={{border:'none'}}
readOnly
cols={20}
rows={Math.min(3, d.altnames.length)}
defaultValue={d.altnames && d.altnames.join('\n') || '-'}
/>
</td>
<td suppressHydrationWarning={true}>
{expiry ? `${daysRemaining} days` : '-'}
</td>
<td>
{d.storageName || '-'}
</td>
</tr>
);
});
return (
<>
@ -178,20 +179,20 @@ export default function Certs(props) {
<title>Certificates</title>
</Head>
<h5 className="fw-bold">
<h5 className='fw-bold'>
HTTPS Certificates:
</h5>
<SearchFilter filter={filter} setFilter={setFilter} />
{/* Certs table */}
<div className="table-responsive">
<form className="d-flex" onSubmit={addCert} action="/forms/cert/add" method="post">
<input type="hidden" name="_csrf" value={csrf} />
<table className="table text-nowrap notaborder">
<div className='table-responsive'>
<form className='d-flex' onSubmit={addCert} action='/forms/cert/add' method='post'>
<input type='hidden' name='_csrf' value={csrf} />
<table className='table text-nowrap notaborder'>
<tbody>
<tr className="align-middle">
<tr className='align-middle'>
<th style={{width:0}} />
<th>
Subject
@ -210,8 +211,8 @@ export default function Certs(props) {
{certList}
{clusterOnlyCerts && clusterOnlyCerts.length > 0 && (<>
<tr className="align-middle">
<th colSpan="5">
<tr className='align-middle'>
<th colSpan='5'>
Not in local DB:
</th>
</tr>
@ -224,40 +225,40 @@ export default function Certs(props) {
</div>
{/* Add new cert form */}
<div className="list-group-item list-group my-2 pb-4">
<form onSubmit={addCert} action="/forms/cert/add" method="post">
<input type="hidden" name="_csrf" value={csrf} />
<div className="mb-2">
<label className="form-label w-100">Subject
<input className="form-control" type="text" name="subject" placeholder="domain.com" required />
<div className='list-group-item list-group my-2 pb-4'>
<form onSubmit={addCert} action='/forms/cert/add' method='post'>
<input type='hidden' name='_csrf' value={csrf} />
<div className='mb-2'>
<label className='form-label w-100'>Subject
<input className='form-control' type='text' name='subject' placeholder='domain.com' required />
</label>
</div>
<div className="mb-2">
<label className="form-label w-100">Altname(s)
<div className='mb-2'>
<label className='form-label w-100'>Altname(s)
<textarea
className="form-control"
name="altnames"
placeholder={`www.domain.com\r\ntest.example.com\r\netc...`}
className='form-control'
name='altnames'
placeholder={'www.domain.com\r\ntest.example.com\r\netc...'}
rows={4}
required />
</label>
</div>
<div className="mb-3">
<label className="form-label w-100">Email (Optional, for expiry notices)
<input className="form-control" type="email" name="email" placeholder="email@example.com" />
<div className='mb-3'>
<label className='form-label w-100'>Email (Optional, for expiry notices)
<input className='form-control' type='email' name='email' placeholder='email@example.com' />
</label>
</div>
<button className="btn btn-sm btn-success" type="submit">
<i className="bi-plus-lg pe-1" width="16" height="16" />
<button className='btn btn-sm btn-success' type='submit'>
<i className='bi-plus-lg pe-1' width='16' height='16' />
New Certificate
</button>
</form>
</div>
{error && <span className="mx-2"><ErrorAlert error={error} /></span>}
{error && <span className='mx-2'><ErrorAlert error={error} /></span>}
{/* back to account */}
<BackButton to="/account" />
<BackButton to='/account' />
</>
);
@ -265,5 +266,5 @@ export default function Certs(props) {
};
export async function getServerSideProps({ req, res, query, resolvedUrl, locale, locales, defaultLocale}) {
return { props: { user: res.locals.user || null, ...query } }
return { props: { user: res.locals.user || null, ...query } };
};

@ -1,8 +1,14 @@
import Head from 'next/head';
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 { useRouter } from 'next/router';
import Link from 'next/link';
import * as API from '../api.js'
import * as API from '../api.js';
import ErrorAlert from '../components/ErrorAlert.js';
import { useState } from 'react';
@ -14,12 +20,12 @@ export default function ChangePassword() {
async function changepassword(e) {
e.preventDefault();
return setError('Invalid email');
await API.changepassword({
username: e.target.username.value,
password: e.target.password.value,
repeat_password: e.target.repeat_password.value,
}, null, setError, router);
router.push('/login');
// await API.changepassword({
// username: e.target.username.value,
// password: e.target.password.value,
// repeat_password: e.target.repeat_password.value,
// }, null, setError, router);
// router.push('/login');
}
return (
@ -30,22 +36,20 @@ export default function ChangePassword() {
{error && <ErrorAlert error={error} />}
<span className="d-flex flex-column align-items-center mt-5 pt-5">
<Link href="/">
<a className="d-flex mb-3 text-decoration-none align-items-center">
<Image src="/favicon.ico" layout="fixed" width="24" height="24" alt=" " />
<span className="mx-2 fs-4 text-decoration-none">BasedFlare</span>
</a>
<span className='d-flex flex-column align-items-center mt-5 pt-5'>
<Link href='/' className='d-flex mb-3 text-decoration-none align-items-center'>
<ResolvedImage src='/favicon.ico' width='24' height='24' alt=' ' />
<span className='mx-2 fs-4 text-decoration-none'>BasedFlare</span>
</Link>
<form className="mb-3" onSubmit={changepassword} action="/forms/changepassword" method="POST">
<div className="mb-2">
<label className="form-label">Email
<input className="form-control" type="text" name="username" maxLength="50" required="required"/>
<form className='mb-3' onSubmit={changepassword} action='/forms/changepassword' method='POST'>
<div className='mb-2'>
<label className='form-label'>Email
<input className='form-control' type='text' name='username' maxLength='50' required='required'/>
</label>
</div>
<input className="btn btn-primary w-100" type="submit" value="Request Reset"/>
<input className='btn btn-primary w-100' type='submit' value='Request Reset'/>
</form>
<span className="fs-xs">Found your password? <Link href="/login">Back to Login</Link>.</span>
<span className='fs-xs'>Found your password? <Link href='/login'>Back to Login</Link>.</span>
</span>
</>

@ -1,9 +1,9 @@
import React, { useState, useEffect } from 'react';
import Head from 'next/head';
import BackButton from '../components/BackButton.js'
import BackButton from '../components/BackButton.js';
import ErrorAlert from '../components/ErrorAlert.js';
import ClusterRow from '../components/ClusterRow.js';
import * as API from '../api.js'
import * as API from '../api.js';
import { useRouter } from 'next/router';
export default function Clusters(props) {
@ -20,11 +20,11 @@ export default function Clusters(props) {
if (!state.user) {
return (
<div className="d-flex flex-column">
<div className='d-flex flex-column'>
{error && <ErrorAlert error={error} />}
<div className="text-center mb-4">
<div className="spinner-border mt-5" role="status">
<span className="visually-hidden">Loading...</span>
<div className='text-center mb-4'>
<div className='spinner-border mt-5' role='status'>
<span className='visually-hidden'>Loading...</span>
</div>
</div>
</div>
@ -73,29 +73,29 @@ export default function Clusters(props) {
<title>Clusters</title>
</Head>
<h5 className="fw-bold">
<h5 className='fw-bold'>
Clusters ({user.clusters.length}):
</h5>
{/* Clusters table */}
<div className="table-responsive">
<form className="d-flex" onSubmit={addCluster} action="/forms/cluster/add" method="post">
<input type="hidden" name="_csrf" value={csrf} />
<table className="table text-nowrap">
<div className='table-responsive'>
<form className='d-flex' onSubmit={addCluster} action='/forms/cluster/add' method='post'>
<input type='hidden' name='_csrf' value={csrf} />
<table className='table text-nowrap'>
<tbody>
{clusterList}
{/* Add new cluster form */}
<tr className="align-middle">
<tr className='align-middle'>
<td>
<button className="btn btn-sm btn-success" type="submit">
<i className="bi-plus-lg pe-none" width="16" height="16" />
<button className='btn btn-sm btn-success' type='submit'>
<i className='bi-plus-lg pe-none' width='16' height='16' />
</button>
</td>
<td colSpan="2">
<td colSpan='2'>
<input className="form-control" type="text" name="cluster" placeholder="http://username:password@host:port, comma separated for multiple" required />
<input className='form-control' type='text' name='cluster' placeholder='http://username:password@host:port, comma separated for multiple' required />
</td>
</tr>
@ -104,12 +104,12 @@ export default function Clusters(props) {
</form>
</div>
{error && <span className="mx-1">
{error && <span className='mx-1'>
<ErrorAlert error={error} />
</span>}
{/* back to account */}
<BackButton to="/account" />
<BackButton to='/account' />
</>
);
@ -117,5 +117,5 @@ export default function Clusters(props) {
}
export async function getServerSideProps({ req, res, query, resolvedUrl, locale, locales, defaultLocale}) {
return { props: { user: res.locals.user || null, ...query } }
return { props: { user: res.locals.user || null, ...query } };
}

@ -2,7 +2,7 @@ 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 * as API from '../api.js';
import { useRouter } from 'next/router';
import NProgress from 'nprogress';
@ -31,11 +31,11 @@ export default function Csr(props) {
if (!state.user) {
return (
<div className="d-flex flex-column">
<div className='d-flex flex-column'>
{error && <ErrorAlert error={error} />}
<div className="text-center mb-4">
<div className="spinner-border mt-5" role="status">
<span className="visually-hidden">Loading...</span>
<div className='text-center mb-4'>
<div className='spinner-border mt-5' role='status'>
<span className='visually-hidden'>Loading...</span>
</div>
</div>
</div>
@ -55,7 +55,7 @@ export default function Csr(props) {
<title>Certificate Signing Request</title>
</Head>
<h5 className="fw-bold">
<h5 className='fw-bold'>
Certificate Signing Request:
</h5>
@ -63,38 +63,38 @@ export default function Csr(props) {
To generate a certificate signing request for your domain and/or subdomain(s):
<div>
<code>
{`openssl req -newkey rsa:4096 -new -nodes -subj "/CN=`}<strong>yourdomain.com</strong>{`/OU=OrganisationUnit/O=Organisation/L=Locality/ST=St/C=Co" -sha256 -extensions v3_req -reqexts SAN -keyout origin.key -out origin.csr -config <(cat /etc/ssl/openssl.cnf \<\(printf "[SAN]\\nsubjectAltName=DNS:`}<strong>yourdomain.com</strong>{`,DNS:`}<strong>www.yourdomain.com</strong>{`"))`}
{'openssl req -newkey rsa:4096 -new -nodes -subj "/CN='}<strong>yourdomain.com</strong>{'/OU=OrganisationUnit/O=Organisation/L=Locality/ST=St/C=Co" -sha256 -extensions v3_req -reqexts SAN -keyout origin.key -out origin.csr -config <(cat /etc/ssl/openssl.cnf \<\(printf "[SAN]\\nsubjectAltName=DNS:'}<strong>yourdomain.com</strong>{',DNS:'}<strong>www.yourdomain.com</strong>{'"))'}
</code>
</div>
</p>
{/* Verify CSR form */}
<div className="list-group-item list-group my-2 pb-4">
<form onSubmit={verifyCSR} action="/forms/csr/verify" method="post">
<input type="hidden" name="_csrf" value={csrf} />
<div className="mb-2">
<label className="form-label w-100">Paste your origin.csr file here:
<div className='list-group-item list-group my-2 pb-4'>
<form onSubmit={verifyCSR} action='/forms/csr/verify' method='post'>
<input type='hidden' name='_csrf' value={csrf} />
<div className='mb-2'>
<label className='form-label w-100'>Paste your origin.csr file here:
<textarea
className="form-control"
name="csr"
placeholder={"-----BEGIN CERTIFICATE REQUEST-----\n..."}
className='form-control'
name='csr'
placeholder={'-----BEGIN CERTIFICATE REQUEST-----\n...'}
rows={4}
required />
</label>
</div>
<button className="btn btn-sm btn-success" type="submit">
<i className="bi-plus-lg pe-1" width="16" height="16" />
<button className='btn btn-sm btn-success' type='submit'>
<i className='bi-plus-lg pe-1' width='16' height='16' />
Verify CSR
</button>
</form>
</div>
{csr && <div className="list-group-item list-group my-2 pb-4">
<div className="mb-2">
<label className="form-label w-100">Here&apos;s your certificate:
{csr && <div className='list-group-item list-group my-2 pb-4'>
<div className='mb-2'>
<label className='form-label w-100'>Here&apos;s your certificate:
<textarea
className="form-control"
name="csr"
className='form-control'
name='csr'
value={csr}
rows={10}
readOnly
@ -103,10 +103,10 @@ export default function Csr(props) {
</div>
</div>}
{error && <span className="mx-2"><ErrorAlert error={error} /></span>}
{error && <span className='mx-2'><ErrorAlert error={error} /></span>}
{/* back to account */}
<BackButton to="/account" />
<BackButton to='/account' />
</>
);
@ -114,5 +114,5 @@ export default function Csr(props) {
}
export async function getServerSideProps({ req, res, query, resolvedUrl, locale, locales, defaultLocale}) {
return { props: { user: res.locals.user || null, ...query } }
return { props: { user: res.locals.user || null, ...query } };
}

@ -1,11 +1,11 @@
import { useRouter } from "next/router";
import { useRouter } from 'next/router';
import React, { useState, useEffect } from 'react';
import Head from 'next/head';
import BackButton from '../../../../components/BackButton.js';
import ErrorAlert from '../../../../components/ErrorAlert.js';
import Select from 'react-select';
import countries from 'i18n-iso-countries';
countries.registerLocale(require("i18n-iso-countries/langs/en.json"));
countries.registerLocale(require('i18n-iso-countries/langs/en.json'));
import * as API from '../../../../api.js';
const continentMap = {
@ -16,7 +16,7 @@ const continentMap = {
'OC': 'Oceania',
'AF': 'Africa',
'AN': 'Antarctica',
}
};
const countryOptions = Object.entries(countries.getNames('en'))
.map(e => ({ value: e[0], label: `${e[1]} (${e[0]})` }));
@ -27,10 +27,10 @@ const fromEntries = (pairs) => {
return {
...obj,
[k]: k in obj
? [].concat(obj[k], v)
: (k.startsWith('geov_') || k.startsWith('fallbacks_') ? [v] : v)
}
}, {})
? [].concat(obj[k], v)
: (k.startsWith('geov_') || k.startsWith('fallbacks_') ? [v] : v)
};
}, {});
};
const DnsEditRecordPage = (props) => {
@ -40,30 +40,30 @@ const DnsEditRecordPage = (props) => {
const { domain, zone: routerZone, type: routerType } = router.query;
const newRecord = router.asPath === `/dns/${domain}/new`;
const [recordSet, setRecordSet] = useState();
const [zone, setZone] = useState(newRecord ? "@" : (routerZone || "@"));
const [type, setType] = useState(routerType || "a");
const [recordSelection, setRecordSelection] = useState("roundrobin");
const [zone, setZone] = useState(newRecord ? '@' : (routerZone || '@'));
const [type, setType] = useState(routerType || 'a');
const [recordSelection, setRecordSelection] = useState('roundrobin');
const [error, setError] = useState();
const handleIdChange = (value, index) => {
recordSet[index].id = value;
setRecordSet([...recordSet]);
}
};
const handleValueChange = (value, index) => {
recordSet[index].ip = value;
setRecordSet([...recordSet]);
}
};
const handleGeoKeyChange = (value, index) => {
recordSet[index].geok = value;
recordSet[index].geov = [];
setRecordSet([...recordSet]);
}
};
const getFallbackValue = (id) => {
const rec = recordSet.find(r => r.id === id);
if (rec) {
return (rec.ip || rec.host || rec.value || rec.ns || rec.text || rec.target || 'No Value');
}
return 'No Value';
}
};
useEffect(() => {
if (!recordSet) {
@ -72,22 +72,22 @@ const DnsEditRecordPage = (props) => {
if (res && res.recordSet) {
if (newRecord) {
setRecordSet([{
"geok": "cc",
"geov": [],
"id": "",
"ip": "",
"fb": [],
"sel": 0,
"bsel": 0,
"t": false,
"h": false,
"u": true,
"ttl": 86400,
'geok': 'cc',
'geov': [],
'id': '',
'ip': '',
'fb': [],
'sel': 0,
'bsel': 0,
't': false,
'h': false,
'u': true,
'ttl': 86400,
}]);
return;
}
setRecordSet(res.recordSet.length > 0 ? [...res.recordSet] : [{}]);
setRecordSelection(res.recordSet.length > 0 && res.recordSet[0].geok ? "geo" : "roundrobin");
setRecordSelection(res.recordSet.length > 0 && res.recordSet[0].geok ? 'geo' : 'roundrobin');
}
});
}
@ -95,11 +95,11 @@ const DnsEditRecordPage = (props) => {
if (!recordSet) {
return (
<div className="d-flex flex-column">
<div className='d-flex flex-column'>
{error && <ErrorAlert error={error} />}
<div className="text-center mb-4">
<div className="spinner-border mt-5" role="status">
<span className="visually-hidden">Loading...</span>
<div className='text-center mb-4'>
<div className='spinner-border mt-5' role='status'>
<span className='visually-hidden'>Loading...</span>
</div>
</div>
</div>
@ -113,8 +113,8 @@ const DnsEditRecordPage = (props) => {
}
const { csrf } = state;
const supportsGeo = ["a", "aaaa"].includes(type) && recordSelection === "geo";
const supportsHealth = ["a", "aaaa"].includes(type);
const supportsGeo = ['a', 'aaaa'].includes(type) && recordSelection === 'geo';
const supportsHealth = ['a', 'aaaa'].includes(type);
return (
<>
@ -127,64 +127,64 @@ const DnsEditRecordPage = (props) => {
{error && <ErrorAlert error={error} />}
<h5 className="fw-bold">
<h5 className='fw-bold'>
{domain} / Records list / {newRecord?'New':'Edit'} record set:
</h5>
{/* Record editing form */}
<form
method="POST"
method='POST'
action={`/forms/dns/${domain}/${zone}/${type}`}
onSubmit={addUpdateRecord}
>
<input type="hidden" name="_csrf" value={csrf} />
{recordSet && Array.isArray(recordSet) && recordSet[0].t === true && <div className="alert alert-warning" role="alert">
<input type='hidden' name='_csrf' value={csrf} />
{recordSet && Array.isArray(recordSet) && recordSet[0].t === true && <div className='alert alert-warning' role='alert'>
This is a template record. Changes may be overwritten with updates to the BasedFlare platform.
</div>}
{newRecord && zone === '@' && <div className="alert alert-info" role="info">
{newRecord && zone === '@' && <div className='alert alert-info' role='info'>
The &quot;@&quot; symbol indicates that this is a record for the root domain i.e &quot;{domain}&quot;. You can change the name to create other subdomains e.g. &quot;www&quot;.
</div>}
<div className="card text-bg-dark col p-3 border-0 shadow-sm">
<div className="row mb-3">
<div className="col">
<label className="w-100">
<div className='card text-bg-dark col p-3 border-0 shadow-sm'>
<div className='row mb-3'>
<div className='col'>
<label className='w-100'>
Type
<select
className="form-select"
name="type"
className='form-select'
name='type'
defaultValue={type}
value={type}
onChange={e => setType(e.target.value)}
required
disabled={!newRecord}>
<option value="">Type</option>
<optgroup label="Standard">
<option value="a">A</option>
<option value="aaaa">AAAA</option>
<option value="txt">TXT</option>
<option value="cname">CNAME</option>
<option value="ns">NS</option>
<option value="mx">MX</option>
<option value="srv">SRV</option>
<option value="caa">CAA</option>
<option value="soa">SOA</option>
<option value=''>Type</option>
<optgroup label='Standard'>
<option value='a'>A</option>
<option value='aaaa'>AAAA</option>
<option value='txt'>TXT</option>
<option value='cname'>CNAME</option>
<option value='ns'>NS</option>
<option value='mx'>MX</option>
<option value='srv'>SRV</option>
<option value='caa'>CAA</option>
<option value='soa'>SOA</option>
</optgroup>
<optgroup label="BasedFlare Templates">
<option value="a_template">A</option>
<option value="aaaa_template">AAAA</option>
<option value="soa_template">SOA</option>
<option value="ns_template">NS</option>
<optgroup label='BasedFlare Templates'>
<option value='a_template'>A</option>
<option value='aaaa_template'>AAAA</option>
<option value='soa_template'>SOA</option>
<option value='ns_template'>NS</option>
</optgroup>
</select>
</label>
</div>
<div className="col">
<label className="w-100">
<div className='col'>
<label className='w-100'>
Name
<input
className="form-control"
type="text"
name="name"
className='form-control'
type='text'
name='name'
defaultValue={zone}
required
disabled={!newRecord}
@ -192,14 +192,14 @@ const DnsEditRecordPage = (props) => {
/>
</label>
</div>
{!type.endsWith('_template') && <div className="col">
<label className="w-100">
{!type.endsWith('_template') && <div className='col'>
<label className='w-100'>
TTL
<input
className="form-control"
type="number"
name="ttl"
min="30"
className='form-control'
type='number'
name='ttl'
min='30'
required
defaultValue={recordSet && recordSet.length > 0 ? recordSet[0].ttl : 300}
/>
@ -207,22 +207,22 @@ const DnsEditRecordPage = (props) => {
</div>}
</div>
{(type === "a" || type === "aaaa") && <div className="row mb-3">
<div className="col-4">
{(type === 'a' || type === 'aaaa') && <div className='row mb-3'>
<div className='col-4'>
Record selection mode:
<div className="form-check">
<div className='form-check'>
<input
className="form-check-input"
type="radio"
name="selection"
id="roundrobin"
value="roundrobin"
checked={recordSelection === "roundrobin"}
className='form-check-input'
type='radio'
name='selection'
id='roundrobin'
value='roundrobin'
checked={recordSelection === 'roundrobin'}
onChange={e => setRecordSelection(e.target.value)}
/>
<label
className="form-check-label"
htmlFor="roundrobin">
className='form-check-label'
htmlFor='roundrobin'>
Round Robin
</label>
</div>
@ -241,111 +241,111 @@ const DnsEditRecordPage = (props) => {
Weighted
</label>
</div>*/}
<div className="form-check">
<div className='form-check'>
<input
className="form-check-input"
type="radio"
name="selection"
value="geo"
id="geo"
className='form-check-input'
type='radio'
name='selection'
value='geo'
id='geo'
onChange={e => setRecordSelection(e.target.value)}
checked={recordSelection === "geo"}
checked={recordSelection === 'geo'}
/>
<label
className="form-check-label"
htmlFor="geo">
className='form-check-label'
htmlFor='geo'>
Geolocation
</label>
</div>
</div>
</div>}
{!type.endsWith('_template') && <div className="col">
<div className="row">
<div className="col">
{!type.endsWith('_template') && <div className='col'>
<div className='row'>
<div className='col'>
Records:
</div>
</div>
{recordSet.map((rec, i) => {
let typeFields;
switch (type) {
case "mx":
typeFields = <div className="row">
<div className="col-sm-12 col-md-3">
<label className="w-100">
case 'mx':
typeFields = <div className='row'>
<div className='col-sm-12 col-md-3'>
<label className='w-100'>
Preference
<input className="form-control" type="number" name={`preference_${i}`} defaultValue={rec.preference} required />
<input className='form-control' type='number' name={`preference_${i}`} defaultValue={rec.preference} required />
</label>
</div>
</div>;
break;
case "srv":
typeFields = <div className="row">
<div className="col-sm-12 col-md-3">
<label className="w-100">
case 'srv':
typeFields = <div className='row'>
<div className='col-sm-12 col-md-3'>
<label className='w-100'>
Preference
<input className="form-control" type="number" name={`preference_${i}`} defaultValue={rec.preference} required />
<input className='form-control' type='number' name={`preference_${i}`} defaultValue={rec.preference} required />
</label>
</div>
<div className="col-sm-12 col-md-3">
<label className="w-100">
<div className='col-sm-12 col-md-3'>
<label className='w-100'>
Port
<input className="form-control" type="number" name={`port_${i}`} defaultValue={rec.port} required />
<input className='form-control' type='number' name={`port_${i}`} defaultValue={rec.port} required />
</label>
</div>
<div className="col-sm-12 col-md-3">
<label className="w-100">
<div className='col-sm-12 col-md-3'>
<label className='w-100'>
Weight
<input className="form-control" type="number" name={`weight_${i}`} defaultValue={rec.weight} required />
<input className='form-control' type='number' name={`weight_${i}`} defaultValue={rec.weight} required />
</label>
</div>
<div className="col-sm-12 col-md-3">
<label className="w-100">
<div className='col-sm-12 col-md-3'>
<label className='w-100'>
Priority
<input className="form-control" type="number" name={`priority_${i}`} defaultValue={rec.priority} required />
<input className='form-control' type='number' name={`priority_${i}`} defaultValue={rec.priority} required />
</label>
</div>
</div>;
break;
case "caa":
typeFields = <div className="row">
<div className="col-sm-12 col-md-3">
<label className="w-100">
case 'caa':
typeFields = <div className='row'>
<div className='col-sm-12 col-md-3'>
<label className='w-100'>
Flag
<input className="form-control" type="number" name={`flag_${i}`} defaultValue={rec.flag} required />
<input className='form-control' type='number' name={`flag_${i}`} defaultValue={rec.flag} required />
</label>
</div>
<div className="col-sm-12 col-md-3">
<label className="w-100">
<div className='col-sm-12 col-md-3'>
<label className='w-100'>
Tag
<input className="form-control" type="text" name={`tag_${i}`} defaultValue={rec.tag} required />
<input className='form-control' type='text' name={`tag_${i}`} defaultValue={rec.tag} required />
</label>
</div>
</div>;
break;
case "soa":
typeFields = <div className="row">
<div className="col-sm-12 col-md-3">
<label className="w-100">
case 'soa':
typeFields = <div className='row'>
<div className='col-sm-12 col-md-3'>
<label className='w-100'>
MBox
<input className="form-control" type="text" name={`mbox_${i}`} defaultValue={rec.MBox} required />
<input className='form-control' type='text' name={`mbox_${i}`} defaultValue={rec.MBox} required />
</label>
</div>
<div className="col-sm-12 col-md-3">
<label className="w-100">
<div className='col-sm-12 col-md-3'>
<label className='w-100'>
Refresh
<input className="form-control" type="number" name={`refresh_${i}`} defaultValue={rec.refresh} required />
<input className='form-control' type='number' name={`refresh_${i}`} defaultValue={rec.refresh} required />
</label>
</div>
<div className="col-sm-12 col-md-3">
<label className="w-100">
<div className='col-sm-12 col-md-3'>
<label className='w-100'>
Retry
<input className="form-control" type="number" name={`retry_${i}`} defaultValue={rec.retry} required />
<input className='form-control' type='number' name={`retry_${i}`} defaultValue={rec.retry} required />
</label>
</div>
<div className="col-sm-12 col-md-3">
<label className="w-100">
<div className='col-sm-12 col-md-3'>
<label className='w-100'>
Expire
<input className="form-control" type="number" name={`expire_${i}`} defaultValue={rec.expire} required />
<input className='form-control' type='number' name={`expire_${i}`} defaultValue={rec.expire} required />
</label>
</div>
{/*<div className="col-sm-12 col-md-3">
@ -360,23 +360,23 @@ const DnsEditRecordPage = (props) => {
break;
}
return (<>
<div className="row" key={`row1_${i}`}>
{supportsHealth && <div className="col-sm-4 col-md-2">
<div className='row' key={`row1_${i}`}>
{supportsHealth && <div className='col-sm-4 col-md-2'>
ID:
<input
className="form-control"
type="text"
className='form-control'
type='text'
name={`id_${i}`}
onChange={(e) => handleIdChange(e.target.value, i)}
defaultValue={rec.id} required
/>
</div>}
<div className="col">
<label className="w-100">
<div className='col'>
<label className='w-100'>
Value
<input
className="form-control"
type="text"
className='form-control'
type='text'
name={`value_${i}`}
onChange={(e) => handleValueChange(e.target.value, i)}
defaultValue={rec.ip || rec.host || rec.value || rec.ns || rec.text || rec.target}
@ -384,9 +384,9 @@ const DnsEditRecordPage = (props) => {
/>
</label>
</div>
<div className="col-auto ms-auto">
<div className='col-auto ms-auto'>
<button
className="btn btn-sm btn-danger mt-4"
className='btn btn-sm btn-danger mt-4'
onClick={(e) =>{
e.preventDefault();
recordSet.splice(i, 1);
@ -399,28 +399,28 @@ const DnsEditRecordPage = (props) => {
</div>
</div>
{typeFields}
{supportsHealth && <div className="row" key={`row2_${i}`}>
<div className="col-sm-12 col-md-2 align-self-end mb-2">
<div className="form-check form-switch">
{supportsHealth && <div className='row' key={`row2_${i}`}>
<div className='col-sm-12 col-md-2 align-self-end mb-2'>
<div className='form-check form-switch'>
<input
className="form-check-input"
type="checkbox"
className='form-check-input'
type='checkbox'
name={`health_${i}`}
value="1"
id="flexCheckDefault"
value='1'
id='flexCheckDefault'
checked={rec.h === true}
onChange={(e) =>{
recordSet[i].h = e.target.checked;
setRecordSet([...recordSet]);
}}
/>
<label className="form-check-label" htmlFor="flexCheckDefault">
<label className='form-check-label' htmlFor='flexCheckDefault'>
Health Check
</label>
</div>
</div>
<div className="col-sm-12 col-md">
<label className="w-100">
<div className='col-sm-12 col-md'>
<label className='w-100'>
Fallback IDs
<Select
theme={(theme) => ({
@ -434,69 +434,69 @@ const DnsEditRecordPage = (props) => {
options={recordSet.filter(x => x.id !== rec.id).map(x => ({ label: x.id, value: x.id}) )}
getOptionLabel={x => `${x.value} (${getFallbackValue(x.value)})`}
defaultValue={(rec.fb||[]).map(x => ({ value: x, label: x }))}
classNamePrefix="select"
classNamePrefix='select'
name={`fallbacks_${i}`}
className="basic-multi-select"
className='basic-multi-select'
/>
</label>
</div>
<div className="col-sm-12 col-md-3">
<label className="w-100">
<div className='col-sm-12 col-md-3'>
<label className='w-100'>
Fallback Selector
<select
className="form-select"
className='form-select'
name={`sel_${i}`}
defaultValue={rec.sel}
disabled={!rec.h}
required
>
<option value="0">None</option>
<option value="1">First alive fallback</option>
<option value="2">Random alive fallback</option>
<option value="3">All alive fallbacks</option>
<option value='0'>None</option>
<option value='1'>First alive fallback</option>
<option value='2'>Random alive fallback</option>
<option value='3'>All alive fallbacks</option>
</select>
</label>
</div>
<div className="col-sm-12 col-md-3">
<label className="w-100">
<div className='col-sm-12 col-md-3'>
<label className='w-100'>
Backup Selector
<select
className="form-select"
className='form-select'
name={`bsel_${i}`}
defaultValue={rec.bsel}
disabled={!rec.h}
required
>
<option value="0">None</option>
<option value="1">First healthy record</option>
<option value="2">Random healthy record</option>
<option value="3">All healthy records</option>
<option value="4">First fallback (ignores health)</option>
<option value="5">Random fallback (ignores health)</option>
<option value="6">All fallbacks (ignores health)</option>
<option value='0'>None</option>
<option value='1'>First healthy record</option>
<option value='2'>Random healthy record</option>
<option value='3'>All healthy records</option>
<option value='4'>First fallback (ignores health)</option>
<option value='5'>Random fallback (ignores health)</option>
<option value='6'>All fallbacks (ignores health)</option>
</select>
</label>
</div>
</div>}
{supportsGeo && <div className="row" key={`row3_${i}`}>
<div className="col-sm-12 col-md-2">
<label className="w-100">
{supportsGeo && <div className='row' key={`row3_${i}`}>
<div className='col-sm-12 col-md-2'>
<label className='w-100'>
Geo Key
<select
className="form-select"
className='form-select'
onChange={(e) => handleGeoKeyChange(e.target.value, i)}
name={`geok_${i}`}
defaultValue={rec.geok}
required>
<option value="cn">Continent</option>
<option value="cc">Country</option>
<option value='cn'>Continent</option>
<option value='cc'>Country</option>
</select>
</label>
</div>
<div className="col">
<label className="w-100">
<div className='col'>
<label className='w-100'>
Geo Value(s)
{rec.geok === "cc"
{rec.geok === 'cc'
? <Select
theme={(theme) => ({
...theme,
@ -509,10 +509,10 @@ const DnsEditRecordPage = (props) => {
// value={(rec.geov||[]).map(x => ({ value: x, label: `${countries.getName(x, 'en')} (${x})` }))}
getOptionLabel={x => `${countries.getName(x.value, 'en')} (${x.value})`}
defaultValue={(rec.geov||[]).map(x => ({ value: x, label: x }))}
classNamePrefix="select"
classNamePrefix='select'
key={`geov_${rec.geok}_${i}`}
name={`geov_${i}`}
className="basic-multi-select"
className='basic-multi-select'
/>
: <Select
theme={(theme) => ({
@ -534,22 +534,22 @@ const DnsEditRecordPage = (props) => {
// value={(rec.geov||[]).map(x => ({ value: x, label: continentMap[x] }))}
getOptionLabel={x => `${continentMap[x.value]} (${x.value})`}
defaultValue={(rec.geov||[]).map(x => ({ value: x, label: x }))}
classNamePrefix="select"
classNamePrefix='select'
key={`geov_${rec.geok}_${i}`}
name={`geov_${i}`}
className="basic-multi-select"
className='basic-multi-select'
/>}
</label>
</div>
</div>}
{i < recordSet.length-1 && <hr className="mb-2 mt-3" />}
{i < recordSet.length-1 && <hr className='mb-2 mt-3' />}
</>);
})}
<div className="row mt-2">
<div className="col-auto ms-auto">
<button className="ms-auto btn btn-sm btn-success mt-2" onClick={(e) =>{
<div className='row mt-2'>
<div className='col-auto ms-auto'>
<button className='ms-auto btn btn-sm btn-success mt-2' onClick={(e) =>{
e.preventDefault();
recordSet.push({})
recordSet.push({});
setRecordSet([...recordSet]);
}}>
+
@ -558,8 +558,8 @@ const DnsEditRecordPage = (props) => {
</div>
</div>}
</div>
<div className="row mt-4">
<div className="col-auto me-auto">
<div className='row mt-4'>
<div className='col-auto me-auto'>
<BackButton to={`/dns/${domain}`} />
</div>
{/*<div className="col-auto ms-auto">
@ -567,8 +567,8 @@ const DnsEditRecordPage = (props) => {
Cancel
</button>
</div>*/}
<div className="col-auto">
<button className="btn btn-sm btn-success">
<div className='col-auto'>
<button className='btn btn-sm btn-success'>
Save
</button>
</div>

@ -1,4 +1,4 @@
import { useRouter } from "next/router";
import { useRouter } from 'next/router';
import Link from 'next/link';
import React, { useState, useEffect } from 'react';
import Head from 'next/head';
@ -16,7 +16,7 @@ const DnsDomainIndexPage = (props) => {
...props,
});
const [error, setError] = useState();
const [sortType, setSortType] = useState("name");
const [sortType, setSortType] = useState('name');
const [sortOrder, setSortOrder] = useState(-1);
const [filter, setFilter] = useState('');
const { user, recordSets, csrf } = state;
@ -24,11 +24,11 @@ const DnsDomainIndexPage = (props) => {
let sorted;
const sameType = newSortType === sortType;
const newSortOrder = sortOrder * (sameType ? -1 : 1);
if (newSortType === "name") {
if (newSortType === 'name') {
sorted = recordSets.sort((a, b) => {
return (Object.keys(a)[0].localeCompare(Object.keys(b)[0]) * newSortOrder);
});
} else if (newSortType === "type") {
} else if (newSortType === 'type') {
sorted = recordSets.map(recordSet => {
const k = Object.keys(recordSet)[0];
let rs = Object.entries(recordSet[k]);
@ -40,7 +40,7 @@ const DnsDomainIndexPage = (props) => {
setSortOrder(newSortOrder);
setSortType(newSortType);
dispatch({ ...state, recordSets: sorted });
}
};
useEffect(() => {
if (!state.recordSets) {
@ -50,11 +50,11 @@ const DnsDomainIndexPage = (props) => {
if (recordSets == null) {
return (
<div className="d-flex flex-column">
<div className='d-flex flex-column'>
{error && <ErrorAlert error={error} />}
<div className="text-center mb-4">
<div className="spinner-border mt-5" role="status">
<span className="visually-hidden">Loading...</span>
<div className='text-center mb-4'>
<div className='spinner-border mt-5' role='status'>
<span className='visually-hidden'>Loading...</span>
</div>
</div>
</div>
@ -81,7 +81,7 @@ const DnsDomainIndexPage = (props) => {
});
});
const sortArrow = sortOrder === 1 ? <i className="bi-caret-down-fill"></i> : <i className="bi-caret-up-fill"></i>
const sortArrow = sortOrder === 1 ? <i className='bi-caret-down-fill'></i> : <i className='bi-caret-up-fill'></i>;
return (
<>
@ -94,27 +94,27 @@ const DnsDomainIndexPage = (props) => {
{error && <ErrorAlert error={error} />}
<h5 className="fw-bold">
<h5 className='fw-bold'>
{domain} / Records list:
</h5>
<SearchFilter filter={filter} setFilter={setFilter} />
{/* Record sets table */}
<div className="table-responsive">
<table className="table text-nowrap">
<div className='table-responsive'>
<table className='table text-nowrap'>
<tbody>
{/* header row */}
<tr>
<th />
<th role="button" className="user-select-none" onClick={() => handleSetSorting("name")}>
<th role='button' className='user-select-none' onClick={() => handleSetSorting('name')}>
Name
{sortType === "name" && sortArrow}
{sortType === 'name' && sortArrow}
</th>
<th role="button" className="user-select-none" onClick={() => handleSetSorting("type")}>
<th role='button' className='user-select-none' onClick={() => handleSetSorting('type')}>
Type
{sortType === "type" && sortArrow}
{sortType === 'type' && sortArrow}
</th>
<th>
Content
@ -133,16 +133,16 @@ const DnsDomainIndexPage = (props) => {
</table>
</div>
<div className="my-3">
<div className='my-3'>
<Link href={`/dns/${domain}/new`}>
<button className="btn btn-sm btn-success">
<button className='btn btn-sm btn-success'>
+
</button>
</Link>
</div>
{/* back to account */}
<BackButton to="/domains" />
<BackButton to='/domains' />
</>
);

@ -4,7 +4,7 @@ import Head from 'next/head';
import BackButton from '../components/BackButton.js';
import ErrorAlert from '../components/ErrorAlert.js';
import SearchFilter from '../components/SearchFilter.js';
import * as API from '../api.js'
import * as API from '../api.js';
import { useRouter } from 'next/router';
import { wildcardCheck, wildcardMatches } from '../util.js';
@ -16,18 +16,18 @@ export default function Domains(props) {
const [filter, setFilter] = useState('');
useEffect(() => {
if (!state.user) {
if (!state.user || state.certs == null) {
API.getDomains(dispatch, setError, router);
}
}, [state.user, router]);
}, [state.user, state.certs, router]);
if (!state.user) {
if (!state.user || state.certs == null) {
return (
<div className="d-flex flex-column">
<div className='d-flex flex-column'>
{error && <ErrorAlert error={error} />}
<div className="text-center mb-4">
<div className="spinner-border mt-5" role="status">
<span className="visually-hidden">Loading...</span>
<div className='text-center mb-4'>
<div className='spinner-border mt-5' role='status'>
<span className='visually-hidden'>Loading...</span>
</div>
</div>
</div>
@ -56,61 +56,57 @@ export default function Domains(props) {
//.sort((a, b) => a.localeCompare(b))
.filter(d => d.includes(filter))
.forEach((d, i) => {
const domainCert = certs.find(c => c.subject === d || c.altnames.includes(d));
const wildcardCert = certs.find(c => {
return ((c.subject.startsWith('*') && wildcardMatches(d, c.subject))
const domainCert = certs.find(c => c.subject === d || c.altnames.includes(d));
const wildcardCert = certs.find(c => {
return ((c.subject.startsWith('*') && wildcardMatches(d, c.subject))
|| c.altnames.some(an => an.startsWith('*') && wildcardMatches(d, an)));
});
const isSubdomain = d.split('.').length > 2;
let daysRemaining;
if (domainCert || wildcardCert) {
const certDate = (domainCert || wildcardCert).date;
const creation = new Date(certDate);
const expiry = creation.setDate(creation.getDate()+90);
daysRemaining = (Math.floor(expiry - Date.now()) / 86400000).toFixed(1);
}
const tableRow = (
<tr key={i} className="align-middle">
<td className="text-left" style={{width:0}}>
<a className="btn btn-sm btn-danger" onClick={() => {
if (window.confirm(`Are you sure you want to delete "${d}"?`)) {
deleteDomain(csrf, d);
}
}}>
<i className="bi-trash-fill pe-none" width="16" height="16" />
</a>
{!isSubdomain && <Link href={`/dns/${d}`} passHref>
<a className="btn btn-sm btn-primary ms-2">
<i className="bi-pencil pe-none" width="16" height="16" />
});
const isSubdomain = d.split('.').length > 2;
let daysRemaining;
if (domainCert || wildcardCert) {
const certDate = (domainCert || wildcardCert).date;
const creation = new Date(certDate);
const expiry = creation.setDate(creation.getDate()+90);
daysRemaining = (Math.floor(expiry - Date.now()) / 86400000).toFixed(1);
}
const tableRow = (
<tr key={i} className='align-middle'>
<td className='text-left' style={{width:0}}>
<a className='btn btn-sm btn-danger' onClick={() => {
if (window.confirm(`Are you sure you want to delete "${d}"?`)) {
deleteDomain(csrf, d);
}
}}>
<i className='bi-trash-fill pe-none' width='16' height='16' />
</a>
</Link>}
</td>
<td>
{d}
{(domainCert || wildcardCert) && <a target="_blank" rel="noreferrer" href={`https://${d}`}>
<i className="bi-box-arrow-up-right pe-none ms-1" width="12" height="12" style={{fontSize: '0.8rem'}} />
</a>}
</td>
<td>
{(domainCert || wildcardCert)
? <Link href={`/certs#${(domainCert||wildcardCert).storageName}`}>
<a className="text-success">
<i className={`${wildcardCert ? 'bi-asterisk' : 'bi-lock-fill'} pe-none me-2`} width="16" height="16" />
{!isSubdomain && <Link href={`/dns/${d}`} passHref className='btn btn-sm btn-primary ms-2'>
<i className='bi-pencil pe-none' width='16' height='16' />
</Link>}
</td>
<td>
{d}
{(domainCert || wildcardCert) && <a target='_blank' rel='noreferrer' href={`https://${d}`}>
<i className='bi-box-arrow-up-right pe-none ms-1' width='12' height='12' style={{fontSize: '0.8rem'}} />
</a>}
</td>
<td>
{(domainCert || wildcardCert)
? <Link href={`/certs#${(domainCert||wildcardCert).storageName}`} className='text-success'>
<i className={`${wildcardCert ? 'bi-asterisk' : 'bi-lock-fill'} pe-none me-2`} width='16' height='16' />
{(domainCert||wildcardCert).storageName}
{wildcardCert ? <small>{' '}(Wildcard)</small> : ''}
</a>
</Link>
: <span>
</Link>
: <span>
No Certificate
</span>}
</td>
<td suppressHydrationWarning={true}>
{daysRemaining ? `${daysRemaining} days` : '-'}
</td>
</tr>
);
isSubdomain ? subdomainList.push(tableRow) : domainList.push(tableRow)
});
</span>}
</td>
<td suppressHydrationWarning={true}>
{daysRemaining ? `${daysRemaining} days` : '-'}
</td>
</tr>
);
isSubdomain ? subdomainList.push(tableRow) : domainList.push(tableRow);
});
return (
<>
@ -119,18 +115,18 @@ export default function Domains(props) {
<title>Domains</title>
</Head>
<h5 className="fw-bold">
<h5 className='fw-bold'>
Domains:
</h5>
<SearchFilter filter={filter} setFilter={setFilter} />
{/* Domains table */}
<div className="table-responsive">
<table className="table text-nowrap">
<div className='table-responsive'>
<table className='table text-nowrap'>
<tbody>
{domainList && domainList.length > 0 && <tr className="align-middle">
{domainList && domainList.length > 0 && <tr className='align-middle'>
<th/>
<th>
Domain
@ -144,22 +140,22 @@ export default function Domains(props) {
</tr>}
{domainList}
{subdomainList.length > 0 && <tr className="align-middle">
<th colSpan="4">
{subdomainList.length > 0 && <tr className='align-middle'>
<th colSpan='4'>
Subdomains:
</th>
</tr>}
{subdomainList}
{/* Add new domain form */}
<tr className="align-middle">
<td className="col-1 text-center" colSpan="4">
<form className="d-flex" onSubmit={addDomain} action="/forms/domain/add" method="post">
<input type="hidden" name="_csrf" value={csrf} />
<button className="btn btn-sm btn-success" type="submit">
<i className="bi-plus-lg pe-none" width="16" height="16" />
<tr className='align-middle'>
<td className='col-1 text-center' colSpan='4'>
<form className='d-flex' onSubmit={addDomain} action='/forms/domain/add' method='post'>
<input type='hidden' name='_csrf' value={csrf} />
<button className='btn btn-sm btn-success' type='submit'>
<i className='bi-plus-lg pe-none' width='16' height='16' />
</button>
<input className="form-control ms-3" type="text" name="domain" placeholder="domain e.g. example.com" required />
<input className='form-control ms-3' type='text' name='domain' placeholder='domain e.g. example.com' required />
</form>
</td>
</tr>
@ -168,10 +164,10 @@ export default function Domains(props) {
</table>
</div>
{error && <span className="mx-2"><ErrorAlert error={error} /></span>}
{error && <span className='mx-2'><ErrorAlert error={error} /></span>}
{/* back to account */}
<BackButton to="/account" />
<BackButton to='/account' />
</>
);
@ -179,5 +175,5 @@ export default function Domains(props) {
}
export async function getServerSideProps({ req, res, query, resolvedUrl, locale, locales, defaultLocale}) {
return { props: { user: res.locals.user || null, ...query } }
return { props: { user: res.locals.user || null, ...query } };
}

@ -0,0 +1,149 @@
:root { font-family: 'Inter',arial,Helvetica,sans-serif; }
@supports (font-variation-settings: normal) {
:root { font-family: 'Inter var',arial,Helvetica,sans-serif; }
}
html, body { font-family: 'Inter',arial,Helvetica,sans-serif; height: 100%; overflow: hidden; background: #F4F5F7; }
.sidebar { background: var(--bs-body-bg); }
.corner-ribbon {z-index:9999; width: 180px;top: 8px;left: auto;text-align: center;line-height: 30px;letter-spacing: 1px;color: white;background: darkorange;box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);text-shadow: 0 0 3px rgba(0, 0, 0, 0.5);right: -70px;transform: rotate(45deg);-webkit-transform: rotate(46deg);position: fixed;overflow: hidden;}
.green { color: green; }
.red { color: red; }
footer { margin-top: auto; }
.btn { font-weight: bold; }
.nav-item:not(:first-child) { margin-top: 10px; }
.nav-link { color: white; }
.nav-link:hover { color: #6aa6fd; }
.mobile-menu { margin: 0 -16px; }
.fs-xs { font-size: small; }
.table, .list-group { box-shadow: 0 0px 3px rgba(0,0,0,.1); max-width: 100%; min-width: 600px; background-color: var(--bs-body-bg); }
.text-decoration-none { color: var(--bs-body-color); }
.sidebar { box-shadow: 0 0px 3px rgba(0,0,0,0.2); }
.card { background: var(--bs-body-bg) !important; color: var(--bs-body-color) !important; }
.table { margin-bottom: 0; }
.table-responsive { box-shadow: 0 0px 3px rgba(0,0,0,0.2); }
a.text-success:visited, a.text-success:hover { color: rgba(var(--bs-success-rgb),var(--bs-text-opacity)) !important }
.select__control {
transition: none;
}
.form-control::placeholder {
color: #6c757d!important;
}
input:autofill, input:-webkit-autofill {
background-color: initial!important;
background-image: initial!important;
color: var(--bs-body-color)!important;
}
.select__control:hover:not(.select__control--is-focused) {
border-color: #ced4da;
}
.select__control--menu-is-open, .select__control--is-focused {
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
border-color: #86b7fe!important;
}
tr:target, tr:target textarea {
background: var(--bs-highlight-bg);
}
table.notaborder textarea {
border: none!important;
}
@media (max-width: 650px) {
.table, .list-group { min-width: unset; }
}
@media (min-width: 800px) {
.mobile-btn { display: none!important; }
}
@media (max-width: 800px) {
.sidebar { display: none; }
}
@media (prefers-color-scheme: dark) {
:root {
--bs-body-color: #fff;
--bs-body-bg: #23272a;
--bs-secondary-color: #fff;
}
table * {
--bs-table-accent-bg: #2c2f33;
--bs-table-color-state: #fff;
}
html, body { background: var(--bs-body-bg); }
.nav-pills {
--bs-nav-pills-link-active-bg: #7289da;
--bs-btn-hover-bg: #7289da;
--bs-btn-hover-border-color: #7289da;
}
.btn-primary {
--bs-btn-bg: #7289da;
--bs-btn-border-color: #7289da;
--bs-btn-hover-bg: #6481e7;
--bs-btn-hover-border-color: #6481e7;
--bs-btn-active-bg: #6481e7;
--bs-btn-active-border-color: #6481e7;
--bs-btn-disabled-bg: #7289da;
--bs-btn-disabled-border-color: #7289da;
}
.badge.bg-primary {
background-color: #7289da;
}
.text-muted, a, a:visited, a:hover, .nav-link, .nav-link:hover { color:#fff!important; }
.list-group-item { color: #fff; background-color: #2c2f33!important; border-color: var(--bs-body-bg) }
input:not(.btn):not(.select__input), option, select.form-select, textarea, .input-group-text { color: #fff!important; background-color: #2c2f33!important; border: 1px solid black!important; }
.list-group-item-action:focus, .list-group-item-action:hover { color: #fff; background-color: #1F1F1F; }
.sidebar, .table { background-color: #2c2f33!important; }
.table { border-color: var(--bs-gray-900)!important; }
tr:target {
background: #ffc10720!important;
}
tr:target textarea {
background: transparent!important;
}
.select__control {
background-color: #393939;
border-color: var(--bs-gray-900);
transition: none;
}
.select__input {
color: white!important;
}
.select__control:hover:not(.select__control--is-focused) {
border-color: black;
}
.select__control--is-focused,
.select__control--menu-is-open {
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
border-color: black!important;
}
.select__multi-value {
background-color: var(--bs-dark);
border-radius: 5px;
}
.select__multi-value__label {
padding: 2px 10px;
color: #fff;
}
.select__menu {
background-color: #393939;
border: 1px solid var(--bs-gray-900);
}
.select__option {
background-color: #393939;
}
.select__option:hover {
background-color: var(--bs-dark);
}
.select__indicator-separator {
background-color: #393939;
}
.select__clear-indicator:hover {
color: #fff;
}
.select__multi-value__remove:hover {
background-color: #5D2F24;
color: #DE350B;
border-radius: 0 5px 5px 0;
}
.select__placeholder,
.select__single-value {
color: #fff;
}
}

@ -1,5 +1,12 @@
import Head from 'next/head';
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';
export default function Index() {
@ -8,28 +15,22 @@ export default function Index() {
<title>BasedFlare</title>
</Head>
<span className="d-flex flex-column align-items-center mt-5 pt-5">
<Link href="#!">
<a className="d-flex mb-3 text-decoration-none align-items-center">
<Image src="/favicon.ico" layout="fixed" width="24" height="24" alt=" " />
<span className="mx-2 fs-4 text-decoration-none">BasedFlare</span>
</a>
<span className='d-flex flex-column align-items-center mt-5 pt-5'>
<Link href='#!' className='d-flex mb-3 text-decoration-none align-items-center'>
<ResolvedImage src='/favicon.ico' width='24' height='24' alt=' ' />
<span className='mx-2 fs-4 text-decoration-none'>BasedFlare</span>
</Link>
<span className="d-flex">
<div className="me-2">
<Link href='/account'>
<a className="btn btn-sm btn-primary">
<i className="bi-person-square pe-none me-2" width="16" height="16" />
<span className='d-flex'>
<div className='me-2'>
<Link href='/account' className='btn btn-sm btn-primary'>
<i className='bi-person-square pe-none me-2' width='16' height='16' />
Account
</a>
</Link>
</div>
<div>
<Link href='/login'>
<a className="btn btn-sm btn-primary">
<i className="bi-door-closed pe-none me-2" width="16" height="16" />
<Link href='/login' className='btn btn-sm btn-primary'>
<i className='bi-door-closed pe-none me-2' width='16' height='16' />
Login
</a>
</Link>
</div>
</span>

@ -1,8 +1,14 @@
import Head from 'next/head';
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 { useRouter } from 'next/router';
import Link from 'next/link';
import * as API from '../api.js'
import * as API from '../api.js';
import ErrorAlert from '../components/ErrorAlert.js';
import { useState } from 'react';
@ -27,34 +33,32 @@ export default function Login() {
{error && <ErrorAlert error={error} />}
<span className="d-flex flex-column align-items-center mt-5 pt-5">
<Link href="/">
<a className="d-flex mb-3 text-decoration-none align-items-center">
<Image src="/favicon.ico" layout="fixed" width="24" height="24" alt=" " />
<span className="mx-2 fs-4 text-decoration-none">BasedFlare</span>
</a>
<span className='d-flex flex-column align-items-center mt-5 pt-5'>
<Link href='/' className='d-flex mb-3 text-decoration-none align-items-center'>
<ResolvedImage src='/favicon.ico' width='24' height='24' alt=' ' />
<span className='mx-2 fs-4 text-decoration-none'>BasedFlare</span>
</Link>
<form className="mb-3" onSubmit={login} action="/forms/login" method="POST">
<div className="mb-2">
<label className="form-label w-100">Username
<input className="form-control" type="text" name="username" maxLength="50" required="required"/>
<form className='mb-3' onSubmit={login} action='/forms/login' method='POST'>
<div className='mb-2'>
<label className='form-label w-100'>Username
<input className='form-control' type='text' name='username' maxLength='50' required='required'/>
</label>
</div>
<div className="mb-2">
<label className="form-label w-100">Password
<input className="form-control" type="password" name="password" maxLength="100" required="required"/>
<div className='mb-2'>
<label className='form-label w-100'>Password
<input className='form-control' type='password' name='password' maxLength='100' required='required'/>
</label>
</div>
<div className="mb-3">
<div className="form-check">
<input className="form-check-input" type="checkbox" name="tos" value="true" id="tos" required />
<label className="form-check-label" htmlFor="tos">I agree to the <Link href="/tos" passHref><a target="_blank">terms of service</a></Link>.</label>
<div className='mb-3'>
<div className='form-check'>
<input className='form-check-input' type='checkbox' name='tos' value='true' id='tos' required />
<label className='form-check-label' htmlFor='tos'>I agree to the <Link href='/tos' passHref target='_blank'>terms of service</Link>.</label>
</div>
</div>
<input className="btn btn-primary w-100" type="submit" value="Login"/>
<input className='btn btn-primary w-100' type='submit' value='Login'/>
</form>
<span className="fs-xs">Don&apos;t have an account? <Link href="/register">Register here</Link>.</span>
<span className="fs-xs"><Link href="/changepassword">Forgot your password?</Link></span>
<span className='fs-xs'>Don&apos;t have an account? <Link href='/register'>Register here</Link>.</span>
<span className='fs-xs'><Link href='/changepassword'>Forgot your password?</Link></span>
</span>
</>

@ -1,4 +1,4 @@
import { useRouter } from "next/router";
import { useRouter } from 'next/router';
import React, { useState, useEffect } from 'react';
import Head from 'next/head';
import MapRow from '../../components/MapRow.js';
@ -24,11 +24,11 @@ const MapPage = (props) => {
if (state.map == null || changedMap) {
return (
<div className="d-flex flex-column">
<div className='d-flex flex-column'>
{error && <ErrorAlert error={error} />}
<div className="text-center mb-4">
<div className="spinner-border mt-5" role="status">
<span className="visually-hidden">Loading...</span>
<div className='text-center mb-4'>
<div className='spinner-border mt-5' role='status'>
<span className='visually-hidden'>Loading...</span>
</div>
</div>
</div>
@ -75,114 +75,113 @@ const MapPage = (props) => {
|| rowValue.includes(filter);
})
.map((row, i) => {
return (
<MapRow
key={i}
row={row}
name={mapInfo.name}
csrf={csrf}
showValues={showValues}
mapValueNames={mapValueNames}
onDeleteSubmit={deleteFromMap}
columnKeys={mapInfo.columnKeys}
/>
)
});
return (
<MapRow
key={i}
row={row}
name={mapInfo.name}
csrf={csrf}
showValues={showValues}
mapValueNames={mapValueNames}
onDeleteSubmit={deleteFromMap}
columnKeys={mapInfo.columnKeys}
/>
);
});
let formElements;
//TODO: env var case map names
switch (mapInfo.name) {
case "ddos": {
case 'ddos': {
const mapValueOptions = Object.entries(mapValueNames)
.map((entry, i) => (<option key={'option'+i} value={entry[0]}>{entry[1]}</option>))
.map((entry, i) => (<option key={'option'+i} value={entry[0]}>{entry[1]}</option>));
formElements = (
<>
<td>
<button className="btn btn-sm btn-success" type="submit">
<i className="bi-plus-lg pe-none" width="16" height="16" />
<button className='btn btn-sm btn-success' type='submit'>
<i className='bi-plus-lg pe-none' width='16' height='16' />
</button>
</td>
<td>
<input className="form-control" type="text" name="key" placeholder="domain/path" required />
<input className='form-control' type='text' name='key' placeholder='domain/path' required />
</td>
<td>
<select className="form-select" name="m" defaultValue="" required>
<option disabled value="">protection mode</option>
<select className='form-select' name='m' defaultValue='' required>
<option disabled value=''>protection mode</option>
{mapValueOptions}
</select>
</td>
<td>
<div className="form-check">
<input className="form-check-input" type="checkbox" name="t" value="t" id="t" />
<label className="form-check-label" htmlFor="t">Tor exits only</label>
<div className='form-check'>
<input className='form-check-input' type='checkbox' name='t' value='t' id='t' />
<label className='form-check-label' htmlFor='t'>Tor exits only</label>
</div>
</td>
</>
);
break;
}
case "ddos_config": {
case 'ddos_config': {
const domainSelectOptions = user.domains.map((d, i) => (<option key={'option'+i} value={d}>{d}</option>));
formElements = (
<>
<td>
<button className="btn btn-sm btn-success" type="submit">
<i className="bi-plus-lg pe-none" width="16" height="16" />
<button className='btn btn-sm btn-success' type='submit'>
<i className='bi-plus-lg pe-none' width='16' height='16' />
</button>
</td>
<td>
<select className="form-select" name="key" defaultValue="" required>
<option value="" />
<select className='form-select' name='key' defaultValue='' required>
<option value='' />
{domainSelectOptions}
</select>
</td>
<td>
<input className="form-control" type="number" min="8" defaultValue="24" name="pd" placeholder="difficulty" required />
<input className='form-control' type='number' min='8' defaultValue='24' name='pd' placeholder='difficulty' required />
</td>
<td>
<select className="form-select" name="pt" required>
<option disabled value="">pow type</option>
<option value="sha256">sha256</option>
<option value="argon2">argon2</option>
<select className='form-select' name='pt' required>
<option disabled value=''>pow type</option>
<option value='sha256'>sha256</option>
<option value='argon2'>argon2</option>
</select>
</td>
<td>
<input className="form-control" type="number" name="cex" placeholder="cookie expiry (seconds)" required />
<input className='form-control' type='number' name='cex' placeholder='cookie expiry (seconds)' required />
</td>
<td>
<div className="form-check">
<input className="form-check-input" type="checkbox" name="cip" value="cip" id="cip" />
<label className="form-check-label" htmlFor="cip">Lock cookie to IP</label>
<div className='form-check'>
<input className='form-check-input' type='checkbox' name='cip' value='cip' id='cip' />
<label className='form-check-label' htmlFor='cip'>Lock cookie to IP</label>
</div>
</td>
</>
);
break;
}
case "hosts": {
case 'hosts': {
const domainSelectOptions = user.domains.map((d, i) => (<option key={'option'+i} value={d}>{d}</option>));
formElements = (
<>
<td>
<button className="btn btn-sm btn-success" type="submit">
<i className="bi-plus-lg pe-none" width="16" height="16" />
<button className='btn btn-sm btn-success' type='submit'>
<i className='bi-plus-lg pe-none' width='16' height='16' />
</button>
</td>
<td>
<select className="form-select" name="key" defaultValue="" required>
<option value="" />
<select className='form-select' name='key' defaultValue='' required>
<option value='' />
{domainSelectOptions}
</select>
</td>
{
(process.env.NEXT_PUBLIC_CUSTOM_BACKENDS_ENABLED && mapInfo.name === "hosts") &&
(process.env.NEXT_PUBLIC_CUSTOM_BACKENDS_ENABLED && mapInfo.name === 'hosts') &&
<td>
<input
className="form-control"
type="text"
name="value"
placeholder="backend ip:port"
className='form-control'
type='text'
name='value'
placeholder='backend ip:port'
required
/>
</td>
@ -191,20 +190,20 @@ const MapPage = (props) => {
);
break;
}
case "maintenance": {
case 'maintenance': {
const activeDomains = map.map(e => e.key);
const inactiveDomains = user.domains.filter(d => !activeDomains.includes(d));
const domainSelectOptions = inactiveDomains.map((d, i) => (<option key={'option'+i} value={d}>{d}</option>));
formElements = (
<>
<td>
<button className="btn btn-sm btn-success" type="submit">
<i className="bi-plus-lg pe-none" width="16" height="16" />
<button className='btn btn-sm btn-success' type='submit'>
<i className='bi-plus-lg pe-none' width='16' height='16' />
</button>
</td>
<td>
<select className="form-select" name="key" defaultValue="" required>
<option value="" />
<select className='form-select' name='key' defaultValue='' required>
<option value='' />
{domainSelectOptions}
</select>
</td>
@ -212,49 +211,49 @@ const MapPage = (props) => {
);
break;
}
case "blockedip":
case "whitelist":
case 'blockedip':
case 'whitelist':
formElements = (
<>
<td>
<button className="btn btn-sm btn-success" type="submit">
<i className="bi-plus-lg pe-none" width="16" height="16" />
<button className='btn btn-sm btn-success' type='submit'>
<i className='bi-plus-lg pe-none' width='16' height='16' />
</button>
</td>
<td>
<input className="form-control" type="text" name="key" placeholder="ip or subnet" required />
<input className='form-control' type='text' name='key' placeholder='ip or subnet' required />
</td>
</>
);
break;
case "blockedasn":
case 'blockedasn':
formElements = (
<>
<td>
<button className="btn btn-sm btn-success" type="submit">
<i className="bi-plus-lg pe-none" width="16" height="16" />
<button className='btn btn-sm btn-success' type='submit'>
<i className='bi-plus-lg pe-none' width='16' height='16' />
</button>
</td>
<td>
<input className="form-control" type="text" name="key" placeholder="ASN" required />
<input className='form-control' type='text' name='key' placeholder='ASN' required />
</td>
</>
);
break;
case "redirect":
case "rewrite":
case 'redirect':
case 'rewrite':
formElements = (
<>
<td>
<button className="btn btn-sm btn-success" type="submit">
<i className="bi-plus-lg pe-none" width="16" height="16" />
<button className='btn btn-sm btn-success' type='submit'>
<i className='bi-plus-lg pe-none' width='16' height='16' />
</button>
</td>
<td>
<input className="form-control" type="text" name="key" placeholder="domain" required />
<input className='form-control' type='text' name='key' placeholder='domain' required />
</td>
<td>
<input className="form-control" type="text" name="value" placeholder="domain or domain/path" required />
<input className='form-control' type='text' name='value' placeholder='domain or domain/path' required />
</td>
</>
);
@ -271,16 +270,16 @@ const MapPage = (props) => {
</Head>
{/* Map friendly name (same as shown on acc page) */}
<h5 className="fw-bold">
<h5 className='fw-bold'>
{mapInfo.fname}:
</h5>
<SearchFilter filter={filter} setFilter={setFilter} />
{/* Map table */}
<div className="table-responsive w-100">
<form onSubmit={addToMap} className="d-flex" action={`/forms/map/${mapInfo.name}/add`} method="post">
<table className="table text-nowrap mb-0">
<div className='table-responsive w-100'>
<form onSubmit={addToMap} className='d-flex' action={`/forms/map/${mapInfo.name}/add`} method='post'>
<table className='table text-nowrap mb-0'>
<tbody>
{/* header row */}
@ -300,7 +299,7 @@ const MapPage = (props) => {
{mapRows}
{/* Add new row form */}
<tr className="align-middle">
<tr className='align-middle'>
{formElements}
</tr>
@ -309,10 +308,10 @@ const MapPage = (props) => {
</form>
</div>
{error && <span className="mx-2"><ErrorAlert error={error} /></span>}
{error && <span className='mx-2'><ErrorAlert error={error} /></span>}
{/* back to account */}
<BackButton to="/account" />
<BackButton to='/account' />
</>
);

@ -7,7 +7,7 @@ export default function Menu() {
<title>Menu</title>
</Head>
<div className="p-4 mobile-menu">
<div className='p-4 mobile-menu'>
<MenuLinks />
</div>
</>);

@ -19,11 +19,11 @@ export default function Onboarding(props) {
if (state.user == null) {
return (
<div className="d-flex flex-column">
<div className='d-flex flex-column'>
{error && <ErrorAlert error={error} />}
<div className="text-center mb-4">
<div className="spinner-border mt-5" role="status">
<span className="visually-hidden">Loading...</span>
<div className='text-center mb-4'>
<div className='spinner-border mt-5' role='status'>
<span className='visually-hidden'>Loading...</span>
</div>
</div>
</div>
@ -75,119 +75,119 @@ export default function Onboarding(props) {
{error && <ErrorAlert error={error} />}
<h5 className="fw-bold">Onboarding</h5>
<h5 className='fw-bold'>Onboarding</h5>
{!user.onboarding && <div className="my-2">
<input onClick={finishOnboarding} className="btn btn-warning" type="submit" value="Skip Onboarding" />
{!user.onboarding && <div className='my-2'>
<input onClick={finishOnboarding} className='btn btn-warning' type='submit' value='Skip Onboarding' />
</div>}
<div className="list-group">
<div className="list-group-item d-flex gap-3">
<input className="form-check-input flex-shrink-0" type="checkbox" value="" checked={domainAdded} disabled />
<span className="pt-1 form-checked-content">
<div className='list-group'>
<div className='list-group-item d-flex gap-3'>
<input className='form-check-input flex-shrink-0' type='checkbox' value='' checked={domainAdded} disabled />
<span className='pt-1 form-checked-content'>
<strong style={{ textDecoration: domainAdded ? 'line-through' : '' }}>
<i className="bi-card-list pe-none me-2" width="1em" height="1em" />
<i className='bi-card-list pe-none me-2' width='1em' height='1em' />
1. Add a domain
</strong>
{!domainAdded && <>
<span className="d-block text-body-secondary mt-3">
<span className='d-block text-body-secondary mt-3'>
<p>Add your first domain (i.e. <code>example.com</code>) that you want to protect with BasedFlare.</p>
<p>You can add other domains and/or subdomains later from the &quot;domains&quot; page.</p>
</span>
<form className="d-flex mb-3" onSubmit={addDomain} action="/forms/domain/add" method="post">
<input type="hidden" name="_csrf" value={csrf} />
<input type="hidden" name="onboarding" value="1" />
<input className="btn btn-success" type="submit" value="+" disabled={domainAdded} />
<input className="form-control mx-3" type="text" name="domain" placeholder="domain" disabled={domainAdded} required />
<form className='d-flex mb-3' onSubmit={addDomain} action='/forms/domain/add' method='post'>
<input type='hidden' name='_csrf' value={csrf} />
<input type='hidden' name='onboarding' value='1' />
<input className='btn btn-success' type='submit' value='+' disabled={domainAdded} />
<input className='form-control mx-3' type='text' name='domain' placeholder='domain' disabled={domainAdded} required />
</form>
</>}
{domainAdded && (<div><strong>
<i className="bi-check-circle-fill me-2" style={{ color: 'green' }} width="1em" height="1em" />
<i className='bi-check-circle-fill me-2' style={{ color: 'green' }} width='1em' height='1em' />
Domain added successfully
</strong></div>)}
</span>
</div>
<div className="list-group-item d-flex gap-3">
<span className="flex-shrink-0 mx-1 mt-2">&bull;</span>
<span className="pt-1 form-checked-content">
<div className='list-group-item d-flex gap-3'>
<span className='flex-shrink-0 mx-1 mt-2'>&bull;</span>
<span className='pt-1 form-checked-content'>
<strong>
<i className="bi-globe2 pe-none me-2" width="1em" height="1em" />
<i className='bi-globe2 pe-none me-2' width='1em' height='1em' />
2. Update the nameservers for your domain to the following:
</strong>
<span className="d-block text-body-secondary mt-3">
<span className='d-block text-body-secondary mt-3'>
<ul>
{txtRecords
.reduceRight((p,v,i,a)=>(v=i?~~(Math.random()*(i+1)):i, v-i?[a[v],a[i]]=[a[i],a[v]]:0, a),[])
.map((r, i) => <li key={'a'+i}>{r}</li>)}
</ul>
</span>
<span className="d-block text-body-secondary mt-3">
<span className='d-block text-body-secondary mt-3'>
<p>This is usually done through your domain registrar. Use all the nameservers, or as many as the registrar allows for your domain.</p>
</span>
</span>
</div>
<div className="list-group-item d-flex gap-3">
<span className="flex-shrink-0 mx-1 mt-2">&bull;</span>
<span className="pt-1 form-checked-content">
<div className='list-group-item d-flex gap-3'>
<span className='flex-shrink-0 mx-1 mt-2'>&bull;</span>
<span className='pt-1 form-checked-content'>
<strong>
<i className="bi-globe2 pe-none me-2" width="1em" height="1em" />
<i className='bi-globe2 pe-none me-2' width='1em' height='1em' />
3. Create DNS Records
</strong>
<span className="d-block text-body-secondary mt-3">
<p>On the <Link href="/domains" passHref><a target="_blank">Domains</a></Link> page, edit the DNS for your domain and add any &quot;A&quot; type records using the templates.</p>
<span className='d-block text-body-secondary mt-3'>
<p>On the <Link href='/domains' passHref target='_blank'>Domains</Link> page, edit the DNS for your domain and add any &quot;A&quot; type records using the templates.</p>
<p>The &quot;name&quot; field for <code>example.com</code> should be &quot;@&quot;, and for subdomains e.g. <code>www.example.com</code> it should be the subdomain &quot;www&quot;.</p>
</span>
</span>
</div>
<div className="list-group-item d-flex gap-3">
<span className="flex-shrink-0 mx-1 mt-2">&bull;</span>
<span className="pt-1 form-checked-content">
<div className='list-group-item d-flex gap-3'>
<span className='flex-shrink-0 mx-1 mt-2'>&bull;</span>
<span className='pt-1 form-checked-content'>
<strong>
<i className="bi-hourglass-split pe-none me-2" width="1em" height="1em" />
<i className='bi-hourglass-split pe-none me-2' width='1em' height='1em' />
Wait for the nameserver and DNS updates to propagate.
</strong>
<span className="d-block text-body-secondary mt-3">
<span className='d-block text-body-secondary mt-3'>
<p>This may take up to 48 hours depending on your domain registrar, but typically starts working within 30 minutes.</p>
<p>You can use these external tools to check the propagation of the DNS:</p>
<ul>
<li><a rel="noreferrer" target="_blank" href="https://ping.sx/dig">{'https://ping.sx/dig'}</a></li>
<li><a rel="noreferrer" target="_blank" href="https://www.whatsmydns.net/">{'https://www.whatsmydns.net/'}</a></li>
<li><a rel="noreferrer" target="_blank" href="https://dnschecker.org/">{'https://dnschecker.org/'}</a></li>
<li><a rel='noreferrer' target='_blank' href='https://ping.sx/dig'>{'https://ping.sx/dig'}</a></li>
<li><a rel='noreferrer' target='_blank' href='https://www.whatsmydns.net/'>{'https://www.whatsmydns.net/'}</a></li>
<li><a rel='noreferrer' target='_blank' href='https://dnschecker.org/'>{'https://dnschecker.org/'}</a></li>
</ul>
</span>
</span>
</div>
{<div className="list-group-item d-flex gap-3">
<input className="form-check-input flex-shrink-0" type="checkbox" value="" checked={backendAdded} disabled />
<span className="pt-1 form-checked-content">
{<div className='list-group-item d-flex gap-3'>
<input className='form-check-input flex-shrink-0' type='checkbox' value='' checked={backendAdded} disabled />
<span className='pt-1 form-checked-content'>
<strong style={{ textDecoration: backendAdded ? 'line-through' : '' }}>
<i className="bi-hdd-network-fill pe-none me-2" width="1em" height="1em" />
<i className='bi-hdd-network-fill pe-none me-2' width='1em' height='1em' />
4. Setup a backend
</strong>
{!backendAdded && <>
<span className="d-block text-body-secondary mt-3">
<span className='d-block text-body-secondary mt-3'>
<p>Enter the backend server IP address and port in ip:port format, e.g. <code>12.34.56.78:443</code>.</p>
<p>This is the &quot;origin&quot; that you want BasedFlare to proxy traffic to.</p>
</span>
<form onSubmit={addToMap} className="d-flex mb-3" action='/forms/map/hosts/add' method="post">
<input type="hidden" name="_csrf" value={csrf} />
<input type="hidden" name="onboarding" value="1" />
<input className="btn btn-success" type="submit" value="+"
<form onSubmit={addToMap} className='d-flex mb-3' action='/forms/map/hosts/add' method='post'>
<input type='hidden' name='_csrf' value={csrf} />
<input type='hidden' name='onboarding' value='1' />
<input className='btn btn-success' type='submit' value='+'
//disabled={backendAdded}
/>
<select className="form-select mx-3" name="key" defaultValue=""
<select className='form-select mx-3' name='key' defaultValue=''
//disabled={backendAdded}
required>
<option value="">select domain</option>
required>
<option value=''>select domain</option>
{user.domains.map((d, i) => (<option key={'option'+i} value={d}>{d}</option>))}
</select>
{
(process.env.NEXT_PUBLIC_CUSTOM_BACKENDS_ENABLED) &&
<input
className="form-control ml-2"
type="text"
name="value"
placeholder="backend ip:port"
className='form-control ml-2'
type='text'
name='value'
placeholder='backend ip:port'
//disabled={backendAdded}
required
/>
@ -195,54 +195,54 @@ export default function Onboarding(props) {
</form>
</>}
{backendAdded && (<div><strong>
<i className="bi-check-circle-fill me-2" style={{ color: 'green' }} width="1em" height="1em" />
<i className='bi-check-circle-fill me-2' style={{ color: 'green' }} width='1em' height='1em' />
Backend server successfully added
</strong></div>)}
</span>
</div>}
<div className="list-group-item d-flex gap-3">
<input className="form-check-input flex-shrink-0" type="checkbox" value="" checked={certAdded} disabled />
<span className="pt-1 form-checked-content">
<div className='list-group-item d-flex gap-3'>
<input className='form-check-input flex-shrink-0' type='checkbox' value='' checked={certAdded} disabled />
<span className='pt-1 form-checked-content'>
<strong style={{ textDecoration: certAdded ? 'line-through' : '' }}>
<i className="bi-file-earmark-lock-fill pe-none me-2" width="1em" height="1em" />
<i className='bi-file-earmark-lock-fill pe-none me-2' width='1em' height='1em' />
5. Generate HTTPS certificate
</strong>
{!certAdded && <>
<span className="d-block text-body-secondary mt-3">
<p>BasedFlare will generate a HTTPS certificate for you using <a href="https://letsencrypt.org/" rel="noreferrer" target="_blank">Let&apos;s Encrypt</a>.</p>
<span className='d-block text-body-secondary mt-3'>
<p>BasedFlare will generate a HTTPS certificate for you using <a href='https://letsencrypt.org/' rel='noreferrer' target='_blank'>Let&apos;s Encrypt</a>.</p>
<p>This certificate will be automatically installed on the BasedFlare edge and visitors will be connected securely.</p>
<p>Certificates last 90 days and will automatically renew when they have less than 30 days remaining.</p>
<p>You can manage certificates later from the &quot;HTTPS Certificates&quot; page.</p>
</span>
<form className="d-flex mb-3" onSubmit={addCert} action="/forms/cert/add" method="post">
<input type="hidden" name="_csrf" value={csrf} />
<input type="hidden" name="onboarding" value="1" />
<input className="btn btn-success" type="submit" value="+" disabled={certAdded} />
<input className="form-control mx-3" type="text" name="subject" placeholder="domain.com" disabled={certAdded} required />
<input className="form-control me-3" type="text" name="altnames" placeholder="www.domain.com,staging.domain.com,etc..." disabled={certAdded} required />
<form className='d-flex mb-3' onSubmit={addCert} action='/forms/cert/add' method='post'>
<input type='hidden' name='_csrf' value={csrf} />
<input type='hidden' name='onboarding' value='1' />
<input className='btn btn-success' type='submit' value='+' disabled={certAdded} />
<input className='form-control mx-3' type='text' name='subject' placeholder='domain.com' disabled={certAdded} required />
<input className='form-control me-3' type='text' name='altnames' placeholder='www.domain.com,staging.domain.com,etc...' disabled={certAdded} required />
</form>
</>}
{certAdded && (<div><strong>
<i className="bi-check-circle-fill me-2" style={{ color: 'green' }} width="1em" height="1em" />
<i className='bi-check-circle-fill me-2' style={{ color: 'green' }} width='1em' height='1em' />
HTTPS Certificate successfully generated
</strong></div>)}
</span>
</div>
<div className="list-group-item d-flex gap-3">
<input className="form-check-input flex-shrink-0" type="checkbox" value="" checked={certAdded} disabled />
<span className="pt-1 form-checked-content">
<div className='list-group-item d-flex gap-3'>
<input className='form-check-input flex-shrink-0' type='checkbox' value='' checked={certAdded} disabled />
<span className='pt-1 form-checked-content'>
<strong>
<i className="bi-file-earmark-lock-fill pe-none me-2" width="1em" height="1em" />
<i className='bi-file-earmark-lock-fill pe-none me-2' width='1em' height='1em' />
6. Get your HTTPS CSR signed
</strong>
<span className="d-block text-body-secondary mt-3">
<span className='d-block text-body-secondary mt-3'>
<p>Finally, generate a certificate signing request for your origin server(s) and have BasedFlare sign it.</p>
<p>This allows BasedFlare servers to verify the connection to your backend and prevents trivial MITM attacks and other weaknesses that are possible with e.g self-signed certificates in CloudFlare&apos;s &quot;flexible&quot; or &quot;full&quot; ssl mode.</p>
<ol>
<li>Generate the private key and certificate signing request for your domains on your origin server:
<p>
<code>
{`openssl req -newkey rsa:4096 -new -nodes -subj "/CN=`}<strong>yourdomain.com</strong>{`/OU=OrganisationUnit/O=Organisation/L=Locality/ST=St/C=Co" -sha256 -extensions v3_req -reqexts SAN -keyout origin.key -out origin.csr -config <(cat /etc/ssl/openssl.cnf \<\(printf "[SAN]\\nsubjectAltName=DNS:`}<strong>yourdomain.com</strong>{`,DNS:`}<strong>www.yourdomain.com</strong>{`"))`}
{'openssl req -newkey rsa:4096 -new -nodes -subj "/CN='}<strong>yourdomain.com</strong>{'/OU=OrganisationUnit/O=Organisation/L=Locality/ST=St/C=Co" -sha256 -extensions v3_req -reqexts SAN -keyout origin.key -out origin.csr -config <(cat /etc/ssl/openssl.cnf \<\(printf "[SAN]\\nsubjectAltName=DNS:'}<strong>yourdomain.com</strong>{',DNS:'}<strong>www.yourdomain.com</strong>{'"))'}
</code>
</p>
Make sure to replace yourdomain.com and www.yourdomain.com. It&apos;s also recommended to put the correct organisational unit, locality, state and country.
@ -252,14 +252,14 @@ export default function Onboarding(props) {
<li>You can then setup <code>origin.key</code> and <code>origin.crt</code>, as the key and certificate respectively, in your origin web server.</li>
</ol>
</span>
<form className="d-flex mb-3" action="/forms/csr/verify" method="post">
<input type="hidden" name="_csrf" value={csrf} />
<textarea className="form-control mx-3" name="csr" placeholder="-----BEGIN CERTIFICATE REQUEST----- ..." required />
<input className="btn btn-success" type="submit" value="Verify" />
<form className='d-flex mb-3' action='/forms/csr/verify' method='post'>
<input type='hidden' name='_csrf' value={csrf} />
<textarea className='form-control mx-3' name='csr' placeholder='-----BEGIN CERTIFICATE REQUEST----- ...' required />
<input className='btn btn-success' type='submit' value='Verify' />
</form>
</span>
</div>
<div className="list-group-item d-flex gap-3 justify-content-center">
<div className='list-group-item d-flex gap-3 justify-content-center'>
<strong>That&apos;s it!</strong>
</div>
</div>
@ -268,5 +268,5 @@ export default function Onboarding(props) {
}
export async function getServerSideProps({ req, res, query, resolvedUrl, locale, locales, defaultLocale}) {
return { props: { user: res.locals.user || null, ...query } }
return { props: { user: res.locals.user || null, ...query } };
};

@ -1,8 +1,14 @@
import Head from 'next/head';
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 { useRouter } from 'next/router';
import Link from 'next/link';
import * as API from '../api.js'
import * as API from '../api.js';
import ErrorAlert from '../components/ErrorAlert.js';
import { useState } from 'react';
@ -28,38 +34,36 @@ export default function Register() {
{error && <ErrorAlert error={error} />}
<span className="d-flex flex-column align-items-center mt-5 pt-5">
<Link href="/">
<a className="d-flex mb-3 text-decoration-none align-items-center">
<Image src="/favicon.ico" layout="fixed" width="24" height="24" alt=" " />
<span className="mx-2 fs-4 text-decoration-none">BasedFlare</span>
</a>
<span className='d-flex flex-column align-items-center mt-5 pt-5'>
<Link href='/' className='d-flex mb-3 text-decoration-none align-items-center'>
<ResolvedImage src='/favicon.ico' width='24' height='24' alt=' ' />
<span className='mx-2 fs-4 text-decoration-none'>BasedFlare</span>
</Link>
<form className="mb-3" onSubmit={register} action="/forms/register" method="POST">
<div className="mb-2">
<label className="form-label w-100">Username
<input className="form-control" type="text" name="username" maxLength="50" required="required"/>
<form className='mb-3' onSubmit={register} action='/forms/register' method='POST'>
<div className='mb-2'>
<label className='form-label w-100'>Username
<input className='form-control' type='text' name='username' maxLength='50' required='required'/>
</label>
</div>
<div className="mb-2">
<label className="form-label w-100">Password
<input className="form-control" type="password" name="password" maxLength="100" required="required"/>
<div className='mb-2'>
<label className='form-label w-100'>Password
<input className='form-control' type='password' name='password' maxLength='100' required='required'/>
</label>
</div>
<div className="mb-2">
<label className="form-label w-100">Repeat Password
<input className="form-control" type="password" name="repeat_password" maxLength="100" required="required"/>
<div className='mb-2'>
<label className='form-label w-100'>Repeat Password
<input className='form-control' type='password' name='repeat_password' maxLength='100' required='required'/>
</label>
</div>
<div className="mb-3">
<div className="form-check">
<input className="form-check-input" type="checkbox" name="tos" value="true" id="tos" required />
<label className="form-check-label" htmlFor="tos">I agree to the <Link href="/tos" passHref><a target="_blank">terms of service</a></Link>.</label>
<div className='mb-3'>
<div className='form-check'>
<input className='form-check-input' type='checkbox' name='tos' value='true' id='tos' required />
<label className='form-check-label' htmlFor='tos'>I agree to the <Link href='/tos' passHref target='_blank'>terms of service</Link>.</label>
</div>
</div>
<input className="btn btn-primary w-100" type="submit" value="Register"/>
<input className='btn btn-primary w-100' type='submit' value='Register'/>
</form>
<span className="fs-xs">Already have an account? <Link href="/login">Login here</Link>.</span>
<span className='fs-xs'>Already have an account? <Link href='/login'>Login here</Link>.</span>
</span>
</>

@ -11,7 +11,7 @@ export default function Tos() {
<title>Terms of Service</title>
</Head>
<h5 className="fw-bold">
<h5 className='fw-bold'>
Terms of Service:
</h5>

@ -1,66 +1,61 @@
'use strict';
const Redis = require('ioredis')
, client = new Redis({
host: process.env.REDIS_HOST || '127.0.0.1',
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASS || '',
db: 0,
})
, lockClient = new Redis({
host: process.env.REDIS_HOST || '127.0.0.1',
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASS || '',
db: 1,
});
module.exports = {
client,
lockClient,
close: () => {
client.quit();
lockClient.quit();
},
//get a value with key
get: (key) => {
return client.get(key).then(res => { return JSON.parse(res); });
},
//get a hash value
hgetall: (key) => {
return client.hgetall(key).then(res => { return res });
},
//get a hash value
hget: (key, hash) => {
return client.hget(key, hash).then(res => { return JSON.parse(res); });
},
//set a hash value
hset: (key, hash, value) => {
return client.hset(key, hash, JSON.stringify(value));
},
//delete a hash
hdel: (key, hash) => {
return client.hdel(key, hash);
},
//set a value on key
set: (key, value) => {
return client.set(key, JSON.stringify(value));
},
//delete value with key
del: (keyOrKeys) => {
if (Array.isArray(keyOrKeys)) {
return client.del(...keyOrKeys);
} else {
return client.del(keyOrKeys);
}
},
};
import Redis from 'ioredis';;
export const client = new Redis({
host: process.env.REDIS_HOST || '127.0.0.1',
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASS || '',
db: 0,
});
export const lockClient = new Redis({
host: process.env.REDIS_HOST || '127.0.0.1',
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASS || '',
db: 1,
});
export function close() {
client.quit();
lockClient.quit();
}
//get a value with key
export function get(key) {
return client.get(key).then(res => { return JSON.parse(res); });
}
//get a hash value
export function hgetall(key) {
return client.hgetall(key).then(res => { return res; });
}
//get a hash value
export function hget(key, hash) {
return client.hget(key, hash).then(res => { return JSON.parse(res); });
}
//set a hash value
export function hset(key, hash, value) {
return client.hset(key, hash, JSON.stringify(value));
}
//delete a hash
export function hdel(key, hash) {
return client.hdel(key, hash);
}
//set a value on key
export function set(key, value) {
return client.set(key, JSON.stringify(value));
}
//delete value with key
export function del(keyOrKeys) {
if (Array.isArray(keyOrKeys)) {
return client.del(...keyOrKeys);
} else {
return client.del(keyOrKeys);
}
}

@ -1,7 +1,7 @@
'use strict';
const Redlock = require('redlock').default;
const redis = require('./redis.js');
import Redlock from 'redlock';
import * as redis from './redis.js';
const redlock = new Redlock([redis.lockClient], {
retryCount: 20,
retryDelay: 500,
@ -11,4 +11,4 @@ const redlock = new Redlock([redis.lockClient], {
redlock.on('clientError', console.error);
module.exports = redlock;
export default redlock;

@ -1,17 +1,18 @@
const dotenv = require("dotenv");
dotenv.config({ path: ".env" });
const db = require('./db.js');
const { randomBytes } = require('crypto');
const bcrypt = require('bcrypt');
import dotenv from 'dotenv';
await dotenv.config({ path: '.env' });
import * as db from './db.js';
import { randomBytes } from 'crypto';
import bcrypt from 'bcrypt';
async function reset() {
await db.connect();
const numAccounts = await db.db.collection('accounts').countDocuments();
const numAccounts = await db.db().collection('accounts').countDocuments();
const randomPassword = randomBytes(20).toString('base64');
console.log(randomPassword);
const passwordHash = await bcrypt.hash(randomPassword, 12);
if (numAccounts === 0) {
await db.db.collection('accounts')
await db.db().collection('accounts')
.insertOne({
_id: 'admin',
passwordHash: passwordHash,
@ -19,19 +20,25 @@ async function reset() {
clusters: [process.env.DEFAULT_CLUSTER],
activeCluster: 0,
balance: 0,
onboarding: null,
});
} else {
await db.db.collection('accounts')
await db.db().collection('accounts')
.updateOne({
_id: 'admin'
}, {
$set: {
passwordHash,
clusters: [process.env.DEFAULT_CLUSTER],
domains: ['localhost'],
clusters: [process.env.DEFAULT_CLUSTER],
activeCluster: 0,
balance: 0,
onboarding: true,
}
});
}
db.client.close();
db.client().close();
}
module.exports.default = reset;
reset();

@ -1,262 +1,604 @@
const express = require('express')
, dev = process.env.NODE_ENV !== 'production'
, session = require('express-session')
, MongoStore = require('connect-mongo')
, db = require('./db.js')
, csrf = require('csurf')
, OpenAPIClientAxios = require('openapi-client-axios').default
, { dynamicResponse } = require('./util.js')
, definition = require('./specification_openapiv3.js')
, fetch = require('node-fetch')
, FormData = require('form-data')
, update = require('./update.js')
, agent = require('./agent.js');
import express from 'express';
import session from 'express-session';
import MongoStore from 'connect-mongo';
import csrf from 'csurf';
import OpenAPIClientAxios from 'openapi-client-axios';
import fetch from 'node-fetch';
import FormData from 'form-data';
const testRouter = (server, app) => {
import * as db from './db.js';
import { dynamicResponse } from './util.js';
import definition from './specification_openapiv3.js';
import * as update from './update.js';
import * as agent from './agent.js';
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: !dev, //TODO: check https
sameSite: 'strict',
maxAge: 1000 * 60 * 60 * 24 * 30, //month
}
});
import * as accountController from './controllers/account.js';
import * as mapsController from './controllers/maps.js';
import * as clustersController from './controllers/clusters.js';
import * as certsController from './controllers/certs.js';
import * as dnsController from './controllers/dns.js';
import * as domainsController from './controllers/domains.js';
const useSession = (req, res, next) => {
sessionStore(req, res, next);
};
const dev = process.env.NODE_ENV !== 'production';
const fetchSession = async (req, res, next) => {
if (req.session.user) {
const account = await db.db.collection('accounts')
.findOne({ _id: req.session.user });
if (account) {
const numCerts = await db.db.collection('certs')
.countDocuments({ username: account._id });
const strippedClusters = account.clusters
.map(c => {
return c.split(',')
.map(clusterString => {
const clusterUrl = new URL(clusterString);
clusterUrl.username = '';
clusterUrl.password = '';
return clusterUrl.toString();
})
.join(',');
});
res.locals.clusters = account.clusters;
res.locals.user = {
username: account._id,
domains: account.domains,
clusters: strippedClusters,
activeCluster: account.activeCluster,
onboarding: account.onboarding,
numCerts,
};
return next();
}
req.session.destroy();
}
next();
};
export default function router(server, app) {
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: !dev, //TODO: check https
sameSite: 'strict',
maxAge: 1000 * 60 * 60 * 24 * 30, //month
},
});
const checkSession = (req, res, next) => {
if (!res.locals.user) {
return dynamicResponse(req, res, 302, { redirect: '/login' });
}
next();
};
const useSession = (req, res, next) => {
sessionStore(req, res, next);
};
const checkOnboarding = (req, res, next) => {
if (!res.locals.user || res.locals.user.onboarding === false) {
return dynamicResponse(req, res, 302, { redirect: '/onboarding' });
const fetchSession = async (req, res, next) => {
if (req.session.user) {
const account = await db.db().collection('accounts')
.findOne({ _id: req.session.user });
if (account) {
const numCerts = await db.db().collection('certs')
.countDocuments({ username: account._id });
const strippedClusters = account.clusters
.map((c) => {
return c.split(',')
.map((clusterString) => {
const clusterUrl = new URL(clusterString);
clusterUrl.username = '';
clusterUrl.password = '';
return clusterUrl.toString();
})
.join(',');
});
res.locals.clusters = account.clusters;
res.locals.user = {
username: account._id,
domains: account.domains,
clusters: strippedClusters,
activeCluster: account.activeCluster,
onboarding: account.onboarding,
numCerts,
};
return next();
}
next();
};
req.session.destroy();
}
next();
};
const csrfMiddleware = csrf();
const checkSession = (req, res, next) => {
if (!res.locals.user) {
return dynamicResponse(req, res, 302, { redirect: '/login' });
}
next();
};
//dataplaneapi middleware
const useHaproxy = (req, res, next) => {
if (res.locals.clusters.length === 0) {
return next();
}
try {
res.locals.fMap = server.locals.fMap;
res.locals.mapValueNames = server.locals.mapValueNames;
const clusterUrls = res.locals.clusters[res.locals.user.activeCluster]
.split(',')
.map(u => new URL(u));
const firstClusterURL = clusterUrls[0];
const checkOnboarding = (req, res, next) => {
if (!res.locals.user || res.locals.user.onboarding === false) {
return dynamicResponse(req, res, 302, { redirect: '/onboarding' });
}
next();
};
const csrfMiddleware = csrf();
//dataplaneapi middleware
const useHaproxy = (req, res, next) => {
if (res.locals.clusters.length === 0) {
return next();
}
try {
res.locals.fMap = server.locals.fMap;
res.locals.mapValueNames = server.locals.mapValueNames;
const clusterUrls = res.locals.clusters[res.locals.user.activeCluster]
.split(',')
.map((u) => new URL(u));
const firstClusterURL = clusterUrls[0];
//NOTE: all servers in cluster must have same credentials for now
const base64Auth = Buffer.from(`${firstClusterURL.username}:${firstClusterURL.password}`).toString("base64");
const api = new OpenAPIClientAxios({
//definition: `${firstClusterURL.origin}/v2/specification_openapiv3`,
definition,
axiosConfigDefaults: {
httpsAgent: agent,
headers: {
'authorization': `Basic ${base64Auth}`,
//NOTE: all servers in cluster must have same credentials for now
const base64Auth = Buffer.from(
`${firstClusterURL.username}:${firstClusterURL.password}`,
).toString('base64');
const api = new OpenAPIClientAxios.default({
//definition: `${firstClusterURL.origin}/v2/specification_openapiv3`,
definition,
axiosConfigDefaults: {
httpsAgent: agent,
headers: {
'authorization': `Basic ${base64Auth}`,
},
},
});
const apiInstance = api.initSync();
apiInstance.defaults.baseURL = `${firstClusterURL.origin}/v2`;
res.locals.dataPlane = apiInstance;
async function dataPlaneRetry(operationId, ...args) {
let retryCnt = 0;
function run() {
return apiInstance[operationId](...args).catch(function (err) {
if (
operationId === 'getRuntimeMapEntry' && err && err.response &&
err.response.data && err.response.data.code === 404
) {
return null;
}
}
});
const apiInstance = api.initSync();
apiInstance.defaults.baseURL = `${firstClusterURL.origin}/v2`;
res.locals.dataPlane = apiInstance;
async function dataPlaneRetry(operationId, ...args) {
let retryCnt = 0;
function run() {
return apiInstance[operationId](...args).catch(function (err) {
if (operationId === 'getRuntimeMapEntry' && err && err.response
&& err.response.data && err.response.data.code === 404) {
return null;
}
++retryCnt;
console.error('dataPlaneRetry retry', retryCnt, ' after error', err);
console.trace();
apiInstance.defaults.baseURL = `${clusterUrls[retryCnt].origin}/v2`;
if (retryCnt > clusterUrls.length-1) {
console.error('Max retries exceeded in dataPlaneRetry', err.message);
throw err;
}
return run();
});
}
return run();
++retryCnt;
console.error(
'dataPlaneRetry retry',
retryCnt,
' after error',
err,
);
console.trace();
apiInstance.defaults.baseURL = `${clusterUrls[retryCnt].origin}/v2`;
if (retryCnt > clusterUrls.length - 1) {
console.error(
'Max retries exceeded in dataPlaneRetry',
err.message,
);
throw err;
}
return run();
});
}
res.locals.dataPlaneRetry = dataPlaneRetry;
return run();
}
res.locals.dataPlaneRetry = dataPlaneRetry;
res.locals.dataPlaneAll = async (operationId, parameters, data, config, all=false) => {
const promiseResults = await Promise.all(clusterUrls.map(clusterUrl => {
const singleApi = new OpenAPIClientAxios({ definition, axiosConfigDefaults: { httpsAgent: agent, headers: { 'authorization': `Basic ${base64Auth}` } } });
res.locals.dataPlaneAll = async (
operationId,
parameters,
data,
config,
all = false,
) => {
const promiseResults = await Promise.all(
clusterUrls.map((clusterUrl) => {
const singleApi = new OpenAPIClientAxios.default({
definition,
axiosConfigDefaults: {
httpsAgent: agent,
headers: { 'authorization': `Basic ${base64Auth}` },
},
});
const singleApiInstance = singleApi.initSync();
singleApiInstance.defaults.baseURL = `${clusterUrl.origin}/v2`;
return singleApiInstance[operationId](parameters, data, { ...config, baseUrl: `${clusterUrl.origin}/v2` });
}));
return all ? promiseResults.map(p => p.data) : promiseResults[0]; //TODO: better desync handling
}
res.locals.postFileAll = async (path, options, file, fdOptions) => {
//used for stuff that dataplaneapi with axios seems to struggle with e.g. multipart body
const promiseResults = await Promise.all(clusterUrls.map(clusterUrl => {
return singleApiInstance[operationId](parameters, data, {
...config,
baseUrl: `${clusterUrl.origin}/v2`,
});
}),
);
return all ? promiseResults.map((p) => p.data) : promiseResults[0]; //TODO: better desync handling
};
res.locals.postFileAll = async (path, options, file, fdOptions) => {
//used for stuff that dataplaneapi with axios seems to struggle with e.g. multipart body
const promiseResults = await Promise.all(
clusterUrls.map((clusterUrl) => {
const fd = new FormData(); //must resonctruct each time, or get a socket hang up
fd.append('file_upload', file, fdOptions);
return fetch(`${clusterUrl.origin}${path}`, { ...options, body: fd, agent }).then(resp => resp.json());
}));
return promiseResults[0]; //TODO: better desync handling
}
next();
} catch (e) {
console.error(e)
return dynamicResponse(req, res, 500, { error: e });
}
};
return fetch(`${clusterUrl.origin}${path}`, {
...options,
body: fd,
agent,
}).then((resp) => resp.json());
}),
);
return promiseResults[0]; //TODO: better desync handling
};
next();
} catch (e) {
console.error(e);
return dynamicResponse(req, res, 500, { error: e });
}
};
const hasCluster = (req, res, next) => {
if (res.locals.user.clusters.length > 0 || (req.baseUrl+req.path) === '/forms/cluster/add') {
return next();
}
return dynamicResponse(req, res, 302, { redirect: '/clusters' });
};
const hasCluster = (req, res, next) => {
if (
res.locals.user.clusters.length > 0 ||
(req.baseUrl + req.path) === '/forms/cluster/add'
) {
return next();
}
return dynamicResponse(req, res, 302, { redirect: '/clusters' });
};
//Controllers
const accountController = require('./controllers/account')
, mapsController = require('./controllers/maps')
, clustersController = require('./controllers/clusters')
, certsController = require('./controllers/certs')
, dnsController = require('./controllers/dns')
, domainsController = require('./controllers/domains');
//unauthed pages
server.get('/', useSession, fetchSession, (req, res, next) => {
return app.render(req, res, '/index');
});
server.get('/login', useSession, fetchSession, (req, res, next) => {
return app.render(req, res, '/login');
});
server.get('/register', useSession, fetchSession, (req, res, next) => {
return app.render(req, res, '/register');
});
//unauthed pages
server.get('/', useSession, fetchSession, (req, res, next) => { return app.render(req, res, '/index') });
server.get('/login', useSession, fetchSession, (req, res, next) => { return app.render(req, res, '/login') });
server.get('/register', useSession, fetchSession, (req, res, next) => { return app.render(req, res, '/register') });
//register/login/logout/onboarding forms
server.post('/forms/login', useSession, accountController.login);
server.post(
'/forms/onboarding',
useSession,
fetchSession,
checkSession,
accountController.finishOnboarding,
);
server.post('/forms/logout', useSession, accountController.logout);
server.post(
'/forms/register',
useSession,
fetchSession,
accountController.register,
);
//register/login/logout/onboarding forms
server.post('/forms/login', useSession, accountController.login);
server.post('/forms/onboarding', useSession, fetchSession, checkSession, accountController.finishOnboarding);
server.post('/forms/logout', useSession, accountController.logout);
server.post('/forms/register', useSession, fetchSession, accountController.register);
const mapNames = [
process.env.BLOCKED_IP_MAP_NAME,
process.env.BLOCKED_ASN_MAP_NAME,
process.env.MAINTENANCE_MAP_NAME,
process.env.WHITELIST_MAP_NAME,
process.env.REDIRECT_MAP_NAME,
process.env.BACKENDS_MAP_NAME,
process.env.DDOS_MAP_NAME,
process.env.DDOS_CONFIG_MAP_NAME,
process.env.HOSTS_MAP_NAME,
process.env.REWRITE_MAP_NAME,
],
mapNamesOrString = mapNames.join('|');
const mapNames = [process.env.BLOCKED_IP_MAP_NAME, process.env.BLOCKED_ASN_MAP_NAME, process.env.MAINTENANCE_MAP_NAME, process.env.WHITELIST_MAP_NAME, process.env.REDIRECT_MAP_NAME,
process.env.BACKENDS_MAP_NAME, process.env.DDOS_MAP_NAME, process.env.DDOS_CONFIG_MAP_NAME, process.env.HOSTS_MAP_NAME, process.env.REWRITE_MAP_NAME]
, mapNamesOrString = mapNames.join('|');
//authed pages
server.get(
'/account',
useSession,
fetchSession,
checkSession,
checkOnboarding,
useHaproxy,
csrfMiddleware,
accountController.accountPage.bind(null, app),
);
server.get(
'/onboarding',
useSession,
fetchSession,
checkSession,
useHaproxy,
csrfMiddleware,
accountController.onboardingPage.bind(null, app),
);
server.get(
'/account.json',
useSession,
fetchSession,
checkSession,
useHaproxy,
csrfMiddleware,
accountController.accountJson,
);
server.get(
`/map/:name(${mapNamesOrString})`,
useSession,
fetchSession,
checkSession,
checkOnboarding,
useHaproxy,
hasCluster,
csrfMiddleware,
mapsController.mapPage.bind(null, app),
);
server.get(
`/map/:name(${mapNamesOrString}).json`,
useSession,
fetchSession,
checkSession,
useHaproxy,
hasCluster,
csrfMiddleware,
mapsController.mapJson,
);
server.get(
'/clusters',
useSession,
fetchSession,
checkSession,
checkOnboarding,
csrfMiddleware,
clustersController.clustersPage.bind(null, app),
);
server.get(
'/clusters.json',
useSession,
fetchSession,
checkSession,
csrfMiddleware,
clustersController.clustersJson,
);
server.get(
'/domains',
useSession,
fetchSession,
checkSession,
csrfMiddleware,
domainsController.domainsPage.bind(null, app),
);
server.get(
'/domains.json',
useSession,
fetchSession,
checkSession,
csrfMiddleware,
domainsController.domainsJson,
);
server.get(
'/dns/:domain([a-zA-Z0-9-\.]+)/new',
useSession,
fetchSession,
checkSession,
csrfMiddleware,
dnsController.dnsRecordPage.bind(null, app),
);
server.get(
'/dns/:domain([a-zA-Z0-9-\.]+).json',
useSession,
fetchSession,
checkSession,
csrfMiddleware,
dnsController.dnsDomainJson,
);
server.get(
'/dns/:domain([a-zA-Z0-9-\.]+)',
useSession,
fetchSession,
checkSession,
csrfMiddleware,
dnsController.dnsDomainPage.bind(null, app),
);
server.get(
'/dns/:domain([a-zA-Z0-9-\.]+)/:zone([a-zA-Z0-9-\.@_]+)/:type([a-z]+).json',
useSession,
fetchSession,
checkSession,
csrfMiddleware,
dnsController.dnsRecordJson,
);
server.get(
'/dns/:domain([a-zA-Z0-9-\.]+)/:zone([a-zA-Z0-9-\.@_]+)/:type([a-z]+)',
useSession,
fetchSession,
checkSession,
csrfMiddleware,
dnsController.dnsRecordPage.bind(null, app),
);
server.get(
'/certs',
useSession,
fetchSession,
checkSession,
checkOnboarding,
useHaproxy,
csrfMiddleware,
certsController.certsPage.bind(null, app),
);
server.get(
'/certs.json',
useSession,
fetchSession,
checkSession,
useHaproxy,
csrfMiddleware,
certsController.certsJson,
);
//authed pages
server.get('/account', useSession, fetchSession, checkSession, checkOnboarding, useHaproxy, csrfMiddleware, accountController.accountPage.bind(null, app));
server.get('/onboarding', useSession, fetchSession, checkSession, useHaproxy, csrfMiddleware, accountController.onboardingPage.bind(null, app));
server.get('/account.json', useSession, fetchSession, checkSession, useHaproxy, csrfMiddleware, accountController.accountJson);
server.get(`/map/:name(${mapNamesOrString})`, useSession, fetchSession, checkSession, checkOnboarding, useHaproxy, hasCluster, csrfMiddleware, mapsController.mapPage.bind(null, app));
server.get(`/map/:name(${mapNamesOrString}).json`, useSession, fetchSession, checkSession, useHaproxy, hasCluster, csrfMiddleware, mapsController.mapJson);
server.get('/clusters', useSession, fetchSession, checkSession, checkOnboarding, csrfMiddleware, clustersController.clustersPage.bind(null, app));
server.get('/clusters.json', useSession, fetchSession, checkSession, csrfMiddleware, clustersController.clustersJson);
server.get('/domains', useSession, fetchSession, checkSession, csrfMiddleware, domainsController.domainsPage.bind(null, app));
server.get('/domains.json', useSession, fetchSession, checkSession, csrfMiddleware, domainsController.domainsJson);
server.get('/dns/:domain([a-zA-Z0-9-\.]+)/new', useSession, fetchSession, checkSession, csrfMiddleware, dnsController.dnsRecordPage.bind(null, app));
server.get('/dns/:domain([a-zA-Z0-9-\.]+).json', useSession, fetchSession, checkSession, csrfMiddleware, dnsController.dnsDomainJson);
server.get('/dns/:domain([a-zA-Z0-9-\.]+)', useSession, fetchSession, checkSession, csrfMiddleware, dnsController.dnsDomainPage.bind(null, app));
server.get('/dns/:domain([a-zA-Z0-9-\.]+)/:zone([a-zA-Z0-9-\.@_]+)/:type([a-z]+).json', useSession, fetchSession, checkSession, csrfMiddleware, dnsController.dnsRecordJson);
server.get('/dns/:domain([a-zA-Z0-9-\.]+)/:zone([a-zA-Z0-9-\.@_]+)/:type([a-z]+)', useSession, fetchSession, checkSession, csrfMiddleware, dnsController.dnsRecordPage.bind(null, app));
server.get('/certs', useSession, fetchSession, checkSession, checkOnboarding, useHaproxy, csrfMiddleware, certsController.certsPage.bind(null, app));
server.get('/certs.json', useSession, fetchSession, checkSession, useHaproxy, csrfMiddleware, certsController.certsJson);
// server.get('/stats', useSession, fetchSession, checkSession, useHaproxy, csrfMiddleware, accountController.statsPage.bind(null, app));
// server.get('/stats.json', useSession, fetchSession, checkSession, useHaproxy, csrfMiddleware, accountController.statsJson);
const clusterRouter = express.Router({ caseSensitive: true });
clusterRouter.post('/global/toggle', useSession, fetchSession, checkSession, useHaproxy, hasCluster, csrfMiddleware, accountController.globalToggle);
clusterRouter.post(`/map/:name(${mapNamesOrString})/add`, useSession, fetchSession, checkSession, useHaproxy, hasCluster, csrfMiddleware, mapsController.patchMapForm);
clusterRouter.post(`/map/:name(${mapNamesOrString})/delete`, useSession, fetchSession, checkSession, useHaproxy, hasCluster, csrfMiddleware, mapsController.deleteMapForm);
clusterRouter.post('/dns/:domain([a-zA-Z0-9-\.]+)/:zone([a-zA-Z0-9-\.@_]+)/:type([a-z_]+)/delete', useSession, fetchSession, checkSession, csrfMiddleware, dnsController.dnsRecordDelete);
clusterRouter.post('/dns/:domain([a-zA-Z0-9-\.]+)/:zone([a-zA-Z0-9-\.@_]+)/:type([a-z_]+)', useSession, fetchSession, checkSession, csrfMiddleware, dnsController.dnsRecordUpdate);
clusterRouter.post('/cluster', useSession, fetchSession, checkSession, hasCluster, csrfMiddleware, clustersController.setCluster);
clusterRouter.post('/cluster/add', useSession, fetchSession, checkSession, hasCluster, csrfMiddleware, clustersController.addCluster);
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, useHaproxy, hasCluster, csrfMiddleware, domainsController.deleteDomain);
clusterRouter.post('/cert/add', useSession, fetchSession, checkSession, useHaproxy, hasCluster, csrfMiddleware, certsController.addCert);
clusterRouter.post('/cert/upload', useSession, fetchSession, checkSession, useHaproxy, hasCluster, csrfMiddleware, certsController.uploadCert);
clusterRouter.post('/cert/delete', useSession, fetchSession, checkSession, useHaproxy, hasCluster, csrfMiddleware, certsController.deleteCert);
clusterRouter.post('/csr/verify', useSession, fetchSession, checkSession, hasCluster, csrfMiddleware, certsController.verifyUserCSR);
clusterRouter.post('/template', useSession, fetchSession, checkSession, hasCluster, csrfMiddleware, async (req, res, next) => {
if (res.locals.user.username !== "admin") {
const clusterRouter = express.Router({ caseSensitive: true });
clusterRouter.post(
'/global/toggle',
useSession,
fetchSession,
checkSession,
useHaproxy,
hasCluster,
csrfMiddleware,
accountController.globalToggle,
);
clusterRouter.post(
`/map/:name(${mapNamesOrString})/add`,
useSession,
fetchSession,
checkSession,
useHaproxy,
hasCluster,
csrfMiddleware,
mapsController.patchMapForm,
);
clusterRouter.post(
`/map/:name(${mapNamesOrString})/delete`,
useSession,
fetchSession,
checkSession,
useHaproxy,
hasCluster,
csrfMiddleware,
mapsController.deleteMapForm,
);
clusterRouter.post(
'/dns/:domain([a-zA-Z0-9-\.]+)/:zone([a-zA-Z0-9-\.@_]+)/:type([a-z_]+)/delete',
useSession,
fetchSession,
checkSession,
csrfMiddleware,
dnsController.dnsRecordDelete,
);
clusterRouter.post(
'/dns/:domain([a-zA-Z0-9-\.]+)/:zone([a-zA-Z0-9-\.@_]+)/:type([a-z_]+)',
useSession,
fetchSession,
checkSession,
csrfMiddleware,
dnsController.dnsRecordUpdate,
);
clusterRouter.post(
'/cluster',
useSession,
fetchSession,
checkSession,
hasCluster,
csrfMiddleware,
clustersController.setCluster,
);
clusterRouter.post(
'/cluster/add',
useSession,
fetchSession,
checkSession,
hasCluster,
csrfMiddleware,
clustersController.addCluster,
);
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,
useHaproxy,
hasCluster,
csrfMiddleware,
domainsController.deleteDomain,
);
clusterRouter.post(
'/cert/add',
useSession,
fetchSession,
checkSession,
useHaproxy,
hasCluster,
csrfMiddleware,
certsController.addCert,
);
clusterRouter.post(
'/cert/upload',
useSession,
fetchSession,
checkSession,
useHaproxy,
hasCluster,
csrfMiddleware,
certsController.uploadCert,
);
clusterRouter.post(
'/cert/delete',
useSession,
fetchSession,
checkSession,
useHaproxy,
hasCluster,
csrfMiddleware,
certsController.deleteCert,
);
clusterRouter.post(
'/csr/verify',
useSession,
fetchSession,
checkSession,
hasCluster,
csrfMiddleware,
certsController.verifyUserCSR,
);
clusterRouter.post(
'/template',
useSession,
fetchSession,
checkSession,
hasCluster,
csrfMiddleware,
async (req, res, next) => {
if (res.locals.user.username !== 'admin') {
return dynamicResponse(req, res, 403, { error: 'No permission' });
}
const { id, data } = req.body
const { id, data } = req.body;
if (!id || !data) {
return dynamicResponse(req, res, 403, { error: 'Invalid input' });
}
await db.db.collection('templates').updateOne({ _id: id }, { $set: { data } }, { upsert: true });
await db.db().collection('templates').updateOne({ _id: id }, {
$set: { data },
}, { upsert: true });
return res.json({ ok: true });
});
clusterRouter.post('/update', useSession, fetchSession, checkSession, hasCluster, csrfMiddleware, async (req, res, next) => {
if (res.locals.user.username !== "admin") {
},
);
clusterRouter.post(
'/update',
useSession,
fetchSession,
checkSession,
hasCluster,
csrfMiddleware,
async (req, res, next) => {
if (res.locals.user.username !== 'admin') {
return dynamicResponse(req, res, 403, { error: 'No permission' });
}
await update();
return res.json({ ok: true });
});
clusterRouter.post('/down', useSession, fetchSession, checkSession, hasCluster, csrfMiddleware, async (req, res, next) => {
if (res.locals.user.username !== "admin") {
},
);
clusterRouter.post(
'/down',
useSession,
fetchSession,
checkSession,
hasCluster,
csrfMiddleware,
async (req, res, next) => {
if (res.locals.user.username !== 'admin') {
return dynamicResponse(req, res, 403, { error: 'No permission' });
}
const ips = req.body.ips.filter(x => x && x.length > 0);
const ips = req.body.ips.filter((x) => x && x.length > 0);
if (ips.length === 0) {
await db.db.collection('down').updateOne({ _id: 'down' }, { $set: { ips: [] } }, { upsert: true });
await db.db().collection('down').updateOne({ _id: 'down' }, {
$set: { ips: [] },
}, { upsert: true });
} else {
await db.db.collection('down').updateOne({ _id: 'down' }, { $addToSet: { ips: { '$each': ips } } }, { upsert: true });
await db.db().collection('down').updateOne({ _id: 'down' }, {
$addToSet: { ips: { '$each': ips } },
}, { upsert: true });
}
return res.json({ ok: true });
});
clusterRouter.get('/csrf', useSession, fetchSession, checkSession, hasCluster, csrfMiddleware, (req, res, next) => {
},
);
clusterRouter.get(
'/csrf',
useSession,
fetchSession,
checkSession,
hasCluster,
csrfMiddleware,
(req, res, next) => {
return res.send(req.csrfToken());
});
server.use('/forms', clusterRouter);
};
module.exports = testRouter;
},
);
server.use('/forms', clusterRouter);
}

@ -4,22 +4,24 @@ process
.on('uncaughtException', console.error)
.on('unhandledRejection', console.error);
const dotenv = require('dotenv');
import dotenv from 'dotenv';
dotenv.config({ path: '.env' });
const server = require('express')
, nextjs = require('next')
, dev = process.env.NODE_ENV !== 'production'
import express from 'express';
import next from 'next';
import bodyParser from 'body-parser';
import cookieParser from 'cookie-parser';
import * as acme from './acme.js';
import * as redis from './redis.js';
import * as db from './db.js';
import router from './router.js';
const dev = process.env.NODE_ENV !== 'production'
, hostname = 'localhost'
, port = 3000
, app = nextjs({ dev, hostname, port })
, handle = app.getRequestHandler()
, express = require('express')
, bodyParser = require('body-parser')
, cookieParser = require('cookie-parser')
, acme = require('./acme.js')
, redis = require('./redis.js')
, db = require('./db.js');
, app = next({ dev, hostname, port })
, handle = app.getRequestHandler();
app.prepare()
.then(async () => {
@ -34,16 +36,15 @@ app.prepare()
server.use(cookieParser(process.env.COOKIE_SECRET));
server.disable('x-powered-by');
server.set('trust proxy', 1);
server.use('/.well-known/acme-challenge', express.static('/tmp/.well-known/acme-challenge'))
server.use('/.well-known/acme-challenge', express.static('/tmp/.well-known/acme-challenge'));
const testRouter = require('./router.js');
testRouter(server, app);
router(server, app);
server.get('*', (req, res) => {
return handle(req, res);
});
server.use((err, req, res, next) => {
server.use((err, _req, res, _next) => {
const now = Date.now();
console.error('An error occurred', now, err);
return res.send('An error occurred. Please contact support with code: '+now);
@ -60,7 +61,6 @@ app.prepare()
console.log('> Ready on http://localhost:3000');
});
//graceful stop handling
const gracefulStop = () => {
console.log('SIGINT SIGNAL RECEIVED');
db.client.close();

File diff suppressed because one or more lines are too long

@ -1,40 +1,40 @@
const db = require('./db.js');
import * as db from './db.js';
exports.aTemplate = () => {
return db.db.collection('templates').findOne({ _id: 'a' }).then(res => res.data);
export function aTemplate() {
return db.db().collection('templates').findOne({ _id: 'a' }).then(res => res.data);
}
exports.aaaaTemplate = () => {
return db.db.collection('templates').findOne({ _id: 'aaaa' }).then(res => res.data);
export function aaaaTemplate() {
return db.db().collection('templates').findOne({ _id: 'aaaa' }).then(res => res.data);
};
exports.soaTemplate = () => Object.seal(Object.freeze(Object.preventExtensions([
export const soaTemplate = () => Object.seal(Object.freeze(Object.preventExtensions([
{
"ttl": 86400,
"ns": "ns1.basedns.net.",
"MBox": "root.basedflare.com.",
"refresh": 7200,
"retry": 3600,
"expire": 3600,
"minttl": 180,
"t": true
'ttl': 86400,
'ns': 'ns1.basedns.net.',
'MBox': 'root.basedflare.com.',
'refresh': 7200,
'retry': 3600,
'expire': 3600,
'minttl': 180,
't': true
},
])));
exports.nsTemplate = () => Object.seal(Object.freeze(Object.preventExtensions([
export const nsTemplate = () => Object.seal(Object.freeze(Object.preventExtensions([
{
"ttl": 86400,
"host": "ns1.basedns.net.",
"t": true
'ttl': 86400,
'host': 'ns1.basedns.net.',
't': true
},
{
"ttl": 86400,
"host": "ns2.basedns.cloud.",
"t": true
'ttl': 86400,
'host': 'ns2.basedns.cloud.',
't': true
},
{
"ttl": 86400,
"host": "ns3.basedns.services.",
"t": true
'ttl': 86400,
'host': 'ns3.basedns.services.',
't': true
}
])));

@ -1,17 +1,19 @@
'use strict';
process
.on('uncaughtException', console.error)
.on('unhandledRejection', console.error);
.on('uncaughtException', console.error)
.on('unhandledRejection', console.error);
const dotenv = require('dotenv');
const db = require('./db.js');
dotenv.config({ path: '.env' });
const redis = require('./redis.js');
let nsTemplate = null;
let soaTemplate = null;
let aTemplate = null;
let aaaaTemplate = null;
import dotenv from 'dotenv';
import * as db from './db.js';
await dotenv.config({ path: '.env' });
import * as redis from './redis.js';
import { pathToFileURL } from 'node:url';
const isMain = import.meta.url === pathToFileURL(process.argv[1]).href;
isMain && await db.connect();
import { nsTemplate, soaTemplate, aTemplate, aaaaTemplate } from './templates.js';
isMain && update();
async function processKey(domainKey) {
const domainHashKeys = await redis.client.hkeys(domainKey);
@ -20,18 +22,18 @@ async function processKey(domainKey) {
try {
console.log('Updating', domain);
const records = await redis.hget(domainKey, hkey);
if (records['a'] && records['a'][0]["t"] === true) {
if (records['a'] && records['a'][0]['t'] === true) {
records['a'] = JSON.parse(JSON.stringify((await aTemplate())));
}
if (records['aaaa'] && records['aaaa'][0]["t"] === true) {
if (records['aaaa'] && records['aaaa'][0]['t'] === true) {
records['aaaa'] = JSON.parse(JSON.stringify((await aaaaTemplate())));
}
if (records['ns'] && records['ns'][0]["t"] === true) {
if (records['ns'] && records['ns'][0]['t'] === true) {
const locked = records['ns']['l'] === true;
records['ns'] = JSON.parse(JSON.stringify(nsTemplate()));
records['ns'].forEach(r => r['l'] = locked);
}
if (records['soa'] && records['soa']["t"] === true) {
if (records['soa'] && records['soa']['t'] === true) {
const locked = records['soa']['l'] === true;
records['soa'] = JSON.parse(JSON.stringify(soaTemplate()))[0];
records['soa']['l'] = locked;
@ -44,7 +46,7 @@ async function processKey(domainKey) {
}));
}
async function update() {
export default async function update() {
let allKeys = [];
const stream = redis.client.scanStream({
match: 'dns:*',
@ -55,27 +57,11 @@ async function update() {
});
stream.on('end', async () => {
await Promise.all(allKeys.map(async k => processKey(k)))
.catch(e => console.error(e))
if (require.main === module) {
redis.close();
}
.catch(e => console.error(e));
isMain && redis.close();
});
stream.on('error', (err) => {
console.err(err);
if (require.main === module) {
redis.close();
}
isMain && redis.close();
});
}
module.exports = update;
if (require.main === module) {
(async () => {
await db.connect();
({ nsTemplate, soaTemplate, aTemplate, aaaaTemplate } = require('./templates.js'));
update();
})();
} else {
({ nsTemplate, soaTemplate, aTemplate, aaaaTemplate } = require('./templates.js'));
}

@ -1,7 +1,9 @@
const url = require('url');
import url from 'url';
const fMap = {
import dotenv from 'dotenv';
dotenv.config({ path: '.env' });
const fMap = {
[process.env.HOSTS_MAP_NAME]: {
fname: 'Backends',
description: 'Backend IP mappings for domains',
@ -18,7 +20,13 @@ const fMap = {
[process.env.DDOS_CONFIG_MAP_NAME]: {
fname: 'Protection Settings',
description: 'Customise protection settings on a per-domain basis',
columnNames: ['Domain/Path', 'Difficulty', 'POW Type', 'Expiry', 'Lock cookie to IP'],
columnNames: [
'Domain/Path',
'Difficulty',
'POW Type',
'Expiry',
'Lock cookie to IP',
],
columnKeys: ['pd', 'pt', 'cex', 'cip'],
},
@ -42,7 +50,8 @@ const fMap = {
[process.env.MAINTENANCE_MAP_NAME]: {
fname: 'Maintenance Mode',
description: 'Disable proxying and show maintenance page for selected domains',
description:
'Disable proxying and show maintenance page for selected domains',
columnNames: ['Domain', ''],
},
@ -57,85 +66,80 @@ const fMap = {
description: 'Redirect one domain to another, stripping path',
columnNames: ['Domain', 'Redirect to'],
},
// [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'],
// },
};
module.exports = {
fMap,
makeArrayIfSingle: (obj) => !Array.isArray(obj) ? [obj] : obj,
validClustersString: (string) => {
return !string.split(',').some(c => {
const cUrl = url.parse(c);
return (cUrl.protocol !== 'http:' || !cUrl.hostname)
});
},
extractMap: (item) => {
const name = item.file && item.file.match(/\/etc\/haproxy\/map\/(?<name>.+).map/).groups.name;
if (!fMap[name]) { return null; }
const count = item.description && item.description.match(/(?:.+entry_cnt=(?<count>\d+)$)?/).groups.count;
return {
name,
count,
id: item.id,
...fMap[name],
};
},
dynamicResponse: (req, res, code, data) => {
const isRedirect = code === 302;
if (req.headers && req.headers['content-type'] === 'application/json') {
return res
.status(isRedirect ? 200 : code)
.json(data);
}
if (isRedirect) {
return res.redirect(data.redirect);
}
//TODO: pass through app (bind to middleware) and app.render an "error" page for nojs users?
return res.status(code).send(data);
},
//check if list includes domain of a wildcard
wildcardAllowed: (domain, allowedDomains) => {
if (domain.includes('\\')) { throw new Error('Illegal wildcardAllowed'); }
const wcRegex = new RegExp(`${domain.replace(/\*\./g, "([^ ]*\\.|^)")}$`);
return allowedDomains.some(d => {
return wcRegex.test(d);
});
},
//check if a domain matches a wildcard
wildcardMatches: (domain, wildcard) => {
if (wildcard.includes('\\')) { throw new Error('Illegal wildcardMatches'); }
const wcRegex = new RegExp(`${wildcard.replace(/\*\./g, "^.*\\.")}$`);
return wcRegex.test(domain);
},
getApproxSubject: (storageName) => {
let ret = storageName
.replaceAll('_', '.')
.substr(0, storageName.length-4);
if (ret.startsWith('.')) {
ret = ret.substring(1);
}
return ret;
},
filterCertsByDomain: (certs, allowedDomains) => {
return certs.filter(c => {
let approxSubject = module.exports.getApproxSubject(c.storage_name);
return allowedDomains.includes(approxSubject);
});
},
};
export function makeArrayIfSingle(obj) {
return !Array.isArray(obj) ? [obj] : obj;
}
export function validClustersString(string) {
return !string.split(',').some((c) => {
const cUrl = url.parse(c);
return (cUrl.protocol !== 'http:' || !cUrl.hostname);
});
}
export function extractMap(item) {
const name = item.file &&
item.file.match(/\/etc\/haproxy\/map\/(?<name>.+).map/).groups.name;
if (!fMap[name]) {return null;}
const count = item.description &&
item.description.match(/(?:.+entry_cnt=(?<count>\d+)$)?/).groups.count;
return {
name,
count,
id: item.id,
...fMap[name],
};
}
export function dynamicResponse(req, res, code, data) {
const isRedirect = code === 302;
if (req.headers && req.headers['content-type'] === 'application/json') {
return res
.status(isRedirect ? 200 : code)
.json(data);
}
if (isRedirect) {
return res.redirect(data.redirect);
}
return res.status(code).send(data);
}
//check if list includes domain of a wildcard
export function wildcardAllowed(domain, allowedDomains) {
if (domain.includes('\\')) {throw new Error('Illegal wildcardAllowed');}
const wcRegex = new RegExp(`${domain.replace(/\*\./g, '([^ ]*\\.|^)')}$`);
return allowedDomains.some((d) => {
return wcRegex.test(d);
});
}
//check if a domain matches a wildcard
export function wildcardMatches(domain, wildcard) {
if (wildcard.includes('\\')) {throw new Error('Illegal wildcardMatches');}
const wcRegex = new RegExp(`${wildcard.replace(/\*\./g, '^.*\\.')}$`);
return wcRegex.test(domain);
}
export function getApproxSubject(storageName) {
let ret = storageName
.replaceAll('_', '.')
.substr(0, storageName.length - 4);
if (ret.startsWith('.')) {
ret = ret.substring(1);
}
return ret;
}
export function filterCertsByDomain(certs, allowedDomains) {
return certs.filter((c) => {
const approxSubject = module.exports.getApproxSubject(c.storage_name);
return allowedDomains.includes(approxSubject);
});
}

Loading…
Cancel
Save