feat(webui): add minimal SPA for managing CAs and certificates
parent
028f768299
commit
1e52006b46
|
|
@ -1,28 +1,43 @@
|
|||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>PKI API - Web UI (Scaffold)</title>
|
||||
<style>
|
||||
body{font-family:system-ui,Arial;margin:2rem}
|
||||
.card{border:1px solid #ddd;padding:1rem;border-radius:6px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>PKI API - Web UI (Scaffold)</h1>
|
||||
<p>Cette page est un squelette minimal. L'interface web sera développée pour gérer les CAs et certificats via l'API REST.</p>
|
||||
<title>PKI API - Web UI</title>
|
||||
<link rel="stylesheet" href="./style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>PKI API - Web UI (Minimal)</h1>
|
||||
<div class="token-row">
|
||||
<label for="token">JWT Token:</label>
|
||||
<input id="token" placeholder="Entrez le token JWT ici" />
|
||||
<button id="saveToken">Enregistrer</button>
|
||||
</div>
|
||||
<nav>
|
||||
<button id="btnCAs">Liste des CAs</button>
|
||||
<button id="btnCerts">Liste des certificats</button>
|
||||
<button id="btnCreateCA">Créer CA</button>
|
||||
<button id="btnCreateCert">Créer Certificat</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="card">
|
||||
<h2>Actions rapides (exemples)</h2>
|
||||
<ul>
|
||||
<li>Connexion via JWT</li>
|
||||
<li>Liste des CAs</li>
|
||||
<li>Créer une CA</li>
|
||||
<li>Créer / signer un certificat</li>
|
||||
</ul>
|
||||
<main>
|
||||
<section id="message" class="card hidden"></section>
|
||||
|
||||
<section id="list" class="card hidden"></section>
|
||||
|
||||
<section id="form" class="card hidden"></section>
|
||||
|
||||
<section id="details" class="card hidden"></section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<small>Consomme l'API sur <code>/api/v1</code>. Assurez-vous d'entrer un token JWT valide.</small>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<p>Pour démarrer le développement, ouvrir `webui/README.md`.</p>
|
||||
</body>
|
||||
<script src="./main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,198 @@
|
|||
(function(){
|
||||
const API_BASE = '/api/v1';
|
||||
let token = localStorage.getItem('pki_token') || '';
|
||||
|
||||
function $(id){ return document.getElementById(id); }
|
||||
function show(el){ el.classList.remove('hidden'); }
|
||||
function hide(el){ el.classList.add('hidden'); }
|
||||
|
||||
function showMessage(msg, isError){
|
||||
const m = $('message');
|
||||
m.textContent = msg;
|
||||
m.className = 'card';
|
||||
if(isError) m.classList.add('error');
|
||||
show(m);
|
||||
setTimeout(()=>{ hide(m); }, 7000);
|
||||
}
|
||||
|
||||
function apiFetch(path, opts={}){
|
||||
opts.headers = opts.headers || {};
|
||||
opts.headers['Content-Type'] = 'application/json';
|
||||
if(token) opts.headers['Authorization'] = 'Bearer ' + token;
|
||||
return fetch(API_BASE + path, opts).then(async res => {
|
||||
const ct = res.headers.get('content-type') || '';
|
||||
if(!res.ok){
|
||||
let body = await res.text();
|
||||
throw new Error(body || res.statusText);
|
||||
}
|
||||
if(ct.includes('application/json')) return res.json();
|
||||
return res.blob();
|
||||
});
|
||||
}
|
||||
|
||||
function renderTable(items, type){
|
||||
const list = $('list');
|
||||
list.innerHTML = '';
|
||||
const h = document.createElement('h3');
|
||||
h.textContent = type === 'ca' ? 'Autorités de Certification' : 'Certificats';
|
||||
list.appendChild(h);
|
||||
|
||||
const table = document.createElement('table');
|
||||
table.className = 'table';
|
||||
const thead = document.createElement('thead');
|
||||
const headRow = document.createElement('tr');
|
||||
['ID','Sujet','Émetteur','Not After','Actions'].forEach(t=>{ const th=document.createElement('th'); th.textContent=t; headRow.appendChild(th); });
|
||||
thead.appendChild(headRow);
|
||||
table.appendChild(thead);
|
||||
|
||||
const tbody = document.createElement('tbody');
|
||||
items.forEach(it=>{
|
||||
const tr = document.createElement('tr');
|
||||
const idCell = document.createElement('td'); idCell.textContent = it.id || it.ID || it.Id || '';
|
||||
const subj = document.createElement('td'); subj.textContent = it.subject || it.Subject || it.common_name || '-';
|
||||
const issuer = document.createElement('td'); issuer.textContent = it.issuer || it.issuer || '-';
|
||||
const na = document.createElement('td'); na.textContent = it.not_after || it.notAfter || it.notAfter || '-';
|
||||
const actions = document.createElement('td');
|
||||
|
||||
const btnView = document.createElement('button'); btnView.textContent = 'Voir';
|
||||
btnView.onclick = ()=> viewDetails(it.id || it.ID || it.Id);
|
||||
actions.appendChild(btnView);
|
||||
|
||||
if(type === 'cert'){
|
||||
const btnExport = document.createElement('button'); btnExport.textContent = 'Export PEM';
|
||||
btnExport.onclick = ()=> exportCertPEM(it.id || it.ID || it.Id);
|
||||
actions.appendChild(btnExport);
|
||||
|
||||
const btnRevoke = document.createElement('button'); btnRevoke.textContent = 'Révoquer';
|
||||
btnRevoke.onclick = ()=> revokeCert(it.id || it.ID || it.Id);
|
||||
actions.appendChild(btnRevoke);
|
||||
}
|
||||
|
||||
tr.appendChild(idCell); tr.appendChild(subj); tr.appendChild(issuer); tr.appendChild(na); tr.appendChild(actions);
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
table.appendChild(tbody);
|
||||
list.appendChild(table);
|
||||
show(list);
|
||||
}
|
||||
|
||||
function setTokenFromInput(){
|
||||
const t = $('token').value.trim();
|
||||
token = t;
|
||||
localStorage.setItem('pki_token', token);
|
||||
showMessage('Token enregistré.');
|
||||
}
|
||||
|
||||
async function loadCAs(){
|
||||
try{
|
||||
const res = await apiFetch('/ca');
|
||||
const cas = res.cas || res.list || res;
|
||||
renderTable(cas, 'ca');
|
||||
}catch(e){ showMessage('Erreur loadCAs: '+e.message, true); }
|
||||
}
|
||||
|
||||
async function loadCerts(){
|
||||
try{
|
||||
const res = await apiFetch('/certificates');
|
||||
const certs = res.certificates || res.list || res;
|
||||
renderTable(certs, 'cert');
|
||||
}catch(e){ showMessage('Erreur loadCerts: '+e.message, true); }
|
||||
}
|
||||
|
||||
function showCreateCAForm(){
|
||||
const f = $('form'); f.innerHTML = '';
|
||||
const h = document.createElement('h3'); h.textContent = 'Créer une CA'; f.appendChild(h);
|
||||
const subject = document.createElement('input'); subject.placeholder='CN=Root CA,O=Example,C=FR'; subject.id='ca_subject';
|
||||
const days = document.createElement('input'); days.placeholder='validity_days'; days.type='number'; days.id='ca_days'; days.value=3650;
|
||||
const btn = document.createElement('button'); btn.textContent='Créer'; btn.onclick = async ()=>{
|
||||
try{
|
||||
const body = { subject: subject.value, validity_days: parseInt(days.value||0,10) };
|
||||
const res = await apiFetch('/ca',{ method:'POST', body: JSON.stringify(body) });
|
||||
showMessage('CA créée: '+(res.ca && res.ca.id));
|
||||
loadCAs();
|
||||
}catch(e){ showMessage('Erreur création CA: '+e.message, true); }
|
||||
};
|
||||
f.appendChild(subject); f.appendChild(document.createElement('br'));
|
||||
f.appendChild(days); f.appendChild(document.createElement('br'));
|
||||
f.appendChild(btn);
|
||||
show(f);
|
||||
}
|
||||
|
||||
function showCreateCertForm(){
|
||||
const f = $('form'); f.innerHTML = '';
|
||||
const h = document.createElement('h3'); h.textContent = 'Créer un Certificat (auto-signé)'; f.appendChild(h);
|
||||
const subject = document.createElement('input'); subject.placeholder='CN=server.example.com,O=Example,C=FR'; subject.id='cert_subject';
|
||||
const days = document.createElement('input'); days.placeholder='validity_days'; days.type='number'; days.id='cert_days'; days.value=365;
|
||||
const btn = document.createElement('button'); btn.textContent='Créer'; btn.onclick = async ()=>{
|
||||
try{
|
||||
const body = { subject: subject.value, validity_days: parseInt(days.value||0,10) };
|
||||
const res = await apiFetch('/certificates',{ method:'POST', body: JSON.stringify(body) });
|
||||
showMessage('Certificat créé: '+(res.certificate && res.certificate.id));
|
||||
loadCerts();
|
||||
}catch(e){ showMessage('Erreur création cert: '+e.message, true); }
|
||||
};
|
||||
f.appendChild(subject); f.appendChild(document.createElement('br'));
|
||||
f.appendChild(days); f.appendChild(document.createElement('br'));
|
||||
f.appendChild(btn);
|
||||
show(f);
|
||||
}
|
||||
|
||||
async function viewDetails(id){
|
||||
try{
|
||||
const res = await apiFetch('/certificates/'+id);
|
||||
const cert = res.certificate || res;
|
||||
const d = $('details');
|
||||
d.innerHTML = '';
|
||||
const h = document.createElement('h3'); h.textContent = 'Détails Certificat'; d.appendChild(h);
|
||||
const pre = document.createElement('pre'); pre.textContent = JSON.stringify(cert, null, 2);
|
||||
d.appendChild(pre);
|
||||
show(d);
|
||||
}catch(e){
|
||||
// peut-être une CA
|
||||
try{
|
||||
const res = await apiFetch('/ca/'+id);
|
||||
const ca = res.ca || res;
|
||||
const d = $('details'); d.innerHTML=''; d.appendChild(document.createElement('h3')).textContent='Détails CA';
|
||||
const pre = document.createElement('pre'); pre.textContent = JSON.stringify(ca, null, 2); d.appendChild(pre); show(d);
|
||||
}catch(er){ showMessage('Erreur viewDetails: '+er.message, true); }
|
||||
}
|
||||
}
|
||||
|
||||
async function revokeCert(id){
|
||||
if(!confirm('Confirmer la révocation du certificat '+id+' ?')) return;
|
||||
try{
|
||||
const body = { certificate_id: id, reason: 'Revoked via UI' };
|
||||
await apiFetch('/revoke',{ method:'POST', body: JSON.stringify(body) });
|
||||
showMessage('Certificat révoqué: '+id);
|
||||
loadCerts();
|
||||
}catch(e){ showMessage('Erreur revoke: '+e.message, true); }
|
||||
}
|
||||
|
||||
async function exportCertPEM(id){
|
||||
try{
|
||||
const blob = await apiFetch('/certificates/'+id+'/export/pem');
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a'); a.href = url; a.download = id + '.pem'; document.body.appendChild(a); a.click(); a.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
}catch(e){ showMessage('Erreur export: '+e.message, true); }
|
||||
}
|
||||
|
||||
function attachEvents(){
|
||||
$('saveToken').onclick = setTokenFromInput;
|
||||
$('btnCAs').onclick = loadCAs;
|
||||
$('btnCerts').onclick = loadCerts;
|
||||
$('btnCreateCA').onclick = showCreateCAForm;
|
||||
$('btnCreateCert').onclick = showCreateCertForm;
|
||||
// prefills
|
||||
$('token').value = token;
|
||||
}
|
||||
|
||||
// init
|
||||
document.addEventListener('DOMContentLoaded', ()=>{
|
||||
attachEvents();
|
||||
if(token) showMessage('Token chargé depuis localStorage.');
|
||||
});
|
||||
|
||||
// export functions for debugging
|
||||
window.pkiUI = { loadCAs, loadCerts, viewDetails };
|
||||
})();
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
:root{--bg:#f7f9fb;--card:#fff;--accent:#2b6cb0}
|
||||
body{font-family:Inter,system-ui,Segoe UI,Arial;background:var(--bg);margin:0;padding:1rem}
|
||||
.container{max-width:1000px;margin:0 auto}
|
||||
header{display:flex;flex-direction:column;gap:0.5rem}
|
||||
.token-row{display:flex;gap:0.5rem;align-items:center}
|
||||
nav{display:flex;gap:0.5rem}
|
||||
button{background:var(--accent);color:#fff;border:none;padding:0.4rem 0.6rem;border-radius:4px;cursor:pointer}
|
||||
button.secondary{background:#666}
|
||||
.card{background:var(--card);padding:1rem;border-radius:6px;box-shadow:0 1px 2px rgba(0,0,0,0.04);margin-top:1rem}
|
||||
.hidden{display:none}
|
||||
.table{width:100%;border-collapse:collapse;margin-top:0.5rem}
|
||||
.table th,.table td{padding:0.5rem;border-bottom:1px solid #eee;text-align:left}
|
||||
pre{white-space:pre-wrap;word-break:break-word}
|
||||
.error{border-left:4px solid #e53e3e;padding-left:0.8rem}
|
||||
label{font-weight:600}
|
||||
input{padding:0.4rem;border:1px solid #ddd;border-radius:4px}
|
||||
footer{margin-top:1rem;color:#666}
|
||||
Loading…
Reference in New Issue