diff --git a/webui/index.html b/webui/index.html
index 26289c8..1871d66 100644
--- a/webui/index.html
+++ b/webui/index.html
@@ -1,28 +1,43 @@
-
-
-
- PKI API - Web UI (Scaffold)
-
-
-
- PKI API - Web UI (Scaffold)
- Cette page est un squelette minimal. L'interface web sera développée pour gérer les CAs et certificats via l'API REST.
+
+
+
+ PKI API - Web UI
+
+
+
+
+
-
-
Actions rapides (exemples)
-
- - Connexion via JWT
- - Liste des CAs
- - Créer une CA
- - Créer / signer un certificat
-
-
+
+
- Pour démarrer le développement, ouvrir `webui/README.md`.
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/webui/main.js b/webui/main.js
new file mode 100644
index 0000000..cadc63b
--- /dev/null
+++ b/webui/main.js
@@ -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 };
+})();
diff --git a/webui/style.css b/webui/style.css
new file mode 100644
index 0000000..82bc568
--- /dev/null
+++ b/webui/style.css
@@ -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}