Start on DNS UI

Change table appearances
Change display of missing/good cert on domains list
Change icons usedin sidebar for consistent outlined variant
develop
Thomas Lynch 10 months ago
parent 22fd2470a6
commit b22bc09d34
  1. 2
      Dockerfile
  2. 10
      README.md
  3. 2
      components/BackButton.js
  4. 16
      components/MenuLinks.js
  5. 73
      components/RecordSetRow.js
  6. 5
      controllers/account.js
  7. 3
      docker-compose.yml
  8. 3
      package.json
  9. 2
      pages/certs.js
  10. 2
      pages/clusters.js
  11. 2
      pages/csr.js
  12. 7
      pages/dns/[domain]/[zone]/[name].js
  13. 149
      pages/dns/[domain]/index.js
  14. 26
      pages/domains.js
  15. 2
      pages/map/[name].js
  16. 2
      pages/stats.js

@ -12,5 +12,3 @@ COPY . /opt
RUN npm run build
RUN npx next telemetry disable
CMD ["npm","start"]

@ -23,14 +23,8 @@ Provides a control panel interface to conveniently manage clusters (groups of id
- Maintenance mode, disables proxying for selected domains and serves an "under maintenance" page from haproxy.
- Statistics page with server and backend-level breakdowns based on haproxy stats socket data. Ability to export statistics to influxdb.
##### Todo:
- Better Multi-user support
- allow domain/cluster editing (with user dupe check) for non-admins
- problems w/ ip whitelist and blacklist
- Some kind of payment system
- More advanced rules and ability to allow/block/bot mode based on those rules.
- Show statistics from clusters or servers within a cluster.
- Intelligent auto toggling of proteciton modes based on these stats
## License
GNU AGPLv3, see [LICENSE](LICENSE).
#### Screenshots

@ -3,7 +3,7 @@ import Link from 'next/link';
export default function BackButton({ to }) {
return (
<Link href={to}>
<a className="btn btn-primary">
<a className="btn btn-primary ms-1 mt-3">
Back
</a>
</Link>

@ -31,15 +31,23 @@ export default withRouter(function MenuLinks({ router }) {
<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-card-list pe-none me-2" width="16" height="16" />
<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-hdd-rack pe-none me-2" width="16" height="16" />
<i className="bi-clouds pe-none me-2" width="16" height="16" />
Clusters
</a>
</Link>
@ -87,7 +95,7 @@ export default withRouter(function MenuLinks({ router }) {
<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-signpost pe-none me-2" width="16" height="16" />
<i className="bi-pencil pe-none me-2" width="16" height="16" />
Rewrites
</a>
</Link>
@ -95,7 +103,7 @@ export default withRouter(function MenuLinks({ router }) {
<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-fill pe-none me-2" width="16" height="16" />
<i className="bi-signpost pe-none me-2" width="16" height="16" />
Redirects
</a>
</Link>

@ -0,0 +1,73 @@
import Link from 'next/link';
export default function RecordSetRow({ domain, name, recordSet }) {
/*
"a": [
{ "id": "a", "ttl": 300, "ip": "203.28.238.247", "geok": "cn", "geov": ["OC"], "h": true, "fb": ["b", "c"], "sel": 1, "bsel": 3 },
{ "id": "b", "ttl": 300, "ip": "38.60.199.224", "geok": "cn", "geov": ["AS"], "h": true, "fb": ["a", "c"], "sel": 1, "bsel": 3 },
{ "id": "c", "ttl": 300, "ip": "45.88.201.168", "geok": "cn", "geov": ["NA"], "h": true, "fb": ["e", "d"], "sel": 1, "bsel": 3 },
{ "id": "d", "ttl": 300, "ip": "185.125.168.21", "geok": "cn", "geov": ["EU", "AF"], "h": true, "fb": ["c"], "sel": 1, "bsel": 3 },
{ "id": "e", "ttl": 300, "ip": "38.54.57.171", "geok": "cn", "geov": ["SA", "AF"], "h": true, "fb": [], "sel": 1, "bsel": 3 }
],
"aaaa": [
{ "id": "a", "ttl": 300, "ip": "2a03:94e0:ffff:185:125:168:0:21", "geok": "cn", "geov": ["EU", "AF"], "h": true, "fb": ["b"], "sel": 1, "bsel": 3 },
{ "id": "b", "ttl": 300, "ip": "2a03:94e1:ffff:45:88:201:0:168", "geok": "cn", "geov": ["NA", "SA", "AS", "OC"], "h": true, "fb": ["a"], "sel": 1, "bsel": 3 }
],
"caa": [
{ "flag": 0, "tag": "issue", "value": "letsencrypt.org" },
{ "flag": 0, "tag": "iodef", "value": "mailto:tom@69420.me" }
],
"soa": { "ttl": 86400, "minttl": 30, "mbox": "root.basedflare.com.", "ns": "esther.kikeflare.com.", "refresh": 86400, "retry": 7200, "expire": 3600 },
"txt": [
{ "ttl": 300, "text": "v=spf1 -all" }
],
"ns": [
{ "ttl": 86400, "host": "esther.kikeflare.com." },
{ "ttl": 86400, "host": "aronowitz.bfcdn.host." },
{ "ttl": 86400, "host": "goldberg.fatpeople.lol." }
]
*/
const type = recordSet[0]
const recordSetArray = Array.isArray(recordSet[1]) ? recordSet[1] : [recordSet[1]];
return (
<tr className="align-middle">
<td>
{name}
</td>
<td>
{type.toUpperCase()}
</td>
<td>
{recordSetArray.map((r, i) => {
const healthClass = r.h != null
? (r.h ? "text-success" : "text-danger")
: "";
return (<div key={i}>
<strong>{r.id ? r.id+': ' : ''}</strong>
<span className={healthClass}>{r.ip || r.host || r.value || r.ns || r.text}</span>
</div>)
})}
</td>
<td>
{recordSetArray[0].ttl}
</td>
<td>
{recordSetArray.map((r, i) => (
<div key={i}>
{r.geok ? 'Geo: ' : ''}{(r.geov||[]).join(', ')}
</div>
))}
</td>
<td>
<Link href={`/dns/${domain}/${name}/${type}`}>
<a className="btn btn-outline-primary">
Edit
</a>
</Link>
</td>
</tr>
);
}

@ -201,9 +201,9 @@ exports.login = async (req, res) => {
* POST /forms/register
* regiser
*/
exports.register = async (req, res) => {
exports.register = (req, res) => {
return dynamicResponse(req, res, 400, { error: 'Registration is currently disabled, please try again later.' });
/*
const username = req.body.username.toLowerCase();
const password = req.body.password;
const rPassword = req.body.repeat_password;
@ -237,6 +237,7 @@ exports.register = async (req, res) => {
});
return dynamicResponse(req, res, 302, { redirect: '/login' });
*/
};
/**

@ -6,7 +6,8 @@ services:
ports:
- "127.0.0.1:3000:3000"
environment:
- NODE_ENV=production
- NODE_ENV=development
command: npm run start-dev
volumes:
- type: bind
source: /tmp/acme-tests/.well-known/acme-challenge/

@ -8,7 +8,8 @@
"build": "next build",
"lint": "next lint",
"next": "next build",
"start": "node_modules/gulp/bin/gulp.js; NODE_ENV=production node server.js"
"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"
},
"keywords": [],
"author": "Thomas Lynch (fatchan) <tom@69420.me>",

@ -152,7 +152,7 @@ export default function Certs(props) {
{/* Certs table */}
<div className="table-responsive">
<table className="table table-bordered text-nowrap">
<table className="table text-nowrap m-1">
<tbody>
<tr className="align-middle">

@ -73,7 +73,7 @@ export default function Clusters(props) {
{/* Clusters table */}
<div className="table-responsive">
<table className="table table-bordered text-nowrap">
<table className="table text-nowrap m-1">
<tbody>
{clusterList}

@ -51,7 +51,7 @@ export default function Csr(props) {
</p>
<div className="table-responsive">
<table className="table table-bordered text-nowrap">
<table className="table text-nowrap m-1">
<tbody>
<tr className="align-middle">
<td className="col-1 text-center">

@ -0,0 +1,7 @@
export default function EditRecordPage({ domain, name, recordSet }) {
return 'todo';
}
export async function getServerSideProps({ req, res, query, resolvedUrl, locale, locales, defaultLocale}) {
return { props: { user: res.locals.user || null, ...query } }
}

@ -0,0 +1,149 @@
import { useRouter } from "next/router";
import React, { useState, useEffect } from 'react';
import Head from 'next/head';
import RecordSetRow from '../../../components/RecordSetRow.js';
import BackButton from '../../../components/BackButton.js';
import ErrorAlert from '../../../components/ErrorAlert.js';
import * as API from '../../../api.js';
const DnsDomainIndexPage = (props) => {
const router = useRouter();
const { domain } = router.query;
const [state, dispatch] = useState({
...props,
recordSets: [
{
"@": {
"a": [
{ "id": "a", "ttl": 300, "ip": "203.28.238.247", "geok": "cn", "geov": ["OC"], "h": true, "fb": ["b", "c"], "sel": 1, "bsel": 3 },
{ "id": "b", "ttl": 300, "ip": "38.60.199.224", "geok": "cn", "geov": ["AS"], "h": false, "fb": ["a", "c"], "sel": 1, "bsel": 3 },
{ "id": "c", "ttl": 300, "ip": "45.88.201.168", "geok": "cn", "geov": ["NA"], "h": true, "fb": ["e", "d"], "sel": 1, "bsel": 3 },
{ "id": "d", "ttl": 300, "ip": "185.125.168.21", "geok": "cn", "geov": ["EU", "AF"], "h": true, "fb": ["c"], "sel": 1, "bsel": 3 },
{ "id": "e", "ttl": 300, "ip": "38.54.57.171", "geok": "cn", "geov": ["SA", "AF"], "h": true, "fb": [], "sel": 1, "bsel": 3 }
],
"aaaa": [
{ "id": "a", "ttl": 300, "ip": "2a03:94e0:ffff:185:125:168:0:21", "geok": "cn", "geov": ["EU", "AF"], "h": true, "fb": ["b"], "sel": 1, "bsel": 3 },
{ "id": "b", "ttl": 300, "ip": "2a03:94e1:ffff:45:88:201:0:168", "geok": "cn", "geov": ["NA", "SA", "AS", "OC"], "h": true, "fb": ["a"], "sel": 1, "bsel": 3 }
],
"caa": [
{ "flag": 0, "tag": "issue", "value": "letsencrypt.org" },
{ "flag": 0, "tag": "iodef", "value": "mailto:tom@69420.me" }
],
"soa": { "ttl": 86400, "minttl": 30, "mbox": "root.basedflare.com.", "ns": "esther.kikeflare.com.", "refresh": 86400, "retry": 7200, "expire": 3600 },
"txt": [
{ "ttl": 300, "text": "v=spf1 -all" }
],
"ns": [
{ "ttl": 86400, "host": "esther.kikeflare.com." },
{ "ttl": 86400, "host": "aronowitz.bfcdn.host." },
{ "ttl": 86400, "host": "goldberg.fatpeople.lol." }
]
}
}
]
});
const [error, setError] = useState();
useEffect(() => {
if (!state.recordSets) {
// API.getDomainRecordSets(domain, dispatch, setError, router);
}
}, [state.recordSets, domain, router]);
if (state.recordSets == null) {
return (
<div className="d-flex flex-column">
{error && <ErrorAlert error={error} />}
<div className="text-center mb-4">
Loading...
</div>
</div>
);
}
const { user, recordSets, csrf } = state;
const recordSetRows = recordSets
.map(recordSet => {
return Object.entries(recordSet)
.map(e => {
return Object.entries(e[1])
.map((recordSet, i) => (
<RecordSetRow
domain={domain}
key={`${e[0]}_${i}`}
name={e[0]}
recordSet={recordSet}
/>
));
});
})
return (
<>
<Head>
<title>
{domain} / Records List
</title>
</Head>
{error && <ErrorAlert error={error} />}
<h5 className="fw-bold">
{domain} / Records List:
</h5>
{/* Record sets table */}
<div className="table-responsive">
<table className="table text-nowrap m-1">
<tbody>
{/* header row */}
<tr>
<th>
Name
</th>
<th>
Type
</th>
<th>
Content
</th>
<th>
TTL
</th>
<th>
Details
</th>
<th>
Actions
</th>
</tr>
{recordSetRows}
</tbody>
</table>
</div>
{/* back to account */}
<BackButton to="/account" />
</>
);
};
export async function getServerSideProps({ req, res, query, resolvedUrl, locale, locales, defaultLocale}) {
return {
props: {
user: res.locals.user || null,
...query
}
};
}
export default DnsDomainIndexPage;

@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import Head from 'next/head';
import BackButton from '../components/BackButton.js';
import ErrorAlert from '../components/ErrorAlert.js';
@ -42,7 +43,9 @@ export default function Domains(props) {
await API.getDomains(dispatch, setError, router);
}
const domainList = user.domains.map((d, i) => {
const domainList = user.domains
.sort((a, b) => a.localeCompare(b))
.map((d, i) => {
//TODO: refactor, to component
const domainCert = certs.find(c => c.subject === d || c.altnames.includes(d));
return (
@ -58,7 +61,17 @@ export default function Domains(props) {
{d}
</td>
<td>
{domainCert ? '🔒 '+domainCert.storageName : '-'}
{domainCert
? <span className="text-success"><i className="bi-lock-fill pe-none me-2" width="16" height="16" />{domainCert.storageName}</span>
: <span className="text-danger"><i className="bi-exclamation-triangle-fill pe-none me-2" width="16" height="16" />No Certificate</span>}
</td>
<td className="col-1 text-center">
{d.split('.').length < 2 && <Link href={`/dns/${d}`}>
<a className="btn btn-outline-secondary">
<i className="bi-card-list pe-none me-2" width="16" height="16" />
DNS
</a>
</Link>}
</td>
</tr>
);
@ -79,7 +92,7 @@ export default function Domains(props) {
{/* Domains table */}
<div className="table-responsive">
<table className="table table-bordered text-nowrap">
<table className="table text-nowrap m-1">
<tbody>
<tr className="align-middle">
@ -88,7 +101,10 @@ export default function Domains(props) {
Domain
</th>
<th>
HTTPS?
HTTPS Certificate
</th>
<th className="col-1">
Edit DNS
</th>
</tr>
@ -96,7 +112,7 @@ export default function Domains(props) {
{/* Add new domain form */}
<tr className="align-middle">
<td className="col-1 text-center" colSpan="3">
<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} />
<input className="btn btn-success" type="submit" value="+" />

@ -161,7 +161,7 @@ const MapPage = (props) => {
{/* Map table */}
<div className="table-responsive">
<table className="table table-bordered text-nowrap">
<table className="table text-nowrap m-1">
<tbody>
{/* header row */}

@ -52,7 +52,7 @@ export default function Domains(props) {
</h5>
<div className="table-responsive">
<table className="table table-bordered text-nowrap">
<table className="table text-nowrap m-1">
<tbody>
<tr>

Loading…
Cancel
Save