diff --git a/container-creation/intern-phxdc-pve1/var-lib-vz-snippets/register-container.sh b/container-creation/intern-phxdc-pve1/var-lib-vz-snippets/register-container.sh index 77f82de2..70a7fa9b 100755 --- a/container-creation/intern-phxdc-pve1/var-lib-vz-snippets/register-container.sh +++ b/container-creation/intern-phxdc-pve1/var-lib-vz-snippets/register-container.sh @@ -150,81 +150,33 @@ if [ ! -z "$ADDITIONAL_PROTOCOLS" ]; then ss_protocols="$(IFS=, ; echo "${list_all_protocols[*]}")" ss_ports="$(IFS=, ; echo "${list_all_ports[*]}")" - #Update NGINX port map JSON on the remote host safely using a heredoc and positional parameters - - ssh root@10.15.20.69 bash -s -- "$hostname" "$container_ip" "$ssh_port" "$http_port" "$ss_protocols" "$ss_ports" "$proxmox_user" "$os_release" "$CTID" "$mac" <<'EOF' -set -euo pipefail - -hostname="$1" -container_ip="$2" -ssh_port="$3" -http_port="$4" -protos_json=$(echo "$5" | tr ',' '\n' | jq -R . | jq -s .) -ports_json=$(echo "$6" | tr ',' '\n' | jq -R . | jq -s 'map(tonumber)') -user="$7" -os_release="$8" -ctid="$9" -mac="${10}" - -jq --arg hn "$hostname" \ - --arg ip "$container_ip" \ - --arg user "$user" \ - --arg osr "$os_release" \ - --argjson ssh "$ssh_port" \ - --argjson http "$http_port" \ - --argjson protos "$protos_json" \ - --argjson ports_list "$ports_json" \ - --argjson ctid "$ctid" \ - --arg mac "$mac" \ - '. + {($hn): { - ip: $ip, - user: $user, - os_release: $osr, - ctid: $ctid, - mac: $mac, - ports: ( reduce range(0; $protos | length) as $i ( - {ssh: $ssh, http: $http}; - . + { ($protos[$i]): $ports_list[$i]} - )) - }}' /etc/nginx/port_map.json > /tmp/port_map.json.new - -mv -f /tmp/port_map.json.new /etc/nginx/port_map.json -nginx -s reload -EOF + # Register container with additional protocols via API + curl -X POST https://create-a-container.opensource.mieweb.org/containers \ + -H "Content-Type: application/x-www-form-urlencoded" \ + --data-urlencode "hostname=$hostname" \ + --data-urlencode "ipv4Address=$container_ip" \ + --data-urlencode "username=$proxmox_user" \ + --data-urlencode "osRelease=$os_release" \ + --data-urlencode "containerId=$CTID" \ + --data-urlencode "macAddress=$mac" \ + --data-urlencode "aiContainer=$AI_CONTAINER" \ + --data-urlencode "sshPort=$ssh_port" \ + --data-urlencode "httpPort=$http_port" \ + --data-urlencode "additionalProtocols=$ss_protocols" \ + --data-urlencode "additionalPorts=$ss_ports" else - # Update NGINX port map JSON on the remote host safely using a heredoc and positional parameters - ssh root@10.15.20.69 bash -s -- "$hostname" "$container_ip" "$ssh_port" "$http_port" "$proxmox_user" "$os_release" "$CTID" "$mac" <<'EOF' -set -euo pipefail - -hostname="$1" -container_ip="$2" -ssh_port="$3" -http_port="$4" -user="$5" -os_release="$6" -ctid="$7" -mac="$8" - -jq --arg hn "$hostname" \ - --arg ip "$container_ip" \ - --arg user "$user" \ - --arg osr "$os_release" \ - --argjson http "$http_port" \ - --argjson ssh "$ssh_port" \ - --argjson ctid "$ctid" \ - --arg mac "$mac" \ - '. + {($hn): { - ip: $ip, - user: $user, - os_release: $osr, - ctid: $ctid, - mac: $mac, - ports: {ssh: $ssh, http: $http} - }}' /etc/nginx/port_map.json > /tmp/port_map.json.new - -mv -f /tmp/port_map.json.new /etc/nginx/port_map.json -nginx -s reload -EOF + # Register container without additional protocols via API + curl -X POST https://create-a-container.opensource.mieweb.org/containers \ + -H "Content-Type: application/x-www-form-urlencoded" \ + --data-urlencode "hostname=$hostname" \ + --data-urlencode "ipv4Address=$container_ip" \ + --data-urlencode "username=$proxmox_user" \ + --data-urlencode "osRelease=$os_release" \ + --data-urlencode "containerId=$CTID" \ + --data-urlencode "macAddress=$mac" \ + --data-urlencode "aiContainer=$AI_CONTAINER" \ + --data-urlencode "sshPort=$ssh_port" \ + --data-urlencode "httpPort=$http_port" fi # Results diff --git a/create-a-container/README.md b/create-a-container/README.md new file mode 100644 index 00000000..d0b457bf --- /dev/null +++ b/create-a-container/README.md @@ -0,0 +1,386 @@ +# Create-a-Container + +A web application for managing LXC container creation, configuration, and lifecycle on Proxmox VE infrastructure. Provides a user-friendly interface and REST API for container management with automated database tracking and nginx reverse proxy configuration generation. + +## Features + +- **User Authentication** - Proxmox VE authentication integration +- **Container Management** - Create, list, and track LXC containers +- **Service Registry** - Track HTTP/TCP/UDP services running on containers +- **Dynamic Nginx Config** - Generate nginx reverse proxy configurations on-demand +- **Real-time Progress** - SSE (Server-Sent Events) for container creation progress +- **User Registration** - Self-service account request system with email notifications +- **Rate Limiting** - Protection against abuse (100 requests per 15 minutes) + +## Prerequisites + +### System Requirements +- **Node.js** 18.x or higher +- **MariaDB/MySQL** 5.7 or higher +- **Proxmox VE** cluster with API access +- **SMTP server** for email notifications (optional) + +### Services +```bash +# Install Node.js (Debian/Ubuntu) +curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - +sudo apt-get install -y nodejs + +# Install MariaDB +sudo apt-get install mariadb-server -y +sudo mysql_secure_installation +``` + +## Installation + +### 1. Clone Repository +```bash +cd /opt +sudo git clone https://github.com/mieweb/opensource-server.git +cd opensource-server/create-a-container +``` + +### 2. Install Dependencies +```bash +npm install +``` + +### 3. Database Setup + +#### Create Database and User +```sql +CREATE DATABASE opensource_containers CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +CREATE USER 'container_manager'@'localhost' IDENTIFIED BY 'secure_password_here'; +GRANT ALL PRIVILEGES ON opensource_containers.* TO 'container_manager'@'localhost'; +FLUSH PRIVILEGES; +``` + +#### Run Migrations +```bash +npm run db:migrate +``` + +This creates the following tables: +- `Containers` - Container records (hostname, IP, MAC, OS, etc.) +- `Services` - Service mappings (ports, protocols, hostnames) + +### 4. Configuration + +Create a `.env` file in the `create-a-container` directory: + +```bash +# Database Configuration +MYSQL_HOST=localhost +MYSQL_PORT=3306 +MYSQL_USER=container_manager +MYSQL_PASSWORD=secure_password_here +MYSQL_DATABASE=opensource_containers + +# Session Configuration +SESSION_SECRET=generate_random_secret_here + +# Application +NODE_ENV=production +``` + +#### Generate Session Secret +```bash +node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" +``` + +### 5. Start Application + +#### Development Mode (with auto-reload) +```bash +npm run dev +``` + +#### Production Mode +```bash +node server.js +``` + +#### As a System Service +Create `/etc/systemd/system/create-a-container.service`: +```ini +[Unit] +Description=Create-a-Container Service +After=network.target mariadb.service + +[Service] +Type=simple +User=www-data +WorkingDirectory=/opt/opensource-server/create-a-container +Environment=NODE_ENV=production +ExecStart=/usr/bin/node server.js +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +``` + +Enable and start: +```bash +sudo systemctl daemon-reload +sudo systemctl enable create-a-container +sudo systemctl start create-a-container +sudo systemctl status create-a-container +``` + +## API Routes + +### Authentication Routes + +#### `GET /login` +Display login page + +#### `POST /login` +Authenticate user with Proxmox VE credentials +- **Body**: `{ username, password }` +- **Returns**: `{ success: true, redirect: "/" }` + +#### `POST /logout` +End user session + +### Container Management Routes + +#### `GET /` +Redirect to `/containers` + +#### `GET /containers` (Auth Required) +List all containers for authenticated user +- **Returns**: HTML page with container list + +#### `GET /containers/new` (Auth Required) +Display container creation form + +#### `POST /containers` +Create or register a container +- **Query Parameter**: `init` (boolean) - If true, requires auth and spawns container creation +- **Body (init=true)**: `{ hostname, osRelease, httpPort, aiContainer }` +- **Body (init=false)**: Container registration data (for scripts) +- **Returns (init=true)**: Redirect to status page +- **Returns (init=false)**: `{ containerId, message }` + +#### `GET /status/:jobId` (Auth Required) +View container creation progress page + +#### `GET /api/stream/:jobId` +SSE stream for real-time container creation progress +- **Returns**: Server-Sent Events stream + +### Configuration Routes + +#### `GET /nginx.conf` +Generate nginx configuration for all registered services +- **Returns**: `text/plain` - Complete nginx configuration with all server blocks + +### User Registration Routes + +#### `GET /register` +Display account request form + +#### `POST /register` +Submit account request (sends email to admins) +- **Body**: `{ name, email, username, reason }` +- **Returns**: Success message + +### Utility Routes + +#### `GET /send-test-email` (Dev Only) +Test email configuration (development/testing) + +## Database Schema + +### Containers Table +```sql +id INT PRIMARY KEY AUTO_INCREMENT +hostname VARCHAR(255) UNIQUE NOT NULL +username VARCHAR(255) NOT NULL +osRelease VARCHAR(255) +containerId INT UNSIGNED UNIQUE +macAddress VARCHAR(17) UNIQUE +ipv4Address VARCHAR(45) UNIQUE +aiContainer VARCHAR(50) DEFAULT 'N' +createdAt DATETIME +updatedAt DATETIME +``` + +### Services Table +```sql +id INT PRIMARY KEY AUTO_INCREMENT +containerId INT FOREIGN KEY REFERENCES Containers(id) +type ENUM('tcp', 'udp', 'http') NOT NULL +internalPort INT NOT NULL +externalPort INT +tls BOOLEAN DEFAULT FALSE +externalHostname VARCHAR(255) +createdAt DATETIME +updatedAt DATETIME +``` + +## Configuration Files + +### `config/config.js` +Sequelize database configuration (reads from `.env`) + +### `models/` +- `container.js` - Container model definition +- `service.js` - Service model definition +- `index.js` - Sequelize initialization + +### `data/services.json` +Service type definitions and port mappings + +### `views/` +- `login.html` - Login form +- `form.html` - Container creation form +- `request-account.html` - Account request form +- `status.html` - Container creation progress viewer +- `containers.ejs` - Container list (EJS template) +- `nginx-conf.ejs` - Nginx config generator (EJS template) + +### `public/` +- `style.css` - Application styles + +### `migrations/` +Database migration files for schema management + +## Environment Variables + +### Required +- `MYSQL_HOST` - Database host (default: localhost) +- `MYSQL_PORT` - Database port (default: 3306) +- `MYSQL_USER` - Database username +- `MYSQL_PASSWORD` - Database password +- `MYSQL_DATABASE` - Database name +- `SESSION_SECRET` - Express session secret (cryptographically random string) + +### Optional +- `NODE_ENV` - Environment (development/production, default: development) + +## Security + +### Authentication +- Proxmox VE integration via API +- Session-based authentication with secure cookies +- Per-route authentication middleware + +### Rate Limiting +- 100 requests per 15-minute window per IP +- Protects against brute force and abuse + +### Session Security +- Session secret required for cookie signing +- Secure cookie flag enabled +- Session data server-side only + +### Input Validation +- URL encoding for all parameters +- Sequelize ORM prevents SQL injection +- Form data validation + +## Troubleshooting + +### Database Connection Issues +```bash +# Test database connection +mysql -h localhost -u container_manager -p opensource_containers + +# Check if migrations ran +npm run db:migrate + +# Verify tables exist +mysql -u container_manager -p -e "USE opensource_containers; SHOW TABLES;" +``` + +### Application Won't Start +```bash +# Check Node.js version +node --version # Should be 18.x or higher + +# Verify .env file exists and is readable +cat .env + +# Check for syntax errors +node -c server.js + +# Run with verbose logging +NODE_ENV=development node server.js +``` + +### Authentication Failing +```bash +# Verify Proxmox API is accessible +curl -k https://10.15.0.4:8006/api2/json/version + +# Check if certificate validation is working +# Edit server.js if using self-signed certs +``` + +### Email Not Sending +```bash +# Test SMTP connection +telnet mail.example.com 25 + +# Test route (development only) +curl http://localhost:3000/send-test-email +``` + +### Port Already in Use +```bash +# Find process using port 3000 +sudo lsof -i :3000 + +# Change port in .env or kill conflicting process +kill -9 +``` + +## Development + +### Database Migrations +```bash +# Create new migration +npx sequelize-cli migration:generate --name description-here + +# Run migrations +npm run db:migrate + +# Undo last migration +npx sequelize-cli db:migrate:undo +``` + +### Code Structure +``` +create-a-container/ +├── server.js # Main Express application +├── package.json # Dependencies and scripts +├── .env # Environment configuration (gitignored) +├── config/ # Sequelize configuration +├── models/ # Database models +├── migrations/ # Database migrations +├── views/ # HTML templates +├── public/ # Static assets +├── data/ # JSON data files +└── bin/ # Utility scripts +``` + +## Integration with Nginx Reverse Proxy + +This application generates nginx configurations consumed by the `nginx-reverse-proxy` component: + +1. Containers register their services in the database +2. The `/nginx.conf` endpoint generates complete nginx configs +3. The reverse proxy polls this endpoint via cron +4. Nginx automatically reloads with updated configurations + +See `../nginx-reverse-proxy/README.md` for reverse proxy setup. + +## License + +See the main repository LICENSE file. + +## Support + +For issues, questions, or contributions, see the main opensource-server repository. diff --git a/create-a-container/bin/json-to-sql.js b/create-a-container/bin/json-to-sql.js new file mode 100644 index 00000000..6f9958e4 --- /dev/null +++ b/create-a-container/bin/json-to-sql.js @@ -0,0 +1,256 @@ +#!/usr/bin/env node +"use strict"; + +// Usage: node bin/json-to-sql.js [--dry-run] + +require('dotenv').config(); +const fs = require('fs'); +const path = require('path'); +const https = require('https'); +const servicesLookup = require('../data/services.json'); + +const PROXMOX_URL = process.env.PROXMOX_URL; +const PROXMOX_USER = process.env.PROXMOX_USER; +const PROXMOX_PASSWORD = process.env.PROXMOX_PASSWORD; + +let pmxAuth = null; // {ticket, CSRF} + +const argv = process.argv.slice(2); +if (argv.length < 1) { + console.error('Usage: json-to-sql.js [--dry-run]'); + process.exit(2); +} + +const inputPath = path.resolve(argv[0]); +const dryRun = argv.includes('--dry-run'); + +async function pmxFetch(pathSuffix, opts = {}) { + console.log('pmxFetch', pathSuffix); + if (!PROXMOX_URL) throw new Error('Proxmox URL not configured'); + const url = PROXMOX_URL.replace(/\/$/, '') + pathSuffix; + const headers = opts.headers || {}; + if (pmxAuth && pmxAuth.ticket) { + headers['Cookie'] = `PVEAuthCookie=${pmxAuth.ticket}`; + if (pmxAuth.CSRFPreventionToken) headers['CSRFPreventionToken'] = pmxAuth.CSRFPreventionToken; + } + const res = await fetch(url, { method: opts.method || 'GET', headers, body: opts.body }); + if (!res.ok) { + const txt = await res.text(); + throw new Error(`Proxmox fetch ${url} failed: ${res.status} ${res.statusText} - ${txt}`); + } + return res.json(); +} + +async function pmxLogin() { + if (!PROXMOX_URL || !PROXMOX_USER || !PROXMOX_PASSWORD) return null; + if (pmxAuth && pmxAuth.ticket) return pmxAuth; + const body = new URLSearchParams(); + body.append('username', PROXMOX_USER); + body.append('password', PROXMOX_PASSWORD); + const url = PROXMOX_URL.replace(/\/$/, '') + '/api2/json/access/ticket'; + const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body.toString() }); + if (!res.ok) { + const txt = await res.text(); + throw new Error(`Proxmox login failed: ${res.status} ${res.statusText} - ${txt}`); + } + const j = await res.json(); + pmxAuth = { + ticket: j.data.ticket, + CSRFPreventionToken: j.data.CSRFPreventionToken + }; + return pmxAuth; +} + +function parseNet0Config(net0) { + // expected formats like: "name=eth0,bridge=vmbr0,hwaddr=BC:24:11:C8:3C:2D,ip=10.15.16.246/24" + const out = {}; + if (!net0 || typeof net0 !== 'string') return out; + const parts = net0.split(','); + for (const p of parts) { + const [k, ...rest] = p.split('='); + if (!k) continue; + const v = rest.join('='); + if (k === 'hwaddr') out.mac = v; + if (k === 'ip') { + const ip = v.split('/')[0]; + out.ip = ip; + } + } + return out; +} + +async function lookupProxmoxByHostname(hostname) { + await pmxLogin(); + + // list LXC resources + const res = await pmxFetch('/api2/json/cluster/resources?type=vm'); + const list = res.data || []; + const needle = String(hostname).toLowerCase(); + const found = list.find(r => (r.name && String(r.name).toLowerCase() === needle) || (r.vname && String(r.vname).toLowerCase() === needle)); + if (!found) throw new Error(`Proxmox: no LXC container found with hostname ${hostname}`); + const node = found.node; + const vmid = found.vmid || found.vmid || found.vmid; // vmid field + const out = { ctid: vmid }; + const cfg = await pmxFetch(`/api2/json/nodes/${encodeURIComponent(node)}/lxc/${encodeURIComponent(vmid)}/config`); + const cfgData = cfg.data || {}; + if (cfgData.net0) { + const parsed = parseNet0Config(cfgData.net0); + if (parsed.mac) out.mac = parsed.mac; + if (parsed.ip) out.ip = parsed.ip; + } + if (!out.ip) { + // try status/current for IP hints + const st = await pmxFetch(`/api2/json/nodes/${encodeURIComponent(node)}/lxc/${encodeURIComponent(vmid)}/status/current`); + const stData = st.data || {}; + // scan for the first IPv4-looking string in the JSON + const text = JSON.stringify(stData); + const m = text.match(/(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}/); + if (m) out.ip = m[0]; + } + // try to get description/user if available + if (cfgData.description) { + // naive parse: look for user:NAME or owner:NAME + const mm = cfgData.description.match(/(?:user|owner):\s*([A-Za-z0-9_\-]+)/i); + if (mm) out.user = mm[1]; + const osm = cfgData.description.match(/os[_ -]?release:\s*([A-Za-z0-9._\-]+)/i); + if (osm) out.os_release = osm[1]; + } + return out; +} + +function normalizeServiceKey(k) { + return String(k).toLowerCase(); +} + +function mapPortEntryToService(hostname, key, value) { + // key: service name like 'http' or 'ssh' + // value: external port number + const k = normalizeServiceKey(key); + if (k === 'http') { + return { + type: 'http', + internalPort: value, + externalPort: null, + tls: null, + externalHostname: hostname + }; + } + + const lookup = servicesLookup[k]; + if (lookup) { + return { + type: lookup.protocol === 'udp' ? 'udp' : 'tcp', + internalPort: lookup.port, + externalPort: value, + tls: lookup.protocol === 'tcp' ? false : null, + externalHostname: null + }; + } + + // fallback: assume tcp, use the provided value as both internal and external + return { + type: 'tcp', + internalPort: value, + externalPort: value, + tls: false, + externalHostname: null + }; +} + +async function run() { + const raw = fs.readFileSync(inputPath, 'utf8'); + const data = JSON.parse(raw); + + // If dry-run, only print what would be created + if (dryRun) { + for (const [hostname, obj] of Object.entries(data)) { + // If fields missing and Proxmox creds provided, try to fill them (dry-run lookup) + if ((obj.user === undefined || obj.os_release === undefined || obj.ctid === undefined || obj.mac === undefined || obj.ip === undefined) && (PROXMOX_URL && PROXMOX_USER && PROXMOX_PASSWORD)) { + const pmx = await lookupProxmoxByHostname(hostname); + if (pmx.ctid && obj.ctid === undefined) obj.ctid = pmx.ctid; + if (pmx.mac && obj.mac === undefined) obj.mac = pmx.mac; + if (pmx.ip && obj.ip === undefined) obj.ip = pmx.ip; + if (pmx.user && obj.user === undefined) obj.user = pmx.user; + if (pmx.os_release && obj.os_release === undefined) obj.os_release = pmx.os_release; + } + + console.log(`Container: hostname=${hostname}`); + console.log(` ipv4Address=${obj.ip}`); + console.log(` username=${obj.user}`); + console.log(` osRelease=${obj.os_release}`); + console.log(` containerId=${obj.ctid}`); + console.log(` macAddress=${obj.mac}`); + if (obj.ports) { + for (const [k, v] of Object.entries(obj.ports)) { + const svc = mapPortEntryToService(hostname, k, v); + console.log(` Service: type=${svc.type} internalPort=${svc.internalPort} externalPort=${svc.externalPort} tls=${svc.tls} externalHostname=${svc.externalHostname}`); + } + } + } + return; + } + + // Real DB mode: use Sequelize models + const models = require(path.resolve(__dirname, '../models')); + const { Container, Service, Sequelize } = models; + + for (const [hostname, obj] of Object.entries(data)) { + // If fields missing and Proxmox creds provided, try to fill them + if ((obj.user === undefined || obj.os_release === undefined || obj.ctid === undefined || obj.mac === undefined || obj.ip === undefined) && (PROXMOX_URL && PROXMOX_USER && PROXMOX_PASSWORD)) { + const pmx = await lookupProxmoxByHostname(hostname); + if (pmx.ctid && obj.ctid === undefined) obj.ctid = pmx.ctid; + if (pmx.mac && obj.mac === undefined) obj.mac = pmx.mac; + if (pmx.ip && obj.ip === undefined) obj.ip = pmx.ip; + if (pmx.user && obj.user === undefined) obj.user = pmx.user; + if (pmx.os_release && obj.os_release === undefined) obj.os_release = pmx.os_release; + } + + // Upsert Container by hostname + // case-insensitive hostname match + let container = await Container.findOne({ + where: Sequelize.where(Sequelize.fn('lower', Sequelize.col('hostname')), hostname.toLowerCase()) + }); + if (!container) { + container = await Container.create({ + hostname, + ipv4Address: obj.ip, + username: obj.user || '', + osRelease: obj.os_release, + containerId: obj.ctid, + macAddress: obj.mac + }); + console.log(`Created container ${hostname}`); + } else { + await container.update({ + ipv4Address: obj.ip, + username: obj.user || '', + osRelease: obj.os_release, + containerId: obj.ctid, + macAddress: obj.mac + }); + console.log(`Updated container ${hostname}`); + } + + // Create service rows + if (obj.ports) { + for (const [k, v] of Object.entries(obj.ports)) { + const svc = mapPortEntryToService(hostname, k, v); + const serviceRow = await Service.create({ + // Service.containerId references the Container primary key (id) + containerId: container.id, + type: svc.type, + internalPort: svc.internalPort, + externalPort: svc.externalPort, + tls: svc.tls, + externalHostname: svc.externalHostname + }); + console.log(`Created service ${serviceRow.id} on container ${hostname}`); + } + } + } +} + +run().catch(err => { + console.error('ERROR', err); + process.exit(1); +}); diff --git a/create-a-container/bin/test-curl.sh b/create-a-container/bin/test-curl.sh new file mode 100755 index 00000000..a5e782bd --- /dev/null +++ b/create-a-container/bin/test-curl.sh @@ -0,0 +1,102 @@ +#!/bin/bash + +set -euo pipefail + +# Usage function +usage() { + cat < [additionalProtocols] [additionalPorts] + +Register a container via the API endpoint. + +Required Parameters: + hostname Container hostname + ipv4Address IPv4 address of the container + username Username/owner of the container + osRelease Operating system release (e.g., debian, rocky) + containerId Container ID (CTID) + macAddress MAC address of the container + aiContainer AI container type (N, PHOENIX, or FORTWAYNE) + sshPort External SSH port + httpPort HTTP port + +Optional Parameters: + additionalProtocols Comma-separated list of additional protocol names + additionalPorts Comma-separated list of additional port numbers + +Environment Variables: + CONTAINER_API_URL Override the API endpoint URL + (default: http://localhost:3000/containers) + +Examples: + # Basic registration + $0 test-container 10.15.7.7 rgingras debian 123 AA:BB:CC:DD:EE:FF N 2222 80 + + # With additional protocols + $0 test-container 10.15.7.7 rgingras debian 123 AA:BB:CC:DD:EE:FF N 2222 80 "dns,smtp" "5353,2525" + + # Override URL + CONTAINER_API_URL=https://create-a-container-dev.opensource.mieweb.org/containers $0 test-container 10.15.7.7 rgingras debian 123 AA:BB:CC:DD:EE:FF N 2222 80 +EOF + exit 1 +} + +# Check required parameters +if [[ $# -lt 9 ]]; then + echo "❌ Error: Missing required parameters" >&2 + echo "" >&2 + usage +fi + +# Assign positional parameters +hostname="$1" +ipv4Address="$2" +username="$3" +osRelease="$4" +containerId="$5" +macAddress="$6" +aiContainer="$7" +sshPort="$8" +httpPort="$9" + +# Optional parameters +additionalProtocols="${10:-}" +additionalPorts="${11:-}" + +# Default URL +url="${CONTAINER_API_URL:-http://localhost:3000/containers}" + +echo "📡 Registering container via API..." +echo " URL: $url" +echo " Hostname: $hostname" +echo " IPv4: $ipv4Address" +echo "" + +# Build curl command +curl_cmd=( + curl -X POST "$url" + -H "Content-Type: application/x-www-form-urlencoded" + --data-urlencode "hostname=$hostname" + --data-urlencode "ipv4Address=$ipv4Address" + --data-urlencode "username=$username" + --data-urlencode "osRelease=$osRelease" + --data-urlencode "containerId=$containerId" + --data-urlencode "macAddress=$macAddress" + --data-urlencode "aiContainer=$aiContainer" + --data-urlencode "sshPort=$sshPort" + --data-urlencode "httpPort=$httpPort" +) + +# Add optional parameters if provided +if [[ -n "$additionalProtocols" ]]; then + curl_cmd+=(--data-urlencode "additionalProtocols=$additionalProtocols") +fi + +if [[ -n "$additionalPorts" ]]; then + curl_cmd+=(--data-urlencode "additionalPorts=$additionalPorts") +fi + +# Execute curl +"${curl_cmd[@]}" +echo "" +echo "✅ Request sent" diff --git a/create-a-container/config/config.js b/create-a-container/config/config.js new file mode 100644 index 00000000..d82773ab --- /dev/null +++ b/create-a-container/config/config.js @@ -0,0 +1,16 @@ +require('dotenv').config(); + +const config = { + host: process.env.MYSQL_HOST, + port: process.env.MYSQL_PORT, + username: process.env.MYSQL_USER, + password: process.env.MYSQL_PASSWORD, + database: process.env.MYSQL_DATABASE, + dialect: 'mysql', +}; + +module.exports = { + development: config, + test: config, + production: config, +}; \ No newline at end of file diff --git a/create-a-container/data/services.json b/create-a-container/data/services.json new file mode 100644 index 00000000..ab48a987 --- /dev/null +++ b/create-a-container/data/services.json @@ -0,0 +1,11 @@ +{ + "ssh": { "port": 22, "protocol": "tcp" }, + "http": { "port": 80, "protocol": "tcp" }, + "https": { "port": 443, "protocol": "tcp" }, + "smtp": { "port": 25, "protocol": "tcp" }, + "domain": { "port": 53, "protocol": "udp" }, + "dns": { "port": 53, "protocol": "udp" }, + "pop3": { "port": 110, "protocol": "tcp" }, + "imap": { "port": 143, "protocol": "tcp" }, + "rdp": { "port": 3389, "protocol": "tcp" } +} diff --git a/create-a-container/example.env b/create-a-container/example.env index 434dd2a6..02ee88b1 100644 --- a/create-a-container/example.env +++ b/create-a-container/example.env @@ -1 +1,11 @@ -SESSION_SECRET= \ No newline at end of file +SESSION_SECRET= +MYSQL_HOST= +MYSQL_PORT= +MYSQL_USER= +MYSQL_PASSWORD= +MYSQL_DATABASE= + +# Only used for bin/json-to-sql.js (for now) +PROXMOX_URL= +PROXMOX_USER= +PROXMOX_PASSWORD= \ No newline at end of file diff --git a/create-a-container/migrations/20251021131421-create-container.js b/create-a-container/migrations/20251021131421-create-container.js new file mode 100644 index 00000000..3f78f13c --- /dev/null +++ b/create-a-container/migrations/20251021131421-create-container.js @@ -0,0 +1,58 @@ +'use strict'; +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.createTable('Containers', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + hostname: { + type: Sequelize.STRING(255), + allowNull: false, + unique: true + }, + username: { + type: Sequelize.STRING(255), + allowNull: false + }, + osRelease: { + type: Sequelize.STRING(255), + allowNull: true + }, + containerId: { + type: Sequelize.INTEGER.UNSIGNED, + allowNull: false, + unique: true + }, + macAddress: { + type: Sequelize.STRING(17), + allowNull: false, + unique: true + }, + ipv4Address: { + type: Sequelize.STRING(45), + allowNull: false, + unique: true + }, + aiContainer: { + type: Sequelize.STRING(50), + allowNull: false, + defaultValue: 'N' + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE + } + }); + }, + async down(queryInterface, Sequelize) { + await queryInterface.dropTable('Containers'); + } +}; \ No newline at end of file diff --git a/create-a-container/migrations/20251021184500-create-services.js b/create-a-container/migrations/20251021184500-create-services.js new file mode 100644 index 00000000..cc7f127c --- /dev/null +++ b/create-a-container/migrations/20251021184500-create-services.js @@ -0,0 +1,59 @@ +"use strict"; + +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + // Services table: each service belongs to a Container + await queryInterface.createTable('Services', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER + }, + containerId: { + type: Sequelize.INTEGER, + allowNull: false, + references: { + model: 'Containers', + key: 'id' + }, + onDelete: 'CASCADE' + }, + type: { + type: Sequelize.ENUM('tcp','udp','http'), + allowNull: false + }, + internalPort: { + type: Sequelize.INTEGER.UNSIGNED, + allowNull: false + }, + externalPort: { + type: Sequelize.INTEGER.UNSIGNED, + allowNull: true // NULL for http services + }, + tls: { + type: Sequelize.BOOLEAN, + allowNull: true // only used for tcp services + }, + externalHostname: { + type: Sequelize.STRING(255), + allowNull: true // only used for http services + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE, + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE, + } + }); + }, + + async down(queryInterface, Sequelize) { + await queryInterface.dropTable('HttpServices'); + await queryInterface.dropTable('Layer4Services'); + await queryInterface.dropTable('Services'); + } +}; diff --git a/create-a-container/models/container.js b/create-a-container/models/container.js new file mode 100644 index 00000000..bf620b07 --- /dev/null +++ b/create-a-container/models/container.js @@ -0,0 +1,56 @@ +'use strict'; +const { + Model +} = require('sequelize'); +module.exports = (sequelize, DataTypes) => { + class Container extends Model { + /** + * Helper method for defining associations. + * This method is not a part of Sequelize lifecycle. + * The `models/index` file will call this method automatically. + */ + static associate(models) { + // a container has many services + Container.hasMany(models.Service, { foreignKey: 'containerId', as: 'services' }); + } + } + Container.init({ + hostname: { + type: DataTypes.STRING(255), + allowNull: false, + unique: true + }, + username: { + type: DataTypes.STRING(255), + allowNull: false + }, + osRelease: { + type: DataTypes.STRING(255), + allowNull: true + }, + containerId: { + type: DataTypes.INTEGER.UNSIGNED, + allowNull: false, + unique: true + }, + macAddress: { + type: DataTypes.STRING(17), + allowNull: false, + unique: true + }, + ipv4Address: { + type: DataTypes.STRING(45), + allowNull: false, + unique: true + }, + aiContainer: { + type: DataTypes.STRING(50), + allowNull: false, + defaultValue: 'N' + } + }, { + sequelize, + modelName: 'Container', + }); + return Container; +}; \ No newline at end of file diff --git a/create-a-container/models/index.js b/create-a-container/models/index.js new file mode 100644 index 00000000..7c9aeee9 --- /dev/null +++ b/create-a-container/models/index.js @@ -0,0 +1,43 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const Sequelize = require('sequelize'); +const process = require('process'); +const basename = path.basename(__filename); +const env = process.env.NODE_ENV || 'development'; +const config = require(__dirname + '/../config/config.js')[env]; +const db = {}; + +let sequelize; +if (config.use_env_variable) { + sequelize = new Sequelize(process.env[config.use_env_variable], config); +} else { + sequelize = new Sequelize(config.database, config.username, config.password, config); +} + +fs + .readdirSync(__dirname) + .filter(file => { + return ( + file.indexOf('.') !== 0 && + file !== basename && + file.slice(-3) === '.js' && + file.indexOf('.test.js') === -1 + ); + }) + .forEach(file => { + const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes); + db[model.name] = model; + }); + +Object.keys(db).forEach(modelName => { + if (db[modelName].associate) { + db[modelName].associate(db); + } +}); + +db.sequelize = sequelize; +db.Sequelize = Sequelize; + +module.exports = db; diff --git a/create-a-container/models/service.js b/create-a-container/models/service.js new file mode 100644 index 00000000..f32a2edd --- /dev/null +++ b/create-a-container/models/service.js @@ -0,0 +1,39 @@ +"use strict"; +const { Model } = require('sequelize'); +module.exports = (sequelize, DataTypes) => { + class Service extends Model { + static associate(models) { + Service.belongsTo(models.Container, { foreignKey: 'containerId' }); + } + } + Service.init({ + containerId: { + type: DataTypes.INTEGER, + allowNull: false + }, + type: { + type: DataTypes.ENUM('tcp','udp','http'), + allowNull: false + }, + internalPort: { + type: DataTypes.INTEGER.UNSIGNED, + allowNull: false + }, + externalPort: { + type: DataTypes.INTEGER.UNSIGNED, + allowNull: true // NULL for http services + }, + tls: { + type: DataTypes.BOOLEAN, + allowNull: true // only used for tcp services + }, + externalHostname: { + type: DataTypes.STRING(255), + allowNull: true // only used for http services + }, + }, { + sequelize, + modelName: 'Service', + }); + return Service; +}; diff --git a/create-a-container/package-lock.json b/create-a-container/package-lock.json index 695edc26..74f29f62 100644 --- a/create-a-container/package-lock.json +++ b/create-a-container/package-lock.json @@ -7,15 +7,91 @@ "dependencies": { "axios": "^1.12.2", "dotenv": "^17.2.3", + "ejs": "^3.1.10", "express": "^5.1.0", "express-rate-limit": "^8.1.0", "express-session": "^1.18.2", - "nodemailer": "^7.0.9" + "mysql2": "^3.15.2", + "nodemailer": "^7.0.9", + "sequelize": "^6.37.7", + "sequelize-cli": "^6.6.3" }, "devDependencies": { "nodemon": "^3.1.10" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "license": "MIT" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.0.tgz", + "integrity": "sha512-MKNwXh3seSK8WurXF7erHPJ2AONmMwkI7zAMrXZDPIru8jRqkk6rGDBVbw4mLwfqA+ZZliiDPg05JQ3uW66tKQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/validator": { + "version": "13.15.3", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.3.tgz", + "integrity": "sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q==", + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -28,6 +104,30 @@ "node": ">= 0.6" } }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -42,12 +142,36 @@ "node": ">= 8" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/axios": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", @@ -63,7 +187,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/binary-extensions": { @@ -79,6 +202,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", @@ -182,6 +311,108 @@ "fsevents": "~2.3.2" } }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -194,6 +425,15 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -201,6 +441,16 @@ "dev": true, "license": "MIT" }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, "node_modules/content-disposition": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", @@ -236,6 +486,20 @@ "node": ">=6.6.0" } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -261,6 +525,15 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -280,6 +553,12 @@ "url": "https://dotenvx.com" } }, + "node_modules/dottie": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", + "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==", + "license": "MIT" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -293,11 +572,80 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/editorconfig": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", + "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", + "license": "MIT", + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -348,6 +696,15 @@ "node": ">= 0.4" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -455,6 +812,36 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -504,6 +891,22 @@ } } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", @@ -557,6 +960,21 @@ "node": ">= 0.8" } }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -580,6 +998,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -615,6 +1051,26 @@ "node": ">= 0.4" } }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -628,6 +1084,30 @@ "node": ">= 6" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -639,6 +1119,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -727,11 +1213,26 @@ "dev": true, "license": "ISC" }, + "node_modules/inflection": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", + "engines": [ + "node >= 0.4.0" + ], + "license": "MIT" + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/ip-address": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", @@ -761,16 +1262,40 @@ "node": ">=8" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -799,6 +1324,128 @@ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/js-beautify": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", + "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", + "license": "MIT", + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.4.2", + "js-cookie": "^3.0.5", + "nopt": "^7.2.1" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/lru.min": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.2.tgz", + "integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -858,11 +1505,89 @@ "node": "*" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.48", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", + "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/mysql2": { + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.2.tgz", + "integrity": "sha512-kFm5+jbwR5mC+lo+3Cy46eHiykWSpUtTLOH3GE+AR7GeLq8PgfJcvpMiyVWk9/O53DjQsqm6a3VOOfq7gYWFRg==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "license": "MIT", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -908,6 +1633,21 @@ "url": "https://opencollective.com/nodemon" } }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -956,6 +1696,12 @@ "wrappy": "1" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -964,6 +1710,43 @@ "node": ">= 0.8" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/path-to-regexp": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", @@ -973,6 +1756,18 @@ "url": "https://opencollective.com/express" } }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -986,6 +1781,12 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "license": "ISC" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1083,6 +1884,41 @@ "node": ">=8.10.0" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/retry-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz", + "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==", + "license": "MIT" + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -1126,7 +1962,6 @@ "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1156,6 +1991,104 @@ "node": ">= 18" } }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, + "node_modules/sequelize": { + "version": "6.37.7", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.7.tgz", + "integrity": "sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/sequelize" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.8", + "@types/validator": "^13.7.17", + "debug": "^4.3.4", + "dottie": "^2.0.6", + "inflection": "^1.13.4", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", + "pg-connection-string": "^2.6.1", + "retry-as-promised": "^7.0.4", + "semver": "^7.5.4", + "sequelize-pool": "^7.1.0", + "toposort-class": "^1.0.1", + "uuid": "^8.3.2", + "validator": "^13.9.0", + "wkx": "^0.5.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependenciesMeta": { + "ibm_db": { + "optional": true + }, + "mariadb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-hstore": { + "optional": true + }, + "snowflake-sdk": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/sequelize-cli": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/sequelize-cli/-/sequelize-cli-6.6.3.tgz", + "integrity": "sha512-1YYPrcSRt/bpMDDSKM5ubY1mnJ2TEwIaGZcqITw4hLtGtE64nIqaBnLtMvH8VKHg6FbWpXTiFNc2mS/BtQCXZw==", + "license": "MIT", + "dependencies": { + "fs-extra": "^9.1.0", + "js-beautify": "1.15.4", + "lodash": "^4.17.21", + "picocolors": "^1.1.1", + "resolve": "^1.22.1", + "umzug": "^2.3.0", + "yargs": "^16.2.0" + }, + "bin": { + "sequelize": "lib/sequelize", + "sequelize-cli": "lib/sequelize" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/sequelize-pool": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", + "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/serve-static": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", @@ -1175,6 +2108,27 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -1243,6 +2197,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -1256,6 +2222,15 @@ "node": ">=10" } }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -1264,6 +2239,102 @@ "node": ">= 0.8" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -1277,6 +2348,18 @@ "node": ">=4" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -1298,6 +2381,12 @@ "node": ">=0.6" } }, + "node_modules/toposort-class": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==", + "license": "MIT" + }, "node_modules/touch": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", @@ -1332,6 +2421,18 @@ "node": ">= 0.8" } }, + "node_modules/umzug": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz", + "integrity": "sha512-Z274K+e8goZK8QJxmbRPhl89HPO1K+ORFtm6rySPhFKfKc5GHhqdzD0SGhSWHkzoXasqJuItdhorSvY7/Cgflw==", + "license": "MIT", + "dependencies": { + "bluebird": "^3.7.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -1339,6 +2440,21 @@ "dev": true, "license": "MIT" }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -1347,6 +2463,24 @@ "node": ">= 0.8" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validator": { + "version": "13.15.15", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", + "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -1355,10 +2489,202 @@ "node": ">= 0.8" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wkx": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", + "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } } } } diff --git a/create-a-container/package.json b/create-a-container/package.json index e96e6196..f02aed29 100644 --- a/create-a-container/package.json +++ b/create-a-container/package.json @@ -1,14 +1,19 @@ { "scripts": { - "dev": "nodemon server.js" + "dev": "nodemon server.js", + "db:migrate": "sequelize db:migrate" }, "dependencies": { "axios": "^1.12.2", "dotenv": "^17.2.3", + "ejs": "^3.1.10", "express": "^5.1.0", "express-rate-limit": "^8.1.0", "express-session": "^1.18.2", - "nodemailer": "^7.0.9" + "mysql2": "^3.15.2", + "nodemailer": "^7.0.9", + "sequelize": "^6.37.7", + "sequelize-cli": "^6.6.3" }, "devDependencies": { "nodemon": "^3.1.10" diff --git a/create-a-container/public/style.css b/create-a-container/public/style.css index a75b954b..95fd47b4 100644 --- a/create-a-container/public/style.css +++ b/create-a-container/public/style.css @@ -4,19 +4,19 @@ body { display: flex; align-items: flex-start; /* Changed from center to flex-start for top alignment */ justify-content: center; - height: 100vh; + min-height: 100vh; margin: 0; - padding-top: 2rem; /* Added padding to give space from the top */ + padding-top: 5rem; /* Added padding to account for fixed navbar */ overflow-y: auto; /* Allow scrolling if content is too long */ } .main-content-wrapper { display: flex; - flex-direction: row; - align-items: flex-start; + flex-direction: column; + align-items: center; gap: 2rem; width: 100%; - max-width: 900px; /* Increased max-width for two columns */ + max-width: 600px; padding: 0 1rem; } @@ -165,16 +165,18 @@ select:focus { /* Navbar container for logout button */ .navbar { - position: absolute; + position: fixed; top: 0; + left: 0; right: 0; + width: 100%; background-color: #2c3e50; color: white; padding: 0.5rem 1rem; - border-bottom-left-radius: 8px; display: flex; align-items: center; justify-content: flex-end; + z-index: 1000; } /* Logout button style */ @@ -183,6 +185,7 @@ select:focus { color: white; border: none; padding: 0.5rem 1rem; + margin-right: 2rem; border-radius: 4px; cursor: pointer; font-size: 1rem; diff --git a/create-a-container/server.js b/create-a-container/server.js index bd0f69e1..8285ff4c 100644 --- a/create-a-container/server.js +++ b/create-a-container/server.js @@ -1,38 +1,42 @@ require('dotenv').config(); const express = require('express'); -const bodyParser = require('body-parser'); const session = require('express-session'); const { spawn, exec } = require('child_process'); const path = require('path'); const crypto = require('crypto'); -const fs = require('fs'); -const rateLimit = require('express-rate-limit'); +const RateLimit = require('express-rate-limit'); const nodemailer = require('nodemailer'); // <-- added const axios = require('axios'); const qs = require('querystring'); const https = require('https'); +const { Container, Service } = require('./models'); +const serviceMap = require('./data/services.json'); const app = express(); -app.use(express.json()); +// setup views +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'ejs'); app.set('trust proxy', 1); -const jobs = {}; - -// --- Middleware Setup --- -if (!process.env.SESSION_SECRET) { - throw new Error("SESSION_SECRET is not set in environment!"); -} - +// setup middleware +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); // Parse form data app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: true, cookie: { secure: true } })); - app.use(express.static('public')); +app.use(RateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // limit each IP to 100 requests per windowMs +})); + +// define globals +const jobs = {}; // --- Authentication middleware (single) --- // Detect API requests and browser requests. API requests return 401 JSON, browser requests redirect to /login. @@ -63,14 +67,7 @@ app.get('/login', (req, res) => { }); // Redirect root to the main form. The form route will enforce authentication -app.get('/', (req, res) => res.redirect('/containers/new')); - -// --- Rate Limiter for Login --- -const loginLimiter = rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 5, - message: { error: "Too many login attempts. Please try again later." } -}); +app.get('/', (req, res) => res.redirect('/containers')); // --- Nodemailer Setup --- const transporter = nodemailer.createTransport({ @@ -92,7 +89,7 @@ app.get('/containers/new', requireAuth, (req, res) => { }); // Handles login -app.post('/login', loginLimiter, async (req, res) => { +app.post('/login', async (req, res) => { const { username, password } = req.body; const response = await axios.request({ @@ -118,56 +115,121 @@ app.post('/login', loginLimiter, async (req, res) => { }); // Fetch user's containers -app.get('/api/my-containers', requireAuth, (req, res) => { +app.get('/containers', requireAuth, async (req, res) => { const username = req.session.user.split('@')[0]; - const command = "ssh root@10.15.20.69 'cat /etc/nginx/port_map.json'"; + // eager-load related services + const containers = await Container.findAll({ where: { username }, include: [{ association: 'services' }] }); + + // Map containers to view models + const rows = containers.map(c => { + const services = c.services || []; + // sshPort: externalPort of service with type tcp and internalPort 22 + const ssh = services.find(s => s.type === 'tcp' && Number(s.internalPort) === 22); + const sshPort = ssh ? ssh.externalPort : null; + // httpPort: internalPort of first service type http + const http = services.find(s => s.type === 'http'); + const httpPort = http ? http.internalPort : null; + return { + hostname: c.hostname, + ipv4Address: c.ipv4Address, + osRelease: c.osRelease, + sshPort, + httpPort + }; + }); - exec(command, (err, stdout, stderr) => { - if (err) { - console.error("Error fetching port_map.json:", stderr); - return res.status(500).json({ error: "Could not fetch container list." }); - } - try { - const portMap = JSON.parse(stdout); - const userContainers = Object.entries(portMap) - .filter(([_, details]) => details && details.user === username) - .map(([name, details]) => ({ name, ...details })); - res.json(userContainers); - } catch (parseError) { - console.error("Error parsing port_map.json:", parseError); - res.status(500).json({ error: "Could not parse container list." }); - } + return res.render('containers', { rows }); +}); + +// Generate nginx configuration for a container +app.get('/nginx.conf', async (req, res) => { + const services = await Service.findAll({ + where: { type: 'http' }, + include: [{ model: Container }] }); + res.contentType('text/plain'); + return res.render('nginx-conf', { services }); }); // Create container -app.post('/containers', requireAuth, (req, res) => { - const jobId = crypto.randomUUID(); - const commandEnv = { - ...process.env, - ...req.body, - PROXMOX_USERNAME: req.session.proxmoxUsername, - PROXMOX_PASSWORD: req.session.proxmoxPassword - }; - const scriptPath = '/opt/container-creator/create-container-wrapper.sh'; +app.post('/containers', async (req, res) => { + const isInit = req.body.init === 'true' || req.body.init === true; - jobs[jobId] = { status: 'running', output: '' }; - - const command = `${scriptPath} 2>&1`; - const child = spawn('bash', ['-c', command], { env: commandEnv }); - - child.stdout.on('data', (data) => { - const message = data.toString(); - console.log(`[${jobId}]: ${message.trim()}`); - jobs[jobId].output += message; + // Only require auth for init=true (user-initiated container creation) + if (isInit) { + return requireAuth(req, res, () => { + // User-initiated container creation via web form + const jobId = crypto.randomUUID(); + + // Map standard form field names to the environment variable names expected by the script + const commandEnv = { + ...process.env, + CONTAINER_NAME: req.body.hostname, + LINUX_DISTRIBUTION: req.body.osRelease, + HTTP_PORT: req.body.httpPort, + AI_CONTAINER: req.body.aiContainer || 'N', + PROXMOX_USERNAME: req.session.proxmoxUsername, + PROXMOX_PASSWORD: req.session.proxmoxPassword + }; + const scriptPath = '/opt/container-creator/create-container-wrapper.sh'; + + jobs[jobId] = { status: 'running', output: '' }; + + const command = `${scriptPath} 2>&1`; + const child = spawn('bash', ['-c', command], { env: commandEnv }); + + child.stdout.on('data', (data) => { + const message = data.toString(); + console.log(`[${jobId}]: ${message.trim()}`); + jobs[jobId].output += message; + }); + + child.on('close', (code) => { + console.log(`[${jobId}] process exited with code ${code}`); + jobs[jobId].status = (code === 0) ? 'completed' : 'failed'; + }); + + return res.redirect(`/status/${jobId}`); + }); + } + + // handle non-init container creation (e.g., admin API) + const container = await Container.create(req.body); + const httpService = await Service.create({ + containerId: container.id, + type: 'http', + internalPort: req.body.httpPort, + externalPort: null, + tls: null, + externalHostname: container.hostname }); - - child.on('close', (code) => { - console.log(`[${jobId}] process exited with code ${code}`); - jobs[jobId].status = (code === 0) ? 'completed' : 'failed'; + const sshService = await Service.create({ + containerId: container.id, + type: 'tcp', + internalPort: 22, + externalPort: req.body.sshPort, + tls: false, + externalHostname: null }); - - res.json({ success: true, redirect: `/status/${jobId}` }); + if (req.body.additionalPorts && req.body.additionalProtocols) { + const additionalPorts = req.body.additionalPorts.split(',').map(p => p.trim()); + const additionalProtocols = req.body.additionalProtocols.split(',').map(p => p.trim().toLowerCase()); + for (let i = 0; i < additionalPorts.length; i++) { + const port = parseInt(additionalPorts[i], 10); + const protocol = additionalProtocols[i].toLowerCase(); + const defaultPort = serviceMap[protocol].port; + const underlyingProtocol = serviceMap[protocol].protocol; + const additionalService = await Service.create({ + containerId: container.id, + type: underlyingProtocol, + internalPort: defaultPort, + externalPort: port, + tls: false, + externalHostname: null + }); + } + } + return res.json({ success: true }); }); // Job status page @@ -215,16 +277,7 @@ app.get('/api/stream/:jobId', (req, res) => { }); // Serve the account request form - -// Apply a rate limiter to protect the request-account form -const requestAccountLimiter = rateLimit({ - windowMs: 1 * 60 * 1000, // 1 minute window - max: 10, // limit each IP to 10 requests per windowMs - standardHeaders: true, - legacyHeaders: false, -}); - -app.get('/register', requestAccountLimiter, (req, res) => { +app.get('/register', (req, res) => { res.sendFile(path.join(__dirname, 'views', 'request-account.html')); }); @@ -282,6 +335,6 @@ app.post('/logout', (req, res) => { return res.status(500).json({ error: 'Failed to log out.' }); } res.clearCookie('connect.sid'); // Clear the session cookie - return res.status(204).send(); // Respond with 204 No Content + return res.redirect('/'); }); }); \ No newline at end of file diff --git a/create-a-container/views/containers.ejs b/create-a-container/views/containers.ejs new file mode 100644 index 00000000..bc0bebb6 --- /dev/null +++ b/create-a-container/views/containers.ejs @@ -0,0 +1,75 @@ + + + + + Your Containers - MIE + + + + + + + +
+
+
+
+
+
+

