mirror of
https://github.com/jeffcaldwellca/mkcertWeb.git
synced 2026-05-20 07:08:55 -05:00
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:
@@ -99,3 +99,8 @@ jspm_packages/
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Docker
|
||||
.dockerignore
|
||||
docker-compose.override.yml
|
||||
.docker/
|
||||
|
||||
|
||||
+33
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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"]
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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">×</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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
Reference in New Issue
Block a user