Improved certs page and break it out into a separate page, hide internal backends map, setup button coming soon
parent
cebe8478e7
commit
17ac71b38e
13 changed files with 376 additions and 95 deletions
@ -0,0 +1,145 @@ |
||||
const db = require('../db.js'); |
||||
const acme = require('../acme.js'); |
||||
const url = require('url'); |
||||
const { dynamicResponse } = require('../util.js'); |
||||
|
||||
/** |
||||
* GET /certs |
||||
* certs page |
||||
*/ |
||||
exports.certsPage = async (app, req, res) => { |
||||
const certs = await db.db.collection('certs') |
||||
.find({ |
||||
username: res.locals.user.username, |
||||
}, { |
||||
projection: { |
||||
_id: 1, |
||||
subject: 1, |
||||
altnames: 1, |
||||
date: 1, |
||||
storageName: 1, |
||||
} |
||||
}) |
||||
.toArray(); |
||||
certs.forEach(c => c.date = c.date.toISOString()) |
||||
return app.render(req, res, '/certs', { |
||||
csrf: req.csrfToken(), |
||||
certs, |
||||
}); |
||||
}; |
||||
|
||||
/** |
||||
* GET /certs.json |
||||
* certs json data |
||||
*/ |
||||
exports.certsJson = async (req, res) => { |
||||
const certs = await db.db.collection('certs') |
||||
.find({ |
||||
username: res.locals.user.username, |
||||
}, { |
||||
projection: { |
||||
_id: 1, |
||||
subject: 1, |
||||
altnames: 1, |
||||
date: 1, |
||||
storageName: 1, |
||||
} |
||||
}) |
||||
.toArray(); |
||||
certs.forEach(c => c.date = c.date.toISOString()) |
||||
return res.json({ |
||||
csrf: req.csrfToken(), |
||||
user: res.locals.user, |
||||
certs, |
||||
}); |
||||
}; |
||||
|
||||
/** |
||||
* POST /cert/add |
||||
* add cert |
||||
*/ |
||||
exports.addCert = async (req, res, next) => { |
||||
|
||||
if (!req.body.subject || typeof req.body.subject !== 'string' || req.body.subject.length === 0 |
||||
|| !res.locals.user.domains.includes(req.body.subject)) { |
||||
return dynamicResponse(req, res, 400, { error: 'Invalid subject' }); |
||||
} |
||||
|
||||
if (!req.body.altnames || typeof req.body.altnames !== 'object' |
||||
|| req.body.altnames.some(d => !res.locals.user.domains.includes(d))) { |
||||
return dynamicResponse(req, res, 400, { error: 'Invalid altname(s)' }); |
||||
} |
||||
|
||||
const existingCert = await db.db.collection('certs').findOne({ _id: req.body.subject }); |
||||
if (existingCert) { |
||||
return dynamicResponse(req, res, 400, { error: 'Cert with this subject already exists' }); |
||||
} |
||||
|
||||
try { |
||||
url.parse(`https://${req.body.subject}`); |
||||
req.body.altnames.forEach(d => { |
||||
url.parse(`https://${d}`); |
||||
}); |
||||
} catch (e) { |
||||
return dynamicResponse(req, res, 400, { error: 'Invalid input' }); |
||||
} |
||||
|
||||
try { |
||||
console.log(req.body.subject, req.body.altnames); |
||||
const { csr, key, cert, haproxyCert, date } = await acme.generate(req.body.subject, req.body.altnames); |
||||
const fd = new FormData(); |
||||
fd.append('file_upload', new Blob([haproxyCert], { type: 'text/plain' }), `${req.body.subject}.pem`); |
||||
const { description, file, storage_name: storageName } = await res.locals.fetchAll('/v2/services/haproxy/storage/ssl_certificates?force_reload=true', { |
||||
method: 'POST', |
||||
headers: { 'authorization': res.locals.dataPlane.defaults.headers.authorization }, |
||||
body: fd, |
||||
}); |
||||
let update = { |
||||
_id: req.body.subject, |
||||
subject: req.body.subject, |
||||
altnames: req.body.altnames, |
||||
username: res.locals.user.username, |
||||
csr, key, cert, haproxyCert, // cert creation data
|
||||
date, |
||||
} |
||||
if (description) { |
||||
//may be null due to "already exists", so we keep existing props
|
||||
update = { ...update, description, file, storageName }; |
||||
} |
||||
await db.db.collection('certs') |
||||
.updateOne({ |
||||
_id: req.body.domain, |
||||
}, { |
||||
$set: update, |
||||
}, { |
||||
upsert: true, |
||||
}); |
||||
} catch (e) { |
||||
return next(e); |
||||
} |
||||
|
||||
return dynamicResponse(req, res, 302, { redirect: '/certs' }); |
||||
}; |
||||
|
||||
//TODO: new route to sync ssl certs throughout cluster
|
||||
|
||||
/** |
||||
* POST /cert/delete |
||||
* delete cers |
||||
*/ |
||||
exports.deleteCert = async (req, res) => { |
||||
|
||||
if (!req.body.subject || typeof req.body.subject !== 'string' || req.body.subject.length === 0 |
||||
//|| !res.locals.user.domains.includes(req.body.subject)
|
||||
) { |
||||
return dynamicResponse(req, res, 400, { error: 'Invalid input' }); |
||||
} |
||||
|
||||
await db.db.collection('accounts') |
||||
.updateOne({ _id: res.locals.user.username }, { $pull: { domains: req.body.subject } }); |
||||
await db.db.collection('certs') |
||||
.deleteOne({ _id: req.body.subject, username: res.locals.user.username }); |
||||
|
||||
return dynamicResponse(req, res, 302, { redirect: '/certs' }); |
||||
|
||||
}; |
@ -0,0 +1,136 @@ |
||||
import React, { useState, useEffect } from 'react'; |
||||
import Head from 'next/head'; |
||||
import BackButton from '../components/BackButton.js'; |
||||
import ErrorAlert from '../components/ErrorAlert.js'; |
||||
import * as API from '../api.js' |
||||
import { useRouter } from 'next/router'; |
||||
|
||||
export default function Certs(props) { |
||||
|
||||
const router = useRouter(); |
||||
const [state, dispatch] = useState(props); |
||||
const [error, setError] = useState(); |
||||
|
||||
useEffect(() => { |
||||
if (!state.user) { |
||||
API.getCerts(dispatch, setError, router); |
||||
} |
||||
}, [state.user, router]); |
||||
|
||||
if (!state.user) { |
||||
return ( |
||||
<> |
||||
Loading... |
||||
{error && <ErrorAlert error={error} />} |
||||
</> |
||||
); |
||||
} |
||||
|
||||
const { user, csrf, certs } = state; |
||||
|
||||
async function addCert(e) { |
||||
e.preventDefault(); |
||||
await API.addCert({ |
||||
_csrf: csrf, |
||||
subject: e.target.subject.value, |
||||
altnames: e.target.altnames.value.split(',').map(x => x.trim()) |
||||
}, dispatch, setError, router); |
||||
await API.getCerts(dispatch, setError, router); |
||||
} |
||||
|
||||
async function deleteCert(e) { |
||||
e.preventDefault(); |
||||
await API.deleteCert({ _csrf: csrf, subject: e.target.subject.value }, dispatch, setError, router); |
||||
await API.getCerts(dispatch, setError, router); |
||||
} |
||||
|
||||
const certList = certs.map((d, i) => { |
||||
//TODO: refactor, to component
|
||||
return ( |
||||
<tr key={i} className="align-middle"> |
||||
<td className="col-1 text-center"> |
||||
<form onSubmit={deleteCert} action="/forms/cert/delete" method="post"> |
||||
<input type="hidden" name="_csrf" value={csrf} /> |
||||
<input type="hidden" name="subject" value={d.subject || d._id} /> |
||||
<input className="btn btn-danger" type="submit" value="×" /> |
||||
</form> |
||||
</td> |
||||
<td> |
||||
{d.subject || '-'} |
||||
</td> |
||||
<td> |
||||
{d.altnames && d.altnames.join(', ') || '-'} |
||||
</td> |
||||
<td> |
||||
{d.date || '-'} |
||||
</td> |
||||
<td> |
||||
{d.storageName || '-'} |
||||
</td> |
||||
</tr> |
||||
); |
||||
}) |
||||
|
||||
return ( |
||||
<> |
||||
|
||||
<Head> |
||||
<title>Certificates</title> |
||||
</Head> |
||||
|
||||
{error && <ErrorAlert error={error} />} |
||||
|
||||
<h5 className="fw-bold"> |
||||
HTTPS Certificates: |
||||
</h5> |
||||
|
||||
{/* Certs table */} |
||||
<div className="table-responsive"> |
||||
<table className="table table-bordered text-nowrap"> |
||||
<tbody> |
||||
|
||||
<tr className="align-middle"> |
||||
<th className="col-1" /> |
||||
<th> |
||||
Subject |
||||
</th> |
||||
<th> |
||||
Altname(s) |
||||
</th> |
||||
<th> |
||||
Creation Date |
||||
</th> |
||||
<th> |
||||
Storage Name |
||||
</th> |
||||
</tr> |
||||
|
||||
{certList} |
||||
|
||||
{/* Add new cert form */} |
||||
<tr className="align-middle"> |
||||
<td className="col-1 text-center" colSpan="3"> |
||||
<form className="d-flex" onSubmit={addCert} action="/forms/cert/add" method="post"> |
||||
<input type="hidden" name="_csrf" value={csrf} /> |
||||
<input className="btn btn-success" type="submit" value="+" /> |
||||
<input className="form-control mx-3" type="text" name="subject" placeholder="domain.com" required /> |
||||
<input className="form-control me-3" type="text" name="altnames" placeholder="www.domain.com,staging.domain.com,etc..." required /> |
||||
</form> |
||||
</td> |
||||
</tr> |
||||
|
||||
</tbody> |
||||
</table> |
||||
</div> |
||||
|
||||
{/* back to account */} |
||||
<BackButton to="/account" /> |
||||
|
||||
</> |
||||
); |
||||
|
||||
}; |
||||
|
||||
export async function getServerSideProps({ req, res, query, resolvedUrl, locale, locales, defaultLocale}) { |
||||
return { props: { user: res.locals.user || null, ...query } } |
||||
} |
Loading…
Reference in new issue