Your Containers

+ New Container +
+ +
+ + + + + + + + + + + + <% if (rows && rows.length) { %> + <% rows.forEach(r => { %> + + + + + + + + <% }) %> + <% } else { %> + + + + <% } %> + +
HostnameIPv4OS ReleaseSSH PortHTTP Port
<%= r.hostname %><%= r.ipv4Address || '-' %><%= r.osRelease || '-' %><%= r.sshPort || '-' %><%= r.httpPort || '-' %>
+ No containers found. Click "New Container" to create your first one. +
+
+
+ +
+
+
+
+ + + + diff --git a/create-a-container/views/form.html b/create-a-container/views/form.html index f03eeacc..36816e57 100644 --- a/create-a-container/views/form.html +++ b/create-a-container/views/form.html @@ -4,34 +4,42 @@ MIE Container Creation + - +
- -
-

Your Active Containers

-
    -
-
- - - \ No newline at end of file diff --git a/create-a-container/views/nginx-conf.ejs b/create-a-container/views/nginx-conf.ejs new file mode 100644 index 00000000..a736d478 --- /dev/null +++ b/create-a-container/views/nginx-conf.ejs @@ -0,0 +1,72 @@ +server_names_hash_bucket_size 128; + +<% services.forEach((service, index) => { %> +server { + listen 443 ssl; + listen [::]:443 ssl; + listen 443 quic; + listen [::]:443 quic; + http2 on; + http3 on; + + server_name <%= service.externalHostname %>.opensource.mieweb.org; + + # SSL certificates + ssl_certificate /root/.acme.sh/opensource.mieweb.org/fullchain.cer; + ssl_certificate_key /root/.acme.sh/opensource.mieweb.org/opensource.mieweb.org.key; + + # Modern TLS configuration + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384'; + ssl_prefer_server_ciphers off; + + # SSL session optimization + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + ssl_session_tickets off; + + # OCSP stapling + ssl_stapling on; + ssl_stapling_verify on; + ssl_trusted_certificate /root/.acme.sh/opensource.mieweb.org/fullchain.cer; + resolver 1.1.1.1 8.8.8.8 valid=300s; + resolver_timeout 5s; + + # Security headers + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Alt-Svc 'h3=":443"; ma=86400' always; + + # Proxy settings + location / { + proxy_pass http://<%= service.Container.ipv4Address %>:<%= service.internalPort %>; + proxy_http_version 1.1; + + # Proxy headers + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + + # WebSocket support + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # Timeouts + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + + # Buffering (disable for SSE/streaming) + proxy_buffering off; + proxy_request_buffering off; + + # Allow large uploads + client_max_body_size 100M; + } +} +<% }) %> \ No newline at end of file diff --git a/nginx-reverse-proxy/README.md b/nginx-reverse-proxy/README.md index f28dcebe..5b529261 100644 --- a/nginx-reverse-proxy/README.md +++ b/nginx-reverse-proxy/README.md @@ -1 +1,195 @@ -# Nginx Reverse Proxy \ No newline at end of file +# Nginx Reverse Proxy + +This directory contains the automation for dynamically configuring nginx reverse proxy based on container services registered in the create-a-container database. + +## Overview + +The reverse proxy configuration is automatically synchronized from the create-a-container API endpoint, which generates nginx server blocks for all registered container services with HTTP endpoints. + +## Components + +### `pull-config.sh` +Bash script that: +1. Backs up the current nginx configuration +2. Downloads the latest configuration from the API endpoint +3. Tests the new configuration with `nginx -t` +4. Rolls back to the backup if validation fails +5. Reloads nginx if validation succeeds + +### `pull-config.cron` +Cron job definition that runs `pull-config.sh` every minute to keep the nginx configuration synchronized with the database. + +### `port_map.js` +NJS (nginx JavaScript) module that performs dynamic subdomain-to-container routing by querying the create-a-container API endpoints. + +### `reverse_proxy.conf` +Static nginx configuration that uses the `port_map.js` module for dynamic routing decisions. + +## Prerequisites + +### System Requirements +- **Nginx** installed and running (version 1.18+ recommended) +- **curl** for downloading configurations +- **Root/sudo access** for nginx configuration management + +### Nginx Installation +```bash +# Debian/Ubuntu +sudo apt-get update +sudo apt-get install nginx -y + +# Start and enable nginx +sudo systemctl start nginx +sudo systemctl enable nginx + +# Verify installation +nginx -v +``` + +## Deployment + +### 1. Clone Repository +```bash +cd /opt +sudo git clone https://github.com/mieweb/opensource-server.git +``` + +### 2. Install Cron Job +```bash +# Copy cron file to system cron directory +sudo cp /opt/opensource-server/nginx-reverse-proxy/pull-config.cron /etc/cron.d/nginx-pull-config + +# Set proper permissions +sudo chmod 644 /etc/cron.d/nginx-pull-config +``` + +### 3. Make Script Executable +```bash +sudo chmod +x /opt/opensource-server/nginx-reverse-proxy/pull-config.sh +``` + +### 4. Initial Configuration Pull +```bash +# Run script manually to get initial configuration +sudo touch /etc/nginx/conf.d/reverse-proxy.conf +sudo /opt/opensource-server/nginx-reverse-proxy/pull-config.sh +``` + +### 5. Verify Setup +```bash +# Check if configuration file was created +ls -la /etc/nginx/conf.d/reverse-proxy.conf + +# Test nginx configuration +sudo nginx -t + +# Check cron logs +sudo tail -f /var/log/syslog | grep pull-config +``` + +## Configuration + +### Environment Variables +The scripts use these default paths (can be modified in the scripts): + +- `CONF_FILE`: `/etc/nginx/conf.d/reverse-proxy.conf` - Target nginx config file +- `CONF_URL`: `https://create-a-container.opensource.mieweb.org/nginx.conf` - API endpoint + +### Cron Schedule +By default, the configuration is pulled every minute: +``` +* * * * * root /opt/opensource-server/nginx-reverse-proxy/pull-config.sh +``` + +To change the schedule, edit `/etc/cron.d/nginx-pull-config` with standard cron syntax: +``` +# Examples: +# Every 5 minutes: */5 * * * * +# Every hour: 0 * * * * +# Every day at midnight: 0 0 * * * +``` + +## Troubleshooting + +### Configuration Pull Failures +```bash +# Check if API is accessible +curl -I https://create-a-container.opensource.mieweb.org/nginx.conf + +# Manually run script with verbose output +sudo bash -x /opt/opensource-server/nginx-reverse-proxy/pull-config.sh +``` + +### Nginx Test Failures +```bash +# Check nginx error log +sudo tail -50 /var/log/nginx/error.log + +# Test configuration manually +sudo nginx -t + +# Check if backup was restored +ls -la /etc/nginx/conf.d/reverse-proxy.conf* +``` + +### Cron Not Running +```bash +# Check cron service status +sudo systemctl status cron + +# Verify cron job exists +sudo cat /etc/cron.d/nginx-pull-config + +# Check syslog for cron execution +sudo grep CRON /var/log/syslog | tail -20 +``` + +### Permission Issues +```bash +# Ensure script is executable +sudo chmod +x /opt/opensource-server/nginx-reverse-proxy/pull-config.sh + +# Ensure cron file has correct permissions +sudo chmod 644 /etc/cron.d/nginx-pull-config + +# Ensure nginx can reload +sudo nginx -s reload +``` + +## Security Considerations + +- The script runs as **root** (required for nginx management) +- Configuration is downloaded over **HTTPS** with certificate verification (`curl -fsSL`) +- Failed configurations are **automatically rolled back** to prevent service disruption +- Nginx configuration is **validated** before being applied + +## Monitoring + +Monitor the automated configuration updates: + +```bash +# Watch nginx reload activity +sudo tail -f /var/log/nginx/error.log + +# Monitor cron execution +sudo tail -f /var/log/syslog | grep pull-config + +# Check configuration update timestamps +ls -lt /etc/nginx/conf.d/reverse-proxy.conf +``` + +## Manual Operations + +### Force Configuration Update +```bash +sudo /opt/opensource-server/nginx-reverse-proxy/pull-config.sh +``` + +### Disable Automatic Updates +```bash +# Temporarily disable cron job +sudo chmod 000 /etc/cron.d/nginx-pull-config + +# Or remove it completely +sudo rm /etc/cron.d/nginx-pull-config +``` diff --git a/nginx-reverse-proxy/nginx.conf b/nginx-reverse-proxy/nginx.conf deleted file mode 100644 index 8b53f751..00000000 --- a/nginx-reverse-proxy/nginx.conf +++ /dev/null @@ -1,29 +0,0 @@ -# /etc/nginx/nginx.conf -user nginx; -worker_processes auto; - -error_log /var/log/nginx/error.log notice; -pid /run/nginx.pid; - -load_module modules/ngx_http_js_module.so; -load_module modules/ngx_stream_js_module.so; - -events { - worker_connections 1024; -} - -http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - - log_format backend '$remote_addr - $remote_user [$time_local] "$request" ' - 'status=$status backend_ip=$backend_ip backend_port=80 ' - '"$http_referer" "$http_user_agent" "$http_x_forwarded_for"'; - - access_log /var/log/nginx/access.log backend; - - sendfile on; - keepalive_timeout 65; - - include /etc/nginx/conf.d/*.conf; -} diff --git a/nginx-reverse-proxy/port-map-server.js b/nginx-reverse-proxy/port-map-server.js deleted file mode 100644 index a935eb27..00000000 --- a/nginx-reverse-proxy/port-map-server.js +++ /dev/null @@ -1,67 +0,0 @@ -// etc/nginx/port-map-server.js -// JSON-server that returns all (filtered) hosts to the https://opensource.mieweb.org -// Last modified on Aug 22, 2025 by Maxwell Klema - -const jsonServer = require('json-server'); -const path = require('path'); -const fs = require('fs'); -const filePath = "/etc/nginx/port_map.json"; - -const server = jsonServer.create(); -const router = jsonServer.router(path.join(__dirname, 'port_map.json')); -const middlewares = jsonServer.defaults(); -server.use(middlewares); - -server.get('/keys', (req, res) => { - const db = router.db; - const keys = Object.keys(db.getState()).filter(key => !['container-creation', 'intern-dnsmasq', 'wazuh-server', 'wazuh-indexer', 'wazuh-dashboard', 'intern-nginx', 'mie-ldap-server', 'create-a-container', 'landing-page'].includes(key)); - - // Filter out keys that are branches of a main/master for a repository - - let content = fs.readFileSync(filePath); - let cachedMapping = JSON.parse(content); - - all_hosts = []; - proxmox_launchpad_lxcs = []; - exclude = []; - for (const key of Object.keys(cachedMapping)) { - all_hosts.push(key); - if (key.endsWith("-main") || key.endsWith("master")) { - proxmox_launchpad_lxcs.push(key.substring(0, key.lastIndexOf("-"))); - } - } - - for (const entry of proxmox_launchpad_lxcs) { - hosts_to_filter = all_hosts.filter(key => { - return key.startsWith(entry) && (!key.endsWith("-main") || !key.startsWith(entry)); - }); - hosts_to_filter.forEach(host => exclude.push(host)); - } - - const filteredKeys = keys.filter(key => !exclude.includes(key)); - res.json(filteredKeys); -}) - -server.get('/:key', (req, res) => { - const key = req.params.key; - const db = router.db; - const value = db.getState()[key]; - - const response = { - name: key, - owner: value.user, - description: value.description || "", - github_url: value.github_url || "", - }; - - if (value) { - res.json(response); - } else { - res.status(404).json({ error: 'Not found' }); - } -}); - -server.use(router); -server.listen(3001, () => { - console.log("JSON Server Running on http://localhost:3001"); -}) diff --git a/nginx-reverse-proxy/port_map.js b/nginx-reverse-proxy/port_map.js deleted file mode 100644 index 0843a810..00000000 --- a/nginx-reverse-proxy/port_map.js +++ /dev/null @@ -1,94 +0,0 @@ -// /etc/nginx/port_map.js -// This is a reverse proxy configuration for Nginx that uses JavaScript to dynamically -// map subdomains to specific IP addresses based on a JSON file. -// Code is based off of bluehive-testflight's port_map.js -// Last updated: 06-08-2025 Carter Myers \\ 06-25-2025 Maxwell Klema - -var fs = require('fs'); -var filePath = "/etc/nginx/port_map.json"; // Make sure Nginx has read access -var cachedMapping = null; - -function loadMapping() { - try { - var content = fs.readFileSync(filePath); - cachedMapping = JSON.parse(content); - return true; - } catch (e) { - // Optionally log error - return false; - } -} - -function extractSubdomain(r) { - var host = r.variables.host; - var match = host.match(/^([^.]+)\.opensource\.mieweb\.(com|org)$/); - if (!match) { - r.error("Invalid hostname format: " + host); - return null; - } - return match[1]; -} - -function httpLookup(r) { - if (cachedMapping === null && !loadMapping()) { - r.error("Failed to load port mapping file."); - r.return(500); - return; - } - - var subdomain = extractSubdomain(r); - if (!subdomain) { - r.return(500); - return; - } - - var entry = cachedMapping[subdomain]; - if (!entry) { - if (!loadMapping()) { - r.error("Reload failed."); - r.return(500); - return; - } - entry = cachedMapping[subdomain]; - if (!entry) { - r.error("No entry found for subdomain: " + subdomain); - r.return(500); - return; - } - } - - return entry.ports.http.toString(); // Always return string -} - -function ipLookup(r) { - if (cachedMapping === null && !loadMapping()) { - r.error("Failed to load port mapping file."); - r.return(500); - return; - } - - var subdomain = extractSubdomain(r); - if (!subdomain) { - r.return(500); - return; - } - - var entry = cachedMapping[subdomain]; - if (!entry) { - if (!loadMapping()) { - r.error("Reload failed."); - r.return(500); - return; - } - entry = cachedMapping[subdomain]; - if (!entry) { - r.error("No entry found for subdomain: " + subdomain); - r.return(500); - return; - } - } - - return entry.ip; -} - -export default { httpLookup, ipLookup }; \ No newline at end of file diff --git a/nginx-reverse-proxy/pull-config.cron b/nginx-reverse-proxy/pull-config.cron new file mode 100644 index 00000000..4a593a04 --- /dev/null +++ b/nginx-reverse-proxy/pull-config.cron @@ -0,0 +1,3 @@ +SHELL=/bin/bash +PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +* * * * * root /opt/opensource-server/nginx-reverse-proxy/pull-config.sh diff --git a/nginx-reverse-proxy/pull-config.sh b/nginx-reverse-proxy/pull-config.sh new file mode 100755 index 00000000..85aa7318 --- /dev/null +++ b/nginx-reverse-proxy/pull-config.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -euo pipefail + +CONF_FILE=/etc/nginx/conf.d/reverse-proxy.conf +CONF_URL=https://create-a-container.opensource.mieweb.org/nginx.conf + +mv "${CONF_FILE}" "${CONF_FILE}.bak" +curl -fsSL -o "${CONF_FILE}" "${CONF_URL}" + +if ! nginx -t; then + mv "${CONF_FILE}.bak" "${CONF_FILE}" + exit 1 +fi + +rm -f "${CONF_FILE}.bak" +nginx -s reload \ No newline at end of file diff --git a/nginx-reverse-proxy/reverse_proxy.conf b/nginx-reverse-proxy/reverse_proxy.conf deleted file mode 100644 index 596949d3..00000000 --- a/nginx-reverse-proxy/reverse_proxy.conf +++ /dev/null @@ -1,76 +0,0 @@ -js_import port_module from /etc/nginx/port_map.js; -js_set $backend_ip port_module.ipLookup; -js_set $http_port port_module.httpLookup; - -# Define a custom log format -log_format proxy_log '$remote_addr - $host [$time_local] ' - '"$request" $status $body_bytes_sent ' - '"$http_referer" "$http_user_agent" ' - 'to $backend_ip:80'; - -# Enable access and error logs -access_log /var/log/nginx/reverse_proxy_access.log proxy_log; -error_log /var/log/nginx/reverse_proxy_error.log info; - -# HTTPS, uncomment when nginx gets private key, will not work w/o it -server { - listen 443 ssl; - server_name .opensource.mieweb.org; - - ssl_certificate /root/.acme.sh/opensource.mieweb.org/fullchain.cer; - ssl_certificate_key /root/.acme.sh/opensource.mieweb.org/opensource.mieweb.org.key; - - location / { - if ($backend_ip = "") { - return 404 "Backend IP not found."; - } - - if ($http_port = "") { - return 404 "http port not found."; - } - - proxy_pass http://$backend_ip:$http_port; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_http_version 1.1; # Use HTTP/1.1 for WebSocket support - proxy_set_header Upgrade $http_upgrade; # Upgrade header for WebSocket support - proxy_set_header Connection "upgrade"; # Connection header for WebSocket support - - # Disable response buffering (important for SSE) - proxy_buffering off; - proxy_cache off; - chunked_transfer_encoding off; - proxy_read_timeout 300s; - - } -} - -server { - listen 80; - server_name .opensource.mieweb.com; - - location / { - if ($backend_ip = "") { - return 404 "Backend IP not found."; - } - - if ($http_port = "") { - return 404 "http port not found."; - } - - proxy_pass http://$backend_ip:$http_port; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - # Disable response buffering (important for SSE) - proxy_buffering off; - proxy_cache off; - chunked_transfer_encoding off; - proxy_read_timeout 300s; - - } -} \ No newline at end of file