Files
mkcertWeb/public/scep.html
2025-10-09 02:02:34 -04:00

980 lines
34 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SCEP Service - mkcert Web UI</title>
<link rel="stylesheet" href="styles.css">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
/* SCEP-specific styles that extend the main theme */
.scep-config {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 15px;
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Courier New', monospace;
font-size: 14px;
margin: 10px 0;
color: var(--text-color);
word-break: break-all;
}
.scep-url {
background: var(--card-bg-secondary);
border: 1px solid var(--border-color);
border-left: 4px solid var(--primary-color);
border-radius: 8px;
padding: 15px;
margin: 15px 0;
}
.scep-url strong {
color: var(--primary-color);
display: block;
margin-bottom: 8px;
}
.scep-url code {
color: var(--secondary-color);
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Courier New', monospace;
font-size: 14px;
word-break: break-all;
}
.challenge-form {
display: grid;
gap: 20px;
max-width: 500px;
}
.challenge-form .btn {
justify-self: start;
width: auto;
}
.form-group {
display: flex;
flex-direction: column;
}
.form-group label {
margin-bottom: 8px;
font-weight: 600;
color: var(--primary-color);
}
.form-group input,
.form-group select {
padding: 12px;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--input-bg);
color: var(--text-color);
font-size: 14px;
transition: all 0.3s ease;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 5px var(--glow-primary);
}
.certificate-table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
background: var(--card-bg);
border-radius: 8px;
overflow: hidden;
}
.certificate-table th,
.certificate-table td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
.certificate-table th {
background: var(--card-bg-secondary);
font-weight: 600;
color: var(--primary-color);
}
.certificate-table td {
color: var(--text-color);
}
.certificate-table code {
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Courier New', monospace;
color: var(--secondary-color);
font-size: 12px;
}
.status-indicator {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 8px;
}
.status-active {
background: var(--success-color);
box-shadow: 0 0 5px var(--success-color);
}
.status-expired {
background: var(--error-color);
box-shadow: 0 0 5px var(--error-color);
}
/* Card styling for SCEP-specific content containers */
.card {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1.5rem;
box-shadow: inset 0 1px 0 var(--glow-primary);
transition: all 0.3s ease;
}
/* Section h3 styling to match site theme */
section h3 {
color: var(--primary-color);
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border-color);
font-size: 1.2rem;
font-weight: 600;
text-shadow: 0 0 5px var(--glow-primary);
}
section h3 i {
margin-right: 0.5rem;
color: var(--secondary-color);
}
/* Section h4 styling for nested headings */
section h4 {
color: var(--text-color);
margin-top: 1rem;
margin-bottom: 0.5rem;
font-size: 1.1rem;
font-weight: 600;
}
section h5 {
color: var(--text-color);
margin-top: 1rem;
margin-bottom: 0.5rem;
font-size: 1rem;
font-weight: 600;
}
/* Validation feedback styling */
.validation-feedback {
margin-top: 0.5rem;
font-size: 0.875rem;
padding: 0.5rem;
border-radius: 4px;
}
.validation-feedback.valid {
color: var(--success-color);
background: rgba(0, 255, 65, 0.1);
border: 1px solid rgba(0, 255, 65, 0.3);
}
.validation-feedback.invalid {
color: var(--error-color);
background: rgba(255, 0, 65, 0.1);
border: 1px solid rgba(255, 0, 65, 0.3);
}
/* Navigation styles to match main page */
.nav-link {
color: var(--primary-color);
text-decoration: none;
padding: 8px 16px;
border-radius: 4px;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 8px;
}
.nav-link:hover {
background: var(--button-hover-bg);
transform: translateY(-1px);
}
.nav-link.active {
background: var(--primary-color);
color: var(--card-bg);
}
.nav-link i {
font-size: 16px;
}
</style>
</head>
<body>
<div class="container">
<header>
<button class="theme-toggle" id="theme-toggle">
<i class="fas fa-sun"></i> Light Mode
</button>
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<h1><i class="fas fa-satellite-dish"></i> SCEP Service</h1>
<p>Simple Certificate Enrollment Protocol for mkcert</p>
</div>
<div id="auth-controls" style="display: none;">
<span id="username-display" style="margin-right: 15px; color: var(--secondary-color);"></span>
<button id="logout-btn" class="btn btn-logout">
<i class="fas fa-sign-out-alt"></i> Logout
</button>
</div>
</div>
</header>
<!-- Navigation -->
<nav style="background: var(--secondary-bg); padding: 10px 0; margin: 20px 0; border-radius: 8px; border: 1px solid var(--border-color);">
<div style="display: flex; gap: 20px; justify-content: center; flex-wrap: wrap;">
<a href="/" style="color: var(--text-color); text-decoration: none; padding: 8px 16px; border-radius: 4px; border: 1px solid var(--border-color); transition: all 0.3s;">
<i class="fas fa-home"></i> Certificate Manager
</a>
<a href="/scep.html" style="text-decoration: none; padding: 8px 16px; border-radius: 4px; background: var(--primary-color); color: white;">
<i class="fas fa-satellite-dish"></i> SCEP Service
</a>
<a href="/settings.html" style="color: var(--text-color); text-decoration: none; padding: 8px 16px; border-radius: 4px; border: 1px solid var(--border-color); transition: all 0.3s;">
<i class="fas fa-cog"></i> Settings
</a>
</div>
</nav>
<section>
<h2><i class="fas fa-info-circle"></i> SCEP Service Overview</h2>
<p>
SCEP (Simple Certificate Enrollment Protocol) allows devices to automatically request and receive certificates from this mkcert Web UI service.
This implementation provides a simplified SCEP server that generates certificates using mkcert.
</p>
<div class="alert alert-info" style="margin-top:20px; animation: none;">
<strong>Note:</strong> This is a simplified SCEP implementation designed for development and testing environments.
For production use, consider a full-featured SCEP server.
</div>
</section>
<section>
<h3><i class="fas fa-cog"></i> SCEP Configuration</h3>
<p>Use these URLs to configure SCEP clients:</p>
<div class="scep-url">
<strong>SCEP Service URL:</strong>
<code id="scep-service-url">Loading...</code>
</div>
<div class="scep-url">
<strong>Get CA Certificate:</strong>
<code id="get-ca-cert-url">Loading...</code>
</div>
<div class="scep-url">
<strong>Get CA Capabilities:</strong>
<code id="get-ca-caps-url">Loading...</code>
</div>
<button type="button" class="btn btn-secondary" onclick="loadSCEPConfig()" style="margin-top: 15px;">
<i class="fas fa-sync-alt"></i> Refresh Configuration
</button>
</section>
<!-- Enterprise CA Status Section -->
<section>
<h3><i class="fas fa-building"></i> Enterprise CA Status</h3>
<div id="enterprise-ca-status" class="card">
<div class="loading">
<i class="fas fa-spinner fa-spin"></i> Checking Enterprise CA status...
</div>
</div>
</section>
<!-- Certificate Templates Section -->
<section>
<h3><i class="fas fa-clipboard-list"></i> Certificate Templates</h3>
<div id="certificate-templates" class="card">
<div class="loading">
<i class="fas fa-spinner fa-spin"></i> Loading certificate templates...
</div>
</div>
</section>
<section>
<h3><i class="fas fa-ticket-alt"></i> Challenge Password Management</h3>
<p>Generate challenge passwords for SCEP clients:</p>
<div class="challenge-form">
<div class="form-group">
<label for="challenge-identifier">Identifier:</label>
<input type="text" id="challenge-identifier" placeholder="Enter unique identifier (e.g., device-001)">
</div>
<div class="form-group">
<label for="challenge-expires">Expires In:</label>
<select id="challenge-expires">
<option value="3600">1 Hour</option>
<option value="7200">2 Hours</option>
<option value="86400">24 Hours</option>
<option value="604800">7 Days</option>
</select>
</div>
<button type="button" class="btn btn-primary" onclick="generateChallenge()">
<i class="fas fa-key"></i> Generate Challenge Password
</button>
</div>
<div id="challenge-result" style="margin-top: 20px;"></div>
</section>
<section>
<h3><i class="fas fa-list"></i> Active Challenge Passwords</h3>
<div id="challenges-list">
<div class="loading">
<i class="fas fa-spinner fa-spin"></i> Loading challenges...
</div>
</div>
<button type="button" class="btn btn-secondary" onclick="loadChallenges()">
<i class="fas fa-sync-alt"></i> Refresh Challenges
</button>
</section>
<section>
<h3><i class="fas fa-flask"></i> Manual Certificate Generation</h3>
<p>Generate certificates using SCEP workflow with Enterprise CA support:</p>
<div class="challenge-form">
<div class="form-group">
<label for="cert-common-name">Common Name (Domain):</label>
<input type="text" id="cert-common-name" placeholder="Enter domain (e.g., user.example.com)">
<small class="help-text">Primary domain name or identity for the certificate</small>
</div>
<div class="form-group">
<label for="cert-template">Certificate Template:</label>
<select id="cert-template" class="form-select">
<option value="User">User - Client Authentication</option>
<option value="M365User">M365 User - Microsoft 365 Integration</option>
<option value="Computer">Computer - Machine Authentication</option>
<option value="WiFi">WiFi - Wireless Authentication</option>
</select>
<small class="help-text">Choose template based on certificate usage</small>
</div>
<div class="form-group" id="cert-upn-group" style="display: none;">
<label for="cert-upn">User Principal Name (UPN):</label>
<input type="email" id="cert-upn" placeholder="user@domain.com">
<small class="help-text">Required for M365 User certificates</small>
<div id="cert-upn-validation" class="validation-feedback"></div>
</div>
<div class="form-group">
<label for="cert-san">Subject Alternative Names (optional):</label>
<textarea id="cert-san" placeholder="subdomain.example.com&#10;192.168.1.100" rows="2"></textarea>
<small class="help-text">Additional domains or IPs. One per line.</small>
</div>
<div class="form-group">
<label for="cert-challenge">Challenge Password (optional):</label>
<input type="password" id="cert-challenge" placeholder="Enter challenge password or leave empty">
<small class="help-text">SCEP challenge password (optional if configured globally)</small>
</div>
<button type="button" class="btn btn-success" onclick="generateSCEPCertificate()">
<i class="fas fa-satellite-dish"></i> Generate SCEP Certificate
</button>
</div>
<div id="cert-result" style="margin-top: 20px;"></div>
</section>
<section>
<h3><i class="fas fa-certificate"></i> SCEP Certificates</h3>
<div id="scep-certificates">
<div class="loading">
<i class="fas fa-spinner fa-spin"></i> Loading SCEP certificates...
</div>
</div>
<button type="button" class="btn btn-secondary" onclick="loadSCEPCertificates()">
<i class="fas fa-sync-alt"></i> Refresh Certificates
</button>
</section>
<!-- Integration Guide Section -->
<section>
<h3><i class="fas fa-book"></i> Enterprise Integration Guide</h3>
<div class="alert alert-info">
<h4><i class="fas fa-mobile-alt"></i> MDM Integration</h4>
<p>Configure your Mobile Device Management system to use this SCEP service:</p>
<div class="scep-config">
<strong>SCEP URL:</strong> <span id="mdm-scep-url">Loading...</span><br>
<strong>Challenge:</strong> Configure via environment variables<br>
<strong>Supported Templates:</strong> User, Computer, WiFi, M365User<br>
<strong>Key Size:</strong> 2048 bits (RSA)
</div>
<p><strong>Compatible with:</strong> Microsoft Intune, VMware Workspace ONE, Cisco Meraki, Apple Profile Manager</p>
</div>
<div class="alert alert-info">
<h4><i class="fas fa-cloud"></i> Microsoft 365 Integration</h4>
<p>For hybrid Azure AD environments, use the M365User template:</p>
<div class="scep-config">
<strong>Template:</strong> M365User<br>
<strong>UPN Format:</strong> user@domain.com<br>
<strong>Subject Alternative Name:</strong> Automatically includes UPN<br>
<strong>Key Usage:</strong> Digital Signature, Key Encipherment, Client Authentication
</div>
<h5>Required Environment Variables:</h5>
<div class="scep-config">
ENTERPRISE_CA_ENABLED=true<br>
ENTERPRISE_CA_CERT_PATH=/path/to/ca.crt<br>
ENTERPRISE_CA_KEY_PATH=/path/to/ca.key<br>
ENTERPRISE_CA_ORGANIZATION="Your Organization"<br>
ENTERPRISE_CA_ORGANIZATIONAL_UNIT="IT Department"
</div>
</div>
<div class="alert alert-info">
<h4><i class="fas fa-cogs"></i> API Endpoints</h4>
<div class="scep-config">
<strong>GET</strong> /scep?operation=GetCACert - Retrieve CA certificate<br>
<strong>POST</strong> /scep?operation=PKIOperation - Process certificate requests<br>
<strong>POST</strong> /api/scep/certificate - Manual certificate generation<br>
<strong>GET</strong> /api/scep/enterprise-ca/status - Enterprise CA status<br>
<strong>GET</strong> /api/scep/templates - Available certificate templates<br>
<strong>POST</strong> /api/scep/validate-upn - UPN validation for M365
</div>
</div>
</section>
</div>
<script>
// Theme system integration (copied from main script.js)
const themeToggle = document.getElementById('theme-toggle');
const body = document.body;
const icon = themeToggle.querySelector('i');
const text = themeToggle.lastChild;
// Load saved theme or default to dark
const savedTheme = localStorage.getItem('theme') || 'dark';
body.setAttribute('data-theme', savedTheme);
updateThemeButton(savedTheme);
themeToggle.addEventListener('click', function() {
const currentTheme = body.getAttribute('data-theme');
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
body.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
updateThemeButton(newTheme);
});
function updateThemeButton(theme) {
if (theme === 'light') {
icon.className = 'fas fa-moon';
text.textContent = ' Dark Mode';
} else {
icon.className = 'fas fa-sun';
text.textContent = ' Light Mode';
}
}
// Authentication check (copied from main script.js)
async function checkAuth() {
try {
const response = await fetch('/auth/check');
const result = await response.json();
if (result.authenticated) {
const authControls = document.getElementById('auth-controls');
const usernameDisplay = document.getElementById('username-display');
if (authControls && usernameDisplay) {
if (result.user?.username) {
usernameDisplay.textContent = `Logged in as: ${result.user.username}`;
} else {
usernameDisplay.textContent = 'Authenticated';
}
authControls.style.display = 'block';
}
}
} catch (error) {
console.error('Auth check failed:', error);
}
}
// Logout functionality
document.getElementById('logout-btn')?.addEventListener('click', async function(e) {
e.preventDefault();
try {
const response = await fetch('/auth/logout', { method: 'POST' });
if (response.ok) {
window.location.href = '/login.html';
}
} catch (error) {
console.error('Logout error:', error);
window.location.href = '/login.html';
}
});
// Initialize page
document.addEventListener('DOMContentLoaded', function() {
checkAuth();
loadSCEPConfig();
loadChallenges();
loadSCEPCertificates();
loadEnterpriseCAStatus();
loadCertificateTemplates();
setupTemplateHandling();
});
// Setup template change handling
function setupTemplateHandling() {
const templateSelect = document.getElementById('cert-template');
const upnGroup = document.getElementById('cert-upn-group');
const upnInput = document.getElementById('cert-upn');
templateSelect.addEventListener('change', function() {
if (this.value === 'M365User') {
upnGroup.style.display = 'block';
upnInput.required = true;
} else {
upnGroup.style.display = 'none';
upnInput.required = false;
upnInput.value = '';
}
});
// UPN validation
upnInput.addEventListener('blur', validateUPNInput);
}
// Validate UPN input
async function validateUPNInput() {
const upnInput = document.getElementById('cert-upn');
const upnValidation = document.getElementById('cert-upn-validation');
const upn = upnInput.value.trim();
if (!upn) {
upnValidation.innerHTML = '';
return;
}
try {
const response = await fetch('/api/scep/validate-upn', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ upn })
});
const data = await response.json();
if (data.valid) {
upnValidation.innerHTML = '<span style="color: var(--success-color);"><i class="fas fa-check-circle"></i> Valid UPN format</span>';
} else {
upnValidation.innerHTML = '<span style="color: var(--error-color);"><i class="fas fa-exclamation-circle"></i> Invalid UPN format</span>';
}
} catch (error) {
upnValidation.innerHTML = '<span style="color: var(--warning-color);"><i class="fas fa-question-circle"></i> Could not validate UPN</span>';
}
}
// Load Enterprise CA status
async function loadEnterpriseCAStatus() {
try {
const response = await fetch('/api/scep/enterprise-ca/status');
if (!response.ok) throw new Error('Failed to load Enterprise CA status');
const status = await response.json();
const statusDiv = document.getElementById('enterprise-ca-status');
let statusHtml = `
<div class="scep-config">
<strong>Status:</strong> <span class="status-indicator ${getStatusClass(status.status)}"></span>${status.status}<br>
<strong>Enabled:</strong> ${status.enabled ? 'Yes' : 'No'}<br>
<strong>Fallback to mkcert:</strong> ${status.fallbackToMkcert ? 'Yes' : 'No'}<br>
`;
if (status.configuration) {
statusHtml += `
<strong>Configuration:</strong><br>
&nbsp;&nbsp;CA Cert Path: ${status.configuration.caCertPath || 'Not configured'}<br>
&nbsp;&nbsp;Organization: ${status.configuration.organization}<br>
&nbsp;&nbsp;Country: ${status.configuration.country}<br>
`;
}
if (status.caDetails && !status.caDetails.error) {
statusHtml += `
<strong>CA Details:</strong><br>
&nbsp;&nbsp;Subject: ${status.caDetails.subject}<br>
&nbsp;&nbsp;Valid To: ${status.caDetails.validTo}<br>
`;
}
statusHtml += '</div>';
statusDiv.innerHTML = statusHtml;
} catch (error) {
console.error('Error loading Enterprise CA status:', error);
document.getElementById('enterprise-ca-status').innerHTML = `
<div class="alert alert-error">
<i class="fas fa-exclamation-triangle"></i> Failed to load Enterprise CA status
</div>
`;
}
}
// Load Certificate Templates
async function loadCertificateTemplates() {
try {
const response = await fetch('/api/scep/templates');
if (!response.ok) throw new Error('Failed to load certificate templates');
const data = await response.json();
const templatesDiv = document.getElementById('certificate-templates');
let templatesHtml = '<div class="scep-config">';
data.available.forEach(template => {
templatesHtml += `
<div style="margin-bottom: 15px; border-left: 3px solid var(--primary-color); padding-left: 10px;">
<strong>${template.name}:</strong> ${template.description}<br>
<small>Key Usage: ${template.keyUsage.join(', ')}</small><br>
<small>Extended Key Usage: ${template.extendedKeyUsage.join(', ')}</small>
</div>
`;
});
templatesHtml += '</div>';
templatesDiv.innerHTML = templatesHtml;
} catch (error) {
console.error('Error loading certificate templates:', error);
document.getElementById('certificate-templates').innerHTML = `
<div class="alert alert-error">
<i class="fas fa-exclamation-triangle"></i> Failed to load certificate templates
</div>
`;
}
}
// Get status CSS class
function getStatusClass(status) {
switch (status) {
case 'active': return 'status-active';
case 'disabled': return 'status-expired';
default: return 'status-expired';
}
}
// Load SCEP configuration
async function loadSCEPConfig() {
try {
const response = await fetch('/api/scep/config');
if (!response.ok) throw new Error('Failed to load SCEP config');
const data = await response.json();
const config = data; // Config properties are directly in the response
document.getElementById('scep-service-url').textContent = config.scepUrl;
document.getElementById('get-ca-cert-url').textContent = config.getCACertUrl;
document.getElementById('get-ca-caps-url').textContent = config.getCACapsUrl;
// Also populate MDM integration guide
const mdmUrlElement = document.getElementById('mdm-scep-url');
if (mdmUrlElement) {
mdmUrlElement.textContent = config.scepUrl;
}
} catch (error) {
console.error('Error loading SCEP config:', error);
showAlert('Failed to load SCEP configuration', 'error');
}
}
// Generate challenge password
async function generateChallenge() {
const identifier = document.getElementById('challenge-identifier').value;
const expiresIn = document.getElementById('challenge-expires').value;
if (!identifier) {
showAlert('Please enter an identifier', 'warning');
return;
}
try {
const response = await fetch('/api/scep/challenge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identifier, expiresIn: parseInt(expiresIn) })
});
if (!response.ok) throw new Error('Failed to generate challenge');
const data = await response.json();
const challenge = data; // Challenge properties are directly in the response
const resultDiv = document.getElementById('challenge-result');
resultDiv.innerHTML = `
<div class="alert alert-success">
<strong><i class="fas fa-check-circle"></i> Challenge Password Generated!</strong><br>
<div class="scep-config" style="margin-top: 10px;">
<strong>Identifier:</strong> ${challenge.identifier}<br>
<strong>Password:</strong> <code>${challenge.challengePassword}</code><br>
<strong>Expires:</strong> ${new Date(challenge.expiresAt).toLocaleString()}
</div>
</div>
`;
// Clear form and refresh challenges
document.getElementById('challenge-identifier').value = '';
setTimeout(loadChallenges, 1000);
} catch (error) {
console.error('Error generating challenge:', error);
showAlert('Failed to generate challenge password', 'error');
}
}
// Load active challenges
async function loadChallenges() {
try {
const response = await fetch('/api/scep/challenges');
if (!response.ok) throw new Error('Failed to load challenges');
const data = await response.json();
const challenges = data.challenges; // Challenges array is directly in the response
const listDiv = document.getElementById('challenges-list');
if (challenges.length === 0) {
listDiv.innerHTML = `
<div class="alert alert-info">
<i class="fas fa-info-circle"></i> No active challenge passwords found.
</div>
`;
return;
}
let html = `
<table class="certificate-table">
<thead>
<tr>
<th><i class="fas fa-tag"></i> Identifier</th>
<th><i class="fas fa-heartbeat"></i> Status</th>
<th><i class="fas fa-calendar-times"></i> Expires At</th>
<th><i class="fas fa-check"></i> Used</th>
</tr>
</thead>
<tbody>
`;
challenges.forEach(challenge => {
const isExpired = challenge.expired;
const statusClass = isExpired ? 'status-expired' : 'status-active';
const statusText = isExpired ? 'Expired' : 'Active';
html += `
<tr>
<td>${challenge.identifier}</td>
<td><span class="status-indicator ${statusClass}"></span>${statusText}</td>
<td>${new Date(challenge.expiresAt).toLocaleString()}</td>
<td>${challenge.used ? 'Yes' : 'No'}</td>
</tr>
`;
});
html += '</tbody></table>';
listDiv.innerHTML = html;
} catch (error) {
console.error('Error loading challenges:', error);
document.getElementById('challenges-list').innerHTML = `
<div class="alert alert-error">
<i class="fas fa-exclamation-triangle"></i> Error loading challenges.
</div>
`;
}
}
// Generate SCEP certificate with enterprise CA support
async function generateSCEPCertificate() {
const commonName = document.getElementById('cert-common-name').value;
const challengePassword = document.getElementById('cert-challenge').value;
const template = document.getElementById('cert-template').value;
const upn = document.getElementById('cert-upn').value;
const sanText = document.getElementById('cert-san').value;
if (!commonName) {
showAlert('Please enter a common name (domain)', 'warning');
return;
}
// Validate UPN for M365User template
if (template === 'M365User' && !upn) {
showAlert('UPN is required for M365 User certificates', 'warning');
return;
}
try {
const payload = {
commonName,
template
};
if (challengePassword) {
payload.challengePassword = challengePassword;
}
if (upn) {
payload.upn = upn;
}
// Parse subject alternative names
if (sanText.trim()) {
payload.subjectAltNames = sanText.split('\n')
.map(san => san.trim())
.filter(san => san.length > 0);
}
const response = await fetch('/api/scep/certificate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to generate certificate');
}
const data = await response.json();
const resultDiv = document.getElementById('cert-result');
resultDiv.innerHTML = `
<div class="alert alert-success">
<strong><i class="fas fa-check-circle"></i> Certificate Generated Successfully!</strong><br>
<div class="scep-config" style="margin-top: 10px;">
<strong>Common Name:</strong> ${data.commonName}<br>
<strong>Template:</strong> ${data.template}<br>
${data.upn ? `<strong>UPN:</strong> ${data.upn}<br>` : ''}
<strong>Method:</strong> ${data.method}<br>
<strong>Enterprise Mode:</strong> ${data.enterpriseMode ? 'Yes' : 'No'}<br>
<strong>Message:</strong> ${data.message}
</div>
</div>
`;
// Clear form and refresh certificates
document.getElementById('cert-common-name').value = '';
document.getElementById('cert-challenge').value = '';
document.getElementById('cert-upn').value = '';
document.getElementById('cert-san').value = '';
setTimeout(loadSCEPCertificates, 1000);
} catch (error) {
console.error('Error generating certificate:', error);
showAlert(`Failed to generate certificate: ${error.message}`, 'error');
}
}
// Load SCEP certificates
async function loadSCEPCertificates() {
try {
const response = await fetch('/api/scep/certificates');
if (!response.ok) throw new Error('Failed to load SCEP certificates');
const data = await response.json();
const certificates = data.certificates; // Certificates array is directly in the response
const listDiv = document.getElementById('scep-certificates');
if (certificates.length === 0) {
listDiv.innerHTML = `
<div class="alert alert-info">
<i class="fas fa-info-circle"></i> No SCEP certificates found.
</div>
`;
return;
}
let html = `
<table class="certificate-table">
<thead>
<tr>
<th><i class="fas fa-globe"></i> Common Name</th>
<th><i class="fas fa-calendar-plus"></i> Created</th>
<th><i class="fas fa-calendar-edit"></i> Modified</th>
<th><i class="fas fa-folder"></i> Path</th>
</tr>
</thead>
<tbody>
`;
certificates.forEach(cert => {
html += `
<tr>
<td>
<span class="status-indicator status-active"></span>
<strong>${cert.commonName}</strong>
</td>
<td>${new Date(cert.created).toLocaleString()}</td>
<td>${new Date(cert.modified).toLocaleString()}</td>
<td><code>${cert.path}</code></td>
</tr>
`;
});
html += '</tbody></table>';
listDiv.innerHTML = html;
} catch (error) {
console.error('Error loading SCEP certificates:', error);
document.getElementById('scep-certificates').innerHTML = `
<div class="alert alert-error">
<i class="fas fa-exclamation-triangle"></i> Error loading SCEP certificates.
</div>
`;
}
}
// Show alert helper function
function showAlert(message, type = 'info') {
// Create temporary alert
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type}`;
alertDiv.innerHTML = `<i class="fas fa-exclamation-triangle"></i> ${message}`;
// Insert at top of container
const container = document.querySelector('.container');
container.insertBefore(alertDiv, container.firstChild.nextSibling);
// Remove after 5 seconds
setTimeout(() => {
if (alertDiv.parentNode) {
alertDiv.parentNode.removeChild(alertDiv);
}
}, 5000);
}
</script>
</body>
</html>