From 7e474a1b0401c6231971e4256f71d8d7d54910fc Mon Sep 17 00:00:00 2001 From: Jeff Caldwell Date: Tue, 29 Jul 2025 14:44:05 -0400 Subject: [PATCH 1/2] let's dockerize! --- .gitignore | 5 + DOCKER.md | 245 +++++++++++++++++++++++++++++++++++++++++++++ Dockerfile | 55 ++++++++++ docker-compose.yml | 44 ++++++++ package.json | 9 +- 5 files changed, 357 insertions(+), 1 deletion(-) create mode 100644 DOCKER.md create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/.gitignore b/.gitignore index 07f6b02..762035a 100644 --- a/.gitignore +++ b/.gitignore @@ -99,3 +99,8 @@ jspm_packages/ ehthumbs.db Thumbs.db +# Docker +.dockerignore +docker-compose.override.yml +.docker/ + diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 0000000..6463920 --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,245 @@ +# Docker Usage Guide + +This document provides comprehensive instructions for running mkcert Web UI using Docker. + +## Quick Start + +### Option 1: Docker Run (Simple) + +Run the application with default settings: + +```bash +docker run -d \ + --name mkcert-web-ui \ + -p 3000:3000 \ + -v mkcert_certificates:/app/certificates \ + -v mkcert_data:/app/data \ + jeffcaldwellca/mkcert-web-ui:latest +``` + +### Option 2: Docker Compose (Recommended) + +1. Download the docker-compose.yml file: +```bash +wget https://raw.githubusercontent.com/jeffcaldwellca/mkcertWeb/main/docker-compose.yml +``` + +2. Start the application: +```bash +docker-compose up -d +``` + +## Configuration Options + +### Environment Variables + +You can customize the application behavior using environment variables: + +#### Basic Configuration +```bash +docker run -d \ + --name mkcert-web-ui \ + -p 3000:3000 \ + -e "DEFAULT_THEME=light" \ + -e "NODE_ENV=production" \ + -v mkcert_certificates:/app/certificates \ + jeffcaldwellca/mkcert-web-ui:latest +``` + +#### With Authentication +```bash +docker run -d \ + --name mkcert-web-ui \ + -p 3000:3000 \ + -e "ENABLE_AUTH=true" \ + -e "AUTH_USERNAME=myuser" \ + -e "AUTH_PASSWORD=mysecurepassword" \ + -e "SESSION_SECRET=your-very-long-random-secret-key" \ + -v mkcert_certificates:/app/certificates \ + jeffcaldwellca/mkcert-web-ui:latest +``` + +#### With HTTPS +```bash +docker run -d \ + --name mkcert-web-ui \ + -p 3000:3000 \ + -p 3443:3443 \ + -e "ENABLE_HTTPS=true" \ + -e "SSL_DOMAIN=your-domain.com" \ + -v mkcert_certificates:/app/certificates \ + jeffcaldwellca/mkcert-web-ui:latest +``` + +### Available Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `PORT` | `3000` | HTTP server port | +| `HTTPS_PORT` | `3443` | HTTPS server port | +| `ENABLE_HTTPS` | `false` | Enable HTTPS server | +| `SSL_DOMAIN` | `localhost` | Domain name for SSL certificate | +| `FORCE_HTTPS` | `false` | Redirect HTTP to HTTPS | +| `NODE_ENV` | `production` | Environment mode | +| `DEFAULT_THEME` | `dark` | Default theme (dark/light) | +| `ENABLE_AUTH` | `false` | Enable user authentication | +| `AUTH_USERNAME` | `admin` | Username for authentication | +| `AUTH_PASSWORD` | `admin` | Password for authentication | +| `SESSION_SECRET` | `mkcert-web-ui-secret-key-change-in-production` | Session secret | + +## Volume Mounts + +### Required Volumes + +- **Certificates**: `/app/certificates` - Stores generated SSL certificates +- **Data**: `/app/data` - Stores application data and configuration + +### Example with Custom Directories +```bash +docker run -d \ + --name mkcert-web-ui \ + -p 3000:3000 \ + -v /host/path/to/certificates:/app/certificates \ + -v /host/path/to/data:/app/data \ + jeffcaldwellca/mkcert-web-ui:latest +``` + +## Production Deployment + +### Recommended Production Setup + +```bash +docker run -d \ + --name mkcert-web-ui \ + --restart unless-stopped \ + -p 3000:3000 \ + -p 3443:3443 \ + -e "NODE_ENV=production" \ + -e "ENABLE_HTTPS=true" \ + -e "FORCE_HTTPS=true" \ + -e "SSL_DOMAIN=your-domain.com" \ + -e "ENABLE_AUTH=true" \ + -e "AUTH_USERNAME=yourusername" \ + -e "AUTH_PASSWORD=yoursecurepassword" \ + -e "SESSION_SECRET=$(openssl rand -base64 32)" \ + -e "DEFAULT_THEME=light" \ + -v mkcert_certificates:/app/certificates \ + -v mkcert_data:/app/data \ + jeffcaldwellca/mkcert-web-ui:latest +``` + +### Using Docker Compose for Production + +Create a `.env` file: +```bash +# Production Configuration +NODE_ENV=production +ENABLE_HTTPS=true +FORCE_HTTPS=true +SSL_DOMAIN=your-domain.com + +# Authentication +ENABLE_AUTH=true +AUTH_USERNAME=yourusername +AUTH_PASSWORD=yoursecurepassword +SESSION_SECRET=your-very-long-random-secret-key + +# Theme +DEFAULT_THEME=light +``` + +Then run: +```bash +docker-compose --env-file .env up -d +``` + +## Building from Source + +If you want to build the Docker image yourself: + +```bash +# Clone the repository +git clone https://github.com/jeffcaldwellca/mkcertWeb.git +cd mkcertWeb + +# Build the image +docker build -t mkcert-web-ui . + +# Run your custom build +docker run -d \ + --name mkcert-web-ui \ + -p 3000:3000 \ + -v mkcert_certificates:/app/certificates \ + mkcert-web-ui +``` + +## Troubleshooting + +### Check Container Logs +```bash +docker logs mkcert-web-ui +``` + +### Access Container Shell +```bash +docker exec -it mkcert-web-ui /bin/sh +``` + +### Health Check +The container includes a health check that verifies the application is responding: +```bash +docker inspect --format='{{.State.Health.Status}}' mkcert-web-ui +``` + +### Port Conflicts +If port 3000 is already in use: +```bash +docker run -d \ + --name mkcert-web-ui \ + -p 8080:3000 \ + jeffcaldwellca/mkcert-web-ui:latest +``` + +### Persistence Issues +Ensure volumes are properly mounted to persist certificates and data: +```bash +# Check volume mounts +docker inspect mkcert-web-ui | grep -A 10 "Mounts" + +# List volumes +docker volume ls | grep mkcert +``` + +## Security Considerations + +1. **Change Default Credentials**: Always change `AUTH_USERNAME` and `AUTH_PASSWORD` in production +2. **Session Secret**: Use a strong, randomly generated `SESSION_SECRET` +3. **HTTPS**: Enable HTTPS for production deployments +4. **Network**: Consider using Docker networks for isolation +5. **Updates**: Regularly update the container image for security patches + +## Examples + +### Development Setup +```bash +docker run -d \ + --name mkcert-web-ui-dev \ + -p 3000:3000 \ + -e "NODE_ENV=development" \ + -e "DEFAULT_THEME=dark" \ + -v mkcert_certificates:/app/certificates \ + jeffcaldwellca/mkcert-web-ui:latest +``` + +### Reverse Proxy Setup (nginx) +```bash +docker run -d \ + --name mkcert-web-ui \ + --network nginx-proxy \ + -e "VIRTUAL_HOST=certs.yourdomain.com" \ + -e "LETSENCRYPT_HOST=certs.yourdomain.com" \ + -v mkcert_certificates:/app/certificates \ + jeffcaldwellca/mkcert-web-ui:latest +``` + +For more information, see the main [README.md](README.md) file. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cb343cd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,55 @@ +# Use Node.js 18 LTS Alpine for smaller image size +FROM node:18-alpine + +# Install mkcert and other required tools +RUN apk add --no-cache \ + ca-certificates \ + wget \ + && wget -O /usr/local/bin/mkcert https://github.com/FiloSottile/mkcert/releases/latest/download/mkcert-v1.4.4-linux-amd64 \ + && chmod +x /usr/local/bin/mkcert + +# Create app directory +WORKDIR /app + +# Create a non-root user for security +RUN addgroup -g 1001 -S nodejs \ + && adduser -S nodejs -u 1001 + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci --only=production && npm cache clean --force + +# Copy application code +COPY . . + +# Create necessary directories with proper permissions +RUN mkdir -p /app/certificates /app/data \ + && chown -R nodejs:nodejs /app + +# Switch to non-root user +USER nodejs + +# Expose ports +EXPOSE 3000 3443 + +# Set default environment variables +ENV NODE_ENV=production +ENV PORT=3000 +ENV HTTPS_PORT=3443 +ENV ENABLE_HTTPS=false +ENV SSL_DOMAIN=localhost +ENV FORCE_HTTPS=false +ENV DEFAULT_THEME=dark +ENV ENABLE_AUTH=false +ENV AUTH_USERNAME=admin +ENV AUTH_PASSWORD=admin +ENV SESSION_SECRET=mkcert-web-ui-secret-key-change-in-production + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3000/ || exit 1 + +# Start the application +CMD ["npm", "start"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5b4bc79 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,44 @@ +version: '3.8' + +services: + mkcert-web-ui: + build: . + ports: + - "3000:3000" # HTTP port + - "3443:3443" # HTTPS port + environment: + # Server Configuration + - PORT=3000 + - HTTPS_PORT=3443 + + # SSL/HTTPS Configuration + - ENABLE_HTTPS=false + - SSL_DOMAIN=localhost + - FORCE_HTTPS=false + + # Application Configuration + - NODE_ENV=production + - DEFAULT_THEME=dark + + # Authentication Configuration (disabled by default) + - ENABLE_AUTH=false + - AUTH_USERNAME=admin + - AUTH_PASSWORD=admin + - SESSION_SECRET=mkcert-web-ui-secret-key-change-in-production + volumes: + # Persist certificates and data + - mkcert_certificates:/app/certificates + - mkcert_data:/app/data + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + +volumes: + mkcert_certificates: + driver: local + mkcert_data: + driver: local diff --git a/package.json b/package.json index 325f72d..1411a05 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,14 @@ "https-dev": "ENABLE_HTTPS=true nodemon server.js", "https-only": "ENABLE_HTTPS=true FORCE_HTTPS=true node server.js", "env": "node -r dotenv/config server.js", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"Error: no test specified\" && exit 1", + "docker:build": "docker build -t mkcert-web-ui .", + "docker:run": "docker run -d --name mkcert-web-ui -p 3000:3000 -v mkcert_certificates:/app/certificates -v mkcert_data:/app/data mkcert-web-ui", + "docker:stop": "docker stop mkcert-web-ui && docker rm mkcert-web-ui", + "docker:logs": "docker logs mkcert-web-ui", + "compose:up": "docker-compose up -d", + "compose:down": "docker-compose down", + "compose:logs": "docker-compose logs -f" }, "keywords": [ "mkcert", From 46ac271afb3614ff3313f2cdda5b805034e6658a Mon Sep 17 00:00:00 2001 From: Jeff Caldwell Date: Tue, 29 Jul 2025 14:48:04 -0400 Subject: [PATCH 2/2] file cleanup --- CHANGELOG.md | 34 +- public/login-new.html | 276 -------------- public/login-old.html | 254 ------------- public/script-fixed.js | 403 -------------------- public/styles.css.backup | 771 --------------------------------------- test_cert_api.js | 33 -- 6 files changed, 33 insertions(+), 1738 deletions(-) delete mode 100644 public/login-new.html delete mode 100644 public/login-old.html delete mode 100644 public/script-fixed.js delete mode 100644 public/styles.css.backup delete mode 100644 test_cert_api.js diff --git a/CHANGELOG.md b/CHANGELOG.md index babac44..aeba595 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,35 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.2.0] - 2025-07-29 + +### Added +- Complete Docker containerization support +- Multi-stage Dockerfile with Node.js 18 Alpine base image +- Pre-installed mkcert CLI in Docker container +- Docker Compose configuration for easy deployment +- Volume persistence for certificates and application data +- Comprehensive Docker documentation (DOCKER.md) +- Docker-specific npm scripts for container management +- Health check configuration for container monitoring +- Non-root user security implementation in containers +- Environment variable support for all configuration options + +### Changed +- Updated .gitignore to exclude Docker-related build files +- Enhanced package.json with Docker-related scripts +- Optimized .dockerignore for efficient Docker builds +- Cleaned up unused backup and development files + +### Removed +- Unused backup files +- Development test utility + +### Security +- Docker container runs as non-root user (nodejs:1001) +- Secure volume mounting for certificate persistence +- Production-ready security configurations + ## [1.1.1] ### Added @@ -75,7 +104,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Version History Summary - **v1.0.0**: Initial release with core functionality -- **Current**: Enhanced UI with red/green theme and improved authentication +- **v1.1.0**: Enhanced UI with red/green theme and improved authentication +- **v1.1.1**: Dark/Light mode toggle with theme persistence +- **v1.2.0**: Complete Docker containerization support +- **Current**: Full-featured mkcert Web UI with Docker deployment options ## Contributing diff --git a/public/login-new.html b/public/login-new.html deleted file mode 100644 index cc4549a..0000000 --- a/public/login-new.html +++ /dev/null @@ -1,276 +0,0 @@ - - - - - - Login - mkcert Web UI - - - - - - - - - - - - diff --git a/public/login-old.html b/public/login-old.html deleted file mode 100644 index b01c949..0000000 --- a/public/login-old.html +++ /dev/null @@ -1,254 +0,0 @@ - - - - - - Login - mkcert Web UI - - - - - - - - - - diff --git a/public/script-fixed.js b/public/script-fixed.js deleted file mode 100644 index d14f65b..0000000 --- a/public/script-fixed.js +++ /dev/null @@ -1,403 +0,0 @@ -// mkcert Web UI - Frontend JavaScript -// Fixed version with proper template literal handling - -// Configuration -const API_BASE = window.location.origin + '/api'; - -// DOM Elements -let certificatesList, generateForm, domainsInput, formatSelect; -let installCaBtn, showCaBtn, hideModal, caModal; -let statusIndicators = {}; - -// Initialize app when DOM is loaded -document.addEventListener('DOMContentLoaded', function() { - initializeElements(); - loadSystemStatus(); - loadCertificates(); - setupEventListeners(); -}); - -// Initialize DOM elements -function initializeElements() { - certificatesList = document.getElementById('certificates-list'); - generateForm = document.getElementById('generate-form'); - domainsInput = document.getElementById('domains'); - formatSelect = document.getElementById('format'); - installCaBtn = document.getElementById('install-ca-btn'); - showCaBtn = document.getElementById('show-ca-btn'); - hideModal = document.getElementById('hide-modal'); - caModal = document.getElementById('ca-modal'); - - // Status indicators - statusIndicators.mkcert = document.getElementById('mkcert-status'); - statusIndicators.ca = document.getElementById('ca-status'); - statusIndicators.openssl = document.getElementById('openssl-status'); -} - -// API request helper -async function apiRequest(endpoint, options = {}) { - try { - const response = await fetch(API_BASE + endpoint, { - headers: { - 'Content-Type': 'application/json', - ...options.headers - }, - ...options - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || 'Request failed'); - } - - return await response.json(); - } catch (error) { - console.error('API request failed:', error); - throw error; - } -} - -// Setup event listeners -function setupEventListeners() { - if (generateForm) { - generateForm.addEventListener('submit', handleGenerate); - } - - if (installCaBtn) { - installCaBtn.addEventListener('click', handleInstallCA); - } - - if (showCaBtn) { - showCaBtn.addEventListener('click', showRootCA); - } - - if (hideModal) { - hideModal.addEventListener('click', hideModalDialog); - } -} - -// Load system status -async function loadSystemStatus() { - try { - const status = await apiRequest('/status'); - - updateStatusIndicator('mkcert', status.mkcertInstalled, 'mkcert installed'); - updateStatusIndicator('ca', status.caExists, 'Root CA exists'); - updateStatusIndicator('openssl', status.opensslAvailable, 'OpenSSL available'); - - } catch (error) { - console.error('Failed to load system status:', error); - updateStatusIndicator('mkcert', false, 'Failed to check mkcert'); - updateStatusIndicator('ca', false, 'Failed to check CA'); - updateStatusIndicator('openssl', false, 'Failed to check OpenSSL'); - } -} - -// Update status indicator -function updateStatusIndicator(type, status, message) { - const indicator = statusIndicators[type]; - if (!indicator) return; - - indicator.className = 'status-indicator ' + (status ? 'status-success' : 'status-error'); - indicator.textContent = message; -} - -// Show Root CA modal -async function showRootCA() { - try { - const caInfo = await apiRequest('/rootca/info'); - - let expiryInfo; - if (caInfo.daysUntilExpiry < 0) { - expiryInfo = 'Expired ' + Math.abs(caInfo.daysUntilExpiry) + ' days ago'; - } else if (caInfo.daysUntilExpiry <= 30) { - expiryInfo = 'Expires in ' + caInfo.daysUntilExpiry + ' days'; - } else if (caInfo.daysUntilExpiry <= 90) { - expiryInfo = 'Expires in ' + caInfo.daysUntilExpiry + ' days'; - } else { - expiryInfo = 'Expires in ' + caInfo.daysUntilExpiry + ' days'; - } - - if (caInfo.expiry) { - expiryInfo += ' (' + caInfo.expiry + ')'; - } - - document.getElementById('ca-subject').textContent = caInfo.subject || 'N/A'; - document.getElementById('ca-issuer').textContent = caInfo.issuer || 'N/A'; - document.getElementById('ca-expiry').textContent = expiryInfo; - document.getElementById('ca-fingerprint').textContent = caInfo.fingerprint || 'N/A'; - document.getElementById('ca-path').textContent = caInfo.caRoot || 'N/A'; - - caModal.style.display = 'block'; - } catch (error) { - showAlert('Failed to load CA information: ' + error.message, 'error'); - } -} - -// Hide modal dialog -function hideModalDialog() { - caModal.style.display = 'none'; -} - -// Handle certificate generation -async function handleGenerate(event) { - event.preventDefault(); - - const domains = domainsInput.value.trim().split('\n').filter(d => d.trim()); - const format = formatSelect.value; - - if (domains.length === 0) { - showAlert('Please enter at least one domain', 'error'); - return; - } - - try { - const result = await apiRequest('/generate', { - method: 'POST', - body: JSON.stringify({ domains, format }) - }); - - const formatName = format.toUpperCase(); - showAlert(formatName + ' certificate generated successfully for: ' + domains.join(', '), 'success'); - loadCertificates(); - generateForm.reset(); - } catch (error) { - showAlert('Failed to generate certificate: ' + error.message, 'error'); - } -} - -// Load and display certificates -async function loadCertificates() { - try { - const certificates = await apiRequest('/certificates'); - displayCertificates(certificates); - } catch (error) { - showAlert('Failed to load certificates: ' + error.message, 'error'); - certificatesList.innerHTML = '

