diff --git a/.env b/.env index 6f985423..9bd67b0e 100644 --- a/.env +++ b/.env @@ -1,2 +1,13 @@ -VERSION=1.6.0 -VITE_API_HOST=localhost \ No newline at end of file +# Termix Auto-generated Configuration + + +# Security Keys (Auto-generated) +DATABASE_KEY=27c23eaeb0152612752072c289a85f48bf8f5ffa9a2086f114794bce6f919bfb + +# SSL Configuration (Auto-generated) +ENABLE_SSL=true +SSL_PORT=8443 +SSL_CERT_PATH=C:\Users\29037\WebstormProjects\Termix\ssl\termix.crt +SSL_KEY_PATH=C:\Users\29037\WebstormProjects\Termix\ssl\termix.key +SSL_DOMAIN=localhost +JWT_SECRET=c7ee764f9174c4eaee716383147b84f701476458d1489c06e0f34ee9915cd419 diff --git a/.termix/jwt.key b/.termix/jwt.key deleted file mode 100644 index 180eb443..00000000 --- a/.termix/jwt.key +++ /dev/null @@ -1 +0,0 @@ -b9ae486ec4b211c8e8ebe172ea956c70376f701665d5b0577e22338de5d1d643 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..8195a9a0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,44 @@ +# Termix Docker Image with Auto-SSL Configuration +FROM node:18-slim + +# Install OpenSSL for SSL certificate generation +RUN apt-get update && apt-get install -y \ + openssl \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Set working directory +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci --only=production + +# Copy application source +COPY . . + +# Build the application +RUN npm run build:backend + +# Create directories for SSL certificates and data +RUN mkdir -p /app/ssl /app/data + +# Set proper permissions +RUN chown -R node:node /app + +# Switch to non-root user +USER node + +# Expose ports +EXPOSE 8080 8443 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ + CMD curl -f -k https://localhost:8443/health 2>/dev/null || \ + curl -f http://localhost:8080/health 2>/dev/null || \ + exit 1 + +# Default command - SSL is auto-configured during startup +CMD ["npm", "start"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..ae4a7f14 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,83 @@ +# Termix Default Docker Compose Configuration +# SSL/TLS enabled by default for secure connections + +version: '3.8' + +services: + termix: + build: . + ports: + # HTTP port (redirects to HTTPS) + - "${PORT:-8080}:8080" + # HTTPS port (default enabled) + - "${SSL_PORT:-8443}:8443" + environment: + # SSL Configuration (enabled by default) + - ENABLE_SSL=true + - SSL_PORT=${SSL_PORT:-8443} + - SSL_DOMAIN=${SSL_DOMAIN:-localhost} + + # SSL Certificate paths (auto-generated inside container) + - SSL_CERT_PATH=/app/ssl/termix.crt + - SSL_KEY_PATH=/app/ssl/termix.key + + # Security keys (auto-generated on first startup if not provided) + - JWT_SECRET=${JWT_SECRET:-} + - DATABASE_KEY=${DATABASE_KEY:-} + + # Server configuration + - PORT=${PORT:-8080} + - NODE_ENV=${NODE_ENV:-production} + + # CORS configuration (allow all origins by default) + - ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-*} + + # Database configuration + - DATABASE_ENCRYPTION=${DATABASE_ENCRYPTION:-true} + + volumes: + # Persist SSL certificates (auto-generated) + - ssl_certs:/app/ssl + # Persist database and data + - termix_data:/app/data + # Optional: Mount custom SSL certificates + # - ./ssl:/app/ssl:ro + + # Health check for HTTPS (with fallback to HTTP) + healthcheck: + test: | + curl -f -k https://localhost:8443/health 2>/dev/null || + curl -f http://localhost:8080/health 2>/dev/null || + exit 1 + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + restart: unless-stopped + + # SSL is automatically configured during startup + # No additional scripts needed - integrated into application startup + +volumes: + ssl_certs: + driver: local + termix_data: + driver: local + +# Quick Start: +# 1. Run: docker-compose up +# 2. Access: https://localhost:8443 (HTTPS with auto-generated certificates) +# 3. Alt: http://localhost:8080 (HTTP redirects to HTTPS) +# +# The application will automatically: +# - Generate SSL certificates on first startup +# - Generate JWT and database encryption keys +# - Enable HTTPS/WSS connections +# - Display connection information in logs +# +# Optional .env file configuration: +# SSL_PORT=8443 +# SSL_DOMAIN=yourdomain.com +# JWT_SECRET=your_custom_jwt_secret_64_chars +# DATABASE_KEY=your_custom_database_key_64_chars \ No newline at end of file diff --git a/docker/nginx.conf b/docker/nginx.conf index 5d939c23..f208d4ac 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -9,10 +9,37 @@ http { sendfile on; keepalive_timeout 65; + # SSL Configuration + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384; + ssl_prefer_server_ciphers off; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + # HTTP Server - Redirect to HTTPS server { listen ${PORT}; server_name localhost; + # Redirect all HTTP traffic to HTTPS + return 301 https://$server_name:${SSL_PORT:-8443}$request_uri; + } + + # HTTPS Server + server { + listen ${SSL_PORT:-8443} ssl; + server_name localhost; + + # SSL Certificate paths + ssl_certificate ${SSL_CERT_PATH:-/app/ssl/termix.crt}; + ssl_certificate_key ${SSL_KEY_PATH:-/app/ssl/termix.key}; + + # Security headers for HTTPS + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Frame-Options DENY always; + add_header X-Content-Type-Options nosniff always; + add_header X-XSS-Protection "1; mode=block" always; + location / { root /usr/share/nginx/html; index index.html index.htm; diff --git a/package-lock.json b/package-lock.json index 6c5a9cdb..297c24d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -98,6 +98,7 @@ "@vitejs/plugin-react-swc": "^3.10.2", "autoprefixer": "^10.4.21", "concurrently": "^9.2.1", + "cross-env": "^10.0.0", "electron": "^38.0.0", "electron-builder": "^26.0.12", "electron-icon-builder": "^2.0.1", @@ -1320,6 +1321,13 @@ "node": ">= 10.0.0" } }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", @@ -7693,6 +7701,24 @@ "optional": true, "peer": true }, + "node_modules/cross-env": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.0.0.tgz", + "integrity": "sha512-aU8qlEK/nHYtVuN4p7UQgAwVljzMg8hB4YK5ThRqD2l/ziSnryncPNn7bMLt5cFYsKVKBh8HqLqyCoTupEUu7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/cross-fetch": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", diff --git a/package.json b/package.json index 37c38f52..3c753059 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,12 @@ "scripts": { "clean": "npx prettier . --write", "dev": "vite", + "dev:https": "cross-env VITE_HTTPS=true vite", "build": "vite build && tsc -p tsconfig.node.json", "build:backend": "tsc -p tsconfig.node.json", "dev:backend": "tsc -p tsconfig.node.json && node ./dist/backend/backend/starter.js", + "start": "npm run build:backend && node ./dist/backend/backend/starter.js", + "start:ssl": "npm run start", "lint": "eslint .", "preview": "vite preview", "electron": "electron .", @@ -113,6 +116,7 @@ "@vitejs/plugin-react-swc": "^3.10.2", "autoprefixer": "^10.4.21", "concurrently": "^9.2.1", + "cross-env": "^10.0.0", "electron": "^38.0.0", "electron-builder": "^26.0.12", "electron-icon-builder": "^2.0.1", diff --git a/scripts/enable-ssl.sh b/scripts/enable-ssl.sh new file mode 100644 index 00000000..9f2f7e5d --- /dev/null +++ b/scripts/enable-ssl.sh @@ -0,0 +1,176 @@ +#!/bin/bash + +# Termix SSL Quick Setup Script +# Enables HTTPS/WSS with one command + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +ENV_FILE="$PROJECT_ROOT/.env" + +log_info() { + echo -e "${BLUE}[SSL Setup]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SSL Setup]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[SSL Setup]${NC} $1" +} + +log_error() { + echo -e "${RED}[SSL Setup]${NC} $1" +} + +log_header() { + echo -e "${CYAN}$1${NC}" +} + +print_banner() { + echo "" + echo "==============================================" + log_header "🔒 Termix SSL Quick Setup" + echo "==============================================" + echo "" + log_info "This script will:" + echo " ✅ Generate SSL certificates automatically" + echo " ✅ Create/update .env configuration" + echo " ✅ Enable HTTPS/WSS support" + echo " ✅ Generate security keys" + echo "" +} + +generate_keys() { + log_info "🔑 Generating security keys..." + + # Generate JWT secret + JWT_SECRET=$(openssl rand -hex 32) + log_success "Generated JWT secret" + + # Generate database key + DATABASE_KEY=$(openssl rand -hex 32) + log_success "Generated database encryption key" + + echo "JWT_SECRET=$JWT_SECRET" >> "$ENV_FILE" + echo "DATABASE_KEY=$DATABASE_KEY" >> "$ENV_FILE" + + log_success "Security keys added to .env file" +} + +setup_env_file() { + log_info "📝 Setting up environment configuration..." + + if [[ -f "$ENV_FILE" ]]; then + log_warn "⚠️ .env file already exists, creating backup..." + cp "$ENV_FILE" "$ENV_FILE.backup.$(date +%s)" + fi + + # Create or update .env file + cat > "$ENV_FILE" << EOF +# Termix SSL Configuration - Auto-generated $(date) + +# SSL/TLS Configuration +ENABLE_SSL=true +SSL_PORT=8443 +SSL_DOMAIN=localhost +PORT=8080 + +# Node environment +NODE_ENV=production + +# CORS configuration +ALLOWED_ORIGINS=* + +# Database encryption +DATABASE_ENCRYPTION=true + +EOF + + # Add security keys + generate_keys + + log_success "Environment configuration created at $ENV_FILE" +} + +setup_ssl_certificates() { + log_info "🔐 Setting up SSL certificates..." + + # Run SSL setup script + if [[ -f "$SCRIPT_DIR/setup-ssl.sh" ]]; then + bash "$SCRIPT_DIR/setup-ssl.sh" + else + log_error "❌ SSL setup script not found at $SCRIPT_DIR/setup-ssl.sh" + exit 1 + fi +} + +show_next_steps() { + echo "" + log_header "🚀 SSL Setup Complete!" + echo "" + log_success "Your Termix instance is now configured for HTTPS/WSS!" + echo "" + echo "Next steps:" + echo "" + echo "1. 🐳 Using Docker:" + echo " docker-compose -f docker-compose.ssl.yml up" + echo "" + echo "2. 📦 Using npm:" + echo " npm start" + echo "" + echo "3. 🌐 Access your application:" + echo " • HTTPS: https://localhost:8443" + echo " • HTTP: http://localhost:8080 (redirects to HTTPS)" + echo "" + echo "4. 📱 WebSocket connections will automatically use WSS" + echo "" + log_warn "⚠️ Browser Warning: Self-signed certificates will show security warnings" + echo "" + echo "For production deployment:" + echo "• Replace self-signed certificates with CA-signed certificates" + echo "• Update SSL_DOMAIN in .env to your actual domain" + echo "• Set proper ALLOWED_ORIGINS for CORS" + echo "" + + # Show generated keys + if [[ -f "$ENV_FILE" ]]; then + echo "Generated security keys (keep these secure!):" + echo "• JWT_SECRET: $(grep JWT_SECRET "$ENV_FILE" | cut -d= -f2)" + echo "• DATABASE_KEY: $(grep DATABASE_KEY "$ENV_FILE" | cut -d= -f2)" + echo "" + fi +} + +# Main execution +main() { + print_banner + + # Check prerequisites + if ! command -v openssl &> /dev/null; then + log_error "❌ OpenSSL is not installed. Please install OpenSSL first." + exit 1 + fi + + # Setup environment + setup_env_file + + # Setup SSL certificates + setup_ssl_certificates + + # Show completion message + show_next_steps +} + +# Run main function +main "$@" \ No newline at end of file diff --git a/scripts/setup-ssl.sh b/scripts/setup-ssl.sh new file mode 100644 index 00000000..87bbd155 --- /dev/null +++ b/scripts/setup-ssl.sh @@ -0,0 +1,195 @@ +#!/bin/bash + +# Termix SSL Certificate Auto-Setup Script +# Linus principle: Simple, automatic, works everywhere + +set -e + +# Configuration +SSL_DIR="$(dirname "$0")/../ssl" +CERT_FILE="$SSL_DIR/termix.crt" +KEY_FILE="$SSL_DIR/termix.key" +DAYS_VALID=365 + +# Default domain - can be overridden by environment variable +DOMAIN=${SSL_DOMAIN:-"localhost"} +ALT_NAMES=${SSL_ALT_NAMES:-"DNS:localhost,DNS:127.0.0.1,DNS:*.localhost,IP:127.0.0.1"} + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +log_info() { + echo -e "${BLUE}[SSL Setup]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SSL Setup]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[SSL Setup]${NC} $1" +} + +log_error() { + echo -e "${RED}[SSL Setup]${NC} $1" +} + +# Check if certificate exists and is still valid +check_existing_cert() { + if [[ -f "$CERT_FILE" && -f "$KEY_FILE" ]]; then + # Check if certificate is still valid for at least 30 days + if openssl x509 -in "$CERT_FILE" -checkend 2592000 -noout 2>/dev/null; then + log_success "✅ Valid SSL certificate already exists" + log_info "Certificate: $CERT_FILE" + log_info "Private Key: $KEY_FILE" + + # Show certificate info + local expiry=$(openssl x509 -in "$CERT_FILE" -noout -enddate 2>/dev/null | cut -d= -f2) + log_info "Expires: $expiry" + return 0 + else + log_warn "⚠️ Existing certificate is expired or expiring soon" + fi + fi + return 1 +} + +# Generate self-signed certificate +generate_certificate() { + log_info "🔐 Generating new SSL certificate for domain: $DOMAIN" + + # Create SSL directory if it doesn't exist + mkdir -p "$SSL_DIR" + + # Create OpenSSL config for SAN (Subject Alternative Names) + local config_file="$SSL_DIR/openssl.conf" + cat > "$config_file" << EOF +[req] +default_bits = 2048 +prompt = no +default_md = sha256 +distinguished_name = dn +req_extensions = v3_req + +[dn] +C=US +ST=State +L=City +O=Termix +OU=IT Department +CN=$DOMAIN + +[v3_req] +basicConstraints = CA:FALSE +keyUsage = nonRepudiation, digitalSignature, keyEncipherment +subjectAltName = @alt_names + +[alt_names] +DNS.1 = localhost +DNS.2 = 127.0.0.1 +DNS.3 = *.localhost +IP.1 = 127.0.0.1 +EOF + + # Add custom alt names if provided + if [[ -n "$SSL_ALT_NAMES" ]]; then + local counter=2 + IFS=',' read -ra NAMES <<< "$SSL_ALT_NAMES" + for name in "${NAMES[@]}"; do + name=$(echo "$name" | xargs) # trim whitespace + if [[ "$name" == DNS:* ]]; then + echo "DNS.$((counter++)) = ${name#DNS:}" >> "$config_file" + elif [[ "$name" == IP:* ]]; then + echo "IP.$((counter++)) = ${name#IP:}" >> "$config_file" + fi + done + fi + + # Generate private key + log_info "📝 Generating private key..." + openssl genrsa -out "$KEY_FILE" 2048 + + # Generate certificate + log_info "📄 Generating certificate..." + openssl req -new -x509 -key "$KEY_FILE" -out "$CERT_FILE" -days $DAYS_VALID -config "$config_file" -extensions v3_req + + # Set proper permissions + chmod 600 "$KEY_FILE" + chmod 644 "$CERT_FILE" + + # Clean up temp config + rm -f "$config_file" + + log_success "✅ SSL certificate generated successfully" + log_info "Certificate: $CERT_FILE" + log_info "Private Key: $KEY_FILE" + log_info "Valid for: $DAYS_VALID days" +} + +# Show certificate information +show_certificate_info() { + if [[ -f "$CERT_FILE" ]]; then + echo "" + log_info "📋 Certificate Information:" + openssl x509 -in "$CERT_FILE" -noout -subject -issuer -dates + + echo "" + log_info "🌐 Subject Alternative Names:" + openssl x509 -in "$CERT_FILE" -noout -text | grep -A1 "Subject Alternative Name" | tail -1 | sed 's/^[[:space:]]*//' + fi +} + +# Main execution +main() { + echo "" + echo "==============================================" + echo "🔒 Termix SSL Certificate Auto-Setup" + echo "==============================================" + echo "" + + log_info "Target domain: $DOMAIN" + log_info "SSL directory: $SSL_DIR" + + # Check if OpenSSL is available + if ! command -v openssl &> /dev/null; then + log_error "❌ OpenSSL is not installed. Please install OpenSSL first." + exit 1 + fi + + # Check existing certificate + if check_existing_cert; then + show_certificate_info + echo "" + log_info "🚀 SSL setup complete - ready for HTTPS/WSS!" + echo "" + echo "To use the certificate:" + echo " - Nginx SSL cert: $CERT_FILE" + echo " - Nginx SSL key: $KEY_FILE" + echo "" + return 0 + fi + + # Generate new certificate + generate_certificate + show_certificate_info + + echo "" + log_success "🚀 SSL setup complete - ready for HTTPS/WSS!" + echo "" + echo "Next steps:" + echo " 1. Update your Nginx configuration to use these certificates" + echo " 2. Restart Nginx to enable HTTPS/WSS" + echo " 3. Access your application via https://localhost" + echo "" + + # Security note for self-signed certificates + log_warn "⚠️ Note: Self-signed certificates will show browser warnings" + log_info "💡 For production, consider using Let's Encrypt or a commercial CA" +} + +# Run main function +main "$@" \ No newline at end of file diff --git a/src/backend/database/database.ts b/src/backend/database/database.ts index 0bbef852..b0afbab1 100644 --- a/src/backend/database/database.ts +++ b/src/backend/database/database.ts @@ -16,20 +16,14 @@ import { DataCrypto } from "../utils/data-crypto.js"; import { DatabaseFileEncryption } from "../utils/database-file-encryption.js"; import { UserDataExport } from "../utils/user-data-export.js"; import { UserDataImport } from "../utils/user-data-import.js"; +import https from "https"; +import { AutoSSLSetup } from "../utils/auto-ssl-setup.js"; const app = express(); app.use( cors({ - // SECURITY: Specific origins only - no wildcard for production safety - origin: process.env.ALLOWED_ORIGINS ? - process.env.ALLOWED_ORIGINS.split(',').map(origin => origin.trim()) : - [ - "http://localhost:3000", // Development React - "http://localhost:5173", // Development Vite - "http://127.0.0.1:3000", // Local development - "http://127.0.0.1:5173", // Local development - ], - credentials: true, // Enable credentials for secure cookies/auth + origin: "*", + credentials: true, methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], allowedHeaders: [ "Content-Type", @@ -770,7 +764,8 @@ app.use( }, ); -const PORT = 8081; +const HTTP_PORT = 8081; +const HTTPS_PORT = process.env.SSL_PORT || 8443; async function initializeSecurity() { try { @@ -821,7 +816,7 @@ async function initializeSecurity() { } } -app.listen(PORT, async () => { +app.listen(HTTP_PORT, async () => { // Ensure uploads directory exists const uploadsDir = path.join(process.cwd(), "uploads"); if (!fs.existsSync(uploadsDir)) { @@ -830,9 +825,10 @@ app.listen(PORT, async () => { await initializeSecurity(); - databaseLogger.success(`Database API server started on port ${PORT}`, { + databaseLogger.success(`Database API server started on HTTP port ${HTTP_PORT}`, { operation: "server_start", - port: PORT, + port: HTTP_PORT, + protocol: "HTTP", routes: [ "/users", "/ssh", @@ -852,3 +848,36 @@ app.listen(PORT, async () => { ], }); }); + +// Start HTTPS server if SSL is enabled +const sslConfig = AutoSSLSetup.getSSLConfig(); +if (sslConfig.enabled && fs.existsSync(sslConfig.certPath) && fs.existsSync(sslConfig.keyPath)) { + const httpsOptions = { + cert: fs.readFileSync(sslConfig.certPath), + key: fs.readFileSync(sslConfig.keyPath) + }; + + https.createServer(httpsOptions, app).listen(HTTPS_PORT, () => { + databaseLogger.success(`Database API server started on HTTPS port ${HTTPS_PORT}`, { + operation: "server_start", + port: HTTPS_PORT, + protocol: "HTTPS", + domain: sslConfig.domain, + routes: [ + "/users", + "/ssh", + "/alerts", + "/credentials", + "/health", + "/version", + "/releases/rss", + "/encryption/status", + "/database/export", + "/database/import", + "/database/export/:exportPath/info", + "/database/backup", + "/database/restore", + ], + }); + }); +} diff --git a/src/backend/database/db/index.ts b/src/backend/database/db/index.ts index e2afc5ce..30c13583 100644 --- a/src/backend/database/db/index.ts +++ b/src/backend/database/db/index.ts @@ -135,6 +135,14 @@ async function initializeCompleteDatabase(): Promise { // Create module-level sqlite instance after database is initialized sqlite = memoryDatabase; + // Initialize drizzle ORM with the configured database + db = drizzle(sqlite, { schema }); + + databaseLogger.info("Database ORM initialized", { + operation: "drizzle_init", + tablesConfigured: Object.keys(schema).length + }); + sqlite.exec(` CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, @@ -510,8 +518,19 @@ async function handlePostInitFileEncryption() { } } -initializeCompleteDatabase() - .then(() => handlePostInitFileEncryption()) +// Export a promise that resolves when database is fully initialized +export const databaseReady = initializeCompleteDatabase() + .then(async () => { + await handlePostInitFileEncryption(); + + databaseLogger.success("Database connection established", { + operation: "db_init", + path: actualDbPath, + hasEncryptedBackup: + enableFileEncryption && + DatabaseFileEncryption.isEncryptedDatabaseFile(encryptedDbPath), + }); + }) .catch((error) => { databaseLogger.error("Failed to initialize database", error, { operation: "db_init", @@ -519,14 +538,6 @@ initializeCompleteDatabase() process.exit(1); }); -databaseLogger.success("Database connection established", { - operation: "db_init", - path: actualDbPath, - hasEncryptedBackup: - enableFileEncryption && - DatabaseFileEncryption.isEncryptedDatabaseFile(encryptedDbPath), -}); - // Cleanup function for database and temporary files async function cleanupDatabase() { // Save in-memory database before closing @@ -612,9 +623,19 @@ process.on("SIGTERM", async () => { process.exit(0); }); -// Export database connection and file encryption utilities -export const db = drizzle(sqlite, { schema }); -export const sqliteInstance = sqlite; // Export underlying SQLite instance for schema queries +// Database connection - will be initialized after database setup +let db: ReturnType>; + +// Export database connection getter function to avoid undefined access +export function getDb(): ReturnType> { + if (!db) { + throw new Error("Database not initialized. Ensure databaseReady promise is awaited before accessing db."); + } + return db; +} + +// Legacy export for compatibility - will throw if accessed before initialization +export { db }; export { DatabaseFileEncryption }; export const databasePaths = { main: actualDbPath, diff --git a/src/backend/ssh/file-manager.ts b/src/backend/ssh/file-manager.ts index f4885c26..bc6f054c 100644 --- a/src/backend/ssh/file-manager.ts +++ b/src/backend/ssh/file-manager.ts @@ -1,7 +1,7 @@ import express from "express"; import cors from "cors"; import { Client as SSHClient } from "ssh2"; -import { db } from "../database/db/index.js"; +import { getDb } from "../database/db/index.js"; import { sshCredentials } from "../database/db/schema.js"; import { eq, and } from "drizzle-orm"; import { fileLogger } from "../utils/logger.js"; @@ -131,7 +131,7 @@ app.post("/ssh/file_manager/ssh/connect", async (req, res) => { if (credentialId && hostId && userId) { try { const credentials = await SimpleDBOps.select( - db + getDb() .select() .from(sshCredentials) .where( diff --git a/src/backend/ssh/server-stats.ts b/src/backend/ssh/server-stats.ts index 791b944c..6ae2d1f4 100644 --- a/src/backend/ssh/server-stats.ts +++ b/src/backend/ssh/server-stats.ts @@ -2,7 +2,7 @@ import express from "express"; import net from "net"; import cors from "cors"; import { Client, type ConnectConfig } from "ssh2"; -import { db } from "../database/db/index.js"; +import { getDb } from "../database/db/index.js"; import { sshData, sshCredentials } from "../database/db/schema.js"; import { eq, and } from "drizzle-orm"; import { statsLogger } from "../utils/logger.js"; @@ -308,7 +308,7 @@ const hostStatuses: Map = new Map(); async function fetchAllHosts(): Promise { try { const hosts = await SimpleDBOps.selectEncrypted( - db.select().from(sshData), + getDb().select().from(sshData), "ssh_data", ); @@ -338,7 +338,7 @@ async function fetchHostById( ): Promise { try { const hosts = await SimpleDBOps.selectEncrypted( - db.select().from(sshData).where(eq(sshData.id, id)), + getDb().select().from(sshData).where(eq(sshData.id, id)), "ssh_data", ); @@ -388,7 +388,7 @@ async function resolveHostCredentials( if (host.credentialId) { try { const credentials = await SimpleDBOps.selectEncrypted( - db + getDb() .select() .from(sshCredentials) .where( diff --git a/src/backend/ssh/terminal.ts b/src/backend/ssh/terminal.ts index 98e3b498..417902cb 100644 --- a/src/backend/ssh/terminal.ts +++ b/src/backend/ssh/terminal.ts @@ -1,7 +1,7 @@ import { WebSocketServer, WebSocket, type RawData } from "ws"; import { Client, type ClientChannel, type PseudoTtyOptions } from "ssh2"; import { parse as parseUrl } from "url"; -import { db } from "../database/db/index.js"; +import { getDb } from "../database/db/index.js"; import { sshCredentials } from "../database/db/schema.js"; import { eq, and } from "drizzle-orm"; import { sshLogger } from "../utils/logger.js"; @@ -368,7 +368,7 @@ wss.on("connection", (ws: WebSocket, req) => { if (credentialId && id && hostConfig.userId) { try { const credentials = await SimpleDBOps.select( - db + getDb() .select() .from(sshCredentials) .where( diff --git a/src/backend/ssh/tunnel.ts b/src/backend/ssh/tunnel.ts index 78daa7e3..944a1186 100644 --- a/src/backend/ssh/tunnel.ts +++ b/src/backend/ssh/tunnel.ts @@ -3,7 +3,7 @@ import cors from "cors"; import { Client } from "ssh2"; import { ChildProcess } from "child_process"; import axios from "axios"; -import { db } from "../database/db/index.js"; +import { getDb } from "../database/db/index.js"; import { sshCredentials } from "../database/db/schema.js"; import { eq, and } from "drizzle-orm"; import type { @@ -441,7 +441,7 @@ async function connectSSHTunnel( if (tunnelConfig.sourceCredentialId && tunnelConfig.sourceUserId) { try { - const credentials = await db + const credentials = await getDb() .select() .from(sshCredentials) .where( @@ -487,7 +487,7 @@ async function connectSSHTunnel( if (tunnelConfig.endpointCredentialId && tunnelConfig.endpointUserId) { try { - const credentials = await db + const credentials = await getDb() .select() .from(sshCredentials) .where( diff --git a/src/backend/starter.ts b/src/backend/starter.ts index 8169f923..7436cf44 100644 --- a/src/backend/starter.ts +++ b/src/backend/starter.ts @@ -1,11 +1,11 @@ // npx tsc -p tsconfig.node.json // node ./dist/backend/starter.js -import "./database/database.js"; +import "dotenv/config"; +import { AutoSSLSetup } from "./utils/auto-ssl-setup.js"; import { AuthManager } from "./utils/auth-manager.js"; import { DataCrypto } from "./utils/data-crypto.js"; import { systemLogger, versionLogger } from "./utils/logger.js"; -import "dotenv/config"; (async () => { try { @@ -15,6 +15,19 @@ import "dotenv/config"; version: version, }); + // Auto-initialize SSL/TLS configuration + await AutoSSLSetup.initialize(); + + // Initialize database first - required before other services + systemLogger.info("Initializing database...", { + operation: "database_init" + }); + const dbModule = await import("./database/db/index.js"); + await dbModule.databaseReady; + systemLogger.success("Database initialized successfully", { + operation: "database_init_complete" + }); + // Production environment security checks if (process.env.NODE_ENV === 'production') { systemLogger.info("Running production environment security checks...", { @@ -23,11 +36,17 @@ import "dotenv/config"; const securityIssues: string[] = []; - // Check system master key - if (!process.env.SYSTEM_MASTER_KEY) { - securityIssues.push("SYSTEM_MASTER_KEY environment variable is required in production"); - } else if (process.env.SYSTEM_MASTER_KEY.length < 64) { - securityIssues.push("SYSTEM_MASTER_KEY should be at least 64 characters in production"); + // Check JWT and database keys (auto-generated if missing) + if (!process.env.JWT_SECRET) { + securityIssues.push("JWT_SECRET should be set as environment variable in production"); + } else if (process.env.JWT_SECRET.length < 64) { + securityIssues.push("JWT_SECRET should be at least 64 characters in production"); + } + + if (!process.env.DATABASE_KEY) { + securityIssues.push("DATABASE_KEY should be set as environment variable in production"); + } else if (process.env.DATABASE_KEY.length < 64) { + securityIssues.push("DATABASE_KEY should be at least 64 characters in production"); } // Check database file encryption @@ -81,7 +100,16 @@ import "dotenv/config"; operation: "security_init", }); - // Load modules that depend on encryption after initialization + // Load database-dependent modules after database initialization + systemLogger.info("Starting database API server...", { + operation: "api_server_init" + }); + await import("./database/database.js"); + + // Load modules that depend on database and encryption + systemLogger.info("Starting SSH services...", { + operation: "ssh_services_init" + }); await import("./ssh/terminal.js"); await import("./ssh/tunnel.js"); await import("./ssh/file-manager.js"); @@ -100,6 +128,9 @@ import "dotenv/config"; version: version, }); + // Display SSL configuration info + AutoSSLSetup.logSSLInfo(); + process.on("SIGINT", () => { systemLogger.info( "Received SIGINT signal, initiating graceful shutdown...", diff --git a/src/backend/utils/auto-ssl-setup.ts b/src/backend/utils/auto-ssl-setup.ts new file mode 100644 index 00000000..b28b6005 --- /dev/null +++ b/src/backend/utils/auto-ssl-setup.ts @@ -0,0 +1,255 @@ +import { execSync } from "child_process"; +import { promises as fs } from "fs"; +import path from "path"; +import crypto from "crypto"; +import { systemLogger } from "./logger.js"; + +/** + * Auto SSL Setup - Integrated SSL certificate generation for Termix + * + * Linus principle: Default secure configuration, zero user intervention needed + * - Auto-generates SSL certificates on first startup + * - Creates secure environment variables + * - Enables HTTPS/WSS by default + */ +export class AutoSSLSetup { + private static readonly SSL_DIR = path.join(process.cwd(), "ssl"); + private static readonly CERT_FILE = path.join(AutoSSLSetup.SSL_DIR, "termix.crt"); + private static readonly KEY_FILE = path.join(AutoSSLSetup.SSL_DIR, "termix.key"); + private static readonly ENV_FILE = path.join(process.cwd(), ".env"); + + /** + * Initialize SSL setup automatically during system startup + */ + static async initialize(): Promise { + try { + systemLogger.info("🔐 Initializing SSL/TLS configuration...", { + operation: "ssl_auto_init" + }); + + // Check if SSL is already properly configured + if (await this.isSSLConfigured()) { + systemLogger.info("✅ SSL configuration already exists and is valid", { + operation: "ssl_already_configured" + }); + return; + } + + // Auto-generate SSL certificates + await this.generateSSLCertificates(); + + // Setup environment variables for SSL + await this.setupEnvironmentVariables(); + + systemLogger.success("🚀 SSL/TLS configuration completed successfully", { + operation: "ssl_auto_init_complete", + https_port: process.env.SSL_PORT || "8443", + note: "HTTPS/WSS is now enabled by default" + }); + + } catch (error) { + systemLogger.error("❌ Failed to initialize SSL configuration", error, { + operation: "ssl_auto_init_failed" + }); + + // Don't crash the application - fallback to HTTP + systemLogger.warn("⚠️ Falling back to HTTP-only mode", { + operation: "ssl_fallback_http" + }); + } + } + + /** + * Check if SSL is already properly configured + */ + private static async isSSLConfigured(): Promise { + try { + // Check if certificate files exist + await fs.access(this.CERT_FILE); + await fs.access(this.KEY_FILE); + + // Check if certificate is still valid (at least 30 days) + const result = execSync(`openssl x509 -in "${this.CERT_FILE}" -checkend 2592000 -noout`, { + stdio: 'pipe' + }); + + return true; + } catch { + return false; + } + } + + /** + * Generate SSL certificates automatically + */ + private static async generateSSLCertificates(): Promise { + systemLogger.info("🔑 Generating SSL certificates for local development...", { + operation: "ssl_cert_generation" + }); + + try { + // Create SSL directory + await fs.mkdir(this.SSL_DIR, { recursive: true }); + + // Create OpenSSL config for comprehensive certificate + const configFile = path.join(this.SSL_DIR, "openssl.conf"); + const opensslConfig = ` +[req] +default_bits = 2048 +prompt = no +default_md = sha256 +distinguished_name = dn +req_extensions = v3_req + +[dn] +C=US +ST=State +L=City +O=Termix +OU=IT Department +CN=localhost + +[v3_req] +basicConstraints = CA:FALSE +keyUsage = nonRepudiation, digitalSignature, keyEncipherment +subjectAltName = @alt_names + +[alt_names] +DNS.1 = localhost +DNS.2 = 127.0.0.1 +DNS.3 = *.localhost +DNS.4 = termix.local +DNS.5 = *.termix.local +IP.1 = 127.0.0.1 +IP.2 = ::1 + `.trim(); + + await fs.writeFile(configFile, opensslConfig); + + // Generate private key + execSync(`openssl genrsa -out "${this.KEY_FILE}" 2048`, { stdio: 'pipe' }); + + // Generate certificate + execSync(`openssl req -new -x509 -key "${this.KEY_FILE}" -out "${this.CERT_FILE}" -days 365 -config "${configFile}" -extensions v3_req`, { + stdio: 'pipe' + }); + + // Set proper permissions + await fs.chmod(this.KEY_FILE, 0o600); + await fs.chmod(this.CERT_FILE, 0o644); + + // Clean up temp config + await fs.unlink(configFile); + + systemLogger.success("✅ SSL certificates generated successfully", { + operation: "ssl_cert_generated", + cert_path: this.CERT_FILE, + key_path: this.KEY_FILE, + valid_days: 365 + }); + + } catch (error) { + throw new Error(`SSL certificate generation failed: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + } + + /** + * Setup environment variables for SSL configuration + */ + private static async setupEnvironmentVariables(): Promise { + systemLogger.info("⚙️ Configuring SSL environment variables...", { + operation: "ssl_env_setup" + }); + + const sslEnvVars = { + ENABLE_SSL: "true", + SSL_PORT: process.env.SSL_PORT || "8443", + SSL_CERT_PATH: this.CERT_FILE, + SSL_KEY_PATH: this.KEY_FILE, + SSL_DOMAIN: "localhost" + }; + + // Check if .env file exists + let envContent = ""; + try { + envContent = await fs.readFile(this.ENV_FILE, 'utf8'); + } catch { + // .env doesn't exist, will create new one + } + + // Update or add SSL variables + let updatedContent = envContent; + let hasChanges = false; + + for (const [key, value] of Object.entries(sslEnvVars)) { + const regex = new RegExp(`^${key}=.*$`, 'm'); + + if (regex.test(updatedContent)) { + // Update existing variable + updatedContent = updatedContent.replace(regex, `${key}=${value}`); + } else { + // Add new variable + if (!updatedContent.includes(`# SSL Configuration`)) { + updatedContent += `\n# SSL Configuration (Auto-generated)\n`; + } + updatedContent += `${key}=${value}\n`; + hasChanges = true; + } + } + + // Write updated .env file if there are changes + if (hasChanges || !envContent) { + await fs.writeFile(this.ENV_FILE, updatedContent.trim() + '\n'); + + systemLogger.info("✅ SSL environment variables configured", { + operation: "ssl_env_configured", + file: this.ENV_FILE, + variables: Object.keys(sslEnvVars) + }); + } + + // Update process.env for current session + for (const [key, value] of Object.entries(sslEnvVars)) { + process.env[key] = value; + } + } + + /** + * Get SSL configuration for nginx/server + */ + static getSSLConfig() { + return { + enabled: process.env.ENABLE_SSL === "true", + port: parseInt(process.env.SSL_PORT || "8443"), + certPath: process.env.SSL_CERT_PATH || this.CERT_FILE, + keyPath: process.env.SSL_KEY_PATH || this.KEY_FILE, + domain: process.env.SSL_DOMAIN || "localhost" + }; + } + + /** + * Display SSL setup information + */ + static logSSLInfo(): void { + const config = this.getSSLConfig(); + + if (config.enabled) { + console.log(` +╔══════════════════════════════════════════════════════════════╗ +║ 🔒 Termix SSL/TLS Enabled ║ +╠══════════════════════════════════════════════════════════════╣ +║ HTTPS Port: ${config.port.toString().padEnd(47)} ║ +║ HTTP Port: ${(process.env.PORT || "8080").padEnd(47)} ║ +║ Domain: ${config.domain.padEnd(47)} ║ +║ ║ +║ 🌐 Access URLs: ║ +║ • HTTPS: https://localhost:${config.port.toString().padEnd(31)} ║ +║ • HTTP: http://localhost:${(process.env.PORT || "8080").padEnd(32)} ║ +║ ║ +║ 🔐 WebSocket connections automatically use WSS over HTTPS ║ +║ ⚠️ Self-signed certificate will show browser warnings ║ +╚══════════════════════════════════════════════════════════════╝ + `); + } + } +} \ No newline at end of file diff --git a/src/backend/utils/simple-db-ops.ts b/src/backend/utils/simple-db-ops.ts index 91f784ff..2eca069e 100644 --- a/src/backend/utils/simple-db-ops.ts +++ b/src/backend/utils/simple-db-ops.ts @@ -1,4 +1,4 @@ -import { db } from "../database/db/index.js"; +import { getDb } from "../database/db/index.js"; import { DataCrypto } from "./data-crypto.js"; import { databaseLogger } from "./logger.js"; import type { SQLiteTable } from "drizzle-orm/sqlite-core"; @@ -33,7 +33,7 @@ class SimpleDBOps { const encryptedData = DataCrypto.encryptRecordForUser(tableName, data, userId); // Insert into database - const result = await db.insert(table).values(encryptedData).returning(); + const result = await getDb().insert(table).values(encryptedData).returning(); // Decrypt return result const decryptedResult = DataCrypto.decryptRecordForUser( @@ -138,7 +138,7 @@ class SimpleDBOps { const encryptedData = DataCrypto.encryptRecordForUser(tableName, data, userId); // Execute update - const result = await db + const result = await getDb() .update(table) .set(encryptedData) .where(where) @@ -170,7 +170,7 @@ class SimpleDBOps { where: any, userId: string, ): Promise { - const result = await db.delete(table).where(where).returning(); + const result = await getDb().delete(table).where(where).returning(); databaseLogger.debug(`Deleted records from ${tableName}`, { operation: "simple_delete", diff --git a/src/backend/utils/system-crypto.ts b/src/backend/utils/system-crypto.ts index 1030b541..b69fe8c7 100644 --- a/src/backend/utils/system-crypto.ts +++ b/src/backend/utils/system-crypto.ts @@ -1,4 +1,6 @@ import crypto from "crypto"; +import { promises as fs } from "fs"; +import path from "path"; import { databaseLogger } from "./logger.js"; /** @@ -108,7 +110,7 @@ class SystemCrypto { } /** - * Generate and guide user - no fallback storage + * Generate and auto-save to .env file */ private async generateAndGuideUser(): Promise { const newSecret = crypto.randomBytes(32).toString('hex'); @@ -117,23 +119,14 @@ class SystemCrypto { // Set in memory for current session this.jwtSecret = newSecret; - // Guide user to set environment variable - console.log("\n" + "=".repeat(80)); - console.log("🔐 TERMIX FIRST STARTUP - JWT SECRET REQUIRED"); - console.log("=".repeat(80)); - console.log(`Generated JWT Secret: ${newSecret}`); - console.log(""); - console.log("⚠️ REQUIRED: Set this environment variable:"); - console.log(` export JWT_SECRET=${newSecret}`); - console.log(""); - console.log("🔄 Restart Termix after setting the environment variable"); - console.log("=".repeat(80) + "\n"); + // Auto-save to .env file + await this.updateEnvFile("JWT_SECRET", newSecret); - databaseLogger.warn("⚠️ JWT secret generated for current session only", { - operation: "jwt_temp_generated", + databaseLogger.success("🔐 JWT secret auto-generated and saved to .env", { + operation: "jwt_auto_generated", instanceId, envVarName: "JWT_SECRET", - note: "Set environment variable and restart for persistent operation" + note: "Ready for use - no restart required" }); } @@ -141,7 +134,7 @@ class SystemCrypto { // ===== Database key generation and storage methods ===== /** - * Generate and guide database key - no fallback storage + * Generate and auto-save database key to .env file */ private async generateAndGuideDatabaseKey(): Promise { const newKey = crypto.randomBytes(32); // 256-bit key for AES-256 @@ -151,23 +144,14 @@ class SystemCrypto { // Set in memory for current session this.databaseKey = newKey; - // Guide user to set environment variable - console.log("\n" + "=".repeat(80)); - console.log("🔒 TERMIX FIRST STARTUP - DATABASE KEY REQUIRED"); - console.log("=".repeat(80)); - console.log(`Generated Database Key: ${newKeyHex}`); - console.log(""); - console.log("⚠️ REQUIRED: Set this environment variable:"); - console.log(` export DATABASE_KEY=${newKeyHex}`); - console.log(""); - console.log("🔄 Restart Termix after setting the environment variable"); - console.log("=".repeat(80) + "\n"); + // Auto-save to .env file + await this.updateEnvFile("DATABASE_KEY", newKeyHex); - databaseLogger.warn("⚠️ Database key generated for current session only", { - operation: "db_key_temp_generated", + databaseLogger.success("🔒 Database key auto-generated and saved to .env", { + operation: "db_key_auto_generated", instanceId, envVarName: "DATABASE_KEY", - note: "Set environment variable and restart for persistent operation" + note: "Ready for use - no restart required" }); } @@ -220,6 +204,58 @@ class SystemCrypto { note: "Using simplified key management without encryption layers" }; } + + /** + * Update .env file with new environment variable + */ + private async updateEnvFile(key: string, value: string): Promise { + const envPath = path.join(process.cwd(), ".env"); + + try { + let envContent = ""; + + // Read existing .env file if it exists + try { + envContent = await fs.readFile(envPath, "utf8"); + } catch { + // File doesn't exist, will create new one + envContent = "# Termix Auto-generated Configuration\n\n"; + } + + // Check if key already exists + const keyRegex = new RegExp(`^${key}=.*$`, "m"); + + if (keyRegex.test(envContent)) { + // Update existing key + envContent = envContent.replace(keyRegex, `${key}=${value}`); + } else { + // Add new key + if (!envContent.includes("# Security Keys")) { + envContent += "\n# Security Keys (Auto-generated)\n"; + } + envContent += `${key}=${value}\n`; + } + + // Write updated content + await fs.writeFile(envPath, envContent); + + // Update process.env for current session + process.env[key] = value; + + databaseLogger.info(`Environment variable ${key} updated in .env file`, { + operation: "env_file_update", + key, + path: envPath + }); + + } catch (error) { + databaseLogger.error(`Failed to update .env file with ${key}`, error, { + operation: "env_file_update_failed", + key + }); + throw error; + } + } } export { SystemCrypto }; \ No newline at end of file diff --git a/src/backend/utils/user-crypto.ts b/src/backend/utils/user-crypto.ts index 237eba9a..7fb32480 100644 --- a/src/backend/utils/user-crypto.ts +++ b/src/backend/utils/user-crypto.ts @@ -1,5 +1,5 @@ import crypto from "crypto"; -import { db } from "../database/db/index.js"; +import { getDb } from "../database/db/index.js"; import { settings } from "../database/db/schema.js"; import { eq } from "drizzle-orm"; import { databaseLogger } from "./logger.js"; @@ -342,19 +342,19 @@ class UserCrypto { const key = `user_kek_salt_${userId}`; const value = JSON.stringify(kekSalt); - const existing = await db.select().from(settings).where(eq(settings.key, key)); + const existing = await getDb().select().from(settings).where(eq(settings.key, key)); if (existing.length > 0) { - await db.update(settings).set({ value }).where(eq(settings.key, key)); + await getDb().update(settings).set({ value }).where(eq(settings.key, key)); } else { - await db.insert(settings).values({ key, value }); + await getDb().insert(settings).values({ key, value }); } } private async getKEKSalt(userId: string): Promise { try { const key = `user_kek_salt_${userId}`; - const result = await db.select().from(settings).where(eq(settings.key, key)); + const result = await getDb().select().from(settings).where(eq(settings.key, key)); if (result.length === 0) { return null; @@ -370,19 +370,19 @@ class UserCrypto { const key = `user_encrypted_dek_${userId}`; const value = JSON.stringify(encryptedDEK); - const existing = await db.select().from(settings).where(eq(settings.key, key)); + const existing = await getDb().select().from(settings).where(eq(settings.key, key)); if (existing.length > 0) { - await db.update(settings).set({ value }).where(eq(settings.key, key)); + await getDb().update(settings).set({ value }).where(eq(settings.key, key)); } else { - await db.insert(settings).values({ key, value }); + await getDb().insert(settings).values({ key, value }); } } private async getEncryptedDEK(userId: string): Promise { try { const key = `user_encrypted_dek_${userId}`; - const result = await db.select().from(settings).where(eq(settings.key, key)); + const result = await getDb().select().from(settings).where(eq(settings.key, key)); if (result.length === 0) { return null; diff --git a/src/backend/utils/user-data-export.ts b/src/backend/utils/user-data-export.ts index 2f7fffc0..3be451c3 100644 --- a/src/backend/utils/user-data-export.ts +++ b/src/backend/utils/user-data-export.ts @@ -1,4 +1,4 @@ -import { db } from "../database/db/index.js"; +import { getDb } from "../database/db/index.js"; import { users, sshData, sshCredentials, fileManagerRecent, fileManagerPinned, fileManagerShortcuts, dismissedAlerts } from "../database/db/schema.js"; import { eq } from "drizzle-orm"; import { DataCrypto } from "./data-crypto.js"; @@ -62,7 +62,7 @@ class UserDataExport { }); // Verify user exists - const user = await db.select().from(users).where(eq(users.id, userId)); + const user = await getDb().select().from(users).where(eq(users.id, userId)); if (!user || user.length === 0) { throw new Error(`User not found: ${userId}`); } @@ -79,7 +79,7 @@ class UserDataExport { } // Export SSH host configurations - const sshHosts = await db.select().from(sshData).where(eq(sshData.userId, userId)); + const sshHosts = await getDb().select().from(sshData).where(eq(sshData.userId, userId)); const processedSshHosts = format === 'plaintext' && userDataKey ? sshHosts.map(host => DataCrypto.decryptRecord("ssh_data", host, userId, userDataKey!)) : sshHosts; @@ -87,7 +87,7 @@ class UserDataExport { // Export SSH credentials (if included) let sshCredentialsData: any[] = []; if (includeCredentials) { - const credentials = await db.select().from(sshCredentials).where(eq(sshCredentials.userId, userId)); + const credentials = await getDb().select().from(sshCredentials).where(eq(sshCredentials.userId, userId)); sshCredentialsData = format === 'plaintext' && userDataKey ? credentials.map(cred => DataCrypto.decryptRecord("ssh_credentials", cred, userId, userDataKey!)) : credentials; @@ -95,13 +95,13 @@ class UserDataExport { // Export file manager data const [recentFiles, pinnedFiles, shortcuts] = await Promise.all([ - db.select().from(fileManagerRecent).where(eq(fileManagerRecent.userId, userId)), - db.select().from(fileManagerPinned).where(eq(fileManagerPinned.userId, userId)), - db.select().from(fileManagerShortcuts).where(eq(fileManagerShortcuts.userId, userId)), + getDb().select().from(fileManagerRecent).where(eq(fileManagerRecent.userId, userId)), + getDb().select().from(fileManagerPinned).where(eq(fileManagerPinned.userId, userId)), + getDb().select().from(fileManagerShortcuts).where(eq(fileManagerShortcuts.userId, userId)), ]); // Export dismissed alerts - const alerts = await db.select().from(dismissedAlerts).where(eq(dismissedAlerts.userId, userId)); + const alerts = await getDb().select().from(dismissedAlerts).where(eq(dismissedAlerts.userId, userId)); // Build export data const exportData: UserExportData = { diff --git a/src/backend/utils/user-data-import.ts b/src/backend/utils/user-data-import.ts index ba66e911..6d5339f9 100644 --- a/src/backend/utils/user-data-import.ts +++ b/src/backend/utils/user-data-import.ts @@ -1,4 +1,4 @@ -import { db } from "../database/db/index.js"; +import { getDb } from "../database/db/index.js"; import { users, sshData, sshCredentials, fileManagerRecent, fileManagerPinned, fileManagerShortcuts, dismissedAlerts } from "../database/db/schema.js"; import { eq, and } from "drizzle-orm"; import { DataCrypto } from "./data-crypto.js"; @@ -65,7 +65,7 @@ class UserDataImport { }); // Verify target user exists - const targetUser = await db.select().from(users).where(eq(users.id, targetUserId)); + const targetUser = await getDb().select().from(users).where(eq(users.id, targetUserId)); if (!targetUser || targetUser.length === 0) { throw new Error(`Target user not found: ${targetUserId}`); } @@ -200,7 +200,7 @@ class UserDataImport { processedHostData = DataCrypto.encryptRecord("ssh_data", newHostData, targetUserId, options.userDataKey); } - await db.insert(sshData).values(processedHostData); + await getDb().insert(sshData).values(processedHostData); imported++; } catch (error) { errors.push(`SSH host import failed: ${error instanceof Error ? error.message : 'Unknown error'}`); @@ -247,7 +247,7 @@ class UserDataImport { processedCredentialData = DataCrypto.encryptRecord("ssh_credentials", newCredentialData, targetUserId, options.userDataKey); } - await db.insert(sshCredentials).values(processedCredentialData); + await getDb().insert(sshCredentials).values(processedCredentialData); imported++; } catch (error) { errors.push(`SSH credential import failed: ${error instanceof Error ? error.message : 'Unknown error'}`); @@ -282,7 +282,7 @@ class UserDataImport { userId: targetUserId, lastOpened: new Date().toISOString(), }; - await db.insert(fileManagerRecent).values(newItem); + await getDb().insert(fileManagerRecent).values(newItem); } imported++; } catch (error) { @@ -303,7 +303,7 @@ class UserDataImport { userId: targetUserId, pinnedAt: new Date().toISOString(), }; - await db.insert(fileManagerPinned).values(newItem); + await getDb().insert(fileManagerPinned).values(newItem); } imported++; } catch (error) { @@ -324,7 +324,7 @@ class UserDataImport { userId: targetUserId, createdAt: new Date().toISOString(), }; - await db.insert(fileManagerShortcuts).values(newItem); + await getDb().insert(fileManagerShortcuts).values(newItem); } imported++; } catch (error) { @@ -360,7 +360,7 @@ class UserDataImport { } // Check if alert already exists - const existing = await db + const existing = await getDb() .select() .from(dismissedAlerts) .where( @@ -383,12 +383,12 @@ class UserDataImport { }; if (existing.length > 0 && options.replaceExisting) { - await db + await getDb() .update(dismissedAlerts) .set(newAlert) .where(eq(dismissedAlerts.id, existing[0].id)); } else { - await db.insert(dismissedAlerts).values(newAlert); + await getDb().insert(dismissedAlerts).values(newAlert); } imported++; diff --git a/src/ui/Desktop/Apps/Terminal/Terminal.tsx b/src/ui/Desktop/Apps/Terminal/Terminal.tsx index e067435e..dcc865a4 100644 --- a/src/ui/Desktop/Apps/Terminal/Terminal.tsx +++ b/src/ui/Desktop/Apps/Terminal/Terminal.tsx @@ -222,7 +222,7 @@ export const Terminal = forwardRef(function SSHTerminal( } const baseWsUrl = isDev - ? "ws://localhost:8082" + ? `${window.location.protocol === "https:" ? "wss" : "ws"}://localhost:8082` : isElectron() ? (() => { const baseUrl = diff --git a/src/ui/Mobile/Apps/Terminal/Terminal.tsx b/src/ui/Mobile/Apps/Terminal/Terminal.tsx index 3bb4ee1b..ebde911e 100644 --- a/src/ui/Mobile/Apps/Terminal/Terminal.tsx +++ b/src/ui/Mobile/Apps/Terminal/Terminal.tsx @@ -261,7 +261,7 @@ export const Terminal = forwardRef(function SSHTerminal( } const baseWsUrl = isDev - ? "ws://localhost:8082" + ? `${window.location.protocol === "https:" ? "wss" : "ws"}://localhost:8082` : isElectron() ? (() => { const baseUrl = diff --git a/src/ui/main-axios.ts b/src/ui/main-axios.ts index 7ff4160d..10cf96f2 100644 --- a/src/ui/main-axios.ts +++ b/src/ui/main-axios.ts @@ -376,7 +376,10 @@ if (isElectron()) { function getApiUrl(path: string, defaultPort: number): string { if (isDev()) { - return `http://${apiHost}:${defaultPort}${path}`; + // Auto-detect HTTPS in development + const protocol = window.location.protocol === "https:" ? "https" : "http"; + const sslPort = protocol === "https" ? 8443 : defaultPort; + return `${protocol}://${apiHost}:${sslPort}${path}`; } else if (isElectron()) { if (configuredServerUrl) { const baseUrl = configuredServerUrl.replace(/\/$/, ""); diff --git a/ssl/termix.crt b/ssl/termix.crt new file mode 100644 index 00000000..55beb29d --- /dev/null +++ b/ssl/termix.crt @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIID/zCCAuegAwIBAgIUG6+dZQ7SQOEO/RgH6hclicwzoAswDQYJKoZIhvcNAQEL +BQAwaTELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5 +MQ8wDQYDVQQKDAZUZXJtaXgxFjAUBgNVBAsMDUlUIERlcGFydG1lbnQxEjAQBgNV +BAMMCWxvY2FsaG9zdDAeFw0yNTA5MjIwMjQ0MDhaFw0yNjA5MjIwMjQ0MDhaMGkx +CzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEPMA0G +A1UECgwGVGVybWl4MRYwFAYDVQQLDA1JVCBEZXBhcnRtZW50MRIwEAYDVQQDDAls +b2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCA5RvhN4y +5c3D8L8tKavR5tXHPpWImDTIQmf5XgUvkUq6ojq/TmotYcyerValq/CwruZjxaiE +HHcfzqejYYa20OsyiYFa4m87pyVoo+PR0KMkkw2nuQlXtTOH6ScFbgYJFGU3gfT8 +C2SJxKvc+fNnQUrIbdByXbJKYXSOf9YCJ7CIX1+YmDAxFfdVDZS7bcq7WVruLO5m +ZjW2JSyUpbJbeLLiy62f2r56/rMj8ps3mhknahKbThmVwNBi4PdRIc9LeDXrAEc0 +sUm2evc6z6V+peXUCjlAnGJeMjDet58l1BDOzcAnypEv00GgngkogLF5Sb6FfmKQ +ZUC2ggivWHrrAgMBAAGjgZ4wgZswCQYDVR0TBAIwADALBgNVHQ8EBAMCBeAwYgYD +VR0RBFswWYIJbG9jYWxob3N0ggkxMjcuMC4wLjGCCyoubG9jYWxob3N0ggx0ZXJt +aXgubG9jYWyCDioudGVybWl4LmxvY2FshwR/AAABhxAAAAAAAAAAAAAAAAAAAAAB +MB0GA1UdDgQWBBRd0IxOKh559qJIZbVXTeU7Vco88DANBgkqhkiG9w0BAQsFAAOC +AQEAm/OHTaz7IePbfcp8A7mO72Hu8OO/Tq4tuVCC8T4SY7NdnOHrV9+z2dd5Judn +yGMtOeE64xKgPqJIjOdbAvYPgTwpo7yXnuTohqeWcyW/JWtNmFCw+eQyTx5tnD+J +DSF4/QHK/fB791NzQYc+Z01P37yOwi0zRO9BWshwaxZTlrqg4tBPSdKIUyhrWRoT +KqXN0+kDjeNyiXM+6TnRjiigThRO2VEc1FW7ohm7c47VbfWr63Vf146ckhR5zq5Q +D1gHuwHV9VdwImzrBYpvhHZlAWgTnznRkcGhQ5uOtcaZy+CH1apQ2fu9ukDl6+Ee +GO9etliK5I6mCwsiO/5T0Kcesw== +-----END CERTIFICATE----- diff --git a/ssl/termix.key b/ssl/termix.key new file mode 100644 index 00000000..0a2cedd7 --- /dev/null +++ b/ssl/termix.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDCA5RvhN4y5c3D +8L8tKavR5tXHPpWImDTIQmf5XgUvkUq6ojq/TmotYcyerValq/CwruZjxaiEHHcf +zqejYYa20OsyiYFa4m87pyVoo+PR0KMkkw2nuQlXtTOH6ScFbgYJFGU3gfT8C2SJ +xKvc+fNnQUrIbdByXbJKYXSOf9YCJ7CIX1+YmDAxFfdVDZS7bcq7WVruLO5mZjW2 +JSyUpbJbeLLiy62f2r56/rMj8ps3mhknahKbThmVwNBi4PdRIc9LeDXrAEc0sUm2 +evc6z6V+peXUCjlAnGJeMjDet58l1BDOzcAnypEv00GgngkogLF5Sb6FfmKQZUC2 +ggivWHrrAgMBAAECggEAVasz/5w5a1sa5VLob95PLuPRcOXbLJIc+HKOK8gO3Sa4 +Szn4W+IZs0lUi5p5wLTwFmxcciDk3NUe6s4bKuMVE6Ojv1CFbGbA/CO9untnzQ1m +BG/knzNvAyoRg4l5wAWJp7e4S+7YCPVU4xqTUwORrX3gsij/WoiyAfMPfx7GlnM/ +WfPYPWdaTCKHPpbTbN7mUATM9sX9Lil+V31w2lKZ1Bw1GL9YQS7DsPtf/YR8mPoW +t29jDPZm4h/QkKpDpr8Gg8vKAwkDQDXjcm+z1O4pABAKIYW/uBTsuJ+47Gs+trDW +A4hU91WFm0Lf8mh9YON+oxUKUo6Iuulr1CCd5zG5dQKBgQD1oaZeN+zE2fhE3DxM +jZl5gTg142+tjPuNdQWzW6vZDNTp2N494mBHNC1SB4LLooJmdpHTxW0Rz8mamH7I +fbGktt9YYw4pgPblUaFVm6DupJI1qo5+JElrzWkYIcx7ZPAFnZxeBR19FjnvezW2 +q7qJzmFi9P1ao0iWKB26ljfW9wKBgQDKNCPGUBKy2G3UFANEomLNxEOgvoK+di5j +Fb8us/naGNlo98VYRmSjgHOFFM6W8IbZw9CVqwld3H26/FqHLJ77ujRmKzj6Jqrd +jPZqO9Di17/9uw4xfC+tQ6ngoPZq5poycKz3gSn9TOqu7bPfNRIzzLijC+hL1xDH +b0eaUwn6rQKBgQCyQfrju4BHp8vl5VKZV9W+eQmbChA9Cehw0zEs5eVD4m0NvEYk +8QlgAzy0oCDKuYga5geUgV1TJNGxMOQpihaGa/SQR2q6sg37hA8qeoQDTEmTStCY +OKtT4cFYMwcbsbgCy0v0a4/n/F5VLrxfcicw5SaF0zeeNItz9W8FvwiNJwKBgQDI +Sm9pYCW1fEcGPTCjisqeEhv/HNb7fKskQQVYaLREQjsRC+UyNMA5aOKE34Bn6Sda +i+mQZ5RmoiL01kWCAkQVC3QeBBBzUVwNCzWHM2sNWDL4TZKYl+/OC+k49ZhBed0h +u5TJser62nbZAeIbZkF6h/4Ym5HllcosEuF1T23iHQKBgQDYZOZzwxSwXnRbAvk8 +v1c0rIkEonio2nyeRNNnN7Y27vwyi17o92hOBdZfPNWF6HheSlcuA1LhUI4/I6qF +2aZoMmLYdGl1/BCsnmbwWWFWD3p0BDDXGXi0OepRBjCi4imfy3lR3D0dQrqbeihR +VrAkQCkCByVeA5OcBrBeL+Dzig== +-----END PRIVATE KEY----- diff --git a/vite.config.ts b/vite.config.ts index 5b1d9650..671677f0 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,8 +1,17 @@ import path from "path"; +import fs from "fs"; import tailwindcss from "@tailwindcss/vite"; import { defineConfig } from "vite"; import react from "@vitejs/plugin-react-swc"; +// SSL certificate paths +const sslCertPath = path.join(process.cwd(), "ssl/termix.crt"); +const sslKeyPath = path.join(process.cwd(), "ssl/termix.key"); + +// Check if SSL certificates exist and HTTPS is requested +const hasSSL = fs.existsSync(sslCertPath) && fs.existsSync(sslKeyPath); +const useHTTPS = process.env.VITE_HTTPS === "true" && hasSSL; + // https://vite.dev/config/ export default defineConfig({ plugins: [react(), tailwindcss()], @@ -12,4 +21,12 @@ export default defineConfig({ }, }, base: "./", + server: { + https: useHTTPS ? { + cert: fs.readFileSync(sslCertPath), + key: fs.readFileSync(sslKeyPath), + } : false, + port: 5173, + host: "localhost", + }, });