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 = '
Loading dashboard...
';
try {
// Charger les données
await this.fetchAllData();
content.innerHTML = `
Root CAs
${this.cas.filter(c => c.is_root).length}
Sub CAs
${this.subcas.length}
Certificates
${this.certificates.length}
Active
${this.certificates.filter(c => !c.revoked && new Date(c.valid_to) > new Date()).length}
Recent Certificates
${this.certificates.length > 0 ? `
| Name | Type | Issued | Expires | Status |
${this.certificates.slice(0, 5).map(cert => `
| ${cert.common_name} |
${cert.type} |
${new Date(cert.created_at).toLocaleDateString()} |
${new Date(cert.valid_to).toLocaleDateString()} |
${cert.revoked ? 'Revoked' : new Date(cert.valid_to) > new Date() ? 'Valid' : 'Expired'}
|
`).join('')}
` : '
No certificates yet
'}
`;
} catch (error) {
console.error('Dashboard error:', error);
content.innerHTML = 'Failed to load dashboard
';
}
}
async loadCAs() {
const content = document.getElementById('casContent');
if (!content) return;
content.innerHTML = 'Loading CAs...
';
try {
await this.fetchCAs();
content.innerHTML = `
${this.cas.length > 0 ? `
| Name |
Common Name |
Organization |
Valid From |
Valid To |
Actions |
${this.cas.map(ca => `
| ${ca.name} |
${ca.common_name} |
${ca.organization} |
${new Date(ca.valid_from).toLocaleDateString()} |
${new Date(ca.valid_to).toLocaleDateString()} |
|
`).join('')}
` : `
No Certificate Authorities found
`}
`;
} catch (error) {
console.error('CAs error:', error);
content.innerHTML = 'Failed to load CAs
';
}
}
async loadSubCAs() {
const content = document.getElementById('subcasContent');
if (!content) return;
content.innerHTML = 'Loading Sub CAs...
';
try {
await this.fetchSubCAs();
await this.fetchCAs(); // Need CAs for parent info
content.innerHTML = `
${this.subcas.length > 0 ? `
| Name |
Common Name |
Parent CA |
Valid From |
Valid To |
Actions |
${this.subcas.map(subca => {
const parent = this.cas.find(ca => ca.id === subca.parent_ca_id);
return `
| ${subca.name} |
${subca.common_name} |
${parent ? parent.name : 'Unknown'} |
${new Date(subca.valid_from).toLocaleDateString()} |
${new Date(subca.valid_to).toLocaleDateString()} |
|
`;
}).join('')}
` : `
No Sub Certificate Authorities found
${this.cas.length > 0 ? `
` : '
Create a CA first to create Sub CAs
'}
`}
`;
} catch (error) {
console.error('SubCAs error:', error);
content.innerHTML = 'Failed to load Sub CAs
';
}
}
async loadCertificates() {
const content = document.getElementById('certsContent');
if (!content) return;
content.innerHTML = 'Loading Certificates...
';
try {
await this.fetchCertificates();
await this.fetchCAs();
await this.fetchSubCAs();
content.innerHTML = `
${this.certificates.length > 0 ? `
| Common Name |
Type |
Issuer |
Issued |
Expires |
Status |
Actions |
${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 `
| ${cert.common_name} |
${cert.type} |
${issuer ? issuer.name : 'Unknown'} |
${new Date(cert.created_at).toLocaleDateString()} |
${new Date(cert.valid_to).toLocaleDateString()} |
${status.charAt(0).toUpperCase() + status.slice(1)} |
${!cert.revoked ? `` : ''}
|
`;
}).join('')}
` : `
No certificates found
${this.cas.length > 0 || this.subcas.length > 0 ? `
` : '
Create a CA or Sub CA first
'}
`}
`;
} catch (error) {
console.error('Certificates error:', error);
content.innerHTML = 'Failed to load certificates
';
}
}
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 =>
``
).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 = '';
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,
organization_unit: data.organization_unit,
country: data.country,
province: data.province || '',
locality: data.locality || '',
street_address: data.street_address || '',
postal_code: data.postal_code || '',
email: data.email || '',
key_size: Number.parseInt(data.key_size) || 4096,
valid_years: Number.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,
organization_unit: data.organization_unit,
email: data.email || '',
country: data.country,
province: data.province || '',
locality: data.locality || '',
parent_ca_id: data.parent_ca_id,
key_size: Number.parseInt(data.key_size) || 4096,
valid_years: Number.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: Number.parseInt(data.key_size) || 2048,
valid_days: Number.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', `
${ca.name}
Common Name: ${ca.common_name}
Organization: ${ca.organization}
Organizational Unit: ${ca.organization_unit}
StreetAddress: ${ca.street_address}
PostalCode: ${ca.postal_code}
Valid From: ${new Date(ca.valid_from).toLocaleString()}
Valid To: ${new Date(ca.valid_to).toLocaleString()}
Serial: ${ca.serial_number}
`);
} 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', `
${subca.name}
Common Name: ${subca.common_name}
Subject: ${subca.subject}
Organization: ${subca.organization}
Organizational Unit: ${subca.organization_unit}
Valid From: ${new Date(subca.valid_from).toLocaleString()}
Valid To: ${new Date(subca.valid_to).toLocaleString()}
Serial: ${subca.serial_number}
`);
} 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', `
${cert.common_name}
Type: ${cert.type}
SANs: ${cert.dns_names}
IP Address: ${cert.ip_adresses}
Valid From: ${new Date(cert.valid_from).toLocaleString()}
Valid To: ${new Date(cert.valid_to).toLocaleString()}
Status: ${cert.revoked ? 'Revoked' : new Date(cert.valid_to) > new Date() ? 'Valid' : 'Expired'}
Serial: ${cert.serial_number}
`);
} 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();
});