Merge pull request #3 from jeffcaldwellca/dockerize

## [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
This commit is contained in:
Jeff Caldwell
2025-07-29 14:49:06 -04:00
committed by GitHub
11 changed files with 390 additions and 1739 deletions
+5
View File
@@ -99,3 +99,8 @@ jspm_packages/
ehthumbs.db
Thumbs.db
# Docker
.dockerignore
docker-compose.override.yml
.docker/
+33 -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.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
+245
View File
@@ -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.
+55
View File
@@ -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"]
+44
View File
@@ -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
+8 -1
View File
@@ -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",
-276
View File
@@ -1,276 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - 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>
body {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.login-container {
max-width: 400px;
width: 90%;
padding: 40px;
background: var(--secondary-bg);
border: 1px solid var(--border-color);
border-radius: 15px;
box-shadow:
0 8px 32px var(--shadow-color),
inset 0 1px 0 var(--glow-primary);
backdrop-filter: blur(20px);
position: relative;
z-index: 1;
}
.theme-toggle {
position: absolute;
top: 20px;
right: 20px;
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;
}
.login-header {
text-align: center;
margin-bottom: 30px;
}
.login-header h1 {
color: var(--primary-color);
margin-bottom: 10px;
font-size: 2rem;
text-shadow: 0 0 10px var(--glow-primary);
font-weight: 700;
}
.login-header h1::before {
content: '> ';
color: var(--accent-color);
text-shadow: 0 0 5px var(--glow-secondary);
}
.login-header p {
color: var(--secondary-color);
font-size: 0.95rem;
}
.login-form .form-group {
margin-bottom: 20px;
}
.login-form label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: var(--primary-color);
font-size: 0.9rem;
text-shadow: 0 0 3px var(--glow-primary);
}
.login-form input {
width: 100%;
padding: 15px;
background: var(--input-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
color: var(--text-color);
font-family: inherit;
font-size: 1rem;
transition: all 0.3s ease;
}
.login-form input:focus {
outline: none;
border-color: var(--success-color);
box-shadow: 0 0 0 3px var(--glow-primary);
background: var(--secondary-bg);
}
.login-form .btn-login {
width: 100%;
padding: 15px;
background: linear-gradient(135deg, var(--success-color) 0%, #00cc33 100%);
border: none;
border-radius: 8px;
color: #000;
font-family: inherit;
font-size: 1rem;
font-weight: 700;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 10px;
text-shadow: none;
box-shadow: 0 0 10px var(--glow-primary);
}
.login-form .btn-login:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px var(--glow-primary), 0 0 25px var(--glow-primary);
background: linear-gradient(135deg, var(--success-color) 0%, #00aa28 100%);
}
.login-form .btn-login:active {
transform: translateY(0);
}
.login-form .btn-login i {
margin-right: 8px;
}
.error-message {
background: rgba(220, 53, 69, 0.1);
border: 1px solid var(--error-color);
color: var(--error-color);
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
text-align: center;
font-weight: 600;
backdrop-filter: blur(10px);
animation: shake 0.5s ease-in-out;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
.home-link {
text-align: center;
margin-top: 20px;
}
.home-link a {
color: var(--secondary-color);
text-decoration: none;
font-size: 0.9rem;
transition: all 0.3s ease;
}
.home-link a:hover {
color: var(--primary-color);
text-shadow: 0 0 5px var(--glow-primary);
}
.home-link a i {
margin-right: 5px;
}
</style>
</head>
<body>
<button class="theme-toggle" id="theme-toggle">
<i class="fas fa-sun"></i> Light Mode
</button>
<div class="login-container">
<div class="login-header">
<h1><i class="fas fa-certificate"></i> mkcert Web UI</h1>
<p>Secure Access Required</p>
</div>
<form class="login-form" action="/login" method="POST">
<div class="form-group">
<label for="username">
<i class="fas fa-user"></i> Username
</label>
<input type="text" id="username" name="username" required autofocus>
</div>
<div class="form-group">
<label for="password">
<i class="fas fa-lock"></i> Password
</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit" class="btn-login">
<i class="fas fa-sign-in-alt"></i> Login
</button>
</form>
<div class="home-link">
<a href="/" title="Return to home page">
<i class="fas fa-home"></i> Return to Home
</a>
</div>
</div>
<script>
// Theme management for login page
let currentTheme = localStorage.getItem('theme') || 'dark';
let themeToggle = document.getElementById('theme-toggle');
function initializeTheme() {
document.documentElement.setAttribute('data-theme', currentTheme);
updateThemeToggleButton();
}
function toggleTheme() {
currentTheme = currentTheme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', currentTheme);
localStorage.setItem('theme', currentTheme);
updateThemeToggleButton();
}
function updateThemeToggleButton() {
if (themeToggle) {
const icon = themeToggle.querySelector('i');
const text = themeToggle.childNodes[themeToggle.childNodes.length - 1];
if (currentTheme === 'dark') {
icon.className = 'fas fa-sun';
text.textContent = ' Light Mode';
} else {
icon.className = 'fas fa-moon';
text.textContent = ' Dark Mode';
}
}
}
// Initialize theme on page load
document.addEventListener('DOMContentLoaded', function() {
initializeTheme();
if (themeToggle) {
themeToggle.addEventListener('click', toggleTheme);
}
});
// Show error message if present
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('error') === 'invalid_credentials') {
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Invalid username or password. Please try again.';
const form = document.querySelector('.login-form');
form.insertBefore(errorDiv, form.firstChild);
}
</script>
</body>
</html>
-254
View File
@@ -1,254 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - 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>
body {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.login-container {
max-width: 400px;
width: 90%;
padding: 40px;
background: var(--secondary-bg);
border: 1px solid var(--border-color);
border-radius: 15px;
box-shadow:
0 8px 32px var(--shadow-color),
inset 0 1px 0 var(--glow-primary);
backdrop-filter: blur(20px);
position: relative;
z-index: 1;
}
.theme-toggle {
position: absolute;
top: 20px;
right: 20px;
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;
}
0 8px 25px rgba(0, 0, 0, 0.4),
0 0 20px rgba(0, 255, 65, 0.1);
backdrop-filter: blur(10px);
}
.login-header {
text-align: center;
margin-bottom: 30px;
}
.login-header h1 {
color: #00ff41;
margin-bottom: 10px;
font-size: 2rem;
text-shadow: 0 0 10px rgba(0, 255, 65, 0.5);
font-weight: 700;
}
.login-header h1::before {
content: '> ';
color: #ff0041;
text-shadow: 0 0 5px rgba(255, 0, 65, 0.8);
}
.login-header p {
color: #8cc8ff;
font-size: 0.95rem;
}
.login-form .form-group {
margin-bottom: 20px;
}
.login-form label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #00ff41;
font-size: 0.9rem;
text-shadow: 0 0 3px rgba(0, 255, 65, 0.3);
}
.login-form input {
width: 100%;
padding: 14px 16px;
background: rgba(13, 17, 23, 0.8);
border: 2px solid #30363d;
border-radius: 8px;
font-size: 16px;
transition: all 0.3s ease;
color: #8cc8ff;
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
}
.login-form input: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);
}
.login-btn {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #00ff41 0%, #00cc33 100%);
color: #000;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 700;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 10px;
box-shadow: 0 0 10px rgba(0, 255, 65, 0.3);
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
}
.login-btn:hover {
background: linear-gradient(135deg, #00ff41 0%, #00aa28 100%);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 255, 65, 0.4), 0 0 20px rgba(0, 255, 65, 0.2);
}
.login-btn:disabled {
background: #21262d;
color: #484f58;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.error-message {
background: rgba(255, 0, 65, 0.1);
border: 1px solid rgba(255, 0, 65, 0.3);
color: #ff0041;
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 20px;
text-align: center;
display: none;
font-size: 0.9rem;
box-shadow: 0 0 10px rgba(255, 0, 65, 0.2);
}
.loading {
text-align: center;
color: #7d8590;
}
.fas {
margin-right: 8px;
}
</style>
</head>
<body>
<div class="login-container">
<div class="login-header">
<h1><i class="fas fa-certificate"></i> mkcert Web UI</h1>
<p>Please log in to access the certificate management interface</p>
</div>
<div id="error-message" class="error-message"></div>
<form id="login-form" class="login-form">
<div class="form-group">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required autocomplete="username">
</div>
<div class="form-group">
<label for="password">Password:</label>
<input type="password" id="password" name="password" required autocomplete="current-password">
</div>
<button type="submit" id="login-btn" class="login-btn">
<i class="fas fa-sign-in-alt"></i> Sign In
</button>
</form>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const loginForm = document.getElementById('login-form');
const errorMessage = document.getElementById('error-message');
const loginBtn = document.getElementById('login-btn');
// Check if already authenticated
fetch('/api/auth/status')
.then(response => response.json())
.then(data => {
if (data.authenticated) {
window.location.href = '/';
}
})
.catch(error => {
console.log('Auth status check failed:', error);
});
loginForm.addEventListener('submit', async function(e) {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
// Disable form
loginBtn.disabled = true;
loginBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Signing In...';
errorMessage.style.display = 'none';
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (data.success) {
window.location.href = data.redirectTo || '/';
} else {
throw new Error(data.error || 'Login failed');
}
} catch (error) {
errorMessage.textContent = error.message;
errorMessage.style.display = 'block';
// Re-enable form
loginBtn.disabled = false;
loginBtn.innerHTML = '<i class="fas fa-sign-in-alt"></i> Sign In';
}
});
});
</script>
</body>
</html>
-403
View File
@@ -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 = '<p class="error">Failed to load certificates</p>';
}
}
// Display certificates list
function displayCertificates(certificates) {
if (!certificates || certificates.length === 0) {
certificatesList.innerHTML = '<p class="empty-state">No certificates found</p>';
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 ?
'<span class="format-badge format-' + cert.format.toLowerCase() + '">' + cert.format.toUpperCase() + '</span>' : '';
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 '<div class="certificate-card ' +
(cert.isExpired ? 'certificate-expired' : '') + ' ' +
(isRootCert ? 'root-certificate' : '') + ' ' +
(isArchived ? 'archived-certificate' : '') + '">' +
'<div class="certificate-header">' +
'<div class="certificate-name">' +
'<i class="fas fa-certificate"></i> ' + cert.name +
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 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>' +
'<div class="certificate-actions">' +
'<a href="' + API_BASE + '/download/cert/' + folderParam + '/' + cert.certFile + '" ' +
'class="btn btn-success btn-small" download>' +
'<i class="fas fa-download"></i> Download Cert</a>' +
(cert.keyFile ?
'<a href="' + API_BASE + '/download/key/' + folderParam + '/' + cert.keyFile + '" ' +
'class="btn btn-success btn-small" download>' +
'<i class="fas fa-key"></i> Download Key</a>' : '') +
'<a href="' + API_BASE + '/download/bundle/' + folderParam + '/' + cert.name + '" ' +
'class="btn btn-primary btn-small" download>' +
'<i class="fas fa-file-archive"></i> Download Bundle</a>' +
(!isRootCert && !isArchived ?
'<button onclick="archiveCertificate(\'' + folderParam + '\', \'' + cert.name + '\')" ' +
'class="btn btn-warning btn-small" title="Archive this certificate">' +
'<i class="fas fa-archive"></i> Archive</button>' :
isArchived ?
'<button onclick="restoreCertificate(\'' + folderParam + '\', \'' + cert.name + '\')" ' +
'class="btn btn-info btn-small" title="Restore certificate from archive">' +
'<i class="fas fa-undo"></i> Restore</button>' +
'<button onclick="if(confirm(\'This will permanently delete the certificate. Are you sure?\')) deleteCertificate(\'' + folderParam + '\', \'' + cert.name + '\')" ' +
'class="btn btn-danger btn-small" title="Permanently delete from archive">' +
'<i class="fas fa-trash"></i> Delete Forever</button>' :
'<span class="btn btn-disabled btn-small" title="Root certificates are read-only">' +
'<i class="fas fa-lock"></i> Protected</span>') +
'</div></div>';
}).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 = '<i class="fas fa-spinner fa-spin"></i> 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 = '<i class="fas fa-download"></i> 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 +
'<button onclick="hideAlert(' + alertId + ')" class="alert-close">&times;</button>';
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();
}
}
-771
View File
@@ -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;
}
}
-33
View File
@@ -1,33 +0,0 @@
// Simple Node.js script to test mkcertWeb certificate archive API
// Usage: node test_cert_api.js <server_url> <folder> <certName>
const fetch = require('node-fetch');
if (process.argv.length < 5) {
console.error('Usage: node test_cert_api.js <server_url> <folder> <certName>');
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();