Merge pull request #10 from jeffcaldwellca/v1.3

## [1.3.0] - 2025-08-01

### Added
- **PFX Generation**: On-demand PKCS#12 (.pfx) file generation for Windows/IIS compatibility
- User-friendly password modal for PFX protection with optional encryption
- Enhanced certificate card layout with improved text handling for long filenames
- Better responsive design for mobile devices with optimized button sizes
- Text truncation with tooltips for long domain lists (100+ characters)
- Structured file information display with dedicated styling for certificate and key files
- URL encoding fixes for proper handling of complex folder paths with special characters

### Changed
- **Certificate Cards**: Complete redesign with better organization and overflow handling
- Improved mobile responsiveness with single-column layout on small screens
- Enhanced button styling and spacing for better user experience
- Updated certificate information display with clearer visual hierarchy
- Better word wrapping and text breaking for long strings

### Fixed
- **Download Functionality**: Fixed 404 errors in download buttons due to URL encoding issues
- **PFX Generation**: Resolved routing issues with complex folder paths containing slashes
- **Archive/Restore**: Fixed double URL encoding problems in certificate management
- **UI Consistency**: Removed confusing question mark cursor from filename displays
- **Mobile Layout**: Fixed text overflow and improved touch-friendly button sizing

### Removed
- Debug console logging from production PFX generation
- Unnecessary cursor help indicators from file name displays
This commit is contained in:
Jeff Caldwell
2025-08-01 11:25:38 -04:00
committed by GitHub
9 changed files with 685 additions and 439 deletions
+15 -1
View File
@@ -7,9 +7,24 @@ package-lock.json
yarn.lock
# Certificate files
certificates/
certificates/*.pem
certificates/*.key
# Temporary files
temp/
*.pfx
# Test files
test/
*test*
*.test.js
*.spec.js
# Cookie files
cookies.txt
*.cookies
# SSL certificates (auto-generated)
ssl/
*.pem
@@ -103,4 +118,3 @@ Thumbs.db
.dockerignore
docker-compose.override.yml
.docker/
+31 -1
View File
@@ -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.3.0] - 2025-08-01
### Added
- **PFX Generation**: On-demand PKCS#12 (.pfx) file generation for Windows/IIS compatibility
- User-friendly password modal for PFX protection with optional encryption
- Enhanced certificate card layout with improved text handling for long filenames
- Better responsive design for mobile devices with optimized button sizes
- Text truncation with tooltips for long domain lists (100+ characters)
- Structured file information display with dedicated styling for certificate and key files
- URL encoding fixes for proper handling of complex folder paths with special characters
### Changed
- **Certificate Cards**: Complete redesign with better organization and overflow handling
- Improved mobile responsiveness with single-column layout on small screens
- Enhanced button styling and spacing for better user experience
- Updated certificate information display with clearer visual hierarchy
- Better word wrapping and text breaking for long strings
### Fixed
- **Download Functionality**: Fixed 404 errors in download buttons due to URL encoding issues
- **PFX Generation**: Resolved routing issues with complex folder paths containing slashes
- **Archive/Restore**: Fixed double URL encoding problems in certificate management
- **UI Consistency**: Removed confusing question mark cursor from filename displays
- **Mobile Layout**: Fixed text overflow and improved touch-friendly button sizing
### Removed
- Debug console logging from production PFX generation
- Unnecessary cursor help indicators from file name displays
## [1.2.0] - 2025-07-29
### Added
@@ -115,7 +144,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **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
- **v1.3.0**: PFX generation, improved UI/UX, and enhanced certificate management
- **Current**: Full-featured mkcert Web UI with comprehensive certificate format support
## Contributing
+157 -340
View File
@@ -1,316 +1,149 @@
# mkcert Web UI
A modern web interface for managing SSL certificates using the mkcert CLI tool. This application provides an easy-to-use interface for generating, downloading, and managing local development certificates with organized storage and comprehensive certificate management features.
A modern web interface for managing SSL certificates using the mkcert CLI tool. Generate, download, and manage local development certificates with an intuitive web interface featuring Docker support, PFX generation, and comprehensive certificate management.
## Screenshot
## ✨ Features
- **🔐 Certificate Generation**: Create SSL certificates for multiple domains and IP addresses
- ** Multiple Formats**: Generate PEM, CRT, and PFX (PKCS#12) certificates on-demand
- **🔒 Optional Authentication**: Secure access with configurable user authentication
- **🌐 HTTPS Support**: Auto-generated SSL certificates for secure web interface
- **📋 Certificate Management**: View, download, archive, and restore certificates
- **🎨 Modern UI**: Dark/light themes with responsive design and mobile support
- ** Docker Support**: Complete containerization with docker-compose deployment
- **🔑 Root CA Management**: Install, view, and download the mkcert Certificate Authority
- **📊 Certificate Details**: Comprehensive information including expiry tracking
- **🔄 Automatic Organization**: Timestamp-based folder structure for easy management
## Quick Start with Docker
The fastest way to get started is using Docker:
```bash
# Clone the repository
git clone https://github.com/jeffcaldwellca/mkcertWeb.git
cd mkcertWeb
# Start with Docker Compose (includes automatic CA generation)
docker-compose up -d
# Access the application
open http://localhost:3000
```
**For detailed Docker setup, configuration, and deployment options, see [DOCKER.md](DOCKER.md)**
For local installation without Docker, you'll need Node.js 16+, mkcert, and OpenSSL. See [Installation Guide](#-installation) below for detailed setup instructions.
## Screenshot
![mkcert Web UI Screenshot](public/assets/screenshot.png)
*The mkcert Web UI featuring the new red/green terminal-style theme with certificate management, system status, and Root CA information.*
*The mkcert Web UI featuring the modern terminal-style theme with certificate management, system status, and Root CA information.*
## Features
## 🔧 Installation
- **🔐 Certificate Generation**: Create SSL certificates for multiple domains and IP addresses
- **📁 Organized Storage**: Automatic timestamp-based folder organization (YYYY-MM-DD/YYYY-MM-DDTHH-MM-SS_domains/)
- **🔒 Optional Authentication**: Secure access with configurable user authentication (can be disabled)
- **🌐 HTTPS Support**: Auto-generated SSL certificates for secure web interface access
- **📋 Certificate Management**: View, download, and archive certificates with expiry tracking
- **📦 Bundle Downloads**: Download certificate and key files as ZIP bundles
- **🔑 Root CA Management**: Install, view, and download the mkcert root Certificate Authority
- **🎨 Terminal-Style UI**: Modern red/green color scheme with monospace fonts and glowing effects
- **🌙 Dark/Light Mode**: Switchable themes with persistent user preference storage
- **🔒 Security**: Root certificates are read-only protected, authenticated sessions, input validation
- **📊 Certificate Details**: View domains, expiry dates, file sizes, and certificate information
- **🔄 Dual Format Support**: Generate certificates in PEM (.pem/.key) or CRT (.crt/.key) formats
### Method 1: Docker (Recommended)
## Prerequisites
See the [Quick Start with Docker](#-quick-start-with-docker) section above, or [DOCKER.md](DOCKER.md) for comprehensive Docker deployment options.
### Required Software
1. **Node.js** (version 16 or higher) - JavaScript runtime
2. **mkcert** - Local certificate authority tool
3. **OpenSSL** - Certificate analysis (usually pre-installed on Ubuntu)
### Method 2: Local Installation
### Ubuntu Installation (Recommended)
#### Prerequisites
- **Node.js** (version 16 or higher)
- **mkcert** CLI tool
- **OpenSSL** (usually pre-installed on most systems)
#### Install Node.js
#### Ubuntu/Debian Setup
```bash
# Install Node.js 18 LTS (recommended)
# Install Node.js
sudo apt update
sudo apt install -y nodejs npm
# Verify installation
node --version # Should be v16+
npm --version
```
#### Install mkcert
```bash
# Install dependencies
# Install mkcert dependencies and download mkcert
sudo apt install -y libnss3-tools wget
# Download and install mkcert (latest version)
wget -O mkcert https://github.com/FiloSottile/mkcert/releases/latest/download/mkcert-v1.4.4-linux-amd64
chmod +x mkcert
sudo mv mkcert /usr/local/bin/
# Verify installation
# Verify installations
node --version # Should be v16+
mkcert -version
```
#### Install OpenSSL (if not present)
```bash
# Usually pre-installed, but if needed:
sudo apt install -y openssl
# Verify installation
openssl version
```
## Installation
### Quick Start (Ubuntu)
1. **Clone the repository**:
#### Application Setup
```bash
# Clone and install
git clone https://github.com/jeffcaldwellca/mkcertWeb.git
cd mkcertWeb
```
2. **Install dependencies**:
```bash
npm install
```
3. **Initialize mkcert** (first time only):
```bash
# Create and install the root CA
# Initialize mkcert (first time only)
mkcert -install
```
4. **Start the application**:
```bash
# Start the application
npm start
# Access at http://localhost:3000
```
5. **Access the web interface**:
- Open your browser to `http://localhost:3000`
- **If authentication is enabled**: You'll be redirected to the login page
- Use credentials from your `.env` file (default: admin/admin123)
- After successful login, you'll access the main interface
- **If authentication is disabled**: You'll go directly to the certificate generation interface
- The application will verify mkcert installation and CA status
## ⚙️ Configuration
## HTTPS Configuration
### Environment Variables
The application supports automatic HTTPS with self-signed certificates generated using mkcert. This provides a secure development environment without browser warnings.
Configure the application using a `.env` file (see `.env.example`) or environment variables:
### Quick HTTPS Setup
#### Option 1: HTTPS with HTTP Fallback (Recommended for Development)
```bash
# Start with both HTTP and HTTPS servers
# Authentication
ENABLE_AUTH=true # Enable user authentication (default: true)
USERNAME=admin # Username for authentication
PASSWORD=admin123 # Password for authentication
# Server Configuration
PORT=3000 # HTTP server port
ENABLE_HTTPS=true # Enable HTTPS server
HTTPS_PORT=3443 # HTTPS server port
SSL_DOMAIN=localhost # Domain for SSL certificate
FORCE_HTTPS=true # Redirect HTTP to HTTPS
# Certificate Settings
CERTIFICATE_FORMAT=pem # Default format: 'pem' or 'crt'
```
### HTTPS Setup
The application supports automatic HTTPS with mkcert-generated certificates:
```bash
# HTTPS with HTTP fallback (recommended for development)
npm run https
# Or with environment variables
ENABLE_HTTPS=true npm start
# Access via:
# HTTP: http://localhost:3000
# HTTPS: https://localhost:3443
```
#### Option 2: HTTPS Only (Redirects HTTP to HTTPS)
```bash
# Start with HTTPS only (HTTP redirects to HTTPS)
# HTTPS only (redirects HTTP to HTTPS)
npm run https-only
# Or with environment variables
ENABLE_HTTPS=true FORCE_HTTPS=true npm start
# All requests redirect to: https://localhost:3443
```
#### Option 3: Custom Domain HTTPS
```bash
# Generate certificate for custom domain
# Custom domain
SSL_DOMAIN=myapp.local ENABLE_HTTPS=true npm start
# Access via: https://myapp.local:3443
# (Add "127.0.0.1 myapp.local" to /etc/hosts)
```
### Environment Variables for HTTPS
SSL certificates are automatically generated and stored in the `./ssl/` directory. Since they're created with mkcert, they're automatically trusted by your browser.
Create a `.env` file (see `.env.example`) or set environment variables:
### Production Deployment
```bash
# Basic HTTPS configuration
ENABLE_HTTPS=true # Enable HTTPS server
HTTPS_PORT=3443 # HTTPS server port (default: 3443)
SSL_DOMAIN=localhost # Domain for SSL certificate (default: localhost)
For production deployments, consider using Docker or see [DOCKER.md](DOCKER.md) for comprehensive deployment options including reverse proxy configurations and security best practices.
# Advanced options
FORCE_HTTPS=true # Redirect all HTTP to HTTPS
PORT=3000 # HTTP server port (default: 3000)
```
### SSL Certificate Management
The application automatically:
1. **Generates SSL certificates** on first HTTPS startup using mkcert
2. **Stores certificates** in `./ssl/` directory
3. **Reuses existing certificates** on subsequent startups
4. **Includes multiple domains**: localhost, 127.0.0.1, ::1, and custom domain
Certificate files:
```
ssl/
├── {domain}.pem # SSL certificate
└── {domain}-key.pem # Private key
```
### Browser Trust Setup
Since the certificates are generated by mkcert, they are automatically trusted if you have:
1. **Installed mkcert**: `mkcert -install` (done during setup)
2. **Root CA installed**: The mkcert root CA should be in your system trust store
If you see browser warnings:
```bash
# Verify mkcert installation
mkcert -install
# Check CA location
mkcert -CAROOT
# Regenerate SSL certificates
rm -rf ssl/
npm run https
```
### Production Deployment (Ubuntu)
#### Option 1: Simple Service User Deployment
```bash
# Create dedicated user
sudo adduser --system --group --home /opt/mkcertui mkcertui
# Install application
sudo cp -r mkcertWeb /opt/mkcertui/
sudo chown -R mkcertui:mkcertui /opt/mkcertui/
# Switch to service user and install dependencies
sudo su - mkcertui
cd mkcertWeb
npm install
# Initialize mkcert for this user
mkcert -install
# Start application
npm start
```
#### Option 2: Systemd Service
```bash
# Create systemd service file
sudo tee /etc/systemd/system/mkcertui.service << 'EOF'
[Unit]
Description=mkcert Web UI
After=network.target
Wants=network.target
[Service]
Type=simple
User=mkcertui
Group=mkcertui
WorkingDirectory=/opt/mkcertui/mkcertWeb
ExecStart=/usr/bin/node server.js
Restart=always
RestartSec=10
Environment=NODE_ENV=production
Environment=PORT=3000
# Security settings
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/opt/mkcertui
PrivateTmp=yes
[Install]
WantedBy=multi-user.target
EOF
# Enable and start service
sudo systemctl daemon-reload
sudo systemctl enable mkcertui
sudo systemctl start mkcertui
# Check status
sudo systemctl status mkcertui
```
### Environment Configuration
#### Environment Variables
```bash
# Server Configuration
PORT=3000 # HTTP server port (default: 3000)
HTTPS_PORT=3443 # HTTPS server port (default: 3443)
# SSL/HTTPS Configuration
ENABLE_HTTPS=true # Enable HTTPS server (default: false)
SSL_DOMAIN=localhost # Domain name for SSL certificate (default: localhost)
FORCE_HTTPS=true # Redirect HTTP to HTTPS (default: false)
# Application Configuration
NODE_ENV=production # Environment mode
CERT_DIR=/custom/path # Custom certificate storage (optional)
```
#### Reverse Proxy (Nginx)
```bash
# Install nginx
sudo apt install -y nginx
# Create nginx configuration
sudo tee /etc/nginx/sites-available/mkcertui << 'EOF'
server {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
EOF
# Enable site
sudo ln -s /etc/nginx/sites-available/mkcertui /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
```
## Usage
## 📚 Usage
### First Time Setup
1. **Verify Prerequisites**: The web interface will check if mkcert and OpenSSL are installed
2. **Install Root CA**: If not already installed, the app will prompt you to install the mkcert root CA
3. **Download Root CA**: Use the Root CA section to download the certificate for other systems
1. **System Check**: The web interface automatically verifies mkcert and OpenSSL installation
2. **Root CA Installation**: Install the mkcert root CA if prompted (`mkcert -install`)
3. **Authentication**: Log in with configured credentials (default: admin/admin123)
### Certificate Generation
### 🔐 Certificate Generation
1. **Access the Web Interface**: Navigate to `http://localhost:3000`
2. **Enter Domains**: In the generation form, enter domain names (one per line):
1. **Access Interface**: Navigate to `http://localhost:3000`
2. **Enter Domains**: Add domain names (one per line):
```
localhost
127.0.0.1
@@ -318,116 +151,100 @@ sudo systemctl reload nginx
example.com
myapp.local
```
3. **Select Format**:
- **PEM Format**: Standard format (.pem certificate, -key.pem private key)
- **CRT Format**: Common for web servers (.crt certificate, .key private key)
3. **Select Format**: Choose PEM (.pem/.key) or CRT (.crt/.key) format
4. **Generate**: Click "Generate Certificate"
### Certificate Organization
### 📁 Certificate Organization
Certificates are automatically organized in a hierarchical structure:
Certificates are automatically organized with timestamps:
```
certificates/
├── root/ # Legacy certificates (read-only)
│ ├── example.pem
── example-key.pem
└── 2025-07-25/ # Date-based folders
├── 2025-07-25T10-30-45_localhost_127-0-0-1/
── localhost_127-0-0-1.pem
│ └── localhost_127-0-0-1-key.pem
└── 2025-07-25T14-15-20_example_com/
── example_com.crt
└── example_com.key
├── root/ # Legacy certificates (protected)
└── 2025-01-20/ # Date-based organization
── 2025-01-20T10-30-45_localhost/
│ ├── localhost.pem
│ ├── localhost-key.pem
── localhost.pfx # Generated on-demand
└── 2025-01-20T14-15-20_example/
├── example.crt
── example.key
```
### Certificate Management
### 🔧 Certificate Management
#### Viewing Certificates
- **Certificate List**: Shows all certificates with details:
- Domain names covered
- Expiry date and status
- File format (PEM/CRT)
- Creation date and file size
- Storage location
- **📋 View Details**: Domain coverage, expiry dates, file sizes
- **⬇️ Download**: Individual files, ZIP bundles, or PFX format
- **🔑 PFX Generation**: Create password-protected PKCS#12 files on-demand
- **🗑️ Delete**: Remove certificates (root certificates are protected)
- **📊 System Status**: View Root CA information and installation status
#### Downloading Certificates
- **Individual Files**: Download certificate or key files separately
- **Bundle Download**: Download both files as a ZIP archive
- **Root CA**: Download the mkcert root certificate for system installation
### 🌐 Advanced Usage
#### Certificate Deletion
- **Subfolder Certificates**: Can be deleted (includes automatic cleanup of empty folders)
- **Root Certificates**: Protected from deletion (read-only)
For production deployments, reverse proxy configurations, and advanced Docker setups, see [DOCKER.md](DOCKER.md).
### Root CA Management
## 🔗 API Reference
#### Viewing CA Information
The Root CA section displays:
- Certificate subject and issuer
- Validity period and expiry status
- SHA256 fingerprint for verification
- File system path
- Installation status
The application provides REST API endpoints for programmatic access. When authentication is enabled, establish a session first via `POST /login`.
#### Installing Root CA on Other Systems
### Key Endpoints
**Linux (Ubuntu/Debian)**:
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/status` | System status and mkcert installation |
| `POST` | `/api/generate` | Generate new certificates |
| `GET` | `/api/certificates` | List all certificates |
| `GET` | `/api/download/bundle/:folder/:certname` | Download certificate bundle |
| `POST` | `/api/generate/pfx/*` | Generate PFX file on-demand |
Example certificate generation:
```bash
# Download the root CA from the web interface, then:
sudo cp mkcert-rootCA.pem /usr/local/share/ca-certificates/mkcert-rootCA.crt
sudo update-ca-certificates
# Generate certificate via API
curl -X POST http://localhost:3000/api/generate \
-H "Content-Type: application/json" \
-d '{"domains":["localhost","127.0.0.1"],"format":"pem"}'
```
**Windows**:
1. Download root CA from web interface
2. Double-click the .pem file
3. Install to "Trusted Root Certification Authorities"
## 🤝 Contributing
**macOS**:
1. Download root CA from web interface
2. Double-click to add to Keychain
3. Set trust settings to "Always Trust"
**Browser Trust**:
1. Import the root CA into browser security settings
2. Add to "Authorities" or "Certificate Authorities" section
1. Fork the repository
2. Create a feature branch: `git checkout -b feature/new-feature`
3. Commit changes: `git commit -am 'Add new feature'`
4. Push to branch: `git push origin feature/new-feature`
5. Submit a pull request
## API Documentation
## 📄 License
### REST API Endpoints
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
The application provides a comprehensive REST API for programmatic access.
## 🙏 Acknowledgments
**🔒 Authentication Note**: When authentication is enabled, API endpoints require valid session cookies. For programmatic access, you may need to:
1. Disable authentication by setting `DISABLE_AUTH=true` in your `.env` file, or
2. First authenticate via `POST /login` to establish a session before making API calls
- [mkcert](https://github.com/FiloSottile/mkcert) - Simple tool for making locally-trusted development certificates
- [Express.js](https://expressjs.com/) - Web application framework
- [Node.js](https://nodejs.org/) - JavaScript runtime
#### Authentication
- `POST /login` - Authenticate user and establish session
- `POST /logout` - Destroy current session
## 📞 Support
#### System Status
- `GET /api/status` - Get mkcert installation and CA status
- `POST /api/install-ca` - Install the mkcert root CA (requires user confirmation)
- 🐛 **Issues**: [GitHub Issues](https://github.com/jeffcaldwellca/mkcertWeb/issues)
- 📖 **Documentation**: [README.md](README.md) and [DOCKER.md](DOCKER.md)
- 💬 **Discussions**: [GitHub Discussions](https://github.com/jeffcaldwellca/mkcertWeb/discussions)
4. Push to branch: `git push origin feature/new-feature`
5. Submit a pull request
#### Root CA Management
- `GET /api/rootca/info` - Get detailed root CA certificate information
- `GET /api/download/rootca` - Download root CA certificate file
## 📄 License
#### Certificate Management
- `POST /api/generate` - Generate new certificates
- `GET /api/certificates` - List all certificates with metadata
- `DELETE /api/certificates/:folder/:certname` - Delete specific certificate
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
#### File Downloads
- `GET /api/download/cert/:folder/:filename` - Download certificate file
- `GET /api/download/key/:folder/:filename` - Download private key file
- `GET /api/download/bundle/:folder/:certname` - Download certificate bundle (ZIP)
## 🙏 Acknowledgments
### API Usage Examples
- [mkcert](https://github.com/FiloSottile/mkcert) - Simple tool for making locally-trusted development certificates
- [Express.js](https://expressjs.com/) - Web application framework
- [Node.js](https://nodejs.org/) - JavaScript runtime
#### Generate Certificate (using wget - built into Ubuntu)
```bash
# Generate PEM format certificate
## 📞 Support
- 🐛 **Issues**: [GitHub Issues](https://github.com/jeffcaldwellca/mkcertWeb/issues)
- 📖 **Documentation**: [README.md](README.md) and [DOCKER.md](DOCKER.md)
- 💬 **Discussions**: [GitHub Discussions](https://github.com/jeffcaldwellca/mkcertWeb/discussions)
wget --post-data='{"domains":["localhost","127.0.0.1","*.local.dev"],"format":"pem"}' \
--header='Content-Type: application/json' \
http://localhost:3000/api/generate \
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mkcert-web-ui",
"version": "1.0.0",
"version": "1.3.0",
"description": "Web UI middleware for managing mkcert CLI and certificate files",
"main": "server.js",
"scripts": {
+146 -12
View File
@@ -409,7 +409,13 @@ function displayCertificates(certificates) {
}
const html = certificates.map(cert => {
const domainsDisplay = cert.domains ? cert.domains.join(', ') : 'Unknown';
// Truncate domains list if too long
let domainsDisplay = cert.domains ? cert.domains.join(', ') : 'Unknown';
if (domainsDisplay.length > 100) {
const truncated = domainsDisplay.substring(0, 97) + '...';
domainsDisplay = `<span title="${domainsDisplay}">${truncated}</span>`;
}
const createdDate = new Date(cert.created).toLocaleDateString();
const createdTime = new Date(cert.created).toLocaleTimeString();
@@ -417,8 +423,8 @@ function displayCertificates(certificates) {
'<span class="format-badge format-' + cert.format.toLowerCase() + '">' + cert.format.toUpperCase() + '</span>' : '';
let expiryInfo, expiryClass = '';
if (cert.expiryDate) {
const expiryDateStr = new Date(cert.expiryDate).toLocaleDateString();
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';
@@ -450,20 +456,23 @@ function displayCertificates(certificates) {
'<div class="certificate-header">' +
'<div class="certificate-name">' +
'<i class="fas fa-certificate"></i> ' + cert.name +
'</div>' +
'<div class="certificate-badges">' +
formatBadge +
(cert.isExpired ? '<span class="expired-badge">EXPIRED</span>' : '') +
(isRootCert ? '<span class="read-only-badge">READ-ONLY</span>' : '') +
(isArchived ? '<span class="archived-badge">ARCHIVED</span>' : '') +
'</div></div>' +
'</div>' +
'</div>' +
'<div class="certificate-info">' +
'<div><strong>Domains:</strong> ' + domainsDisplay + '</div>' +
'<div><strong>Location:</strong> ' + folderDisplay + '</div>' +
'<div><strong>Created:</strong> ' + createdDate + ' ' + createdTime + '</div>' +
'<div class="' + expiryClass + '"><strong>Expiry:</strong> ' + expiryInfo + '</div>' +
'<div><strong>Cert File:</strong> ' + cert.certFile + '</div>' +
'<div><strong>Key File:</strong> ' + (cert.keyFile || 'Missing') + '</div>' +
'<div><strong>Size:</strong> ' + formatFileSize(cert.size) + '</div>' +
'<div><strong>Status:</strong> ' + (isArchived ? 'Archived' : 'Active') + '</div>' +
'<div><strong>Domains:</strong><br>' + domainsDisplay + '</div>' +
'<div><strong>Location:</strong><br>' + folderDisplay + '</div>' +
'<div><strong>Created:</strong><br>' + createdDate + ' ' + createdTime + '</div>' +
'<div class="' + expiryClass + '"><strong>Expiry:</strong><br>' + expiryInfo + '</div>' +
'<div><strong>Certificate File:</strong><div class="file-name">' + cert.certFile + '</div></div>' +
'<div><strong>Private Key File:</strong><div class="file-name">' + (cert.keyFile || '<em>Missing</em>') + '</div></div>' +
'<div><strong>File Size:</strong><br>' + formatFileSize(cert.size) + '</div>' +
'<div><strong>Status:</strong><br>' + (isArchived ? 'Archived' : 'Active') + '</div>' +
'</div>' +
'<div class="certificate-actions">' +
'<button onclick="downloadCert(\'' + folderParam + '\', \'' + cert.certFile + '\')" ' +
@@ -476,6 +485,10 @@ function displayCertificates(certificates) {
'<button onclick="downloadBundle(\'' + folderParam + '\', \'' + cert.name + '\')" ' +
'class="btn btn-primary btn-small">' +
'<i class="fas fa-file-archive"></i> Download Bundle</button>' +
(cert.keyFile && !isRootCert ?
'<button onclick="testPFX(\'' + folderParam + '\', \'' + cert.name + '\')" ' +
'class="btn btn-windows btn-small" title="Generate password-protected PFX file">' +
'<i class="fas fa-shield-alt"></i> Generate PFX</button>' : '') +
(!isRootCert && !isArchived ?
'<button onclick="archiveCertificate(\'' + folderParam + '\', \'' + cert.name + '\')" ' +
'class="btn btn-warning btn-small" title="Archive this certificate">' +
@@ -821,3 +834,124 @@ function downloadRootCA() {
const url = API_BASE + '/download/rootca';
downloadFile(url, 'mkcert-rootCA.pem');
}
function testPFX(folderParam, certname) {
generatePFX(folderParam, certname);
}
async function generatePFX(folderParam, certname) {
console.log('generatePFX called with:', folderParam, certname);
// Create a modal for password input
const modal = document.createElement('div');
modal.className = 'modal';
modal.style.display = 'block'; // Show the modal
modal.innerHTML = `
<div class="modal-content">
<h3><i class="fas fa-shield-alt"></i> Generate PFX File</h3>
<p>Enter a password to protect the PFX file. This file will contain both the certificate and private key.</p>
<div class="form-group">
<label for="pfx-password">Password (optional):</label>
<input type="password" id="pfx-password" placeholder="Leave empty for no password protection" />
<small class="help-text">Strong passwords are recommended for security.</small>
</div>
<div class="modal-actions">
<button id="generate-pfx-btn" class="btn btn-primary">
<i class="fas fa-cog"></i> Generate PFX
</button>
<button id="cancel-pfx-btn" class="btn btn-secondary">Cancel</button>
</div>
</div>
`;
document.body.appendChild(modal);
console.log('Modal created and added to DOM');
// Focus on password input
const passwordInput = document.getElementById('pfx-password');
passwordInput.focus();
return new Promise((resolve, reject) => {
const generateBtn = document.getElementById('generate-pfx-btn');
const cancelBtn = document.getElementById('cancel-pfx-btn');
const cleanup = () => {
document.body.removeChild(modal);
};
const handleGenerate = async () => {
console.log('Generate button clicked!');
const password = passwordInput.value;
console.log('Password entered:', password ? 'Yes' : 'No');
generateBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Generating...';
generateBtn.disabled = true;
try {
console.log('Making API request...');
const apiUrl = API_BASE + '/generate/pfx/' + folderParam + '/' + encodeURIComponent(certname);
console.log('API URL:', apiUrl);
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'same-origin',
body: JSON.stringify({ password: password || '' })
});
console.log('Response status:', response.status);
if (!response.ok) {
const errorData = await response.json();
console.error('Server error:', errorData);
throw new Error(errorData.error || `PFX generation failed: ${response.status} ${response.statusText}`);
}
console.log('Downloading blob...');
const blob = await response.blob();
console.log('Blob size:', blob.size);
const downloadUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = downloadUrl;
a.download = certname + '.pfx';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(downloadUrl);
showAlert('PFX file generated and downloaded successfully', 'success');
cleanup();
resolve();
} catch (error) {
console.error('PFX generation error:', error);
showAlert('PFX generation failed: ' + error.message, 'error');
generateBtn.innerHTML = '<i class="fas fa-cog"></i> Generate PFX';
generateBtn.disabled = false;
reject(error);
}
};
const handleCancel = () => {
cleanup();
resolve();
};
generateBtn.addEventListener('click', handleGenerate);
cancelBtn.addEventListener('click', handleCancel);
console.log('Event listeners attached to buttons');
// Allow Enter to trigger generation
passwordInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
handleGenerate();
}
});
// Allow Escape to cancel
modal.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
handleCancel();
}
});
});
}
+215 -10
View File
@@ -632,6 +632,59 @@ section h2::before {
filter: none;
}
[data-theme="light"] .btn-warning {
background: white;
color: var(--warning-color);
border: 2px solid var(--warning-color);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
[data-theme="light"] .btn-warning:hover {
background: var(--warning-color);
color: white;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
filter: none;
}
[data-theme="light"] .btn-info {
background: white;
color: var(--secondary-color);
border: 2px solid var(--secondary-color);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
[data-theme="light"] .btn-info:hover {
background: var(--secondary-color);
color: white;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
filter: none;
}
[data-theme="light"] .btn-windows {
background: linear-gradient(to bottom, #f8f9fa, #e9ecef);
color: #2d5a87;
border: 1px solid #5b9bd5;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.9),
inset 0 -1px 0 rgba(0, 0, 0, 0.1),
0 1px 3px rgba(0, 0, 0, 0.1);
text-shadow: none;
font-weight: 600;
}
[data-theme="light"] .btn-windows:hover {
background: linear-gradient(to bottom, #5b9bd5, #4285c4);
color: white;
border-color: #2d5a87;
transform: translateY(-1px);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.2),
0 3px 6px rgba(0, 0, 0, 0.2);
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.3);
}
.btn-danger {
background: var(--error-color);
color: white;
@@ -646,6 +699,60 @@ section h2::before {
box-shadow: 0 4px 12px var(--glow-secondary), 0 0 20px var(--glow-secondary);
}
.btn-warning {
background: var(--warning-color);
color: #000;
font-weight: 600;
border: 1px solid var(--warning-color);
box-shadow: 0 0 8px rgba(251, 191, 36, 0.3);
}
.btn-warning:hover {
background: var(--warning-color);
filter: brightness(0.9);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(251, 191, 36, 0.4), 0 0 16px rgba(251, 191, 36, 0.3);
color: #000;
}
.btn-info {
background: var(--secondary-color);
color: #000;
font-weight: 600;
border: 1px solid var(--secondary-color);
box-shadow: 0 0 8px rgba(140, 200, 255, 0.3);
}
.btn-info:hover {
background: var(--secondary-color);
filter: brightness(0.9);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(140, 200, 255, 0.4), 0 0 16px rgba(140, 200, 255, 0.3);
color: #000;
}
.btn-windows {
background: linear-gradient(to bottom, #5b9bd5, #3b73a8);
color: white;
font-weight: 600;
border: 1px solid #2d5a87;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.2),
0 2px 4px rgba(45, 90, 135, 0.3);
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.3);
}
.btn-windows:hover {
background: linear-gradient(to bottom, #6db3f2, #4285c4);
border-color: #1e4d72;
transform: translateY(-1px);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.25),
0 4px 8px rgba(45, 90, 135, 0.4),
0 0 12px rgba(91, 155, 213, 0.3);
color: white;
}
.btn-small {
padding: 0.5rem 1rem;
font-size: 0.875rem;
@@ -683,6 +790,8 @@ section h2::before {
padding: 1rem;
transition: all 0.3s ease;
box-shadow: inset 0 1px 0 rgba(0, 255, 65, 0.05);
overflow: hidden; /* Prevent content overflow */
word-wrap: break-word; /* Allow long words to break */
}
.certificate-card:hover {
@@ -736,9 +845,9 @@ section h2::before {
.certificate-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.certificate-name {
@@ -751,6 +860,15 @@ section h2::before {
flex-wrap: wrap;
text-shadow: 0 0 3px rgba(0, 255, 65, 0.3);
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
word-break: break-all;
line-height: 1.3;
}
.certificate-badges {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
align-items: center;
}
.expired-badge {
@@ -789,27 +907,86 @@ section h2::before {
}
.format-badge.format-pem {
background: var(--border-color);
color: var(--secondary-color);
border: 1px solid var(--border-color-secondary);
background: linear-gradient(135deg, rgba(64, 224, 208, 0.15), rgba(64, 224, 208, 0.25));
color: var(--primary-color);
border: 1px solid rgba(64, 224, 208, 0.3);
text-shadow: 0 0 2px rgba(64, 224, 208, 0.3);
box-shadow: 0 0 4px rgba(64, 224, 208, 0.2);
}
.format-badge.format-crt {
background: var(--success-color);
color: #000;
background: linear-gradient(135deg, rgba(74, 222, 128, 0.15), rgba(74, 222, 128, 0.25));
color: var(--success-color);
border: 1px solid rgba(74, 222, 128, 0.3);
text-shadow: 0 0 2px rgba(74, 222, 128, 0.3);
box-shadow: 0 0 4px rgba(74, 222, 128, 0.2);
font-weight: 600;
}
.format-badge.format-pfx {
background: linear-gradient(135deg, rgba(255, 107, 107, 0.15), rgba(255, 107, 107, 0.25));
color: var(--accent-color);
border: 1px solid rgba(255, 107, 107, 0.3);
text-shadow: 0 0 2px rgba(255, 107, 107, 0.3);
box-shadow: 0 0 4px rgba(255, 107, 107, 0.2);
font-weight: 600;
}
.certificate-info {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0.5rem;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 0.75rem;
margin-bottom: 1rem;
font-size: 0.875rem;
color: var(--text-dim);
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
}
.certificate-info > div {
display: flex;
flex-direction: column;
word-wrap: break-word;
overflow-wrap: break-word;
hyphens: auto;
}
.certificate-info > div strong {
color: var(--text-color);
margin-bottom: 0.25rem;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.certificate-info .file-name {
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
background: var(--border-color);
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
word-break: break-all;
margin-top: 0.25rem;
border: 1px solid var(--border-color-secondary);
cursor: default;
transition: all 0.2s ease;
}
.certificate-info .file-name:hover {
background: var(--input-bg);
border-color: var(--success-color);
}
/* Truncated text with tooltip */
.certificate-info [title] {
cursor: default;
text-decoration: underline dotted;
text-decoration-color: var(--text-dim);
}
.certificate-info [title]:hover {
text-decoration-color: var(--success-color);
}
.certificate-actions {
display: flex;
gap: 0.5rem;
@@ -969,8 +1146,36 @@ section h2::before {
font-size: 2rem;
}
.certificate-info {
grid-template-columns: 1fr;
gap: 0.5rem;
}
.certificate-info > div {
padding: 0.5rem;
background: var(--input-bg);
border-radius: 4px;
border: 1px solid var(--border-color-secondary);
}
.certificate-name {
font-size: 1rem;
word-break: break-all;
}
.file-name {
font-size: 0.75rem !important;
padding: 0.2rem 0.4rem !important;
}
.certificate-actions {
justify-content: center;
gap: 0.25rem;
}
.certificate-actions .btn-small {
font-size: 0.75rem;
padding: 0.4rem 0.6rem;
}
.modal-content {
+120
View File
@@ -893,6 +893,126 @@ app.get('/api/download/bundle/:certname', requireAuth, (req, res) => {
archive.finalize();
});
// Generate PFX file from certificate and key
app.post('/api/generate/pfx/*', requireAuth, async (req, res) => {
try {
// Parse the wildcard path to extract folder and certname
const fullPath = req.params[0]; // Get the wildcard part
const pathParts = fullPath.split('/');
if (pathParts.length < 2) {
return res.status(400).json({
success: false,
error: 'Invalid path format. Expected: /folder/certname'
});
}
// Last part is certname, everything else is folder path
const certName = pathParts.pop();
const encodedFolder = pathParts.join('/');
const folder = encodedFolder === 'root' ? '' : decodeURIComponent(encodedFolder);
const password = req.body.password || '';
console.log('PFX generation request:', { encodedFolder, folder, certName });
// Protect root directory certificates
if (encodedFolder === 'root' || folder === '') {
return res.status(403).json({
success: false,
error: 'PFX generation not available for root certificates'
});
}
// Find certificate and key files
const possibleCertFiles = [`${certName}.pem`, `${certName}.crt`];
const possibleKeyFiles = [`${certName}-key.pem`, `${certName}.key`];
let certPath = null;
let keyPath = null;
for (const cert of possibleCertFiles) {
const testPath = path.join(CERT_DIR, folder, cert);
if (fs.existsSync(testPath)) {
certPath = testPath;
break;
}
}
for (const key of possibleKeyFiles) {
const testPath = path.join(CERT_DIR, folder, key);
if (fs.existsSync(testPath)) {
keyPath = testPath;
break;
}
}
if (!certPath || !keyPath) {
return res.status(404).json({
success: false,
error: 'Certificate or key file not found'
});
}
// Create temporary PFX file
const tempDir = path.join(__dirname, 'temp');
await fs.ensureDir(tempDir);
const tempPfxPath = path.join(tempDir, `${certName}_${Date.now()}.pfx`);
try {
// Generate PFX using OpenSSL
const opensslCmd = password
? `openssl pkcs12 -export -out "${tempPfxPath}" -inkey "${keyPath}" -in "${certPath}" -passout pass:"${password}"`
: `openssl pkcs12 -export -out "${tempPfxPath}" -inkey "${keyPath}" -in "${certPath}" -passout pass:`;
await executeCommand(opensslCmd);
// Check if PFX file was created
if (!fs.existsSync(tempPfxPath)) {
throw new Error('PFX file generation failed');
}
// Set headers for download
res.setHeader('Content-Type', 'application/x-pkcs12');
res.setHeader('Content-Disposition', `attachment; filename="${certName}.pfx"`);
res.setHeader('Cache-Control', 'no-cache');
// Stream the file and clean up
const fileStream = fs.createReadStream(tempPfxPath);
fileStream.pipe(res);
fileStream.on('end', () => {
// Clean up temp file
fs.unlink(tempPfxPath).catch(err => {
console.error('Failed to cleanup temp PFX file:', err);
});
});
fileStream.on('error', (error) => {
console.error('Error streaming PFX file:', error);
fs.unlink(tempPfxPath).catch(() => {});
res.status(500).json({
success: false,
error: 'Failed to download PFX file'
});
});
} catch (error) {
// Clean up temp file on error
if (fs.existsSync(tempPfxPath)) {
fs.unlinkSync(tempPfxPath);
}
throw error;
}
} catch (error) {
console.error('PFX generation error:', error);
res.status(500).json({
success: false,
error: 'PFX generation failed: ' + error.message
});
}
});
// Archive certificate (instead of deleting)
app.post('/api/certificates/:folder/:certname/archive', requireAuth, async (req, res) => {
try {
-74
View File
@@ -1,74 +0,0 @@
#!/bin/bash
# HTTPS Auto-Trust Test Script for Ubuntu
echo "🔐 Testing mkcert Web UI HTTPS Auto-Trust on Ubuntu"
echo "=================================================="
# Check if mkcert is installed
if ! command -v mkcert &> /dev/null; then
echo "❌ mkcert not found. Please install mkcert first."
exit 1
fi
# Check if CA is installed
if [ ! -f "$(mkcert -CAROOT)/rootCA.pem" ]; then
echo "⚠️ mkcert CA not installed. Installing..."
mkcert -install
fi
echo "✅ mkcert CA location: $(mkcert -CAROOT)"
# Start the server with HTTPS in background
echo "🚀 Starting server with HTTPS..."
ENABLE_HTTPS=true npm start &
SERVER_PID=$!
# Wait for server to start
sleep 5
# Test HTTP
echo "🌐 Testing HTTP endpoint..."
if curl -s http://localhost:3000/api/status | grep -q "success"; then
echo "✅ HTTP endpoint working"
else
echo "❌ HTTP endpoint failed"
fi
# Test HTTPS
echo "🔒 Testing HTTPS endpoint..."
if curl -s https://localhost:3443/api/status | grep -q "success"; then
echo "✅ HTTPS endpoint working (certificate trusted!)"
else
echo "⚠️ HTTPS endpoint failed - checking certificate..."
curl -k https://localhost:3443/api/status >/dev/null 2>&1 && echo "✅ HTTPS works with certificate bypass"
fi
# Check SSL certificate
echo "🔍 Checking auto-generated SSL certificate..."
if [ -f "ssl/localhost.pem" ]; then
echo "✅ SSL certificate found: ssl/localhost.pem"
echo "📋 Certificate details:"
openssl x509 -in ssl/localhost.pem -noout -subject -issuer -dates
else
echo "❌ SSL certificate not found"
fi
# Test certificate validation
echo "🔐 Testing certificate validation..."
if openssl verify -CAfile "$(mkcert -CAROOT)/rootCA.pem" ssl/localhost.pem >/dev/null 2>&1; then
echo "✅ Certificate validates against mkcert CA"
else
echo "❌ Certificate validation failed"
fi
# Cleanup
echo "🧹 Cleaning up..."
kill $SERVER_PID 2>/dev/null
wait $SERVER_PID 2>/dev/null
echo ""
echo "🎉 Test complete! Access your HTTPS mkcert Web UI at:"
echo " https://localhost:3443"
echo ""
echo "To start with HTTPS permanently:"
echo " ENABLE_HTTPS=true npm start"
View File