Make all the tables aligned and not ugly as fuck

develop
Thomas Lynch 9 months ago
parent 1794b9b3a5
commit 20b28f3def
  1. 1
      README.md
  2. 3
      components/BackButton.js
  3. 14
      components/ClusterRow.js
  4. 8
      components/MapRow.js
  5. 2
      components/MenuLinks.js
  6. 2
      pages/_app.js
  7. 121
      pages/certs.js
  8. 41
      pages/clusters.js
  9. 10
      pages/csr.js
  10. 21
      pages/domains.js
  11. 170
      pages/map/[name].js

@ -8,7 +8,6 @@ Intended for use with [haproxy-protection](https://gitgud.io/fatchan/haproxy-pro
Provides a control panel interface to conveniently manage clusters (groups of identically configured) HAProxy servers. Can be used with a single server cluster. Uses haproxy runtime apis to update maps, acls, etc.
##### Features:
- Works with javascript disabled on the client (next.js server side rendering).
- List/add/remove clusters (server groups).
- List/add/remove domains for your account.
- Control allowed hosts for a cluster.

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

@ -4,19 +4,11 @@ export default function ClusterRow({ i, cluster, setCluster, deleteCluster, csrf
return (
<tr className="align-middle">
<td className="col-1 text-center">
<form onSubmit={deleteCluster} action="/forms/cluster/delete" method="post">
<input type="hidden" name="_csrf" value={csrf} />
<input type="hidden" name="cluster" value={cluster} />
<input className="btn btn-danger" type="submit" value="×" />
</form>
<td className="text-left" style={{width:0}}>
<input onClick={() => deleteCluster(csrf, key)} className="btn btn-danger" type="button" value="×" />
</td>
<td className="col-1 text-center">
<form onSubmit={setCluster} action="/forms/cluster" method="post">
<input type="hidden" name="_csrf" value={csrf} />
<input type="hidden" name="cluster" value={i} />
<input className="btn btn-primary" type="submit" value="Select" disabled={(i === user.activeCluster ? 'disabled' : null)} />
</form>
<input onSubmit={() => setCluster(csrf, i)} className="btn btn-primary" type="button" value="Select" disabled={(i === user.activeCluster ? 'disabled' : null)} />
</td>
<td>
({splitCluster.length}): {splitCluster.map(c => new URL(c).hostname).join(', ')}

@ -4,12 +4,8 @@ export default function MapRow({ row, onDeleteSubmit, name, csrf, showValues, ma
return (
<tr className="align-middle">
<td className="col-1 text-center">
<form onSubmit={onDeleteSubmit} action={`/forms/map/${name}/delete`} method="post">
<input type="hidden" name="_csrf" value={csrf} />
<input type="hidden" name="key" value={key} />
<input className="btn btn-danger" type="submit" value="×" />
</form>
<td className="text-left">
<input onClick={() => onDeleteSubmit(csrf, key)} className="btn btn-danger" type="button" value="×" />
</td>
<td>
{key}

@ -86,7 +86,7 @@ export default withRouter(function MenuLinks({ router }) {
</li>
<li className="nav-item">
<Link href="/map/ddos_config">
<a className={router.pathname === "/map/[name]" && router.query.name === "ddos" ? "nav-link active" : "nav-link text-body"} aria-current="page">
<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" />
Protection Settings
</a>

@ -28,7 +28,7 @@ export default function App({ Component, pageProps }) {
.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: max-content; min-width: 600px; background-color: var(--bs-body-bg); }
.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; }

@ -42,12 +42,11 @@ export default function Certs(props) {
await API.getCerts(dispatch, setError, router);
}
async function deleteCert(e) {
e.preventDefault();
async function deleteCert(csrf, subject, storageName) {
await API.deleteCert({
_csrf: csrf,
subject: e.target.subject.value,
storage_name: e.target.storage_name ? e.target.storage_name.value : null,
subject,
storage_name: storageName,
}, dispatch, setError, router);
await API.getCerts(dispatch, setError, router);
}
@ -71,12 +70,7 @@ export default function Certs(props) {
return (
<tr key={'clusterOnlyCertList'+i} className="align-middle">
<td className="col-1 text-center">
{<form onSubmit={deleteCert} action="/forms/cert/delete" method="post">
<input type="hidden" name="_csrf" value={csrf} />
<input type="hidden" name="subject" value={approxSubject} />
<input type="hidden" name="storage_name" value={c.storage_name} />
<input className="btn btn-danger" type="submit" value="×" />
</form>}
<input onClick={() => deleteCert(csrf, approxSubject, c.storage_name)} className="btn btn-danger" type="button" value="×" />
</td>
<td>
{approxSubject || '-'}
@ -102,25 +96,12 @@ export default function Certs(props) {
const inCluster = clusterCerts.some(c => c.storage_name === d.storageName);
return (
<tr key={'certList'+i} className="align-middle">
<td className="col-1 text-center">
<td className="text-left" style={{width:0}}>
{inCluster
? (<form onSubmit={deleteCert} action="/forms/cert/delete" method="post">
<input type="hidden" name="_csrf" value={csrf} />
<input type="hidden" name="subject" value={d.subject || d._id} />
<input type="hidden" name="storage_name" value={d.storageName} />
<input className="btn btn-danger" type="submit" value="×" />
</form>)
? <input onClick={() => deleteCert(csrf, (d.subject || d._id), d.storageName)} className="btn btn-danger" type="button" value="×" />
: (<>
<form onSubmit={uploadCert} action="/forms/cert/upload" method="post">
<input type="hidden" name="_csrf" value={csrf} />
<input type="hidden" name="domain" value={d.subject || d._id} />
<input className="btn btn-warning mb-2" type="submit" value="↑" />
</form>
<form onSubmit={deleteCert} action="/forms/cert/delete" method="post">
<input type="hidden" name="_csrf" value={csrf} />
<input type="hidden" name="subject" value={d.subject || d._id} />
<input className="btn btn-danger" type="submit" value="×" />
</form>
<input onClick={() => uploadCert(csrf, (d.subject || d._id))} className="btn btn-warning mb-2" type="button" value="↑" />
<input onClick={() => deleteCert(csrf, (d.subject || d._id))} className="btn btn-warning mb-2" type="button" value="×" />
</>)
}
</td>
@ -161,51 +142,55 @@ export default function Certs(props) {
{/* Certs table */}
<div className="table-responsive">
<table className="table text-nowrap m-1">
<tbody>
<tr className="align-middle">
<th className="col-1" />
<th>
Subject
</th>
<th>
Altname(s)
</th>
<th>
Expires
</th>
<th>
Storage Name
</th>
</tr>
{certList}
{clusterOnlyCerts && clusterOnlyCerts.length > 0 && (<>
<form className="d-flex" onSubmit={addCert} action="/forms/cert/add" method="post">
<input type="hidden" name="_csrf" value={csrf} />
<table className="table text-nowrap">
<tbody>
<tr className="align-middle">
<th colSpan="5">
Not in local DB:
<th style={{width:0}} />
<th>
Subject
</th>
<th>
Altname(s)
</th>
<th>
Expires
</th>
<th>
Storage Name
</th>
</tr>
{clusterOnlyCertList}
</>)}
{/* Add new cert form */}
<tr className="align-middle">
<td className="col-1 text-center" colSpan="3">
<form className="d-flex" onSubmit={addCert} action="/forms/cert/add" method="post">
<input type="hidden" name="_csrf" value={csrf} />
{certList}
{clusterOnlyCerts && clusterOnlyCerts.length > 0 && (<>
<tr className="align-middle">
<th colSpan="5">
Not in local DB:
</th>
</tr>
{clusterOnlyCertList}
</>)}
{/* Add new cert form */}
<tr className="align-middle">
<td>
<input className="btn btn-success" type="submit" value="+" />
<input className="form-control mx-3" type="text" name="subject" placeholder="domain.com" required />
<input className="form-control me-3" type="text" name="altnames" placeholder="www.domain.com,staging.domain.com,etc..." required />
</form>
</td>
<td colSpan="2"></td>
</tr>
</tbody>
</table>
</td>
<td>
<input className="form-control" type="text" name="subject" placeholder="domain.com" required />
</td>
<td>
<input className="form-control" type="text" name="altnames" placeholder="www.domain.com,staging.domain.com,etc..." required />
</td>
<td colSpan="2"></td>
</tr>
</tbody>
</table>
</form>
</div>
{/* back to account */}

@ -39,15 +39,13 @@ export default function Clusters(props) {
await API.getClusters(dispatch, setError, router);
}
async function deleteCluster(e) {
e.preventDefault();
await API.deleteCluster({ _csrf: csrf, cluster: e.target.cluster.value }, dispatch, setError, router);
async function deleteCluster(csrf, cluster) {
await API.deleteCluster({ _csrf: csrf, cluster }, dispatch, setError, router);
await API.getClusters(dispatch, setError, router);
}
async function setCluster(e) {
e.preventDefault();
await API.changeCluster({ _csrf: csrf, cluster: e.target.cluster.value }, dispatch, setError, router);
async function setCluster(csrf, cluster) {
await API.changeCluster({ _csrf: csrf, cluster }, dispatch, setError, router);
await API.getClusters(dispatch, setError, router);
}
@ -75,24 +73,27 @@ export default function Clusters(props) {
{/* Clusters table */}
<div className="table-responsive">
<table className="table text-nowrap m-1">
<tbody>
<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}
{clusterList}
{/* Add new cluster form */}
<tr className="align-middle">
<td className="col-1 text-center" colSpan="3">
<form className="d-flex" onSubmit={addCluster} action="/forms/cluster/add" method="post">
<input type="hidden" name="_csrf" value={csrf} />
{/* Add new cluster form */}
<tr className="align-middle">
<td>
<input className="btn btn-success" type="submit" value="+" />
<input className="form-control mx-3" type="text" name="cluster" placeholder="http://username:password@host:port, comma separated for multiple" required />
</form>
</td>
</tr>
</td>
<td colSpan="2">
<input className="form-control" type="text" name="cluster" placeholder="http://username:password@host:port, comma separated for multiple" required />
</td>
</tr>
</tbody>
</table>
</tbody>
</table>
</form>
</div>
{/* back to account */}

@ -47,13 +47,15 @@ export default function Csr(props) {
<p>
To generate a certificate signing request for your domain and/or subdomain(s):
<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>www.yourdomain.com</strong>{`"))`}
</code>
<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>www.yourdomain.com</strong>{`"))`}
</code>
</div>
</p>
<div className="table-responsive">
<table className="table text-nowrap m-1">
<table className="table text-nowrap">
<tbody>
<tr className="align-middle">
<td className="col-1 text-center">

@ -39,9 +39,8 @@ export default function Domains(props) {
await API.getDomains(dispatch, setError, router);
}
async function deleteDomain(e) {
e.preventDefault();
await API.deleteDomain({ _csrf: csrf, domain: e.target.domain.value }, dispatch, setError, router);
async function deleteDomain(csrf, domain) {
await API.deleteDomain({ _csrf: csrf, domain }, dispatch, setError, router);
await API.getDomains(dispatch, setError, router);
}
@ -55,12 +54,8 @@ export default function Domains(props) {
const isSubdomain = d.split('.').length > 2;
const tableRow = (
<tr key={i} className="align-middle">
<td className="col-1 text-center">
<form onSubmit={deleteDomain} action="/forms/domain/delete" method="post">
<input type="hidden" name="_csrf" value={csrf} />
<input type="hidden" name="domain" value={d} />
<input className="btn btn-danger" type="submit" value="×" />
</form>
<td className="text-left" style={{width:0}}>
<input onClick={() => deleteDomain(csrf, d)} className="btn btn-danger" type="button" value="×" />
</td>
<td>
{d}
@ -98,18 +93,18 @@ export default function Domains(props) {
{/* Domains table */}
<div className="table-responsive">
<table className="table text-nowrap m-1">
<table className="table text-nowrap">
<tbody>
<tr className="align-middle">
<th className="col-1" />
<th/>
<th>
Domain
</th>
<th>
HTTPS Certificate
</th>
<th className="col-1">
<th>
Edit DNS
</th>
</tr>
@ -128,7 +123,7 @@ export default function Domains(props) {
<form className="d-flex" onSubmit={addDomain} action="/forms/domain/add" method="post">
<input type="hidden" name="_csrf" value={csrf} />
<input className="btn btn-success" type="submit" value="+" />
<input className="form-control mx-3" type="text" name="domain" placeholder="domain" required />
<input className="form-control ms-3" type="text" name="domain" placeholder="domain" required />
</form>
</td>
</tr>

@ -50,9 +50,8 @@ const MapPage = (props) => {
e.target.reset();
}
async function deleteFromMap(e) {
e.preventDefault();
await API.deleteFromMap(mapInfo.name, { _csrf: csrf, key: e.target.key.value }, dispatch, setError, router);
async function deleteFromMap(csrf, key) {
await API.deleteFromMap(mapInfo.name, { _csrf: csrf, key }, dispatch, setError, router);
await API.getMap(mapName, dispatch, setError, router);
}
@ -80,13 +79,18 @@ const MapPage = (props) => {
.map((entry, i) => (<option key={'option'+i} value={entry[0]}>{entry[1]}</option>))
formElements = (
<>
<input type="hidden" name="_csrf" value={csrf} />
<input className="btn btn-success" type="submit" value="+" />
<input className="form-control mx-3" type="text" name="key" placeholder="domain/path" required />
<select className="form-select mx-3" name="value" defaultValue="" required>
<option value="" />
{mapValueOptions}
</select>
<td>
<input className="btn btn-success" type="submit" value="+" />
</td>
<td>
<input className="form-control" type="text" name="key" placeholder="domain/path" required />
</td>
<td>
<select className="form-select" name="value" defaultValue="" required>
<option value="" />
{mapValueOptions}
</select>
</td>
</>
);
break;
@ -95,23 +99,34 @@ const MapPage = (props) => {
const domainSelectOptions = user.domains.map((d, i) => (<option key={'option'+i} value={d}>{d}</option>));
formElements = (
<>
<input type="hidden" name="_csrf" value={csrf} />
<input className="btn btn-success" type="submit" value="+" />
<select className="form-select mx-3" name="key" defaultValue="" required>
<option value="" />
{domainSelectOptions}
</select>
<input className="form-control mx-3" type="number" name="pd" placeholder="difficulty" required />
<select className="form-select mx-3" name="pt" required>
<option disabled value="">pow type</option>
<option value="sha256">sha256</option>
<option value="argon2">argon2</option>
</select>
<input className="form-control mx-3" type="number" name="cex" placeholder="cookie expiry (seconds)" required />
<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>
<input className="btn btn-success" type="submit" value="+" />
</td>
<td>
<select className="form-select" name="key" defaultValue="" required>
<option value="" />
{domainSelectOptions}
</select>
</td>
<td>
<input className="form-control" type="number" 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>
</td>
<td>
<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>
</td>
</>
);
break;
@ -120,21 +135,26 @@ const MapPage = (props) => {
const domainSelectOptions = user.domains.map((d, i) => (<option key={'option'+i} value={d}>{d}</option>));
formElements = (
<>
<input type="hidden" name="_csrf" value={csrf} />
<input className="btn btn-success" type="submit" value="+" />
<select className="form-select mx-3" name="key" defaultValue="" required>
<option value="" />
{domainSelectOptions}
</select>
<td>
<input className="btn btn-success" type="submit" value="+" />
</td>
<td>
<select className="form-select" name="key" defaultValue="" required>
<option value="" />
{domainSelectOptions}
</select>
</td>
{
(process.env.NEXT_PUBLIC_CUSTOM_BACKENDS_ENABLED && mapInfo.name === "hosts") &&
<input
className="form-control ml-2"
type="text"
name="value"
placeholder="backend ip:port"
required
/>
<td>
<input
className="form-control"
type="text"
name="value"
placeholder="backend ip:port"
required
/>
</td>
}
</>
);
@ -146,12 +166,15 @@ const MapPage = (props) => {
const domainSelectOptions = inactiveDomains.map((d, i) => (<option key={'option'+i} value={d}>{d}</option>));
formElements = (
<>
<input type="hidden" name="_csrf" value={csrf} />
<input className="btn btn-success" type="submit" value="+" />
<select className="form-select mx-3" name="key" defaultValue="" required>
<option value="" />
{domainSelectOptions}
</select>
<td>
<input className="btn btn-success" type="submit" value="+" />
</td>
<td>
<select className="form-select" name="key" defaultValue="" required>
<option value="" />
{domainSelectOptions}
</select>
</td>
</>
);
break;
@ -160,9 +183,12 @@ const MapPage = (props) => {
case "whitelist":
formElements = (
<>
<input type="hidden" name="_csrf" value={csrf} />
<input className="btn btn-success" type="submit" value="+" />
<input className="form-control mx-3" type="text" name="key" placeholder="ip or subnet" required />
<td>
<input className="btn btn-success" type="submit" value="+" />
</td>
<td>
<input className="form-control" type="text" name="key" placeholder="ip or subnet" required />
</td>
</>
);
break;
@ -170,10 +196,15 @@ const MapPage = (props) => {
case "rewrite":
formElements = (
<>
<input type="hidden" name="_csrf" value={csrf} />
<input className="btn btn-success" type="submit" value="+" />
<input className="form-control mx-3" type="text" name="key" placeholder="domain" required />
<input className="form-control mx-3" type="text" name="value" placeholder="domain or domain/path" required />
<td>
<input className="btn btn-success" type="submit" value="+" />
</td>
<td>
<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 />
</td>
</>
);
break;
@ -196,14 +227,14 @@ const MapPage = (props) => {
</h5>
{/* Map table */}
<div className="table-responsive">
<table className="table text-nowrap m-1">
<tbody>
<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 */}
{mapRows.length > 0 && (
{/* header row */}
<tr>
<th />
<th style={{width:0}} />
<th>
{mapInfo.columnNames[0]}
</th>
@ -212,22 +243,19 @@ const MapPage = (props) => {
{x}
</th>
))}
</tr>
)}
{mapRows}
{mapRows}
{/* Add new row form */}
<tr className="align-middle">
<td className="col-1 text-center" colSpan={mapInfo.columnNames.length+(showValues?1:0)}>
<form onSubmit={addToMap} className="d-flex" action={`/forms/map/${mapInfo.name}/add`} method="post">
{formElements}
</form>
</td>
</tr>
{/* Add new row form */}
<tr className="align-middle">
{formElements}
</tr>
</tbody>
</table>
</tbody>
</table>
</form>
</div>
{/* back to account */}

Loading…
Cancel
Save