Failed to load certificates

'; - } -} - -// Display certificates list -function displayCertificates(certificates) { - if (!certificates || certificates.length === 0) { - certificatesList.innerHTML = '

No certificates found

'; - return; - } - - const html = certificates.map(cert => { - const domainsDisplay = cert.domains ? cert.domains.join(', ') : 'Unknown'; - const createdDate = new Date(cert.created).toLocaleDateString(); - const createdTime = new Date(cert.created).toLocaleTimeString(); - - const formatBadge = cert.format ? - '' + cert.format.toUpperCase() + '' : ''; - - let expiryInfo, expiryClass = ''; - if (cert.expiry) { - const expiryDateStr = new Date(cert.expiry).toLocaleDateString(); - if (cert.daysUntilExpiry < 0) { - expiryInfo = 'Expired ' + Math.abs(cert.daysUntilExpiry) + ' days ago'; - expiryClass = 'expiry-expired'; - } else if (cert.daysUntilExpiry <= 30) { - expiryInfo = 'Expires in ' + cert.daysUntilExpiry + ' days'; - expiryClass = 'expiry-warning'; - } else if (cert.daysUntilExpiry <= 90) { - expiryInfo = 'Expires in ' + cert.daysUntilExpiry + ' days'; - expiryClass = 'expiry-caution'; - } else { - expiryInfo = 'Expires in ' + cert.daysUntilExpiry + ' days'; - expiryClass = 'expiry-good'; - } - expiryInfo += ' (' + expiryDateStr + ')'; - } else { - expiryInfo = 'Unknown'; - } - - // Format folder display - const folderDisplay = cert.folder === 'root' ? 'Root folder' : cert.folder; - const folderParam = cert.folder.replace(/[/\\]/g, '_'); - const isRootCert = cert.folder === 'root'; - const isArchived = cert.isArchived || false; - - return '
' + - '
' + - '
' + - ' ' + cert.name + - formatBadge + - (cert.isExpired ? 'EXPIRED' : '') + - (isRootCert ? 'READ-ONLY' : '') + - (isArchived ? 'ARCHIVED' : '') + - '
' + - '
' + - '
Domains: ' + domainsDisplay + '
' + - '
Location: ' + folderDisplay + '
' + - '
Created: ' + createdDate + ' ' + createdTime + '
' + - '
Expiry: ' + expiryInfo + '
' + - '
Cert File: ' + cert.certFile + '
' + - '
Key File: ' + (cert.keyFile || 'Missing') + '
' + - '
Size: ' + formatFileSize(cert.size) + '
' + - '
Status: ' + (isArchived ? 'Archived' : 'Active') + '
' + - '
' + - '
' + - '' + - ' Download Cert' + - (cert.keyFile ? - '' + - ' Download Key' : '') + - '' + - ' Download Bundle' + - (!isRootCert && !isArchived ? - '' : - isArchived ? - '' + - '' : - '' + - ' Protected') + - '
'; - }).join(''); - - certificatesList.innerHTML = html; -} - -// Certificate management functions -async function deleteCertificate(folder, certName) { - // Check if this is a root certificate - if (folder === 'root') { - showAlert('Root certificates are read-only and cannot be deleted', 'error'); - return; - } - - try { - let endpoint; - if (folder === 'root') { - endpoint = '/certificates/' + certName; - } else { - endpoint = '/certificates/' + folder + '/' + certName; - } - - await apiRequest(endpoint, { - method: 'DELETE' - }); - - showAlert('Certificate "' + certName + '" deleted permanently', 'success'); - loadCertificates(); - } catch (error) { - if (error.message.includes('read-only')) { - showAlert('Root certificates are read-only and cannot be deleted', 'error'); - } else { - showAlert('Failed to delete certificate: ' + error.message, 'error'); - } - } -} - -async function archiveCertificate(folder, certName) { - // Check if this is a root certificate - if (folder === 'root') { - showAlert('Root certificates are read-only and cannot be archived', 'error'); - return; - } - - if (!confirm('Are you sure you want to archive the certificate "' + certName + '"?')) { - return; - } - - try { - const endpoint = '/archive/' + folder + '/' + certName; - await apiRequest(endpoint, { - method: 'POST' - }); - - showAlert('Certificate "' + certName + '" archived successfully', 'success'); - loadCertificates(); - } catch (error) { - showAlert('Failed to archive certificate: ' + error.message, 'error'); - } -} - -async function restoreCertificate(folder, certName) { - if (!confirm('Are you sure you want to restore the certificate "' + certName + '"?')) { - return; - } - - try { - const endpoint = '/restore/' + folder + '/' + certName; - await apiRequest(endpoint, { - method: 'POST' - }); - - showAlert('Certificate "' + certName + '" restored successfully', 'success'); - loadCertificates(); - } catch (error) { - showAlert('Failed to restore certificate: ' + error.message, 'error'); - } -} - -// CA Installation -async function handleInstallCA() { - installCaBtn.innerHTML = ' Installing...'; - installCaBtn.disabled = true; - - try { - await apiRequest('/install-ca', { - method: 'POST' - }); - - showAlert('Root CA installed successfully', 'success'); - hideModalDialog(); - loadSystemStatus(); - } catch (error) { - showAlert('Failed to install CA: ' + error.message, 'error'); - } finally { - installCaBtn.innerHTML = ' Install CA'; - installCaBtn.disabled = false; - } -} - -// Utility functions -function formatFileSize(bytes) { - if (bytes === 0) return '0 Bytes'; - - const k = 1024; - const sizes = ['Bytes', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; -} - -// Alert system -function showAlert(message, type = 'info') { - const alertId = Date.now(); - - const alertDiv = document.createElement('div'); - alertDiv.className = 'alert alert-' + type; - alertDiv.id = 'alert-' + alertId; - alertDiv.innerHTML = message + - ''; - - const container = document.querySelector('.alerts-container') || document.body; - container.appendChild(alertDiv); - - // Auto-hide after 5 seconds - setTimeout(() => { - hideAlert(alertId); - }, 5000); -} - -function hideAlert(alertId) { - const alert = document.getElementById('alert-' + alertId); - if (alert) { - alert.remove(); - } -} diff --git a/public/styles.css.backup b/public/styles.css.backup deleted file mode 100644 index 9cce8cd..0000000 --- a/public/styles.css.backup +++ /dev/null @@ -1,771 +0,0 @@ -/* Reset and Base Styles */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -/* CSS Custom Properties for Theme System */ -:root { - /* Dark Mode Colors (Default) */ - --primary-bg: linear-gradient(135deg, #0d1117 0%, #1a1e23 50%, #0f1419 100%); - --secondary-bg: rgba(13, 17, 23, 0.8); - --card-bg: rgba(22, 27, 34, 0.8); - --border-color: #30363d; - --primary-color: #00ff41; - --secondary-color: #8cc8ff; - --accent-color: #ff0041; - --text-color: #00ff41; - --text-muted: rgba(255, 255, 255, 0.8); - --shadow-color: rgba(0, 0, 0, 0.3); - --success-color: #00ff41; - --error-color: #ff4444; - --warning-color: #ffaa00; - --input-bg: rgba(22, 27, 34, 0.9); - --button-bg: rgba(0, 255, 65, 0.1); - --button-hover-bg: rgba(0, 255, 65, 0.2); - --glow-primary: rgba(0, 255, 65, 0.3); - --glow-secondary: rgba(255, 0, 65, 0.3); - --backdrop-overlay: - radial-gradient(circle at 20% 80%, rgba(0, 255, 65, 0.03) 0%, transparent 50%), - radial-gradient(circle at 80% 20%, rgba(255, 0, 65, 0.03) 0%, transparent 50%); -} - -/* Light Mode Colors */ -[data-theme="light"] { - --primary-bg: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 50%, #f1f3f4 100%); - --secondary-bg: rgba(255, 255, 255, 0.9); - --card-bg: rgba(248, 249, 250, 0.9); - --border-color: #d1d9e0; - --primary-color: #198754; - --secondary-color: #0d6efd; - --accent-color: #dc3545; - --text-color: #198754; - --text-muted: rgba(0, 0, 0, 0.7); - --shadow-color: rgba(0, 0, 0, 0.1); - --success-color: #198754; - --error-color: #dc3545; - --warning-color: #fd7e14; - --input-bg: rgba(255, 255, 255, 0.9); - --button-bg: rgba(25, 135, 84, 0.1); - --button-hover-bg: rgba(25, 135, 84, 0.2); - --glow-primary: rgba(25, 135, 84, 0.3); - --glow-secondary: rgba(220, 53, 69, 0.3); - --backdrop-overlay: - radial-gradient(circle at 20% 80%, rgba(25, 135, 84, 0.05) 0%, transparent 50%), - radial-gradient(circle at 80% 20%, rgba(220, 53, 69, 0.05) 0%, transparent 50%); -} - -body { - font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Courier New', monospace; - background: var(--primary-bg); - min-height: 100vh; - color: var(--text-color); - line-height: 1.6; - position: relative; - transition: background 0.3s ease, color 0.3s ease; -} - -body::before { - content: ''; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: var(--backdrop-overlay); - pointer-events: none; - z-index: -1; - transition: background 0.3s ease; -} - -.container { - max-width: 1200px; - margin: 0 auto; - padding: 20px; -} - -/* Header */ -header { - text-align: center; - margin-bottom: 2rem; - color: var(--primary-color); - position: relative; -} - -header h1 { - font-size: 2.5rem; - margin-bottom: 0.5rem; - text-shadow: 0 0 10px var(--glow-primary), 0 0 20px var(--glow-primary); - font-weight: 700; - letter-spacing: 0.05em; - transition: text-shadow 0.3s ease; -} - -header h1::before { - content: '> '; - color: var(--accent-color); - text-shadow: 0 0 5px var(--glow-secondary); -} - -header p { - font-size: 1.1rem; - opacity: 0.8; - color: var(--secondary-color); - transition: color 0.3s ease; -} - -/* Theme Toggle Button */ -.theme-toggle { - position: absolute; - top: 0; - right: 0; - background: var(--button-bg); - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 0.5rem 1rem; - color: var(--primary-color); - cursor: pointer; - font-family: inherit; - font-size: 0.9rem; - transition: all 0.3s ease; - backdrop-filter: blur(10px); -} - -.theme-toggle:hover { - background: var(--button-hover-bg); - box-shadow: 0 0 10px var(--glow-primary); - transform: translateY(-1px); -} - -.theme-toggle i { - margin-right: 0.5rem; -} - -/* Sections */ -section { - background: var(--secondary-bg); - border: 1px solid var(--border-color); - border-radius: 12px; - padding: 1.5rem; - margin-bottom: 2rem; - box-shadow: - 0 4px 6px var(--shadow-color), - inset 0 1px 0 var(--glow-primary); - backdrop-filter: blur(10px); - transition: all 0.3s ease; -} - -section h2 { - color: var(--primary-color); - margin-bottom: 1rem; - padding-bottom: 0.5rem; - border-bottom: 2px solid var(--border-color); - font-size: 1.4rem; - font-weight: 600; - text-shadow: 0 0 5px var(--glow-primary); - transition: all 0.3s ease; -} - -section h2::before { - content: '$ '; - color: var(--accent-color); - font-weight: bold; -} - -/* Cards */ -.status-card, .generate-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; -} - -/* Status Info */ -.status-info { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 1rem; - margin-top: 1rem; -} - -.status-item { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.status-item i { - font-size: 1.2rem; -} - -.status-success { - color: var(--success-color); - text-shadow: 0 0 3px var(--glow-primary); -} -.status-error { - color: var(--error-color); - text-shadow: 0 0 3px var(--glow-secondary); -} -.status-warning { - color: var(--warning-color); - text-shadow: 0 0 3px rgba(255, 170, 0, 0.5); -} - -/* Root CA Section */ -.rootca-card { - background: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 1.5rem; - box-shadow: inset 0 1px 0 rgba(0, 255, 65, 0.05); -} - -.rootca-info-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 2rem; - align-items: start; -} - -.rootca-details { - display: grid; - gap: 1rem; -} - -.ca-info-item { - padding: 0.75rem; - background: rgba(13, 17, 23, 0.6); - border: 1px solid #21262d; - border-radius: 6px; -} - -.ca-detail { - font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace; - font-size: 0.9rem; - color: #8cc8ff; - word-break: break-all; -} - -.fingerprint { - font-size: 0.8rem; - color: #7d8590; -} - -.rootca-actions { - background: rgba(13, 17, 23, 0.6); - border: 1px solid #21262d; - padding: 1.5rem; - border-radius: 8px; -} - -.rootca-actions h3 { - color: #00ff41; - margin-bottom: 0.5rem; - font-size: 1.1rem; - text-shadow: 0 0 3px rgba(0, 255, 65, 0.3); -} - -.btn-group { - display: flex; - gap: 1rem; - margin-bottom: 1.5rem; - flex-wrap: wrap; -} - -.btn-group .btn { - flex: 1; - min-width: 200px; -} - -.ca-usage-info { - margin-top: 1.5rem; - padding: 1rem; - background: rgba(22, 27, 34, 0.8); - border: 1px solid #30363d; - border-radius: 6px; -} - -.ca-usage-info h4 { - color: #00ff41; - margin-bottom: 0.75rem; - font-size: 1rem; -} - -.ca-usage-info ul { - margin: 0; - padding-left: 1.5rem; -} - -.ca-usage-info li { - margin-bottom: 0.5rem; - font-size: 0.9rem; - line-height: 1.4; - color: #8cc8ff; -} - -.ca-usage-info code { - background: rgba(13, 17, 23, 0.8); - border: 1px solid #21262d; - padding: 0.125rem 0.25rem; - border-radius: 3px; - font-size: 0.85rem; - color: #00ff41; -} - -@media (max-width: 768px) { - .rootca-info-grid { - grid-template-columns: 1fr; - gap: 1rem; - } -} - -/* Forms */ -.form-group { - margin-bottom: 1rem; -} - -.form-group label { - display: block; - margin-bottom: 0.5rem; - font-weight: 600; - color: #00ff41; - text-shadow: 0 0 3px rgba(0, 255, 65, 0.3); -} - -.form-group input, -.form-group textarea, -.form-group select { - width: 100%; - padding: 0.75rem; - background: rgba(13, 17, 23, 0.8); - border: 2px solid #30363d; - border-radius: 6px; - font-size: 1rem; - color: #8cc8ff; - font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace; - transition: all 0.3s ease; -} - -.form-group input:focus, -.form-group textarea:focus, -.form-group select:focus { - outline: none; - border-color: #00ff41; - box-shadow: 0 0 0 3px rgba(0, 255, 65, 0.2); - background: rgba(13, 17, 23, 0.9); -} - -.help-text { - display: block; - margin-top: 0.25rem; - color: #7d8590; - font-size: 0.875rem; -} - -/* Buttons */ -.btn { - display: inline-flex; - align-items: center; - gap: 0.5rem; - padding: 0.75rem 1.5rem; - border: none; - border-radius: 6px; - font-size: 1rem; - font-weight: 600; - text-decoration: none; - cursor: pointer; - transition: all 0.3s ease; - text-align: center; -} - -.btn-primary { - background: linear-gradient(135deg, #00ff41 0%, #00cc33 100%); - color: #000; - font-weight: 700; - text-shadow: none; - box-shadow: 0 0 10px rgba(0, 255, 65, 0.3); -} - -.btn-primary:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 255, 65, 0.4), 0 0 20px rgba(0, 255, 65, 0.2); - background: linear-gradient(135deg, #00ff41 0%, #00aa28 100%); -} - -.btn-secondary { - background: #30363d; - border: 1px solid #4a525a; - color: #8cc8ff; -} - -.btn-secondary:hover { - background: #4a525a; - border-color: #00ff41; - transform: translateY(-1px); - box-shadow: 0 0 5px rgba(0, 255, 65, 0.2); -} - -.btn-success { - background: linear-gradient(135deg, #00ff41 0%, #00cc33 100%); - color: #000; - font-weight: 700; -} - -.btn-success:hover { - background: linear-gradient(135deg, #00ff41 0%, #00aa28 100%); - transform: translateY(-1px); - box-shadow: 0 0 10px rgba(0, 255, 65, 0.4); -} - -.btn-danger { - background: linear-gradient(135deg, #ff0041 0%, #cc0033 100%); - color: #fff; - font-weight: 700; - box-shadow: 0 0 10px rgba(255, 0, 65, 0.3); -} - -.btn-danger:hover { - background: linear-gradient(135deg, #ff0041 0%, #aa0028 100%); - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(255, 0, 65, 0.4), 0 0 20px rgba(255, 0, 65, 0.2); -} - -.btn-small { - padding: 0.5rem 1rem; - font-size: 0.875rem; -} - -.btn-disabled { - background: #21262d; - color: #484f58; - cursor: not-allowed; - opacity: 0.6; - border: 1px solid #30363d; -} - -.btn-disabled:hover { - background: #21262d; - transform: none; - box-shadow: none; -} - -/* Certificates List */ -.certificates-controls { - margin-bottom: 1rem; - text-align: right; -} - -.certificates-list { - display: grid; - gap: 1rem; -} - -.certificate-card { - background: rgba(22, 27, 34, 0.8); - border: 1px solid #30363d; - border-radius: 8px; - padding: 1rem; - transition: all 0.3s ease; - box-shadow: inset 0 1px 0 rgba(0, 255, 65, 0.05); -} - -.certificate-card:hover { - border-color: #00ff41; - box-shadow: 0 2px 8px rgba(0, 255, 65, 0.15), inset 0 1px 0 rgba(0, 255, 65, 0.1); -} - -.certificate-card.certificate-expired { - border-color: #ff0041; - background: rgba(255, 0, 65, 0.05); -} - -.certificate-card.certificate-expired:hover { - border-color: #ff0041; - box-shadow: 0 2px 8px rgba(255, 0, 65, 0.15); -} - -.certificate-card.root-certificate { - border-color: #ffaa00; - background: rgba(255, 170, 0, 0.05); - position: relative; -} - -.certificate-card.archived-certificate { - border-color: #7d8590; - background: rgba(125, 133, 144, 0.05); - opacity: 0.8; - position: relative; -} - -.certificate-card.archived-certificate:hover { - border-color: #7d8590; - box-shadow: 0 2px 8px rgba(125, 133, 144, 0.15); -} - -.certificate-card.root-certificate:hover { - border-color: #ffaa00; - box-shadow: 0 2px 8px rgba(255, 170, 0, 0.15); -} - -.certificate-card.root-certificate::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 3px; - background: linear-gradient(90deg, #ffaa00, #ff8800); - border-radius: 8px 8px 0 0; -} - -.certificate-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 0.5rem; -} - -.certificate-name { - font-weight: 600; - color: #00ff41; - font-size: 1.1rem; - display: flex; - align-items: center; - gap: 0.5rem; - flex-wrap: wrap; - text-shadow: 0 0 3px rgba(0, 255, 65, 0.3); - font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace; -} - -.expired-badge { - background: linear-gradient(135deg, #ff0041 0%, #cc0033 100%); - color: white; - font-size: 0.75rem; - padding: 0.25rem 0.5rem; - border-radius: 4px; - font-weight: 600; - text-shadow: 0 0 3px rgba(255, 0, 65, 0.5); -} - -.read-only-badge { - background: linear-gradient(135deg, #ffaa00 0%, #ff8800 100%); - color: #000; - font-size: 0.75rem; - padding: 0.25rem 0.5rem; - border-radius: 4px; - font-weight: 600; -} - -.archived-badge { - background: #7d8590; - color: white; - font-size: 0.75rem; - padding: 0.25rem 0.5rem; - border-radius: 4px; - font-weight: 600; -} - -.format-badge { - font-size: 0.75rem; - padding: 0.25rem 0.5rem; - border-radius: 4px; - font-weight: 600; -} - -.format-badge.format-pem { - background: #30363d; - color: #8cc8ff; - border: 1px solid #4a525a; -} - -.format-badge.format-crt { - background: #00ff41; - color: #000; - font-weight: 600; -} - -.certificate-info { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 0.5rem; - margin-bottom: 1rem; - font-size: 0.875rem; - color: #7d8590; - font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace; -} - -.certificate-actions { - display: flex; - gap: 0.5rem; - flex-wrap: wrap; -} - -/* Loading */ -.loading { - text-align: center; - padding: 2rem; - color: #7d8590; -} - -.loading i { - margin-right: 0.5rem; - color: #00ff41; - animation: pulse 2s infinite; -} - -@keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } -} - -/* Alerts */ -#alerts-container { - position: fixed; - top: 20px; - right: 20px; - z-index: 1000; - max-width: 400px; -} - -.alert { - padding: 1rem 1.5rem; - border-radius: 6px; - margin-bottom: 1rem; - box-shadow: 0 4px 12px rgba(0,0,0,0.15); - animation: slideIn 0.3s ease; - position: relative; - padding-right: 3rem; -} - -.alert-success { - background: rgba(0, 255, 65, 0.1); - border: 1px solid rgba(0, 255, 65, 0.3); - color: #00ff41; - box-shadow: 0 0 10px rgba(0, 255, 65, 0.2); -} - -.alert-error { - background: rgba(255, 0, 65, 0.1); - border: 1px solid rgba(255, 0, 65, 0.3); - color: #ff0041; - box-shadow: 0 0 10px rgba(255, 0, 65, 0.2); -} - -.alert-warning { - background: rgba(255, 170, 0, 0.1); - border: 1px solid rgba(255, 170, 0, 0.3); - color: #ffaa00; - box-shadow: 0 0 10px rgba(255, 170, 0, 0.2); -} - -.alert-close { - position: absolute; - top: 50%; - right: 1rem; - transform: translateY(-50%); - background: none; - border: none; - font-size: 1.2rem; - cursor: pointer; - opacity: 0.7; - transition: opacity 0.3s ease; -} - -.alert-close:hover { - opacity: 1; -} - -/* Modal */ -.modal { - display: none; - position: fixed; - z-index: 1000; - left: 0; - top: 0; - width: 100%; - height: 100%; - background-color: rgba(0,0,0,0.8); - animation: fadeIn 0.3s ease; -} - -.modal-content { - background: rgba(13, 17, 23, 0.95); - border: 1px solid #30363d; - margin: 10% auto; - padding: 2rem; - border-radius: 12px; - max-width: 500px; - box-shadow: - 0 8px 32px rgba(0, 0, 0, 0.5), - 0 0 20px rgba(0, 255, 65, 0.1); - animation: scaleIn 0.3s ease; - backdrop-filter: blur(10px); -} - -.modal-content h3 { - margin-bottom: 1rem; - color: #00ff41; - text-shadow: 0 0 5px rgba(0, 255, 65, 0.3); -} - -.modal-actions { - margin-top: 2rem; - display: flex; - gap: 1rem; - justify-content: flex-end; -} - -/* Animations */ -@keyframes slideIn { - from { - transform: translateX(100%); - opacity: 0; - } - to { - transform: translateX(0); - opacity: 1; - } -} - -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } -} - -@keyframes scaleIn { - from { - transform: scale(0.7); - opacity: 0; - } - to { - transform: scale(1); - opacity: 1; - } -} - -/* Responsive Design */ -@media (max-width: 768px) { - .container { - padding: 1rem; - } - - header h1 { - font-size: 2rem; - } - - .certificate-actions { - justify-content: center; - } - - .modal-content { - margin: 20% 1rem; - padding: 1.5rem; - } - - .modal-actions { - flex-direction: column; - } -} diff --git a/test_cert_api.js b/test_cert_api.js deleted file mode 100644 index a0dbe07..0000000 --- a/test_cert_api.js +++ /dev/null @@ -1,33 +0,0 @@ -// Simple Node.js script to test mkcertWeb certificate archive API -// Usage: node test_cert_api.js - -const fetch = require('node-fetch'); - -if (process.argv.length < 5) { - console.error('Usage: node test_cert_api.js '); - process.exit(1); -} - -const serverUrl = process.argv[2].replace(/\/$/, ''); -const folder = process.argv[3]; -const certName = process.argv[4]; - -// Encode folder slashes as underscores for backend -const folderParam = folder.replace(/\//g, '_'); -const endpoint = `${serverUrl}/certificates/${encodeURIComponent(folderParam)}/${encodeURIComponent(certName)}/archive`; - -async function testArchive() { - try { - const res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' } }); - const data = await res.json(); - if (!res.ok) { - console.error('Error:', data.error || data); - } else { - console.log('Success:', data); - } - } catch (err) { - console.error('Request failed:', err); - } -} - -testArchive();