First commit
This commit is contained in:
502
internal/web/static/css/style.css
Normal file
502
internal/web/static/css/style.css
Normal file
@@ -0,0 +1,502 @@
|
||||
/* Ajoutez ces styles à la fin du fichier CSS existant */
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-card h3 {
|
||||
font-size: 16px;
|
||||
margin-bottom: 10px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.recent-activity {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.recent-activity h3 {
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.table-container .btn {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Spinner styles */
|
||||
.fa-spinner {
|
||||
color: #667eea;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
/* Modal improvements */
|
||||
.modal-content {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.modal-content h2 {
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
/* Form improvements */
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
color: #4a5568;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #cbd5e0;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.form-group input[type="checkbox"] {
|
||||
width: auto;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* Button improvements */
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: linear-gradient(135deg, #e53e3e 0%, #c53030 100%);
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: linear-gradient(135deg, #c53030 0%, #9b2c2c 100%);
|
||||
}
|
||||
|
||||
/* Close button */
|
||||
.close-btn {
|
||||
float: right;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #a0aec0;
|
||||
cursor: pointer;
|
||||
line-height: 20px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
header {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
nav {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 95%;
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Badge styles */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.badge-server {
|
||||
background-color: #4299e1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-client {
|
||||
background-color: #48bb78;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
background-color: #ed8936;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Detail view styles */
|
||||
.detail-group {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.detail-group:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.detail-group label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
color: #4a5568;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.detail-group span {
|
||||
display: block;
|
||||
color: #2d3748;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.monospace {
|
||||
font-family: 'Courier New', monospace;
|
||||
background-color: #f7fafc;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.certificate-preview {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.certificate-preview label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.cert-pem {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
padding: 10px;
|
||||
border: 1px solid #cbd5e0;
|
||||
border-radius: 6px;
|
||||
background-color: #f7fafc;
|
||||
resize: vertical;
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Loading spinner */
|
||||
.loading-spinner {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.loading-spinner i {
|
||||
font-size: 48px;
|
||||
color: #667eea;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.loading-spinner p {
|
||||
color: #718096;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 48px;
|
||||
margin-bottom: 20px;
|
||||
color: #cbd5e0;
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
margin-bottom: 10px;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
/* Tooltip for buttons */
|
||||
button[title] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
button[title]:hover::after {
|
||||
content: attr(title);
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: #2d3748;
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
z-index: 1000;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
/* Loading states */
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.error {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #e53e3e;
|
||||
background: #fed7d7;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Section headers */
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.section-header h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Table improvements */
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.table th {
|
||||
background: #f7fafc;
|
||||
padding: 12px 15px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #4a5568;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.table td {
|
||||
padding: 12px 15px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.table tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.table tr:hover {
|
||||
background: #f7fafc;
|
||||
}
|
||||
|
||||
/* Status badges */
|
||||
.status {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status.valid {
|
||||
background: #c6f6d5;
|
||||
color: #22543d;
|
||||
}
|
||||
|
||||
.status.revoked {
|
||||
background: #fed7d7;
|
||||
color: #742a2a;
|
||||
}
|
||||
|
||||
.status.expired {
|
||||
background: #feebc8;
|
||||
color: #744210;
|
||||
}
|
||||
|
||||
/* Button styles */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn i {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn.primary:hover {
|
||||
background: #5a67d8;
|
||||
}
|
||||
|
||||
.btn.small {
|
||||
padding: 4px 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.btn.danger {
|
||||
background: #fc8181;
|
||||
color: #742a2a;
|
||||
}
|
||||
|
||||
.btn.danger:hover {
|
||||
background: #f56565;
|
||||
}
|
||||
|
||||
.btn.warning {
|
||||
background: #ed8936;
|
||||
color: #744210;
|
||||
}
|
||||
|
||||
.btn.warning:hover {
|
||||
background: #dd6b20;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
/* Modal animations */
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOut {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
887
internal/web/static/js/app.js
Normal file
887
internal/web/static/js/app.js
Normal file
@@ -0,0 +1,887 @@
|
||||
class PKIManager {
|
||||
constructor() {
|
||||
this.apiBase = '/api/v1';
|
||||
this.currentTab = 'dashboard';
|
||||
this.cas = [];
|
||||
this.subcas = [];
|
||||
this.certificates = [];
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.bindEvents();
|
||||
this.showTab('dashboard');
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// Tab navigation
|
||||
document.querySelectorAll('.nav-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const tab = e.target.dataset.tab || e.target.closest('.nav-btn').dataset.tab;
|
||||
this.showTab(tab);
|
||||
});
|
||||
});
|
||||
|
||||
// Form submissions
|
||||
const caForm = document.getElementById('createCAForm');
|
||||
const subcaForm = document.getElementById('createSubCAForm');
|
||||
const certForm = document.getElementById('createCertForm');
|
||||
|
||||
if (caForm) caForm.addEventListener('submit', (e) => this.handleCreateCA(e));
|
||||
if (subcaForm) subcaForm.addEventListener('submit', (e) => this.handleCreateSubCA(e));
|
||||
if (certForm) certForm.addEventListener('submit', (e) => this.handleCreateCertificate(e));
|
||||
|
||||
// Close modal buttons
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('close-btn') || e.target.closest('.close-btn')) {
|
||||
this.hideModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Click outside modal
|
||||
window.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('modal')) {
|
||||
this.hideModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
showTab(tabName) {
|
||||
console.log(`Showing tab: ${tabName}`);
|
||||
|
||||
// Hide all tabs
|
||||
document.querySelectorAll('.tab-content').forEach(tab => {
|
||||
tab.classList.add('hidden');
|
||||
});
|
||||
|
||||
// Update active button
|
||||
document.querySelectorAll('.nav-btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
|
||||
const activeBtn = document.querySelector(`[data-tab="${tabName}"]`);
|
||||
if (activeBtn) activeBtn.classList.add('active');
|
||||
|
||||
// Show selected tab
|
||||
const tabElement = document.getElementById(tabName + 'Tab');
|
||||
if (tabElement) {
|
||||
tabElement.classList.remove('hidden');
|
||||
}
|
||||
|
||||
this.currentTab = tabName;
|
||||
|
||||
// Load data for tab
|
||||
setTimeout(() => this.loadTabData(tabName), 50);
|
||||
}
|
||||
|
||||
async loadTabData(tabName) {
|
||||
switch(tabName) {
|
||||
case 'dashboard':
|
||||
await this.loadDashboard();
|
||||
break;
|
||||
case 'cas':
|
||||
await this.loadCAs();
|
||||
break;
|
||||
case 'subcas':
|
||||
await this.loadSubCAs();
|
||||
break;
|
||||
case 'certificates':
|
||||
await this.loadCertificates();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async loadDashboard() {
|
||||
const content = document.getElementById('dashboardContent');
|
||||
if (!content) return;
|
||||
|
||||
content.innerHTML = '<div class="loading">Loading dashboard...</div>';
|
||||
|
||||
try {
|
||||
// Charger les données
|
||||
await this.fetchAllData();
|
||||
|
||||
content.innerHTML = `
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<h3>Root CAs</h3>
|
||||
<p class="stat-number">${this.cas.filter(c => c.is_root).length}</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Sub CAs</h3>
|
||||
<p class="stat-number">${this.subcas.length}</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Certificates</h3>
|
||||
<p class="stat-number">${this.certificates.length}</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>Active</h3>
|
||||
<p class="stat-number">${this.certificates.filter(c => !c.revoked && new Date(c.valid_to) > new Date()).length}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="recent-activity">
|
||||
<h3>Recent Certificates</h3>
|
||||
${this.certificates.length > 0 ? `
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>Name</th><th>Type</th><th>Issued</th><th>Expires</th><th>Status</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${this.certificates.slice(0, 5).map(cert => `
|
||||
<tr>
|
||||
<td>${cert.common_name}</td>
|
||||
<td>${cert.type}</td>
|
||||
<td>${new Date(cert.created_at).toLocaleDateString()}</td>
|
||||
<td>${new Date(cert.valid_to).toLocaleDateString()}</td>
|
||||
<td><span class="status ${cert.revoked ? 'revoked' : new Date(cert.valid_to) > new Date() ? 'valid' : 'expired'}">
|
||||
${cert.revoked ? 'Revoked' : new Date(cert.valid_to) > new Date() ? 'Valid' : 'Expired'}
|
||||
</span></td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
` : '<p>No certificates yet</p>'}
|
||||
</div>
|
||||
`;
|
||||
} catch (error) {
|
||||
console.error('Dashboard error:', error);
|
||||
content.innerHTML = '<div class="error">Failed to load dashboard</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async loadCAs() {
|
||||
const content = document.getElementById('casContent');
|
||||
if (!content) return;
|
||||
|
||||
content.innerHTML = '<div class="loading">Loading CAs...</div>';
|
||||
|
||||
try {
|
||||
await this.fetchCAs();
|
||||
|
||||
content.innerHTML = `
|
||||
<div class="section-header">
|
||||
<h3>Certificate Authorities</h3>
|
||||
<button class="btn primary" onclick="pki.showCreateCAModal()">
|
||||
<i class="fas fa-plus"></i> New CA
|
||||
</button>
|
||||
</div>
|
||||
${this.cas.length > 0 ? `
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Common Name</th>
|
||||
<th>Organization</th>
|
||||
<th>Valid From</th>
|
||||
<th>Valid To</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${this.cas.map(ca => `
|
||||
<tr>
|
||||
<td>${ca.name}</td>
|
||||
<td>${ca.common_name}</td>
|
||||
<td>${ca.organization}</td>
|
||||
<td>${new Date(ca.valid_from).toLocaleDateString()}</td>
|
||||
<td>${new Date(ca.valid_to).toLocaleDateString()}</td>
|
||||
<td class="actions">
|
||||
<button class="btn small" onclick="pki.viewCA('${ca.id}')">View</button>
|
||||
<button class="btn small danger" onclick="pki.deleteCA('${ca.id}')">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
` : `
|
||||
<div class="empty-state">
|
||||
<p>No Certificate Authorities found</p>
|
||||
<button class="btn primary" onclick="pki.showCreateCAModal()">
|
||||
Create your first CA
|
||||
</button>
|
||||
</div>
|
||||
`}
|
||||
`;
|
||||
} catch (error) {
|
||||
console.error('CAs error:', error);
|
||||
content.innerHTML = '<div class="error">Failed to load CAs</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async loadSubCAs() {
|
||||
const content = document.getElementById('subcasContent');
|
||||
if (!content) return;
|
||||
|
||||
content.innerHTML = '<div class="loading">Loading Sub CAs...</div>';
|
||||
|
||||
try {
|
||||
await this.fetchSubCAs();
|
||||
await this.fetchCAs(); // Need CAs for parent info
|
||||
|
||||
content.innerHTML = `
|
||||
<div class="section-header">
|
||||
<h3>Sub Certificate Authorities</h3>
|
||||
<button class="btn primary" onclick="pki.showCreateSubCAModal()">
|
||||
<i class="fas fa-plus"></i> New Sub CA
|
||||
</button>
|
||||
</div>
|
||||
${this.subcas.length > 0 ? `
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Common Name</th>
|
||||
<th>Parent CA</th>
|
||||
<th>Valid From</th>
|
||||
<th>Valid To</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${this.subcas.map(subca => {
|
||||
const parent = this.cas.find(ca => ca.id === subca.parent_ca_id);
|
||||
return `
|
||||
<tr>
|
||||
<td>${subca.name}</td>
|
||||
<td>${subca.common_name}</td>
|
||||
<td>${parent ? parent.name : 'Unknown'}</td>
|
||||
<td>${new Date(subca.valid_from).toLocaleDateString()}</td>
|
||||
<td>${new Date(subca.valid_to).toLocaleDateString()}</td>
|
||||
<td class="actions">
|
||||
<button class="btn small" onclick="pki.viewSubCA('${subca.id}')">View</button>
|
||||
<button class="btn small danger" onclick="pki.deleteSubCA('${subca.id}')">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
` : `
|
||||
<div class="empty-state">
|
||||
<p>No Sub Certificate Authorities found</p>
|
||||
${this.cas.length > 0 ? `
|
||||
<button class="btn primary" onclick="pki.showCreateSubCAModal()">
|
||||
Create your first Sub CA
|
||||
</button>
|
||||
` : '<p>Create a CA first to create Sub CAs</p>'}
|
||||
</div>
|
||||
`}
|
||||
`;
|
||||
} catch (error) {
|
||||
console.error('SubCAs error:', error);
|
||||
content.innerHTML = '<div class="error">Failed to load Sub CAs</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async loadCertificates() {
|
||||
const content = document.getElementById('certsContent');
|
||||
if (!content) return;
|
||||
|
||||
content.innerHTML = '<div class="loading">Loading Certificates...</div>';
|
||||
|
||||
try {
|
||||
await this.fetchCertificates();
|
||||
await this.fetchCAs();
|
||||
await this.fetchSubCAs();
|
||||
|
||||
content.innerHTML = `
|
||||
<div class="section-header">
|
||||
<h3>Certificates</h3>
|
||||
<button class="btn primary" onclick="pki.showCreateCertModal()">
|
||||
<i class="fas fa-plus"></i> New Certificate
|
||||
</button>
|
||||
</div>
|
||||
${this.certificates.length > 0 ? `
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Common Name</th>
|
||||
<th>Type</th>
|
||||
<th>Issuer</th>
|
||||
<th>Issued</th>
|
||||
<th>Expires</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${this.certificates.map(cert => {
|
||||
const issuer = [...this.cas, ...this.subcas].find(i => i.id === cert.issuer_ca_id);
|
||||
const status = cert.revoked ? 'revoked' : new Date(cert.valid_to) > new Date() ? 'valid' : 'expired';
|
||||
return `
|
||||
<tr>
|
||||
<td>${cert.common_name}</td>
|
||||
<td>${cert.type}</td>
|
||||
<td>${issuer ? issuer.name : 'Unknown'}</td>
|
||||
<td>${new Date(cert.created_at).toLocaleDateString()}</td>
|
||||
<td>${new Date(cert.valid_to).toLocaleDateString()}</td>
|
||||
<td><span class="status ${status}">${status.charAt(0).toUpperCase() + status.slice(1)}</span></td>
|
||||
<td class="actions">
|
||||
<button class="btn small" onclick="pki.viewCertificate('${cert.id}')">View</button>
|
||||
<button class="btn small" onclick="pki.downloadCertificate('${cert.id}')">Download</button>
|
||||
${!cert.revoked ? `<button class="btn small warning" onclick="pki.revokeCertificate('${cert.id}')">Revoke</button>` : ''}
|
||||
<button class="btn small danger" onclick="pki.deleteCertificate('${cert.id}')">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
` : `
|
||||
<div class="empty-state">
|
||||
<p>No certificates found</p>
|
||||
${this.cas.length > 0 || this.subcas.length > 0 ? `
|
||||
<button class="btn primary" onclick="pki.showCreateCertModal()">
|
||||
Create your first certificate
|
||||
</button>
|
||||
` : '<p>Create a CA or Sub CA first</p>'}
|
||||
</div>
|
||||
`}
|
||||
`;
|
||||
} catch (error) {
|
||||
console.error('Certificates error:', error);
|
||||
content.innerHTML = '<div class="error">Failed to load certificates</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async fetchAllData() {
|
||||
try {
|
||||
const [cas, subcas, certs] = await Promise.all([
|
||||
this.fetchCAs(),
|
||||
this.fetchSubCAs(),
|
||||
this.fetchCertificates()
|
||||
]);
|
||||
return { cas, subcas, certs };
|
||||
} catch (error) {
|
||||
console.error('Error fetching all data:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchCAs() {
|
||||
try {
|
||||
console.log('Fetching CAs from:', `${this.apiBase}/cas`);
|
||||
const response = await fetch(`${this.apiBase}/cas`);
|
||||
|
||||
console.log('CAs response status:', response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn(`CAs fetch failed: ${response.status} ${response.statusText}`);
|
||||
this.cas = [];
|
||||
return this.cas;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('CAs raw data:', data);
|
||||
|
||||
// S'assurer que c'est un tableau
|
||||
this.cas = Array.isArray(data) ? data : [];
|
||||
console.log(`Loaded ${this.cas.length} CAs`);
|
||||
|
||||
return this.cas;
|
||||
} catch (error) {
|
||||
console.error('CAs fetch error:', error);
|
||||
this.cas = [];
|
||||
return this.cas;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchSubCAs() {
|
||||
try {
|
||||
console.log('Fetching SubCAs from:', `${this.apiBase}/subcas`);
|
||||
const response = await fetch(`${this.apiBase}/subcas`);
|
||||
|
||||
console.log('SubCAs response status:', response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn(`SubCAs fetch failed: ${response.status} ${response.statusText}`);
|
||||
this.subcas = [];
|
||||
return this.subcas;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('SubCAs raw data:', data);
|
||||
|
||||
// S'assurer que c'est un tableau
|
||||
this.subcas = Array.isArray(data) ? data : [];
|
||||
console.log(`Loaded ${this.subcas.length} SubCAs`);
|
||||
|
||||
return this.subcas;
|
||||
} catch (error) {
|
||||
console.error('SubCAs fetch error:', error);
|
||||
this.subcas = [];
|
||||
return this.subcas;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchCertificates() {
|
||||
try {
|
||||
console.log('Fetching certificates...');
|
||||
const response = await fetch(`${this.apiBase}/certificates`);
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn(`Certificates fetch failed: ${response.status}`);
|
||||
this.certificates = [];
|
||||
return this.certificates;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
this.certificates = Array.isArray(data) ? data : [];
|
||||
console.log(`Loaded ${this.certificates.length} certificates`);
|
||||
return this.certificates;
|
||||
} catch (error) {
|
||||
console.warn('Certificates fetch error:', error);
|
||||
this.certificates = [];
|
||||
return this.certificates;
|
||||
}
|
||||
}
|
||||
|
||||
async showCreateCAModal() {
|
||||
const modal = document.getElementById('createCAModal');
|
||||
if (modal) modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
async showCreateSubCAModal() {
|
||||
try {
|
||||
// Charger les CAs pour le dropdown
|
||||
await this.fetchCAs();
|
||||
|
||||
if (this.cas.length === 0) {
|
||||
this.showError('Create a CA first before creating a Sub CA');
|
||||
return;
|
||||
}
|
||||
|
||||
// Mettre à jour le dropdown
|
||||
const dropdown = document.getElementById('parentCA');
|
||||
if (dropdown) {
|
||||
dropdown.innerHTML = this.cas.map(ca =>
|
||||
`<option value="${ca.id}">${ca.name} (${ca.common_name})</option>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
const modal = document.getElementById('createSubCAModal');
|
||||
if (modal) modal.classList.remove('hidden');
|
||||
} catch (error) {
|
||||
console.error('Error showing SubCA modal:', error);
|
||||
this.showError('Failed to load CAs');
|
||||
}
|
||||
}
|
||||
|
||||
async showCreateCertModal() {
|
||||
console.log('=== showCreateCertModal START ===');
|
||||
|
||||
try {
|
||||
// 1. Charger les données
|
||||
console.log('Loading issuers...');
|
||||
await this.fetchCAs();
|
||||
await this.fetchSubCAs();
|
||||
|
||||
// 2. Vérifier les données
|
||||
console.log(`CAs: ${this.cas.length} items`, this.cas);
|
||||
console.log(`SubCAs: ${this.subcas.length} items`, this.subcas);
|
||||
|
||||
// 3. Combiner tous les émetteurs
|
||||
const allIssuers = [];
|
||||
|
||||
// Ajouter les CAs
|
||||
if (this.cas && Array.isArray(this.cas)) {
|
||||
this.cas.forEach(ca => {
|
||||
if (ca && ca.id) {
|
||||
allIssuers.push({
|
||||
id: ca.id,
|
||||
name: ca.name || ca.common_name || 'Unnamed CA',
|
||||
common_name: ca.common_name || 'No CN',
|
||||
type: 'CA'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Ajouter les SubCAs
|
||||
if (this.subcas && Array.isArray(this.subcas)) {
|
||||
this.subcas.forEach(subca => {
|
||||
if (subca && subca.id) {
|
||||
allIssuers.push({
|
||||
id: subca.id,
|
||||
name: subca.name || subca.common_name || 'Unnamed SubCA',
|
||||
common_name: subca.common_name || 'No CN',
|
||||
type: 'SubCA'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('All issuers combined:', allIssuers);
|
||||
|
||||
if (allIssuers.length === 0) {
|
||||
this.showError('Please create a CA or Sub CA first.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Remplir le dropdown
|
||||
const dropdown = document.getElementById('issuerCA');
|
||||
if (!dropdown) {
|
||||
console.error('ERROR: Dropdown #issuerCA not found in DOM!');
|
||||
// Vérifier si l'élément existe avec un autre ID
|
||||
console.log('Available select elements:', document.querySelectorAll('select'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Dropdown found, populating...');
|
||||
|
||||
// Sauvegarder la sélection actuelle
|
||||
const currentValue = dropdown.value;
|
||||
|
||||
// Vider et remplir
|
||||
dropdown.innerHTML = '<option value="">-- Select Issuer --</option>';
|
||||
|
||||
allIssuers.forEach(issuer => {
|
||||
const option = document.createElement('option');
|
||||
option.value = issuer.id;
|
||||
option.textContent = `${issuer.name} (${issuer.type})`;
|
||||
dropdown.appendChild(option);
|
||||
});
|
||||
|
||||
// Restaurer la sélection si possible
|
||||
if (currentValue && allIssuers.some(i => i.id === currentValue)) {
|
||||
dropdown.value = currentValue;
|
||||
}
|
||||
|
||||
console.log(`Dropdown populated with ${allIssuers.length} options`);
|
||||
console.log('Dropdown HTML:', dropdown.innerHTML);
|
||||
|
||||
// 5. Afficher la modal
|
||||
const modal = document.getElementById('createCertModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
console.log('Modal shown');
|
||||
|
||||
// Focus sur le premier champ
|
||||
setTimeout(() => {
|
||||
const firstInput = modal.querySelector('input, select');
|
||||
if (firstInput) firstInput.focus();
|
||||
}, 100);
|
||||
} else {
|
||||
console.error('ERROR: Modal #createCertModal not found!');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in showCreateCertModal:', error);
|
||||
this.showError('Failed to open certificate form: ' + error.message);
|
||||
}
|
||||
|
||||
console.log('=== showCreateCertModal END ===');
|
||||
}
|
||||
|
||||
hideModal() {
|
||||
document.querySelectorAll('.modal').forEach(modal => {
|
||||
modal.classList.add('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
async handleCreateCA(e) {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.apiBase}/cas`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: data.name,
|
||||
common_name: data.common_name,
|
||||
organization: data.organization,
|
||||
country: data.country,
|
||||
province: data.province || '',
|
||||
locality: data.locality || '',
|
||||
email: data.email || '',
|
||||
key_size: parseInt(data.key_size) || 4096,
|
||||
valid_years: parseInt(data.valid_years) || 10,
|
||||
is_root: data.is_root === 'true'
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
this.showSuccess('CA created successfully');
|
||||
this.hideModal();
|
||||
e.target.reset();
|
||||
await this.fetchAllData();
|
||||
this.showTab(this.currentTab);
|
||||
} else {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to create CA');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('CA creation error:', error);
|
||||
this.showError('Failed to create CA: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async handleCreateSubCA(e) {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.apiBase}/subcas`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: data.name,
|
||||
common_name: data.common_name,
|
||||
organization: data.organization,
|
||||
email: data.email || '',
|
||||
country: data.country,
|
||||
province: data.province || '',
|
||||
locality: data.locality || '',
|
||||
parent_ca_id: data.parent_ca_id,
|
||||
key_size: parseInt(data.key_size) || 4096,
|
||||
valid_years: parseInt(data.valid_years) || 5
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
this.showSuccess('Sub CA created successfully');
|
||||
this.hideModal();
|
||||
e.target.reset();
|
||||
await this.fetchAllData();
|
||||
this.showTab(this.currentTab);
|
||||
} else {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to create Sub CA');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('SubCA creation error:', error);
|
||||
this.showError('Failed to create Sub CA: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async handleCreateCertificate(e) {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.target);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
|
||||
try {
|
||||
const dnsNames = data.dns_names ? data.dns_names.split(',').map(s => s.trim()).filter(s => s) : [];
|
||||
const ipAddresses = data.ip_addresses ? data.ip_addresses.split(',').map(s => s.trim()).filter(s => s) : [];
|
||||
|
||||
const response = await fetch(`${this.apiBase}/certificates`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
common_name: data.common_name,
|
||||
type: data.type,
|
||||
dns_names: dnsNames,
|
||||
ip_addresses: ipAddresses,
|
||||
issuer_ca_id: data.issuer_ca_id,
|
||||
key_size: parseInt(data.key_size) || 2048,
|
||||
valid_days: parseInt(data.valid_days) || 365
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
this.showSuccess('Certificate created successfully');
|
||||
this.hideModal();
|
||||
e.target.reset();
|
||||
await this.fetchAllData();
|
||||
this.showTab(this.currentTab);
|
||||
} else {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to create certificate');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Certificate creation error:', error);
|
||||
this.showError('Failed to create certificate: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteCA(id) {
|
||||
if (!confirm('Delete this CA?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.apiBase}/cas/${id}`, { method: 'DELETE' });
|
||||
if (response.ok) {
|
||||
this.showSuccess('CA deleted');
|
||||
await this.fetchAllData();
|
||||
this.showTab(this.currentTab);
|
||||
} else {
|
||||
throw new Error('Delete failed');
|
||||
}
|
||||
} catch (error) {
|
||||
this.showError('Failed to delete CA');
|
||||
}
|
||||
}
|
||||
|
||||
async deleteSubCA(id) {
|
||||
if (!confirm('Delete this Sub CA?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.apiBase}/subcas/${id}`, { method: 'DELETE' });
|
||||
if (response.ok) {
|
||||
this.showSuccess('Sub CA deleted');
|
||||
await this.fetchAllData();
|
||||
this.showTab(this.currentTab);
|
||||
} else {
|
||||
throw new Error('Delete failed');
|
||||
}
|
||||
} catch (error) {
|
||||
this.showError('Failed to delete Sub CA');
|
||||
}
|
||||
}
|
||||
|
||||
async deleteCertificate(id) {
|
||||
if (!confirm('Delete this certificate?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.apiBase}/certificates/${id}`, { method: 'DELETE' });
|
||||
if (response.ok) {
|
||||
this.showSuccess('Certificate deleted');
|
||||
await this.fetchAllData();
|
||||
this.showTab(this.currentTab);
|
||||
} else {
|
||||
throw new Error('Delete failed');
|
||||
}
|
||||
} catch (error) {
|
||||
this.showError('Failed to delete certificate');
|
||||
}
|
||||
}
|
||||
|
||||
async revokeCertificate(id) {
|
||||
const reason = prompt('Revocation reason:') || 'Administrative revocation';
|
||||
if (!reason) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.apiBase}/certificates/${id}/revoke`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ reason })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
this.showSuccess('Certificate revoked');
|
||||
await this.fetchAllData();
|
||||
this.showTab(this.currentTab);
|
||||
} else {
|
||||
throw new Error('Revocation failed');
|
||||
}
|
||||
} catch (error) {
|
||||
this.showError('Failed to revoke certificate');
|
||||
}
|
||||
}
|
||||
|
||||
downloadCertificate(id) {
|
||||
window.open(`${this.apiBase}/certificates/${id}/download/cert`, '_blank');
|
||||
}
|
||||
|
||||
async viewCA(id) {
|
||||
try {
|
||||
const response = await fetch(`${this.apiBase}/cas/${id}`);
|
||||
if (!response.ok) throw new Error('Not found');
|
||||
|
||||
const ca = await response.json();
|
||||
this.showModal('CA Details', `
|
||||
<h3>${ca.name}</h3>
|
||||
<p><strong>Common Name:</strong> ${ca.common_name}</p>
|
||||
<p><strong>Organization:</strong> ${ca.organization}</p>
|
||||
<p><strong>Valid From:</strong> ${new Date(ca.valid_from).toLocaleString()}</p>
|
||||
<p><strong>Valid To:</strong> ${new Date(ca.valid_to).toLocaleString()}</p>
|
||||
<p><strong>Serial:</strong> ${ca.serial_number}</p>
|
||||
<button class="btn" onclick="window.open('${this.apiBase}/cas/${id}/download/cert')">Download Certificate</button>
|
||||
`);
|
||||
} catch (error) {
|
||||
this.showError('Failed to load CA details');
|
||||
}
|
||||
}
|
||||
|
||||
async viewSubCA(id) {
|
||||
try {
|
||||
const response = await fetch(`${this.apiBase}/subcas/${id}`);
|
||||
if (!response.ok) throw new Error('Not found');
|
||||
|
||||
const subca = await response.json();
|
||||
this.showModal('Sub CA Details', `
|
||||
<h3>${subca.name}</h3>
|
||||
<p><strong>Common Name:</strong> ${subca.common_name}</p>
|
||||
<p><strong>Organization:</strong> ${subca.organization}</p>
|
||||
<p><strong>Valid From:</strong> ${new Date(subca.valid_from).toLocaleString()}</p>
|
||||
<p><strong>Valid To:</strong> ${new Date(subca.valid_to).toLocaleString()}</p>
|
||||
<p><strong>Serial:</strong> ${subca.serial_number}</p>
|
||||
<button class="btn" onclick="window.open('${this.apiBase}/subcas/${id}/download/cert')">Download Certificate</button>
|
||||
`);
|
||||
} catch (error) {
|
||||
this.showError('Failed to load Sub CA details');
|
||||
}
|
||||
}
|
||||
|
||||
async viewCertificate(id) {
|
||||
try {
|
||||
const response = await fetch(`${this.apiBase}/certificates/${id}`);
|
||||
if (!response.ok) throw new Error('Not found');
|
||||
|
||||
const cert = await response.json();
|
||||
this.showModal('Certificate Details', `
|
||||
<h3>${cert.common_name}</h3>
|
||||
<p><strong>Type:</strong> ${cert.type}</p>
|
||||
<p><strong>Valid From:</strong> ${new Date(cert.valid_from).toLocaleString()}</p>
|
||||
<p><strong>Valid To:</strong> ${new Date(cert.valid_to).toLocaleString()}</p>
|
||||
<p><strong>Status:</strong> ${cert.revoked ? 'Revoked' : new Date(cert.valid_to) > new Date() ? 'Valid' : 'Expired'}</p>
|
||||
<p><strong>Serial:</strong> ${cert.serial_number}</p>
|
||||
<div class="button-group">
|
||||
<button class="btn" onclick="window.open('${this.apiBase}/certificates/${id}/download/cert')">Download Certificate</button>
|
||||
<button class="btn" onclick="window.open('${this.apiBase}/certificates/${id}/download/key')">Download Private Key</button>
|
||||
</div>
|
||||
`);
|
||||
} catch (error) {
|
||||
this.showError('Failed to load certificate details');
|
||||
}
|
||||
}
|
||||
|
||||
showModal(title, content) {
|
||||
const modal = document.getElementById('viewModal');
|
||||
if (!modal) return;
|
||||
|
||||
modal.querySelector('h2').textContent = title;
|
||||
modal.querySelector('.modal-body').innerHTML = content;
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
showSuccess(message) {
|
||||
this.showAlert(message, 'success');
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
this.showAlert(message, 'error');
|
||||
}
|
||||
|
||||
showAlert(message, type) {
|
||||
// Remove existing alerts
|
||||
document.querySelectorAll('.alert').forEach(a => a.remove());
|
||||
|
||||
const alert = document.createElement('div');
|
||||
alert.className = `alert alert-${type}`;
|
||||
alert.textContent = message;
|
||||
alert.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 15px 20px;
|
||||
border-radius: 5px;
|
||||
z-index: 10000;
|
||||
animation: slideIn 0.3s ease;
|
||||
`;
|
||||
|
||||
document.body.appendChild(alert);
|
||||
|
||||
setTimeout(() => {
|
||||
alert.style.animation = 'slideOut 0.3s ease';
|
||||
setTimeout(() => alert.remove(), 300);
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.pki = new PKIManager();
|
||||
});
|
||||
307
internal/web/templates/index.html
Normal file
307
internal/web/templates/index.html
Normal file
@@ -0,0 +1,307 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PKI Manager</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1><i class="fas fa-shield-alt"></i> PKI Manager</h1>
|
||||
<p>Manage your Certificate Authority infrastructure</p>
|
||||
<div style="margin-bottom: 10px;">
|
||||
<button class="btn" onclick="pki.debugData()" style="background: #38a169;">
|
||||
<i class="fas fa-bug"></i> Debug Data
|
||||
</button>
|
||||
<button onclick="testDropdown()" style="padding: 8px 12px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||
Test Dropdown
|
||||
</button>
|
||||
<script>
|
||||
function testDropdown() {
|
||||
console.log('=== TEST DROPDOWN ===');
|
||||
const dropdown = document.getElementById('issuerCA');
|
||||
console.log('Dropdown:', dropdown);
|
||||
console.log('Parent:', dropdown?.parentElement);
|
||||
console.log('All selects:', document.querySelectorAll('select'));
|
||||
|
||||
// Simuler le remplissage
|
||||
if (dropdown) {
|
||||
dropdown.innerHTML = '';
|
||||
const option = document.createElement('option');
|
||||
option.value = 'test';
|
||||
option.textContent = 'Test Option';
|
||||
dropdown.appendChild(option);
|
||||
console.log('Test option added');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
</div>
|
||||
<nav>
|
||||
<button class="nav-btn" data-tab="dashboard">
|
||||
<i class="fas fa-home"></i> Dashboard
|
||||
</button>
|
||||
<button class="nav-btn" data-tab="cas">
|
||||
<i class="fas fa-certificate"></i> CAs
|
||||
</button>
|
||||
<button class="nav-btn" data-tab="subcas">
|
||||
<i class="fas fa-sitemap"></i> Sub CAs
|
||||
</button>
|
||||
<button class="nav-btn" data-tab="certificates">
|
||||
<i class="fas fa-key"></i> Certificates
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<!-- Dashboard Tab -->
|
||||
<div id="dashboardTab" class="tab-content">
|
||||
<div id="dashboardContent">
|
||||
<!-- Dashboard content will be loaded here -->
|
||||
<div style="text-align: center; padding: 50px;">
|
||||
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
||||
<p>Loading dashboard...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CAs Tab -->
|
||||
<div id="casTab" class="tab-content hidden">
|
||||
<div id="casContent">
|
||||
<div style="text-align: center; padding: 50px;">
|
||||
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
||||
<p>Loading CAs...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sub CAs Tab -->
|
||||
<div id="subcasTab" class="tab-content hidden">
|
||||
<div id="subcasContent">
|
||||
<div style="text-align: center; padding: 50px;">
|
||||
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
||||
<p>Loading Sub CAs...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Certificates Tab -->
|
||||
<div id="certificatesTab" class="tab-content hidden">
|
||||
<div id="certsContent">
|
||||
<div style="text-align: center; padding: 50px;">
|
||||
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
||||
<p>Loading Certificates...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Create CA Modal -->
|
||||
<div id="createCAModal" class="modal hidden">
|
||||
<div class="modal-content">
|
||||
<button class="close-btn" onclick="pki.hideModal()" style="background: none; border: none; float: right; font-size: 24px; cursor: pointer;">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
<h2>Create Root CA</h2>
|
||||
<form id="createCAForm">
|
||||
<div class="form-group">
|
||||
<label for="caName">Name</label>
|
||||
<input type="text" id="caName" name="name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="caCommonName">Common Name</label>
|
||||
<input type="text" id="caCommonName" name="common_name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="caOrganization">Organization</label>
|
||||
<input type="text" id="caOrganization" name="organization" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="caCountry">Country (2 letters)</label>
|
||||
<input type="text" id="caCountry" name="country" maxlength="2" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="caProvince">Province/State</label>
|
||||
<input type="text" id="caProvince" name="province">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="caLocality">Locality/City</label>
|
||||
<input type="text" id="caLocality" name="locality">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="caEmail">Email</label>
|
||||
<input type="email" id="caEmail" name="email" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="caKeySize">Key Size</label>
|
||||
<select id="caKeySize" name="key_size" required>
|
||||
<option value="2048">2048 bits</option>
|
||||
<option value="4096" selected>4096 bits</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="caValidYears">Validity (Years)</label>
|
||||
<input type="number" id="caValidYears" name="valid_years" value="10" min="1" max="20" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" id="caIsRoot" name="is_root" value="true" checked>
|
||||
Is Root CA
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" class="btn">Create CA</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Sub CA Modal -->
|
||||
<div id="createSubCAModal" class="modal hidden">
|
||||
<div class="modal-content">
|
||||
<button class="close-btn" onclick="pki.hideModal()" style="background: none; border: none; float: right; font-size: 24px; cursor: pointer;">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
<h2>Create Sub CA</h2>
|
||||
<form id="createSubCAForm">
|
||||
<div class="form-group">
|
||||
<label for="subcaName">Name</label>
|
||||
<input type="text" id="subcaName" name="name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="subcaCommonName">Common Name</label>
|
||||
<input type="text" id="subcaCommonName" name="common_name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="subcaOrganization">Organization</label>
|
||||
<input type="text" id="subcaOrganization" name="organization" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="subcaEmail">Email (optional)</label>
|
||||
<input type="email" id="subcaEmail" name="email" placeholder="Optional email address">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="subcaCountry">Country (2 letters)</label>
|
||||
<input type="text" id="subcaCountry" name="country" maxlength="2" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="subcaProvince">Province/State</label>
|
||||
<input type="text" id="subcaProvince" name="province">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="subcaLocality">Locality/City</label>
|
||||
<input type="text" id="subcaLocality" name="locality">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="subcaParentCA">Parent CA</label>
|
||||
<select id="parentCA" name="parent_ca_id" required></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="subcaKeySize">Key Size</label>
|
||||
<select id="subcaKeySize" name="key_size" required>
|
||||
<option value="2048">2048 bits</option>
|
||||
<option value="4096" selected>4096 bits</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="subcaValidYears">Validity (Years)</label>
|
||||
<input type="number" id="subcaValidYears" name="valid_years" value="5" min="1" max="10" required>
|
||||
</div>
|
||||
<button type="submit" class="btn">Create Sub CA</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Create Certificate Modal -->
|
||||
|
||||
<div id="createCertModal" class="modal hidden">
|
||||
<div class="modal-content" style="max-width: 600px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||
<h2 style="margin: 0;">Create Certificate</h2>
|
||||
<button class="close-btn" onclick="pki.hideModal()"
|
||||
style="background: none; border: none; font-size: 24px; cursor: pointer; color: #666;">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form id="createCertForm">
|
||||
<div class="form-group">
|
||||
<label for="certCommonName">Common Name *</label>
|
||||
<input type="text" id="certCommonName" name="common_name" required
|
||||
placeholder="e.g., server.example.com">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="certType">Certificate Type *</label>
|
||||
<select id="certType" name="type" required>
|
||||
<option value="">-- Select Type --</option>
|
||||
<option value="server">Server Certificate</option>
|
||||
<option value="client">Client Certificate</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- IMPORTANT: Ce dropdown doit avoir l'ID "issuerCA" -->
|
||||
<div class="form-group">
|
||||
<label for="issuerCA">Issuer (CA or Sub CA) *</label>
|
||||
<select id="issuerCA" name="issuer_ca_id" required
|
||||
style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px;">
|
||||
<option value="">-- Loading issuers... --</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="certDNSNames">DNS Names (optional, comma-separated)</label>
|
||||
<input type="text" id="certDNSNames" name="dns_names"
|
||||
placeholder="e.g., example.com, www.example.com">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="certIPs">IP Addresses (optional, comma-separated)</label>
|
||||
<input type="text" id="certIPs" name="ip_addresses"
|
||||
placeholder="e.g., 192.168.1.1, 10.0.0.1">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="certKeySize">Key Size *</label>
|
||||
<select id="certKeySize" name="key_size" required>
|
||||
<option value="2048">2048 bits (Recommended)</option>
|
||||
<option value="4096">4096 bits (High Security)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="certValidDays">Validity Period (Days) *</label>
|
||||
<input type="number" id="certValidDays" name="valid_days"
|
||||
value="365" min="1" max="3650" required>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 10px; margin-top: 30px;">
|
||||
<button type="submit" class="btn"
|
||||
style="flex: 1; padding: 12px; background: #667eea; color: white;">
|
||||
<i class="fas fa-plus"></i> Create Certificate
|
||||
</button>
|
||||
<button type="button" class="btn" onclick="pki.hideModal()"
|
||||
style="padding: 12px; background: #ccc; color: #333;">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- View Details Modal -->
|
||||
<div id="viewModal" class="modal hidden">
|
||||
<div class="modal-content">
|
||||
<button class="close-btn" onclick="pki.hideModal()" style="background: none; border: none; float: right; font-size: 24px; cursor: pointer;">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
<h2>View Details</h2>
|
||||
<div class="modal-body">
|
||||
<!-- Content will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user