commit ac7df9160079753ed8d84ef0381923c356a3e066 Author: hunternick87 <47934810+hunternick87@users.noreply.github.com> Date: Thu Jun 12 01:33:06 2025 -0400 main Initial commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9e0a11e --- /dev/null +++ b/.env.example @@ -0,0 +1,38 @@ +# Example environment configuration +# Copy this file to .env and update the values + +# Server Configuration +PORT=3000 +NODE_ENV=development + +# Database Configuration +DATABASE_PATH=./data/proxy_manager.db + +# JWT Configuration +JWT_SECRET=your-super-secret-jwt-key-change-this-in-production +JWT_EXPIRES_IN=24h + +# Admin Configuration +ADMIN_USERNAME=admin +ADMIN_PASSWORD=admin123 + +# NGINX Configuration +NGINX_CONFIG_PATH=/etc/nginx/conf.d +NGINX_BINARY_PATH=/usr/sbin/nginx + +# SSL Configuration +ACME_SH_PATH=/root/.acme.sh +CERTBOT_PATH=/usr/bin/certbot +SSL_METHOD=acme.sh +CUSTOM_CERTS_PATH=./certs + +# Logging +LOG_LEVEL=info +LOG_FILE=./logs/app.log + +# CORS Configuration +CORS_ORIGIN=http://localhost:3001 + +# Cloudflare Configuration +CLOUDFLARE_API_TOKEN=your-cloudflare-api-token +CLOUDFLARE_API_EMAIL= # your-cloudflare-email \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..c9b4b8e --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,276 @@ +# ๐Ÿš€ Deployment Guide + +This guide covers different deployment options for the NGINX Proxy Manager Backend. + +## ๐Ÿณ Docker Deployment (Recommended) + +### Prerequisites +- Docker and Docker Compose installed +- Ports 80, 443, and optionally 3000 available +- Domain(s) pointing to your server + +### Quick Start + +1. **Clone and Configure** + ```bash + git clone + cd reverse-proxy + cp .env.example .env + # Edit .env with your configuration + ``` + +2. **Update Docker Compose** + Edit `docker-compose.yml` and change: + - `JWT_SECRET` to a secure random string + - `ADMIN_PASSWORD` to a secure password + - `CORS_ORIGIN` to your frontend domain + +3. **Deploy** + ```bash + docker-compose up -d + ``` + +4. **Check Status** + ```bash + docker-compose ps + docker-compose logs -f nginx-proxy-manager + ``` + +5. **Access API** + - Health check: `http://your-server:3000/api/health` + - Login: `POST http://your-server:3000/api/auth/login` + +### Production Configuration + +For production, edit `docker-compose.yml`: + +```yaml +# Remove API port exposure for security +ports: + - "80:80" + - "443:443" + # - "3000:3000" # Remove this line + +# Use environment file +env_file: + - .env.production + +# Add resource limits +deploy: + resources: + limits: + memory: 512M + cpus: '0.5' +``` + +## ๐Ÿ–ฅ๏ธ Native Installation + +### Prerequisites +- Ubuntu 20.04+ or similar Linux distribution +- Node.js with Bun runtime +- NGINX installed and running +- acme.sh or certbot for SSL certificates + +### Installation Steps + +1. **Install Dependencies** + ```bash + # Install Bun + curl -fsSL https://bun.sh/install | bash + + # Install NGINX + sudo apt update + sudo apt install nginx + + # Install acme.sh + curl https://get.acme.sh | sh -s email=your-email@domain.com + ``` + +2. **Setup Application** + ```bash + git clone + cd reverse-proxy + bun install + cp .env.example .env + # Edit .env with your configuration + ``` + +3. **Initialize Database** + ```bash + bun run db:init + ``` + +4. **Create Systemd Service** + ```bash + sudo tee /etc/systemd/system/nginx-proxy-manager.service > /dev/null < + cd reverse-proxy + bun install + ``` + +2. **Configure Environment** + ```bash + cp .env.example .env + # Edit .env with your configuration + ``` + +3. **Initialize Database** + ```bash + bun run db:init + ``` + +4. **Start Development Server** + ```bash + bun run dev + ``` + +5. **Start Production Server** + ```bash + bun run start + ``` + +## ๐Ÿ”ง Configuration + +### Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `PORT` | Server port | `3000` | +| `NODE_ENV` | Environment | `development` | +| `DATABASE_PATH` | SQLite database path | `./data/proxy_manager.db` | +| `JWT_SECRET` | JWT signing secret | `your-secret-key` | +| `JWT_EXPIRES_IN` | JWT expiration time | `24h` | +| `ADMIN_USERNAME` | Default admin username | `admin` | +| `ADMIN_PASSWORD` | Default admin password | `admin123` | +| `NGINX_CONFIG_PATH` | NGINX config directory | `/etc/nginx/conf.d` | +| `NGINX_BINARY_PATH` | NGINX binary path | `/usr/sbin/nginx` | +| `SSL_METHOD` | SSL method (acme.sh/certbot) | `acme.sh` | +| `ACME_SH_PATH` | acme.sh installation path | `/root/.acme.sh` | +| `CERTBOT_PATH` | certbot binary path | `/usr/bin/certbot` | +| `CUSTOM_CERTS_PATH` | Custom certificates directory | `./certs` | + +## ๐Ÿ“š API Documentation + +### Authentication + +#### Login +```http +POST /api/auth/login +Content-Type: application/json + +{ + "username": "admin", + "password": "admin123" +} +``` + +#### Get Current User +```http +GET /api/auth/me +Authorization: Bearer +``` + +### Proxy Management + +#### Get All Proxies +```http +GET /api/proxies +Authorization: Bearer +``` + +#### Create Proxy +```http +POST /api/proxies +Authorization: Bearer +Content-Type: application/json + +{ + "domain": "example.com", + "target": "http://localhost:8080", + "ssl_type": "letsencrypt", + "options": { + "redirect_http_to_https": true, + "custom_headers": { + "X-Custom-Header": "value" + }, + "path_forwarding": { + "/api": "http://api-server:3000" + }, + "enable_websockets": true, + "client_max_body_size": "10m" + } +} +``` + +#### Update Proxy +```http +PUT /api/proxies/:id +Authorization: Bearer +Content-Type: application/json + +{ + "target": "http://localhost:9000", + "options": { + "redirect_http_to_https": false + } +} +``` + +#### Delete Proxy +```http +DELETE /api/proxies/:id +Authorization: Bearer +``` + +### Certificate Management + +#### Request Let's Encrypt Certificate +```http +POST /api/certificates/letsencrypt +Authorization: Bearer +Content-Type: application/json + +{ + "domain": "example.com" +} +``` + +#### Upload Custom Certificate +```http +POST /api/certificates/custom +Authorization: Bearer +Content-Type: multipart/form-data + +{ + "domain": "example.com", + "certificate": , + "privateKey": +} +``` + +#### Get Expiring Certificates +```http +GET /api/certificates/expiring/check?days=30 +Authorization: Bearer +``` + +### NGINX Management + +#### Test NGINX Configuration +```http +POST /api/proxies/nginx/test +Authorization: Bearer +``` + +#### Reload NGINX +```http +POST /api/proxies/nginx/reload +Authorization: Bearer +``` + +## ๐Ÿ”„ Automatic Certificate Renewal + +The system includes automatic certificate renewal that: +- Runs daily at 2:00 AM UTC +- Checks for certificates expiring within 30 days +- Automatically renews Let's Encrypt certificates +- Logs all renewal activities + +## ๐Ÿ› Troubleshooting + +### Common Issues + +1. **NGINX reload fails** + - Check NGINX configuration syntax + - Verify file permissions + - Check NGINX error logs + +2. **Certificate request fails** + - Ensure domain points to server + - Check firewall settings (port 80/443) + - Verify acme.sh/certbot installation + +3. **Database errors** + - Check file permissions for database directory + - Ensure SQLite is available + +### Logs + +Application logs are stored in the `logs/` directory: +- `app.log` - General application logs +- `app-error.log` - Error logs only + +## ๐Ÿ”’ Security Considerations + +1. **Change default admin credentials** immediately after setup +2. **Use strong JWT secrets** in production +3. **Configure proper file permissions** for certificates +4. **Enable HTTPS** for the API in production +5. **Regular security updates** for all components + +## ๐Ÿค Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Add tests if applicable +5. Submit a pull request + +## ๐Ÿ“„ License + +This project is licensed under the MIT License. + +## ๐Ÿ†˜ Support + +For issues and questions: +1. Check the troubleshooting section +2. Review application logs +3. Create an issue on GitHub + +--- + +**โš ๏ธ Important**: This is a powerful tool that manages NGINX configurations and SSL certificates. Always test changes in a development environment first. diff --git a/arfire_dns.json b/arfire_dns.json new file mode 100644 index 0000000..e96dc91 --- /dev/null +++ b/arfire_dns.json @@ -0,0 +1,213 @@ +{ + "options": { + "method": "get", + "path": "/zones/9d050d996cfc81b76f67697cc81e51cc/dns_records", + "query": {} + }, + "response": {}, + "body": { + "result": [ + { + "id": "92c4433130f94fb9830e577b8eae9099", + "name": "arcfire.tv", + "type": "A", + "content": "150.136.51.173", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2023-07-11T03:49:23.309601Z", + "modified_on": "2023-07-11T03:49:23.309601Z" + }, + { + "id": "091c6a90187c7392143d0c8fef3c0210", + "name": "alpha.arcfire.tv", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2024-07-15T23:39:11.400857Z", + "modified_on": "2024-07-15T23:39:11.400857Z" + }, + { + "id": "5bbaea35b92ea70f5452598ef2b9f281", + "name": "arcfire.tv", + "type": "MX", + "content": "mail.hcws.dev", + "priority": 5, + "proxiable": false, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2023-11-13T05:37:07.86752Z", + "modified_on": "2023-11-13T05:37:07.86752Z" + }, + { + "id": "b415c571eb50aa35e785a8848fdfc243", + "name": "arcfire.tv", + "type": "TXT", + "content": "v=spf1 a mx ip4:5.161.120.112 ~all", + "proxiable": false, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2023-12-24T03:08:25.100168Z", + "modified_on": "2023-12-24T03:22:42.397852Z" + }, + { + "id": "054d392f025451d79b45a07ad07f22ec", + "name": "dkim._domainkey.arcfire.tv", + "type": "TXT", + "content": "v=DKIM1;k=rsa;t=s;s=email;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtqqr2smTXEbIaJgxjzKRAbbcY88COMUv3TYwWIYS+Icup74V4ir6cqsC6LZ0skMWBXy8r+vkZogBJUGsiJM9amPxxYu/40fe+Jllfk/qtY1hycGNXZXCrjKlS+esR0wBerL0mohXP7U8XDTmhLR59MlqRTUWxx1f9FTTvJ0oGDVxhxd6uSGITeBlHFxiveBhbyMUETPxlmjBrecInNNmSjOyrFl4S7fAZ5HzMvjL8pAm8QvZvMhpsabEjMsutRSzWZFdks3IK3FlGxU5aKoM3autYjvOLhJLsFGaMkwUasM3wBa9lZz8qDUzjsu/gxhRO5RqNk6k9DUuFI0Qb2IAXwIDAQAB", + "proxiable": false, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2023-12-24T03:09:14.437922Z", + "modified_on": "2023-12-24T03:09:14.437922Z" + }, + { + "id": "907524ef963504cf4aa4772d80a1c604", + "name": "_dmarc.arcfire.tv", + "type": "TXT", + "content": "v=DMARC1; p=reject; rua=mailto:postmaster@arcfire.tv", + "proxiable": false, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2023-12-24T03:10:11.949492Z", + "modified_on": "2023-12-24T03:37:45.440484Z" + } + ], + "success": true, + "errors": [], + "messages": [], + "result_info": { + "page": 1, + "per_page": 100, + "count": 6, + "total_count": 6, + "total_pages": 1 + } + }, + "result": [ + { + "id": "92c4433130f94fb9830e577b8eae9099", + "name": "arcfire.tv", + "type": "A", + "content": "150.136.51.173", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2023-07-11T03:49:23.309601Z", + "modified_on": "2023-07-11T03:49:23.309601Z" + }, + { + "id": "091c6a90187c7392143d0c8fef3c0210", + "name": "alpha.arcfire.tv", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2024-07-15T23:39:11.400857Z", + "modified_on": "2024-07-15T23:39:11.400857Z" + }, + { + "id": "5bbaea35b92ea70f5452598ef2b9f281", + "name": "arcfire.tv", + "type": "MX", + "content": "mail.hcws.dev", + "priority": 5, + "proxiable": false, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2023-11-13T05:37:07.86752Z", + "modified_on": "2023-11-13T05:37:07.86752Z" + }, + { + "id": "b415c571eb50aa35e785a8848fdfc243", + "name": "arcfire.tv", + "type": "TXT", + "content": "v=spf1 a mx ip4:5.161.120.112 ~all", + "proxiable": false, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2023-12-24T03:08:25.100168Z", + "modified_on": "2023-12-24T03:22:42.397852Z" + }, + { + "id": "054d392f025451d79b45a07ad07f22ec", + "name": "dkim._domainkey.arcfire.tv", + "type": "TXT", + "content": "v=DKIM1;k=rsa;t=s;s=email;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtqqr2smTXEbIaJgxjzKRAbbcY88COMUv3TYwWIYS+Icup74V4ir6cqsC6LZ0skMWBXy8r+vkZogBJUGsiJM9amPxxYu/40fe+Jllfk/qtY1hycGNXZXCrjKlS+esR0wBerL0mohXP7U8XDTmhLR59MlqRTUWxx1f9FTTvJ0oGDVxhxd6uSGITeBlHFxiveBhbyMUETPxlmjBrecInNNmSjOyrFl4S7fAZ5HzMvjL8pAm8QvZvMhpsabEjMsutRSzWZFdks3IK3FlGxU5aKoM3autYjvOLhJLsFGaMkwUasM3wBa9lZz8qDUzjsu/gxhRO5RqNk6k9DUuFI0Qb2IAXwIDAQAB", + "proxiable": false, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2023-12-24T03:09:14.437922Z", + "modified_on": "2023-12-24T03:09:14.437922Z" + }, + { + "id": "907524ef963504cf4aa4772d80a1c604", + "name": "_dmarc.arcfire.tv", + "type": "TXT", + "content": "v=DMARC1; p=reject; rua=mailto:postmaster@arcfire.tv", + "proxiable": false, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2023-12-24T03:10:11.949492Z", + "modified_on": "2023-12-24T03:37:45.440484Z" + } + ], + "result_info": { + "page": 1, + "per_page": 100, + "count": 6, + "total_count": 6, + "total_pages": 1 + } +} \ No newline at end of file diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..ac97ac6 --- /dev/null +++ b/bun.lock @@ -0,0 +1,828 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "reverse-proxy", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-slot": "^1.2.3", + "@types/bcryptjs": "^3.0.0", + "bcryptjs": "^3.0.2", + "class-variance-authority": "^0.7.1", + "cloudflare": "^4.3.0", + "clsx": "^2.1.1", + "commander": "^14.0.0", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "helmet": "^7.1.0", + "joi": "^17.11.0", + "jsonwebtoken": "^9.0.2", + "lucide-react": "^0.514.0", + "multer": "^1.4.5-lts.1", + "node-cron": "^3.0.3", + "react-router": "^7.6.2", + "tailwind-merge": "^3.3.1", + "winston": "^3.11.0", + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.9", + "@types/bun": "latest", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.5", + "@types/multer": "^1.4.11", + "@types/node-cron": "^3.0.11", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "^4.5.2", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "tailwindcss": "^4.1.9", + "tw-animate-css": "^1.3.4", + "vite": "^6.3.5", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], + + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], + + "@babel/compat-data": ["@babel/compat-data@7.27.5", "", {}, "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg=="], + + "@babel/core": ["@babel/core@7.27.4", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.4", "@babel/parser": "^7.27.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.27.4", "@babel/types": "^7.27.3", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g=="], + + "@babel/generator": ["@babel/generator@7.27.5", "", { "dependencies": { "@babel/parser": "^7.27.5", "@babel/types": "^7.27.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.27.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.27.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.27.6", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.27.6" } }, "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug=="], + + "@babel/parser": ["@babel/parser@7.27.5", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg=="], + + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], + + "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], + + "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], + + "@babel/traverse": ["@babel/traverse@7.27.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.3", "@babel/parser": "^7.27.4", "@babel/template": "^7.27.2", "@babel/types": "^7.27.3", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA=="], + + "@babel/types": ["@babel/types@7.27.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q=="], + + "@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="], + + "@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.5", "", { "os": "android", "cpu": "arm" }, "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.5", "", { "os": "android", "cpu": "arm64" }, "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.5", "", { "os": "android", "cpu": "x64" }, "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.5", "", { "os": "linux", "cpu": "arm" }, "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.5", "", { "os": "linux", "cpu": "x64" }, "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.5", "", { "os": "none", "cpu": "arm64" }, "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.5", "", { "os": "none", "cpu": "x64" }, "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.5", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g=="], + + "@floating-ui/core": ["@floating-ui/core@1.7.1", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.1", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/utils": "^0.2.9" } }, "sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ=="], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.3", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-huMBfiU9UnQ2oBwIhgzyIiSpVgvlDstU8CX0AF+wS+KzmYMs0J2a3GwuFHV1Lz+jlrQGeC1fF+Nv0QoumyV0bA=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="], + + "@hapi/hoek": ["@hapi/hoek@9.3.0", "", {}, "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ=="], + + "@hapi/topo": ["@hapi/topo@5.1.0", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg=="], + + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], + + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="], + + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], + + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], + + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], + + "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw=="], + + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], + + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ=="], + + "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ=="], + + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA=="], + + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], + + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], + + "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.10", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew=="], + + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.7", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ=="], + + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], + + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA=="], + + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q=="], + + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], + + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], + + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], + + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], + + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + + "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], + + "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], + + "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.11", "", {}, "sha512-L/gAA/hyCSuzTF1ftlzUSI/IKr2POHsv1Dd78GfqkR83KMNuswWD61JxGV2L7nRwBBBSDr6R1gCkdTmoN7W4ag=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.43.0", "", { "os": "android", "cpu": "arm" }, "sha512-Krjy9awJl6rKbruhQDgivNbD1WuLb8xAclM4IR4cN5pHGAs2oIMMQJEiC3IC/9TZJ+QZkmZhlMO/6MBGxPidpw=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.43.0", "", { "os": "android", "cpu": "arm64" }, "sha512-ss4YJwRt5I63454Rpj+mXCXicakdFmKnUNxr1dLK+5rv5FJgAxnN7s31a5VchRYxCFWdmnDWKd0wbAdTr0J5EA=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.43.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eKoL8ykZ7zz8MjgBenEF2OoTNFAPFz1/lyJ5UmmFSz5jW+7XbH1+MAgCVHy72aG59rbuQLcJeiMrP8qP5d/N0A=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.43.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-SYwXJgaBYW33Wi/q4ubN+ldWC4DzQY62S4Ll2dgfr/dbPoF50dlQwEaEHSKrQdSjC6oIe1WgzosoaNoHCdNuMg=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.43.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-SV+U5sSo0yujrjzBF7/YidieK2iF6E7MdF6EbYxNz94lA+R0wKl3SiixGyG/9Klab6uNBIqsN7j4Y/Fya7wAjQ=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.43.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J7uCsiV13L/VOeHJBo5SjasKiGxJ0g+nQTrBkAsmQBIdil3KhPnSE9GnRon4ejX1XDdsmK/l30IYLiAaQEO0Cg=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.43.0", "", { "os": "linux", "cpu": "arm" }, "sha512-gTJ/JnnjCMc15uwB10TTATBEhK9meBIY+gXP4s0sHD1zHOaIh4Dmy1X9wup18IiY9tTNk5gJc4yx9ctj/fjrIw=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.43.0", "", { "os": "linux", "cpu": "arm" }, "sha512-ZJ3gZynL1LDSIvRfz0qXtTNs56n5DI2Mq+WACWZ7yGHFUEirHBRt7fyIk0NsCKhmRhn7WAcjgSkSVVxKlPNFFw=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.43.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-8FnkipasmOOSSlfucGYEu58U8cxEdhziKjPD2FIa0ONVMxvl/hmONtX/7y4vGjdUhjcTHlKlDhw3H9t98fPvyA=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.43.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-KPPyAdlcIZ6S9C3S2cndXDkV0Bb1OSMsX0Eelr2Bay4EsF9yi9u9uzc9RniK3mcUGCLhWY9oLr6er80P5DE6XA=="], + + "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.43.0", "", { "os": "linux", "cpu": "none" }, "sha512-HPGDIH0/ZzAZjvtlXj6g+KDQ9ZMHfSP553za7o2Odegb/BEfwJcR0Sw0RLNpQ9nC6Gy8s+3mSS9xjZ0n3rhcYg=="], + + "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.43.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gEmwbOws4U4GLAJDhhtSPWPXUzDfMRedT3hFMyRAvM9Mrnj+dJIFIeL7otsv2WF3D7GrV0GIewW0y28dOYWkmw=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.43.0", "", { "os": "linux", "cpu": "none" }, "sha512-XXKvo2e+wFtXZF/9xoWohHg+MuRnvO29TI5Hqe9xwN5uN8NKUYy7tXUG3EZAlfchufNCTHNGjEx7uN78KsBo0g=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.43.0", "", { "os": "linux", "cpu": "none" }, "sha512-ruf3hPWhjw6uDFsOAzmbNIvlXFXlBQ4nk57Sec8E8rUxs/AI4HD6xmiiasOOx/3QxS2f5eQMKTAwk7KHwpzr/Q=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.43.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-QmNIAqDiEMEvFV15rsSnjoSmO0+eJLoKRD9EAa9rrYNwO/XRCtOGM3A5A0X+wmG+XRrw9Fxdsw+LnyYiZWWcVw=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.43.0", "", { "os": "linux", "cpu": "x64" }, "sha512-jAHr/S0iiBtFyzjhOkAics/2SrXE092qyqEg96e90L3t9Op8OTzS6+IX0Fy5wCt2+KqeHAkti+eitV0wvblEoQ=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.43.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3yATWgdeXyuHtBhrLt98w+5fKurdqvs8B53LaoKD7P7H7FKOONLsBVMNl9ghPQZQuYcceV5CDyPfyfGpMWD9mQ=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.43.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-wVzXp2qDSCOpcBCT5WRWLmpJRIzv23valvcTwMHEobkjippNf+C3ys/+wf07poPkeNix0paTNemB2XrHr2TnGw=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.43.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-fYCTEyzf8d+7diCw8b+asvWDCLMjsCEA8alvtAutqJOJp/wL5hs1rWSqJ1vkjgW0L2NB4bsYJrpKkiIPRR9dvw=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.43.0", "", { "os": "win32", "cpu": "x64" }, "sha512-SnGhLiE5rlK0ofq8kzuDkM0g7FN1s5VYY+YSMTibP7CqShxCQvqtNxTARS4xX4PFJfHjG0ZQYX9iGzI3FQh5Aw=="], + + "@sideway/address": ["@sideway/address@4.1.5", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q=="], + + "@sideway/formula": ["@sideway/formula@3.0.1", "", {}, "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg=="], + + "@sideway/pinpoint": ["@sideway/pinpoint@2.0.0", "", {}, "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.1.9", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.9" } }, "sha512-ZFsgw6lbtcZKYPWvf6zAuCVSuer7UQ2Z5P8BETHcpA4x/3NwOjAIXmRnYfG77F14f9bPeuR4GaNz3ji1JkQMeQ=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.9", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.9", "@tailwindcss/oxide-darwin-arm64": "4.1.9", "@tailwindcss/oxide-darwin-x64": "4.1.9", "@tailwindcss/oxide-freebsd-x64": "4.1.9", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.9", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.9", "@tailwindcss/oxide-linux-arm64-musl": "4.1.9", "@tailwindcss/oxide-linux-x64-gnu": "4.1.9", "@tailwindcss/oxide-linux-x64-musl": "4.1.9", "@tailwindcss/oxide-wasm32-wasi": "4.1.9", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.9", "@tailwindcss/oxide-win32-x64-msvc": "4.1.9" } }, "sha512-oqjNxOBt1iNRAywjiH+VFsfovx/hVt4mxe0kOkRMAbbcCwbJg5e2AweFqyGN7gtmE1TJXnvnyX7RWTR1l72ciQ=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.9", "", { "os": "android", "cpu": "arm64" }, "sha512-X4mBUUJ3DPqODhtdT5Ju55feJwBN+hP855Z7c0t11Jzece9KRtdM41ljMrCcureKMh96mcOh2gxahkp1yE+BOQ=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jnWnqz71ZLXUbJLW53m9dSQakLBfaWxAd9TAibimrNdQfZKyie+xGppdDCZExtYwUdflt3kOT9y1JUgYXVEQmw=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-+Ui6LlvZ6aCPvSwv3l16nYb6gu1N6RamFz7hSu5aqaiPrDQqD1LPT/e8r2/laSVwFjRyOZxQQ/gvGxP3ihA2rw=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.9", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BWqCh0uoXMprwWfG7+oyPW53VCh6G08pxY0IIN/i5DQTpPnCJ4zm2W8neH9kW1v1f6RXP3b2qQjAzrAcnQ5e9w=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.9", "", { "os": "linux", "cpu": "arm" }, "sha512-U8itjQb5TVc80aV5Yo+JtKo+qS95CV4XLrKEtSLQFoTD/c9j3jk4WZipYT+9Jxqem29qCMRPxjEZ3s+wTT4XCw=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-dKlGraoNvyTrR7ovLw3Id9yTwc+l0NYg8bwOkYqk+zltvGns8bPvVr6PH5jATdc75kCGd6kDRmP4p1LwqCnPJQ=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-qCZ4QTrZaBEgNM13pGjvakdmid1Kw3CUCEQzgVAn64Iud7zSxOGwK1usg+hrwrOfFH7vXZZr8OhzC8fJTRq5NA=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.9", "", { "os": "linux", "cpu": "x64" }, "sha512-bmzkAWQjRlY9udmg/a1bOtZpV14ZCdrB74PZrd7Oz/wK62Rk+m9+UV3BsgGfOghyO5Qu5ZDciADzDMZbi9n1+g=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.9", "", { "os": "linux", "cpu": "x64" }, "sha512-NpvPQsXj1raDHhd+g2SUvZQoTPWfYAsyYo9h4ZqV7EOmR+aj7LCAE5hnXNnrJ5Egy/NiO3Hs7BNpSbsPEOpORg=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.9", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@emnapi/wasi-threads": "^1.0.2", "@napi-rs/wasm-runtime": "^0.2.10", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-G93Yuf3xrpTxDUCSh685d1dvOkqOB0Gy+Bchv9Zy3k+lNw/9SEgsHit50xdvp1/p9yRH2TeDHJeDLUiV4mlTkA=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-Eq9FZzZe/NPkUiSMY+eY7r5l7msuFlm6wC6lnV11m8885z0vs9zx48AKTfw0UbVecTRV5wMxKb3Kmzx2LoUIWg=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.9", "", { "os": "win32", "cpu": "x64" }, "sha512-oZ4zkthMXMJN2w/vu3jEfuqWTW7n8giGYDV/SfhBGRNehNMOBqh3YUAEv+8fv2YDJEzL4JpXTNTiSXW3UiUwBw=="], + + "@tailwindcss/vite": ["@tailwindcss/vite@4.1.9", "", { "dependencies": { "@tailwindcss/node": "4.1.9", "@tailwindcss/oxide": "4.1.9", "tailwindcss": "4.1.9" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-JdcROJysSGRpDq0JT5XPxRjF3rq4QnZD/PsNUVIQrMyYHUIBxRFPTUmGlWjy24igeC3rAgcRIDGLSd9AsljW5A=="], + + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], + + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="], + + "@types/bcryptjs": ["@types/bcryptjs@3.0.0", "", { "dependencies": { "bcryptjs": "*" } }, "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg=="], + + "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], + + "@types/bun": ["@types/bun@1.2.15", "", { "dependencies": { "bun-types": "1.2.15" } }, "sha512-U1ljPdBEphF0nw1MIk0hI7kPg7dFdPyM7EenHsp6W5loNHl7zqy6JQf/RKCgnUn2KDzUpkBwHPnEJEjII594bA=="], + + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + + "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="], + + "@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], + + "@types/express": ["@types/express@4.17.23", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "*" } }, "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ=="], + + "@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A=="], + + "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], + + "@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.9", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ=="], + + "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], + + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + + "@types/multer": ["@types/multer@1.4.13", "", { "dependencies": { "@types/express": "*" } }, "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw=="], + + "@types/node": ["@types/node@24.0.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-yZQa2zm87aRVcqDyH5+4Hv9KYgSdgwX1rFnGvpbzMaC7YAljmhBET93TPiTd3ObwTL+gSpIzPKg5BqVxdCvxKg=="], + + "@types/node-cron": ["@types/node-cron@3.0.11", "", {}, "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg=="], + + "@types/node-fetch": ["@types/node-fetch@2.6.12", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.0" } }, "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA=="], + + "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], + + "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], + + "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], + + "@types/react-dom": ["@types/react-dom@19.1.6", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw=="], + + "@types/send": ["@types/send@0.17.5", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w=="], + + "@types/serve-static": ["@types/serve-static@1.15.8", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg=="], + + "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@4.5.2", "", { "dependencies": { "@babel/core": "^7.27.4", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.11", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" } }, "sha512-QNVT3/Lxx99nMQWJWF7K4N6apUEuT0KlZA3mx/mVaoGj3smm/8rc8ezz15J1pcbcjDK0V15rpHetVfya08r76Q=="], + + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], + + "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + + "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], + + "append-field": ["append-field@1.0.0", "", {}, "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw=="], + + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], + + "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], + + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "bcryptjs": ["bcryptjs@3.0.2", "", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog=="], + + "body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], + + "browserslist": ["browserslist@4.25.0", "", { "dependencies": { "caniuse-lite": "^1.0.30001718", "electron-to-chromium": "^1.5.160", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA=="], + + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + + "bun-types": ["bun-types@1.2.15", "", { "dependencies": { "@types/node": "*" } }, "sha512-NarRIaS+iOaQU1JPfyKhZm4AsUOrwUOqRNHY0XxI8GI8jYxiLXLcdjYMG9UKS+fwWasc1uw1htV9AX24dD+p4w=="], + + "busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001722", "", {}, "sha512-DCQHBBZtiK6JVkAGw7drvAMK0Q0POD/xZvEmDp6baiMMP6QXXk9HpD6mNYBZWhOPG6LvIDb82ITqtWjhDckHCA=="], + + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + + "cloudflare": ["cloudflare@4.3.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-C+4Jhsl/OY4V5sykRB1yJxComDld5BkKW1xd3s0MDJ1yYamT2sFAoC2FEUQg5zipyxMaaGU4N7hZ6il+gfJxZg=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="], + + "color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + + "colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="], + + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + + "commander": ["commander@14.0.0", "", {}, "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA=="], + + "concat-stream": ["concat-stream@1.6.2", "", { "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^2.2.2", "typedarray": "^0.0.6" } }, "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw=="], + + "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="], + + "cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="], + + "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], + + "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], + + "detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="], + + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + + "dotenv": ["dotenv@16.5.0", "", {}, "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.166", "", {}, "sha512-QPWqHL0BglzPYyJJ1zSSmwFFL6MFXhbACOCcsCdUMCkzPdS9/OIBVxg516X/Ado2qwAq8k0nJJ7phQPCqiaFAw=="], + + "enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "esbuild": ["esbuild@0.25.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.5", "@esbuild/android-arm": "0.25.5", "@esbuild/android-arm64": "0.25.5", "@esbuild/android-x64": "0.25.5", "@esbuild/darwin-arm64": "0.25.5", "@esbuild/darwin-x64": "0.25.5", "@esbuild/freebsd-arm64": "0.25.5", "@esbuild/freebsd-x64": "0.25.5", "@esbuild/linux-arm": "0.25.5", "@esbuild/linux-arm64": "0.25.5", "@esbuild/linux-ia32": "0.25.5", "@esbuild/linux-loong64": "0.25.5", "@esbuild/linux-mips64el": "0.25.5", "@esbuild/linux-ppc64": "0.25.5", "@esbuild/linux-riscv64": "0.25.5", "@esbuild/linux-s390x": "0.25.5", "@esbuild/linux-x64": "0.25.5", "@esbuild/netbsd-arm64": "0.25.5", "@esbuild/netbsd-x64": "0.25.5", "@esbuild/openbsd-arm64": "0.25.5", "@esbuild/openbsd-x64": "0.25.5", "@esbuild/sunos-x64": "0.25.5", "@esbuild/win32-arm64": "0.25.5", "@esbuild/win32-ia32": "0.25.5", "@esbuild/win32-x64": "0.25.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], + + "express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="], + + "fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="], + + "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], + + "finalhandler": ["finalhandler@1.3.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ=="], + + "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], + + "form-data": ["form-data@4.0.3", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA=="], + + "form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="], + + "formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "helmet": ["helmet@7.2.0", "", {}, "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw=="], + + "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], + + "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], + + "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], + + "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + + "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], + + "joi": ["joi@17.13.3", "", { "dependencies": { "@hapi/hoek": "^9.3.0", "@hapi/topo": "^5.1.0", "@sideway/address": "^4.1.5", "@sideway/formula": "^3.0.1", "@sideway/pinpoint": "^2.0.0" } }, "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="], + + "jwa": ["jwa@1.4.2", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw=="], + + "jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="], + + "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], + + "lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="], + + "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], + + "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], + + "lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="], + + "lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="], + + "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="], + + "lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="], + + "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], + + "logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "lucide-react": ["lucide-react@0.514.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-HXD0OAMd+JM2xCjlwG1EGW9Nuab64dhjO3+MvdyD+pSUeOTBaVAPhQblKIYmmX4RyBYbdzW0VWnJpjJmxWGr6w=="], + + "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + + "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], + + "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], + + "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="], + + "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "multer": ["multer@1.4.5-lts.2", "", { "dependencies": { "append-field": "^1.0.0", "busboy": "^1.0.0", "concat-stream": "^1.5.2", "mkdirp": "^0.5.4", "object-assign": "^4.1.1", "type-is": "^1.6.4", "xtend": "^4.0.0" } }, "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + + "node-cron": ["node-cron@3.0.3", "", { "dependencies": { "uuid": "8.3.2" } }, "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A=="], + + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + + "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], + + "postcss": ["postcss@8.5.5", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg=="], + + "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="], + + "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], + + "react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="], + + "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], + + "react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="], + + "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + + "react-router": ["react-router@7.6.2", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-U7Nv3y+bMimgWjhlT5CRdzHPu2/KVmqPwKUCChW8en5P3znxUqwlYFlbmyj8Rgp1SF6zs5X4+77kBVknkg6a0w=="], + + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "rollup": ["rollup@4.43.0", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.43.0", "@rollup/rollup-android-arm64": "4.43.0", "@rollup/rollup-darwin-arm64": "4.43.0", "@rollup/rollup-darwin-x64": "4.43.0", "@rollup/rollup-freebsd-arm64": "4.43.0", "@rollup/rollup-freebsd-x64": "4.43.0", "@rollup/rollup-linux-arm-gnueabihf": "4.43.0", "@rollup/rollup-linux-arm-musleabihf": "4.43.0", "@rollup/rollup-linux-arm64-gnu": "4.43.0", "@rollup/rollup-linux-arm64-musl": "4.43.0", "@rollup/rollup-linux-loongarch64-gnu": "4.43.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.43.0", "@rollup/rollup-linux-riscv64-gnu": "4.43.0", "@rollup/rollup-linux-riscv64-musl": "4.43.0", "@rollup/rollup-linux-s390x-gnu": "4.43.0", "@rollup/rollup-linux-x64-gnu": "4.43.0", "@rollup/rollup-linux-x64-musl": "4.43.0", "@rollup/rollup-win32-arm64-msvc": "4.43.0", "@rollup/rollup-win32-ia32-msvc": "4.43.0", "@rollup/rollup-win32-x64-msvc": "4.43.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-wdN2Kd3Twh8MAEOEJZsuxuLKCsBEo4PVNLK6tQWAn10VhsVewQLzcucMgLolRlhFybGxfclbPeEYBaP6RvUFGg=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], + + "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="], + + "serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="], + + "set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], + + "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + + "streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="], + + "tailwindcss": ["tailwindcss@4.1.9", "", {}, "sha512-anBZRcvfNMsQdHB9XSGzAtIQWlhs49uK75jfkwrqjRUbjt4d7q9RE1wR1xWyfYZhLFnFX4ahWp88Au2lcEw5IQ=="], + + "tapable": ["tapable@2.2.2", "", {}, "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg=="], + + "tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="], + + "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], + + "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tw-animate-css": ["tw-animate-css@1.3.4", "", {}, "sha512-dd1Ht6/YQHcNbq0znIT6dG8uhO7Ce+VIIhZUhjsryXsMPJQz3bZg7Q2eNzLwipb25bRZslGb2myio5mScd1TFg=="], + + "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + + "typedarray": ["typedarray@0.0.6", "", {}, "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="], + + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + + "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], + + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], + + "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="], + + "web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="], + + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="], + + "winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="], + + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + + "@babel/core/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/traverse/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="], + + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.11", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.9.0" }, "bundled": true }, "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="], + + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "cloudflare/@types/node": ["@types/node@18.19.111", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-90sGdgA+QLJr1F9X79tQuEut0gEYIfkX9pydI4XGRgvFo9g2JWswefI+WUSUHPYVBHYSEfTEqBxA5hQvAZB3Mw=="], + + "concat-stream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], + + "debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "react-router/cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], + + "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], + + "tar/mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="], + + "cloudflare/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "concat-stream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "concat-stream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + } +} diff --git a/cloudflare_zones.json b/cloudflare_zones.json new file mode 100644 index 0000000..a65843e --- /dev/null +++ b/cloudflare_zones.json @@ -0,0 +1,859 @@ +{ + "options": { + "method": "get", + "path": "/zones", + "query": {} + }, + "response": {}, + "body": { + "result": [ + { + "id": "3ee3e5559089d1e812fa088e0955a989", + "name": "arc1.xyz", + "status": "active", + "paused": false, + "type": "full", + "development_mode": 0, + "name_servers": [ + "emerson.ns.cloudflare.com", + "maria.ns.cloudflare.com" + ], + "original_name_servers": [ + "ns-cloud-a1.googledomains.com", + "ns-cloud-a2.googledomains.com", + "ns-cloud-a3.googledomains.com", + "ns-cloud-a4.googledomains.com" + ], + "original_registrar": null, + "original_dnshost": null, + "modified_on": "2023-12-23T08:24:46.967231Z", + "created_on": "2023-12-23T08:03:24.100233Z", + "activated_on": "2023-12-23T08:24:46.967231Z", + "meta": { + "step": 2, + "custom_certificate_quota": 0, + "page_rule_quota": 3, + "phishing_detected": false + }, + "owner": { + "id": null, + "type": "user", + "email": null + }, + "account": { + "id": "9ae3d1245a28663b3bea2cf84a5a4877", + "name": "Hunternick133@gmail.com's Account" + }, + "tenant": { + "id": null, + "name": null + }, + "tenant_unit": { + "id": null + }, + "permissions": [ + "#dns_records:edit", + "#dns_records:read", + "#zone:read" + ], + "plan": { + "id": "0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "name": "Free Website", + "price": 0, + "currency": "USD", + "frequency": "", + "is_subscribed": false, + "can_subscribe": false, + "legacy_id": "free", + "legacy_discount": false, + "externally_managed": false + } + }, + { + "id": "9d050d996cfc81b76f67697cc81e51cc", + "name": "arcfire.tv", + "status": "active", + "paused": false, + "type": "full", + "development_mode": 0, + "name_servers": [ + "emerson.ns.cloudflare.com", + "maria.ns.cloudflare.com" + ], + "original_name_servers": null, + "original_registrar": null, + "original_dnshost": null, + "modified_on": "2024-06-05T19:09:40.598805Z", + "created_on": "2022-09-25T23:25:58.660773Z", + "activated_on": "2022-09-26T03:42:39.678297Z", + "meta": { + "step": 2, + "custom_certificate_quota": 0, + "page_rule_quota": 3, + "phishing_detected": false + }, + "owner": { + "id": null, + "type": "user", + "email": null + }, + "account": { + "id": "9ae3d1245a28663b3bea2cf84a5a4877", + "name": "Hunternick133@gmail.com's Account" + }, + "tenant": { + "id": null, + "name": null + }, + "tenant_unit": { + "id": null + }, + "permissions": [ + "#dns_records:edit", + "#dns_records:read", + "#zone:read" + ], + "plan": { + "id": "0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "name": "Free Website", + "price": 0, + "currency": "USD", + "frequency": "", + "is_subscribed": false, + "can_subscribe": false, + "legacy_id": "free", + "legacy_discount": false, + "externally_managed": false + } + }, + { + "id": "592167cc7cfa67a24dd2861cd4b82b00", + "name": "hcws.dev", + "status": "active", + "paused": false, + "type": "full", + "development_mode": 0, + "name_servers": [ + "emerson.ns.cloudflare.com", + "maria.ns.cloudflare.com" + ], + "original_name_servers": null, + "original_registrar": null, + "original_dnshost": null, + "modified_on": "2024-06-06T05:53:04.048390Z", + "created_on": "2022-10-25T18:52:04.979140Z", + "activated_on": "2022-10-25T18:58:14.534925Z", + "meta": { + "step": 2, + "custom_certificate_quota": 0, + "page_rule_quota": 3, + "phishing_detected": false + }, + "owner": { + "id": null, + "type": "user", + "email": null + }, + "account": { + "id": "9ae3d1245a28663b3bea2cf84a5a4877", + "name": "Hunternick133@gmail.com's Account" + }, + "tenant": { + "id": null, + "name": null + }, + "tenant_unit": { + "id": null + }, + "permissions": [ + "#dns_records:edit", + "#dns_records:read", + "#zone:read" + ], + "plan": { + "id": "0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "name": "Free Website", + "price": 0, + "currency": "USD", + "frequency": "", + "is_subscribed": false, + "can_subscribe": false, + "legacy_id": "free", + "legacy_discount": false, + "externally_managed": false + } + }, + { + "id": "207a90944c84f72e9c8e64910e806cd9", + "name": "hcws.one", + "status": "active", + "paused": false, + "type": "full", + "development_mode": 0, + "name_servers": [ + "emerson.ns.cloudflare.com", + "maria.ns.cloudflare.com" + ], + "original_name_servers": [ + "ns-cloud-b1.googledomains.com", + "ns-cloud-b3.googledomains.com", + "ns-cloud-b4.googledomains.com", + "ns-cloud-b2.googledomains.com" + ], + "original_registrar": "google llc (id: 895)", + "original_dnshost": null, + "modified_on": "2022-03-29T17:37:45.711682Z", + "created_on": "2022-03-29T17:31:19.524978Z", + "activated_on": "2022-03-29T17:37:45.711682Z", + "meta": { + "step": 2, + "custom_certificate_quota": 0, + "page_rule_quota": 3, + "phishing_detected": false + }, + "owner": { + "id": null, + "type": "user", + "email": null + }, + "account": { + "id": "9ae3d1245a28663b3bea2cf84a5a4877", + "name": "Hunternick133@gmail.com's Account" + }, + "tenant": { + "id": null, + "name": null + }, + "tenant_unit": { + "id": null + }, + "permissions": [ + "#dns_records:edit", + "#dns_records:read", + "#zone:read" + ], + "plan": { + "id": "0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "name": "Free Website", + "price": 0, + "currency": "USD", + "frequency": "", + "is_subscribed": false, + "can_subscribe": false, + "legacy_id": "free", + "legacy_discount": false, + "externally_managed": false + } + }, + { + "id": "61e8f31d061c73dd9949e21123202f61", + "name": "snarecords.com", + "status": "active", + "paused": false, + "type": "full", + "development_mode": 0, + "name_servers": [ + "emerson.ns.cloudflare.com", + "maria.ns.cloudflare.com" + ], + "original_name_servers": null, + "original_registrar": null, + "original_dnshost": null, + "modified_on": "2024-06-06T02:43:54.282478Z", + "created_on": "2023-10-15T21:27:21.237947Z", + "activated_on": "2023-10-15T21:27:22.808673Z", + "meta": { + "step": 4, + "custom_certificate_quota": 0, + "page_rule_quota": 3, + "phishing_detected": false + }, + "owner": { + "id": null, + "type": "user", + "email": null + }, + "account": { + "id": "9ae3d1245a28663b3bea2cf84a5a4877", + "name": "Hunternick133@gmail.com's Account" + }, + "tenant": { + "id": null, + "name": null + }, + "tenant_unit": { + "id": null + }, + "permissions": [ + "#dns_records:edit", + "#dns_records:read", + "#zone:read" + ], + "plan": { + "id": "0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "name": "Free Website", + "price": 0, + "currency": "USD", + "frequency": "", + "is_subscribed": false, + "can_subscribe": false, + "legacy_id": "free", + "legacy_discount": false, + "externally_managed": false + } + }, + { + "id": "00dbbf5f0c05b8e634bc3a57f50352f8", + "name": "stellanace.com", + "status": "active", + "paused": false, + "type": "full", + "development_mode": 0, + "name_servers": [ + "emerson.ns.cloudflare.com", + "maria.ns.cloudflare.com" + ], + "original_name_servers": [ + "ns-cloud-d1.googledomains.com", + "ns-cloud-d2.googledomains.com", + "ns-cloud-d3.googledomains.com", + "ns-cloud-d4.googledomains.com" + ], + "original_registrar": "google llc (id: 895)", + "original_dnshost": null, + "modified_on": "2024-06-06T01:50:33.497179Z", + "created_on": "2023-06-17T03:16:45.399557Z", + "activated_on": "2023-06-17T03:23:34.039367Z", + "meta": { + "step": 2, + "custom_certificate_quota": 0, + "page_rule_quota": 3, + "phishing_detected": false + }, + "owner": { + "id": null, + "type": "user", + "email": null + }, + "account": { + "id": "9ae3d1245a28663b3bea2cf84a5a4877", + "name": "Hunternick133@gmail.com's Account" + }, + "tenant": { + "id": null, + "name": null + }, + "tenant_unit": { + "id": null + }, + "permissions": [ + "#dns_records:edit", + "#dns_records:read", + "#zone:read" + ], + "plan": { + "id": "0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "name": "Free Website", + "price": 0, + "currency": "USD", + "frequency": "", + "is_subscribed": false, + "can_subscribe": false, + "legacy_id": "free", + "legacy_discount": false, + "externally_managed": false + } + }, + { + "id": "98a8e392eecf04fb8f3099840e309cb2", + "name": "umbc.dev", + "status": "active", + "paused": false, + "type": "full", + "development_mode": 0, + "name_servers": [ + "emerson.ns.cloudflare.com", + "maria.ns.cloudflare.com" + ], + "original_name_servers": null, + "original_registrar": null, + "original_dnshost": null, + "modified_on": "2024-10-16T04:12:14.293880Z", + "created_on": "2024-10-16T04:12:11.075269Z", + "activated_on": "2024-10-16T04:12:14.135577Z", + "meta": { + "step": 4, + "custom_certificate_quota": 0, + "page_rule_quota": 3, + "phishing_detected": false + }, + "owner": { + "id": null, + "type": "user", + "email": null + }, + "account": { + "id": "9ae3d1245a28663b3bea2cf84a5a4877", + "name": "Hunternick133@gmail.com's Account" + }, + "tenant": { + "id": null, + "name": null + }, + "tenant_unit": { + "id": null + }, + "permissions": [ + "#dns_records:edit", + "#dns_records:read", + "#zone:read" + ], + "plan": { + "id": "0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "name": "Free Website", + "price": 0, + "currency": "USD", + "frequency": "", + "is_subscribed": false, + "can_subscribe": false, + "legacy_id": "free", + "legacy_discount": false, + "externally_managed": false + } + } + ], + "result_info": { + "page": 1, + "per_page": 20, + "total_pages": 1, + "count": 7, + "total_count": 7 + }, + "success": true, + "errors": [], + "messages": [] + }, + "result": [ + { + "id": "3ee3e5559089d1e812fa088e0955a989", + "name": "arc1.xyz", + "status": "active", + "paused": false, + "type": "full", + "development_mode": 0, + "name_servers": [ + "emerson.ns.cloudflare.com", + "maria.ns.cloudflare.com" + ], + "original_name_servers": [ + "ns-cloud-a1.googledomains.com", + "ns-cloud-a2.googledomains.com", + "ns-cloud-a3.googledomains.com", + "ns-cloud-a4.googledomains.com" + ], + "original_registrar": null, + "original_dnshost": null, + "modified_on": "2023-12-23T08:24:46.967231Z", + "created_on": "2023-12-23T08:03:24.100233Z", + "activated_on": "2023-12-23T08:24:46.967231Z", + "meta": { + "step": 2, + "custom_certificate_quota": 0, + "page_rule_quota": 3, + "phishing_detected": false + }, + "owner": { + "id": null, + "type": "user", + "email": null + }, + "account": { + "id": "9ae3d1245a28663b3bea2cf84a5a4877", + "name": "Hunternick133@gmail.com's Account" + }, + "tenant": { + "id": null, + "name": null + }, + "tenant_unit": { + "id": null + }, + "permissions": [ + "#dns_records:edit", + "#dns_records:read", + "#zone:read" + ], + "plan": { + "id": "0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "name": "Free Website", + "price": 0, + "currency": "USD", + "frequency": "", + "is_subscribed": false, + "can_subscribe": false, + "legacy_id": "free", + "legacy_discount": false, + "externally_managed": false + } + }, + { + "id": "9d050d996cfc81b76f67697cc81e51cc", + "name": "arcfire.tv", + "status": "active", + "paused": false, + "type": "full", + "development_mode": 0, + "name_servers": [ + "emerson.ns.cloudflare.com", + "maria.ns.cloudflare.com" + ], + "original_name_servers": null, + "original_registrar": null, + "original_dnshost": null, + "modified_on": "2024-06-05T19:09:40.598805Z", + "created_on": "2022-09-25T23:25:58.660773Z", + "activated_on": "2022-09-26T03:42:39.678297Z", + "meta": { + "step": 2, + "custom_certificate_quota": 0, + "page_rule_quota": 3, + "phishing_detected": false + }, + "owner": { + "id": null, + "type": "user", + "email": null + }, + "account": { + "id": "9ae3d1245a28663b3bea2cf84a5a4877", + "name": "Hunternick133@gmail.com's Account" + }, + "tenant": { + "id": null, + "name": null + }, + "tenant_unit": { + "id": null + }, + "permissions": [ + "#dns_records:edit", + "#dns_records:read", + "#zone:read" + ], + "plan": { + "id": "0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "name": "Free Website", + "price": 0, + "currency": "USD", + "frequency": "", + "is_subscribed": false, + "can_subscribe": false, + "legacy_id": "free", + "legacy_discount": false, + "externally_managed": false + } + }, + { + "id": "592167cc7cfa67a24dd2861cd4b82b00", + "name": "hcws.dev", + "status": "active", + "paused": false, + "type": "full", + "development_mode": 0, + "name_servers": [ + "emerson.ns.cloudflare.com", + "maria.ns.cloudflare.com" + ], + "original_name_servers": null, + "original_registrar": null, + "original_dnshost": null, + "modified_on": "2024-06-06T05:53:04.048390Z", + "created_on": "2022-10-25T18:52:04.979140Z", + "activated_on": "2022-10-25T18:58:14.534925Z", + "meta": { + "step": 2, + "custom_certificate_quota": 0, + "page_rule_quota": 3, + "phishing_detected": false + }, + "owner": { + "id": null, + "type": "user", + "email": null + }, + "account": { + "id": "9ae3d1245a28663b3bea2cf84a5a4877", + "name": "Hunternick133@gmail.com's Account" + }, + "tenant": { + "id": null, + "name": null + }, + "tenant_unit": { + "id": null + }, + "permissions": [ + "#dns_records:edit", + "#dns_records:read", + "#zone:read" + ], + "plan": { + "id": "0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "name": "Free Website", + "price": 0, + "currency": "USD", + "frequency": "", + "is_subscribed": false, + "can_subscribe": false, + "legacy_id": "free", + "legacy_discount": false, + "externally_managed": false + } + }, + { + "id": "207a90944c84f72e9c8e64910e806cd9", + "name": "hcws.one", + "status": "active", + "paused": false, + "type": "full", + "development_mode": 0, + "name_servers": [ + "emerson.ns.cloudflare.com", + "maria.ns.cloudflare.com" + ], + "original_name_servers": [ + "ns-cloud-b1.googledomains.com", + "ns-cloud-b3.googledomains.com", + "ns-cloud-b4.googledomains.com", + "ns-cloud-b2.googledomains.com" + ], + "original_registrar": "google llc (id: 895)", + "original_dnshost": null, + "modified_on": "2022-03-29T17:37:45.711682Z", + "created_on": "2022-03-29T17:31:19.524978Z", + "activated_on": "2022-03-29T17:37:45.711682Z", + "meta": { + "step": 2, + "custom_certificate_quota": 0, + "page_rule_quota": 3, + "phishing_detected": false + }, + "owner": { + "id": null, + "type": "user", + "email": null + }, + "account": { + "id": "9ae3d1245a28663b3bea2cf84a5a4877", + "name": "Hunternick133@gmail.com's Account" + }, + "tenant": { + "id": null, + "name": null + }, + "tenant_unit": { + "id": null + }, + "permissions": [ + "#dns_records:edit", + "#dns_records:read", + "#zone:read" + ], + "plan": { + "id": "0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "name": "Free Website", + "price": 0, + "currency": "USD", + "frequency": "", + "is_subscribed": false, + "can_subscribe": false, + "legacy_id": "free", + "legacy_discount": false, + "externally_managed": false + } + }, + { + "id": "61e8f31d061c73dd9949e21123202f61", + "name": "snarecords.com", + "status": "active", + "paused": false, + "type": "full", + "development_mode": 0, + "name_servers": [ + "emerson.ns.cloudflare.com", + "maria.ns.cloudflare.com" + ], + "original_name_servers": null, + "original_registrar": null, + "original_dnshost": null, + "modified_on": "2024-06-06T02:43:54.282478Z", + "created_on": "2023-10-15T21:27:21.237947Z", + "activated_on": "2023-10-15T21:27:22.808673Z", + "meta": { + "step": 4, + "custom_certificate_quota": 0, + "page_rule_quota": 3, + "phishing_detected": false + }, + "owner": { + "id": null, + "type": "user", + "email": null + }, + "account": { + "id": "9ae3d1245a28663b3bea2cf84a5a4877", + "name": "Hunternick133@gmail.com's Account" + }, + "tenant": { + "id": null, + "name": null + }, + "tenant_unit": { + "id": null + }, + "permissions": [ + "#dns_records:edit", + "#dns_records:read", + "#zone:read" + ], + "plan": { + "id": "0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "name": "Free Website", + "price": 0, + "currency": "USD", + "frequency": "", + "is_subscribed": false, + "can_subscribe": false, + "legacy_id": "free", + "legacy_discount": false, + "externally_managed": false + } + }, + { + "id": "00dbbf5f0c05b8e634bc3a57f50352f8", + "name": "stellanace.com", + "status": "active", + "paused": false, + "type": "full", + "development_mode": 0, + "name_servers": [ + "emerson.ns.cloudflare.com", + "maria.ns.cloudflare.com" + ], + "original_name_servers": [ + "ns-cloud-d1.googledomains.com", + "ns-cloud-d2.googledomains.com", + "ns-cloud-d3.googledomains.com", + "ns-cloud-d4.googledomains.com" + ], + "original_registrar": "google llc (id: 895)", + "original_dnshost": null, + "modified_on": "2024-06-06T01:50:33.497179Z", + "created_on": "2023-06-17T03:16:45.399557Z", + "activated_on": "2023-06-17T03:23:34.039367Z", + "meta": { + "step": 2, + "custom_certificate_quota": 0, + "page_rule_quota": 3, + "phishing_detected": false + }, + "owner": { + "id": null, + "type": "user", + "email": null + }, + "account": { + "id": "9ae3d1245a28663b3bea2cf84a5a4877", + "name": "Hunternick133@gmail.com's Account" + }, + "tenant": { + "id": null, + "name": null + }, + "tenant_unit": { + "id": null + }, + "permissions": [ + "#dns_records:edit", + "#dns_records:read", + "#zone:read" + ], + "plan": { + "id": "0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "name": "Free Website", + "price": 0, + "currency": "USD", + "frequency": "", + "is_subscribed": false, + "can_subscribe": false, + "legacy_id": "free", + "legacy_discount": false, + "externally_managed": false + } + }, + { + "id": "98a8e392eecf04fb8f3099840e309cb2", + "name": "umbc.dev", + "status": "active", + "paused": false, + "type": "full", + "development_mode": 0, + "name_servers": [ + "emerson.ns.cloudflare.com", + "maria.ns.cloudflare.com" + ], + "original_name_servers": null, + "original_registrar": null, + "original_dnshost": null, + "modified_on": "2024-10-16T04:12:14.293880Z", + "created_on": "2024-10-16T04:12:11.075269Z", + "activated_on": "2024-10-16T04:12:14.135577Z", + "meta": { + "step": 4, + "custom_certificate_quota": 0, + "page_rule_quota": 3, + "phishing_detected": false + }, + "owner": { + "id": null, + "type": "user", + "email": null + }, + "account": { + "id": "9ae3d1245a28663b3bea2cf84a5a4877", + "name": "Hunternick133@gmail.com's Account" + }, + "tenant": { + "id": null, + "name": null + }, + "tenant_unit": { + "id": null + }, + "permissions": [ + "#dns_records:edit", + "#dns_records:read", + "#zone:read" + ], + "plan": { + "id": "0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "name": "Free Website", + "price": 0, + "currency": "USD", + "frequency": "", + "is_subscribed": false, + "can_subscribe": false, + "legacy_id": "free", + "legacy_discount": false, + "externally_managed": false + } + } + ], + "result_info": { + "page": 1, + "per_page": 20, + "total_pages": 1, + "count": 7, + "total_count": 7 + } +} \ No newline at end of file diff --git a/components.json b/components.json new file mode 100644 index 0000000..38648dd --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/web/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/data/proxy_manager.db b/data/proxy_manager.db new file mode 100644 index 0000000..6ac5e74 Binary files /dev/null and b/data/proxy_manager.db differ diff --git a/data/proxy_manager.db-shm b/data/proxy_manager.db-shm new file mode 100644 index 0000000..fe9ac28 Binary files /dev/null and b/data/proxy_manager.db-shm differ diff --git a/data/proxy_manager.db-wal b/data/proxy_manager.db-wal new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..eed1b11 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,72 @@ +version: '3.8' + +services: + nginx-proxy-manager: + build: . + container_name: nginx-proxy-manager + restart: unless-stopped + ports: + - "80:80" # HTTP + - "443:443" # HTTPS + - "3000:3000" # API (can be removed in production) + volumes: + # Persistent data + - ./data:/app/data + - ./logs:/app/logs + - ./certs:/app/certs + # NGINX configurations + - nginx_configs:/etc/nginx/conf.d + # Let's Encrypt certificates + - acme_data:/root/.acme.sh + environment: + - NODE_ENV=production + - PORT=3000 + - DATABASE_PATH=/app/data/proxy_manager.db + - JWT_SECRET=your-production-jwt-secret-change-this + - JWT_EXPIRES_IN=24h + - ADMIN_USERNAME=admin + - ADMIN_PASSWORD=admin123 + - NGINX_CONFIG_PATH=/etc/nginx/conf.d + - NGINX_BINARY_PATH=/usr/sbin/nginx + - SSL_METHOD=acme.sh + - ACME_SH_PATH=/root/.acme.sh + - CERTBOT_PATH=/usr/bin/certbot + - CUSTOM_CERTS_PATH=/app/certs + - LOG_LEVEL=info + - LOG_FILE=/app/logs/app.log + - CORS_ORIGIN=* + networks: + - proxy-network + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # Optional: Database backup service + backup: + image: alpine:latest + container_name: nginx-proxy-manager-backup + restart: unless-stopped + volumes: + - ./data:/data + - ./backups:/backups + command: > + sh -c " + while true; do + sleep 86400; + tar -czf /backups/backup-$(date +%Y%m%d-%H%M%S).tar.gz /data; + find /backups -name '*.tar.gz' -mtime +7 -delete; + done + " + networks: + - proxy-network + +volumes: + nginx_configs: + acme_data: + +networks: + proxy-network: + driver: bridge diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..b5ae797 --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,91 @@ +user www-data; +worker_processes auto; +pid /run/nginx.pid; +include /etc/nginx/modules-enabled/*.conf; + +events { + worker_connections 768; + multi_accept on; + use epoll; +} + +http { + # Basic Settings + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + client_max_body_size 100m; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # SSL Settings + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384; + + # Logging Settings + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + error_log /var/log/nginx/error.log; + + # Gzip Settings + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/json + application/javascript + application/xml+rss + application/atom+xml + image/svg+xml; + + # Rate limiting + limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; + limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s; + + # Default server block (catchall) + server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + + # API proxy for management interface + location /api/ { + limit_req zone=api burst=20 nodelay; + proxy_pass http://127.0.0.1:3000; + 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; + } + + # Login rate limiting + location /api/auth/login { + limit_req zone=login burst=5 nodelay; + proxy_pass http://127.0.0.1:3000; + 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; + } + + # Default response for unconfigured domains + location / { + return 404 "Domain not configured"; + } + } + + # Include proxy configurations + include /etc/nginx/conf.d/*.conf; +} diff --git a/docker/start.sh b/docker/start.sh new file mode 100644 index 0000000..5792fa7 --- /dev/null +++ b/docker/start.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +# Start script for Docker container + +set -e + +echo "๐Ÿš€ Starting NGINX Proxy Manager..." + +# Initialize database +echo "๐Ÿ“Š Initializing database..." +cd /app && bun src/database/init.ts + +# Start supervisor (manages nginx and our app) +echo "๐Ÿ”ง Starting services..." +exec supervisord -c /etc/supervisor/conf.d/supervisord.conf -n diff --git a/docker/supervisord.conf b/docker/supervisord.conf new file mode 100644 index 0000000..0406f2b --- /dev/null +++ b/docker/supervisord.conf @@ -0,0 +1,37 @@ +[supervisord] +nodaemon=true +user=root +logfile=/var/log/supervisor/supervisord.log +pidfile=/var/run/supervisord.pid + +[program:nginx] +command=nginx -g "daemon off;" +autostart=true +autorestart=true +priority=10 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + +[program:proxy-manager] +command=bun index.ts +directory=/app +autostart=true +autorestart=true +priority=20 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +environment=NODE_ENV=production + +[program:cron] +command=cron -f +autostart=true +autorestart=true +priority=5 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 diff --git a/hcwsone_dns.json b/hcwsone_dns.json new file mode 100644 index 0000000..9b7f817 --- /dev/null +++ b/hcwsone_dns.json @@ -0,0 +1,1261 @@ +{ + "options": { + "method": "get", + "path": "/zones/207a90944c84f72e9c8e64910e806cd9/dns_records", + "query": {} + }, + "response": {}, + "body": { + "result": [ + { + "id": "641aab94b897f5030230f0f64a166159", + "name": "internal.hcws.one", + "type": "A", + "content": "5.161.120.112", + "proxiable": true, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-03-24T00:02:13.266417Z", + "modified_on": "2025-03-24T00:02:13.266417Z" + }, + { + "id": "f4ea688ba2bb851ddfd6e4feb1bf15c5", + "name": "jenna.gr.hcws.one", + "type": "A", + "content": "71.245.169.58", + "proxiable": true, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-03-21T21:48:27.850252Z", + "modified_on": "2025-03-21T21:48:27.850252Z" + }, + { + "id": "cc92dc966be3f56dd276514a71b1497e", + "name": "otter.node.hcws.one", + "type": "A", + "content": "5.161.224.170", + "proxiable": true, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2024-03-23T18:50:55.448289Z", + "modified_on": "2024-03-23T18:50:55.448289Z" + }, + { + "id": "91df297a5f30ad06183294eb3ee75394", + "name": "panel.hcws.one", + "type": "A", + "content": "5.161.224.170", + "proxiable": true, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2024-04-01T02:55:06.338632Z", + "modified_on": "2024-04-01T02:55:06.338632Z" + }, + { + "id": "4fbea82c87a6ca19fb89e78d486a1ff5", + "name": "portainer.gr.hcws.one", + "type": "A", + "content": "71.245.169.58", + "proxiable": true, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-03-21T21:51:32.315716Z", + "modified_on": "2025-03-21T21:51:32.315716Z" + }, + { + "id": "b834463eee54152da3c8a26dada687bb", + "name": "*.pr.hcws.one", + "type": "A", + "content": "5.161.120.112", + "proxiable": true, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-03-23T23:29:58.541953Z", + "modified_on": "2025-03-23T23:30:44.647626Z" + }, + { + "id": "fb0215b8b6a2b225c3f5510393a70900", + "name": "pve.gr.hcws.one", + "type": "A", + "content": "71.245.169.58", + "proxiable": true, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-03-21T21:39:36.945211Z", + "modified_on": "2025-03-21T21:39:36.945211Z" + }, + { + "id": "432c560f483f5b08bd59ad43d62b75be", + "name": "api.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2024-03-28T22:56:51.541021Z", + "modified_on": "2024-03-28T22:56:51.541021Z" + }, + { + "id": "0c0578695c904dd6ca7f349956c0c9bb", + "name": "api-spotify.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2024-05-30T01:06:56.337344Z", + "modified_on": "2024-05-30T01:06:56.337344Z" + }, + { + "id": "bb5e44ce70c0ebe369c8f9227ad98004", + "name": "audiobook.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2024-04-08T14:20:12.912559Z", + "modified_on": "2024-04-08T14:20:12.912559Z" + }, + { + "id": "f2ebb41c59f633952284c44b60fa79fe", + "name": "auth.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2023-12-17T07:14:28.1674Z", + "modified_on": "2023-12-17T07:14:28.1674Z" + }, + { + "id": "5a0eac6b1a3ef32c08e28524a4c4a0cc", + "name": "docker-registry.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2023-10-31T02:56:32.963054Z", + "modified_on": "2023-10-31T02:56:32.963054Z" + }, + { + "id": "e9f1721ec9907dd8a515c3544d911c60", + "name": "_domainconnect.hcws.one", + "type": "CNAME", + "content": "connect.domains.google.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2022-03-29T17:31:33.993423Z", + "modified_on": "2022-03-29T17:31:33.993423Z" + }, + { + "id": "441cf0e09289dc653594d5e5ab0a66c5", + "name": "files-jason.hcws.one", + "type": "CNAME", + "content": "a7401598-9112-4dfb-85d2-8ccfc0d812bd.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2022-11-12T03:59:57.294605Z", + "modified_on": "2022-11-12T03:59:57.294605Z" + }, + { + "id": "b957b0b1138464632a548377cee8b2e8", + "name": "gpt.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-03-27T19:12:53.707216Z", + "modified_on": "2025-03-27T19:12:53.707216Z" + }, + { + "id": "33fa486a3415b945fed041e060af27e5", + "name": "gr-pve-jason.hcws.one", + "type": "CNAME", + "content": "a7401598-9112-4dfb-85d2-8ccfc0d812bd.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2024-12-22T02:20:37.518128Z", + "modified_on": "2024-12-22T02:20:37.518128Z" + }, + { + "id": "56e08388d187b93fb01fd601b0f3d658", + "name": "homeassistant.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-04-12T03:02:51.166953Z", + "modified_on": "2025-04-12T03:02:51.166953Z" + }, + { + "id": "0d390111afb224b1992e19e9afa3ba8f", + "name": "home.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-03-23T23:43:08.308407Z", + "modified_on": "2025-03-23T23:43:08.308407Z" + }, + { + "id": "5748b56bb09660958b3ab30915ad2518", + "name": "homepage.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-03-24T23:56:14.347379Z", + "modified_on": "2025-03-24T23:56:14.347379Z" + }, + { + "id": "e125881ca2b195b458a878f1f152f1a7", + "name": "home.panel-node.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2023-12-01T01:06:38.844135Z", + "modified_on": "2023-12-01T01:06:38.844135Z" + }, + { + "id": "640b60f3cb92c13c6a9216ec3450a51c", + "name": "home-proxy.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-06-10T05:29:19.380342Z", + "modified_on": "2025-06-10T05:29:19.380342Z" + }, + { + "id": "d767666122d4a7a2b49b07cc7747c350", + "name": "join.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2024-08-20T01:23:48.541718Z", + "modified_on": "2024-08-20T01:23:48.541718Z" + }, + { + "id": "f1eca4728d3251bab6be5490124e51a6", + "name": "learngerman.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-05-28T21:07:03.901053Z", + "modified_on": "2025-05-28T21:07:03.901053Z" + }, + { + "id": "d6ddd741735923b659b32e74cbc72964", + "name": "mealie.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-03-19T01:10:35.478372Z", + "modified_on": "2025-03-19T01:10:35.478372Z" + }, + { + "id": "8dca01fc53c2f7ffef6ba849d7784b1c", + "name": "netaccount.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2022-06-02T05:49:44.291583Z", + "modified_on": "2022-06-02T05:49:44.291583Z" + }, + { + "id": "6d5ec1d38c342d1d06797a1142c12bf4", + "name": "netrequest.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2022-05-19T20:35:58.168039Z", + "modified_on": "2022-05-19T20:35:58.168039Z" + }, + { + "id": "520f5403524d8235c3116677462afcd3", + "name": "netshow-beta2.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2024-11-19T06:58:53.044954Z", + "modified_on": "2024-11-19T06:58:53.044954Z" + }, + { + "id": "84c65b8e7bed8787deb9dc4408d17c38", + "name": "netshow-beta.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2024-11-12T21:12:54.499418Z", + "modified_on": "2024-11-12T21:12:54.499418Z" + }, + { + "id": "46ebb687b187875e72380184b2a3b526", + "name": "netshow.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2022-05-16T22:29:03.240197Z", + "modified_on": "2022-05-16T22:29:03.240197Z" + }, + { + "id": "a42ab4eb48ec53fe9addf9aaca65190c", + "name": "netshow-old.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2024-08-08T06:30:34.385567Z", + "modified_on": "2024-08-11T01:59:04.858568Z" + }, + { + "id": "93e62c601cc3c45bc0a8a3b2c523a65b", + "name": "nicks-apis.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-04-25T16:21:34.879939Z", + "modified_on": "2025-04-25T16:21:34.879939Z" + }, + { + "id": "3319dc55f5f460796df7c2d717ec47ce", + "name": "notes.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2024-05-14T03:06:53.142974Z", + "modified_on": "2024-05-14T03:06:53.142974Z" + }, + { + "id": "bfbcdfd373067b80a3afc5f0bdd8a45e", + "name": "npm-jason.hcws.one", + "type": "CNAME", + "content": "a7401598-9112-4dfb-85d2-8ccfc0d812bd.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-03-21T01:55:06.17624Z", + "modified_on": "2025-03-21T01:55:06.17624Z" + }, + { + "id": "82ed08069dd71c3655dd533c19b56445", + "name": "oauth.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-06-09T05:43:10.126152Z", + "modified_on": "2025-06-09T05:43:10.126152Z" + }, + { + "id": "f7d276a531bc840e37607320cb860a1f", + "name": "openlingo.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2024-11-21T06:59:17.179311Z", + "modified_on": "2024-11-21T06:59:17.179311Z" + }, + { + "id": "65031c4ff1d4200b9ab5baa2eeab9c45", + "name": "portainer-jason.hcws.one", + "type": "CNAME", + "content": "a7401598-9112-4dfb-85d2-8ccfc0d812bd.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2024-04-13T21:05:52.421323Z", + "modified_on": "2024-04-13T21:05:52.421323Z" + }, + { + "id": "9874e7b4d754c70bab111a1cf0af1abb", + "name": "spotify.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2024-05-30T01:05:27.579248Z", + "modified_on": "2024-05-30T01:05:27.579248Z" + }, + { + "id": "6e85d80479cc91c8e98b707e44226452", + "name": "test.netrequest.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2024-07-10T04:06:23.354506Z", + "modified_on": "2024-07-10T04:06:23.354506Z" + }, + { + "id": "ed7f5caf1836938fc0cb372c8dc910ad", + "name": "webhost.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-05-28T19:53:29.060693Z", + "modified_on": "2025-05-28T19:53:29.060693Z" + }, + { + "id": "f774c807ab70cba2cfaef0fee79c2610", + "name": "webportal-proxy.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-02-08T05:33:54.705277Z", + "modified_on": "2025-02-08T05:33:54.705277Z" + }, + { + "id": "9fe6c479df6889d2a8a968de5bc74bb7", + "name": "wireguard-jason.hcws.one", + "type": "CNAME", + "content": "a7401598-9112-4dfb-85d2-8ccfc0d812bd.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-03-21T19:22:38.500846Z", + "modified_on": "2025-03-21T19:22:38.500846Z" + } + ], + "success": true, + "errors": [], + "messages": [], + "result_info": { + "page": 1, + "per_page": 100, + "count": 41, + "total_count": 41, + "total_pages": 1 + } + }, + "result": [ + { + "id": "641aab94b897f5030230f0f64a166159", + "name": "internal.hcws.one", + "type": "A", + "content": "5.161.120.112", + "proxiable": true, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-03-24T00:02:13.266417Z", + "modified_on": "2025-03-24T00:02:13.266417Z" + }, + { + "id": "f4ea688ba2bb851ddfd6e4feb1bf15c5", + "name": "jenna.gr.hcws.one", + "type": "A", + "content": "71.245.169.58", + "proxiable": true, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-03-21T21:48:27.850252Z", + "modified_on": "2025-03-21T21:48:27.850252Z" + }, + { + "id": "cc92dc966be3f56dd276514a71b1497e", + "name": "otter.node.hcws.one", + "type": "A", + "content": "5.161.224.170", + "proxiable": true, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2024-03-23T18:50:55.448289Z", + "modified_on": "2024-03-23T18:50:55.448289Z" + }, + { + "id": "91df297a5f30ad06183294eb3ee75394", + "name": "panel.hcws.one", + "type": "A", + "content": "5.161.224.170", + "proxiable": true, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2024-04-01T02:55:06.338632Z", + "modified_on": "2024-04-01T02:55:06.338632Z" + }, + { + "id": "4fbea82c87a6ca19fb89e78d486a1ff5", + "name": "portainer.gr.hcws.one", + "type": "A", + "content": "71.245.169.58", + "proxiable": true, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-03-21T21:51:32.315716Z", + "modified_on": "2025-03-21T21:51:32.315716Z" + }, + { + "id": "b834463eee54152da3c8a26dada687bb", + "name": "*.pr.hcws.one", + "type": "A", + "content": "5.161.120.112", + "proxiable": true, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-03-23T23:29:58.541953Z", + "modified_on": "2025-03-23T23:30:44.647626Z" + }, + { + "id": "fb0215b8b6a2b225c3f5510393a70900", + "name": "pve.gr.hcws.one", + "type": "A", + "content": "71.245.169.58", + "proxiable": true, + "proxied": false, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-03-21T21:39:36.945211Z", + "modified_on": "2025-03-21T21:39:36.945211Z" + }, + { + "id": "432c560f483f5b08bd59ad43d62b75be", + "name": "api.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2024-03-28T22:56:51.541021Z", + "modified_on": "2024-03-28T22:56:51.541021Z" + }, + { + "id": "0c0578695c904dd6ca7f349956c0c9bb", + "name": "api-spotify.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2024-05-30T01:06:56.337344Z", + "modified_on": "2024-05-30T01:06:56.337344Z" + }, + { + "id": "bb5e44ce70c0ebe369c8f9227ad98004", + "name": "audiobook.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2024-04-08T14:20:12.912559Z", + "modified_on": "2024-04-08T14:20:12.912559Z" + }, + { + "id": "f2ebb41c59f633952284c44b60fa79fe", + "name": "auth.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2023-12-17T07:14:28.1674Z", + "modified_on": "2023-12-17T07:14:28.1674Z" + }, + { + "id": "5a0eac6b1a3ef32c08e28524a4c4a0cc", + "name": "docker-registry.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2023-10-31T02:56:32.963054Z", + "modified_on": "2023-10-31T02:56:32.963054Z" + }, + { + "id": "e9f1721ec9907dd8a515c3544d911c60", + "name": "_domainconnect.hcws.one", + "type": "CNAME", + "content": "connect.domains.google.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2022-03-29T17:31:33.993423Z", + "modified_on": "2022-03-29T17:31:33.993423Z" + }, + { + "id": "441cf0e09289dc653594d5e5ab0a66c5", + "name": "files-jason.hcws.one", + "type": "CNAME", + "content": "a7401598-9112-4dfb-85d2-8ccfc0d812bd.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2022-11-12T03:59:57.294605Z", + "modified_on": "2022-11-12T03:59:57.294605Z" + }, + { + "id": "b957b0b1138464632a548377cee8b2e8", + "name": "gpt.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-03-27T19:12:53.707216Z", + "modified_on": "2025-03-27T19:12:53.707216Z" + }, + { + "id": "33fa486a3415b945fed041e060af27e5", + "name": "gr-pve-jason.hcws.one", + "type": "CNAME", + "content": "a7401598-9112-4dfb-85d2-8ccfc0d812bd.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2024-12-22T02:20:37.518128Z", + "modified_on": "2024-12-22T02:20:37.518128Z" + }, + { + "id": "56e08388d187b93fb01fd601b0f3d658", + "name": "homeassistant.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-04-12T03:02:51.166953Z", + "modified_on": "2025-04-12T03:02:51.166953Z" + }, + { + "id": "0d390111afb224b1992e19e9afa3ba8f", + "name": "home.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-03-23T23:43:08.308407Z", + "modified_on": "2025-03-23T23:43:08.308407Z" + }, + { + "id": "5748b56bb09660958b3ab30915ad2518", + "name": "homepage.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-03-24T23:56:14.347379Z", + "modified_on": "2025-03-24T23:56:14.347379Z" + }, + { + "id": "e125881ca2b195b458a878f1f152f1a7", + "name": "home.panel-node.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2023-12-01T01:06:38.844135Z", + "modified_on": "2023-12-01T01:06:38.844135Z" + }, + { + "id": "640b60f3cb92c13c6a9216ec3450a51c", + "name": "home-proxy.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-06-10T05:29:19.380342Z", + "modified_on": "2025-06-10T05:29:19.380342Z" + }, + { + "id": "d767666122d4a7a2b49b07cc7747c350", + "name": "join.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2024-08-20T01:23:48.541718Z", + "modified_on": "2024-08-20T01:23:48.541718Z" + }, + { + "id": "f1eca4728d3251bab6be5490124e51a6", + "name": "learngerman.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-05-28T21:07:03.901053Z", + "modified_on": "2025-05-28T21:07:03.901053Z" + }, + { + "id": "d6ddd741735923b659b32e74cbc72964", + "name": "mealie.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-03-19T01:10:35.478372Z", + "modified_on": "2025-03-19T01:10:35.478372Z" + }, + { + "id": "8dca01fc53c2f7ffef6ba849d7784b1c", + "name": "netaccount.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2022-06-02T05:49:44.291583Z", + "modified_on": "2022-06-02T05:49:44.291583Z" + }, + { + "id": "6d5ec1d38c342d1d06797a1142c12bf4", + "name": "netrequest.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2022-05-19T20:35:58.168039Z", + "modified_on": "2022-05-19T20:35:58.168039Z" + }, + { + "id": "520f5403524d8235c3116677462afcd3", + "name": "netshow-beta2.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2024-11-19T06:58:53.044954Z", + "modified_on": "2024-11-19T06:58:53.044954Z" + }, + { + "id": "84c65b8e7bed8787deb9dc4408d17c38", + "name": "netshow-beta.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2024-11-12T21:12:54.499418Z", + "modified_on": "2024-11-12T21:12:54.499418Z" + }, + { + "id": "46ebb687b187875e72380184b2a3b526", + "name": "netshow.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2022-05-16T22:29:03.240197Z", + "modified_on": "2022-05-16T22:29:03.240197Z" + }, + { + "id": "a42ab4eb48ec53fe9addf9aaca65190c", + "name": "netshow-old.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2024-08-08T06:30:34.385567Z", + "modified_on": "2024-08-11T01:59:04.858568Z" + }, + { + "id": "93e62c601cc3c45bc0a8a3b2c523a65b", + "name": "nicks-apis.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-04-25T16:21:34.879939Z", + "modified_on": "2025-04-25T16:21:34.879939Z" + }, + { + "id": "3319dc55f5f460796df7c2d717ec47ce", + "name": "notes.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2024-05-14T03:06:53.142974Z", + "modified_on": "2024-05-14T03:06:53.142974Z" + }, + { + "id": "bfbcdfd373067b80a3afc5f0bdd8a45e", + "name": "npm-jason.hcws.one", + "type": "CNAME", + "content": "a7401598-9112-4dfb-85d2-8ccfc0d812bd.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-03-21T01:55:06.17624Z", + "modified_on": "2025-03-21T01:55:06.17624Z" + }, + { + "id": "82ed08069dd71c3655dd533c19b56445", + "name": "oauth.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-06-09T05:43:10.126152Z", + "modified_on": "2025-06-09T05:43:10.126152Z" + }, + { + "id": "f7d276a531bc840e37607320cb860a1f", + "name": "openlingo.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2024-11-21T06:59:17.179311Z", + "modified_on": "2024-11-21T06:59:17.179311Z" + }, + { + "id": "65031c4ff1d4200b9ab5baa2eeab9c45", + "name": "portainer-jason.hcws.one", + "type": "CNAME", + "content": "a7401598-9112-4dfb-85d2-8ccfc0d812bd.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2024-04-13T21:05:52.421323Z", + "modified_on": "2024-04-13T21:05:52.421323Z" + }, + { + "id": "9874e7b4d754c70bab111a1cf0af1abb", + "name": "spotify.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2024-05-30T01:05:27.579248Z", + "modified_on": "2024-05-30T01:05:27.579248Z" + }, + { + "id": "6e85d80479cc91c8e98b707e44226452", + "name": "test.netrequest.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2024-07-10T04:06:23.354506Z", + "modified_on": "2024-07-10T04:06:23.354506Z" + }, + { + "id": "ed7f5caf1836938fc0cb372c8dc910ad", + "name": "webhost.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-05-28T19:53:29.060693Z", + "modified_on": "2025-05-28T19:53:29.060693Z" + }, + { + "id": "f774c807ab70cba2cfaef0fee79c2610", + "name": "webportal-proxy.hcws.one", + "type": "CNAME", + "content": "b78bc119-48fb-486e-873a-523bb0692ed3.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-02-08T05:33:54.705277Z", + "modified_on": "2025-02-08T05:33:54.705277Z" + }, + { + "id": "9fe6c479df6889d2a8a968de5bc74bb7", + "name": "wireguard-jason.hcws.one", + "type": "CNAME", + "content": "a7401598-9112-4dfb-85d2-8ccfc0d812bd.cfargotunnel.com", + "proxiable": true, + "proxied": true, + "ttl": 1, + "settings": {}, + "meta": {}, + "comment": null, + "tags": [], + "created_on": "2025-03-21T19:22:38.500846Z", + "modified_on": "2025-03-21T19:22:38.500846Z" + } + ], + "result_info": { + "page": 1, + "per_page": 100, + "count": 41, + "total_count": 41, + "total_pages": 1 + } +} \ No newline at end of file diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..5c4907d --- /dev/null +++ b/index.ts @@ -0,0 +1,134 @@ +import express from 'express'; +import cors from 'cors'; +import helmet from 'helmet'; +import { config } from './src/config/index.js'; +import logger from './src/utils/logger.js'; +import routes from './src/routes/index.js'; +import { CronService } from './src/services/CronService.js'; +import { ViteService } from './src/services/ViteService.js'; +import fs from 'fs'; +import path from 'path'; +import { CloudflareService } from './src/services/CloudflareService.js'; + +const app = express(); + +// Middleware +app.use(helmet()); +app.use(cors({ + origin: config.cors.origin, + credentials: true +})); +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); + +// Request logging +app.use((req, res, next) => { + if (req.path.startsWith('/api')) { + logger.info(`${req.method} ${req.path}`, { + ip: req.ip, + userAgent: req.get('User-Agent') + }); + } + next(); +}); + +// Routes +app.use(routes); + +// Error handling middleware +app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.error('Unhandled error:', { + error: err.message, + stack: err.stack, + path: req.path, + method: req.method, + body: req.body + }); + + res.status(500).json({ + success: false, + message: config.server.env === 'production' ? 'Internal server error' : err.message + }); +}); + +// Initialize application +async function initializeApp() { + try { + // Ensure required directories exist + const directories = [ + 'logs', + 'data', + 'certs', + 'nginx' + ]; + + directories.forEach(dir => { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + logger.info(`Created directory: ${dir}`); + } + }); + + // Initialize cron service + CronService.init(); + + // Initialize Cloudflare service + const cloudflare = CloudflareService.getInstance() + cloudflare.init({ + apiToken: config.cloudflare.apiToken, + active: config.cloudflare.active + }); // Setup Vite middleware for frontend + const isProduction = config.server.env === 'production'; + await ViteService.setupMiddleware(app, isProduction, '/'); + + // 404 handler - must be after Vite middleware setup + app.use('*', (req, res) => { + res.status(404).json({ + success: false, + message: 'Route not found' + }); + }); + + // Start server + app.listen(config.server.port, () => { + logger.info(`๐Ÿš€ NGINX Proxy Manager API started on port ${config.server.port}`); + logger.info(`๐Ÿ“ Environment: ${config.server.env}`); + logger.info(`๐Ÿ”’ JWT Secret: ${config.jwt.secret.substring(0, 10)}...`); + logger.info(`๐Ÿ—„๏ธ Database: ${config.database.path}`); + logger.info(`โšก Health check: http://localhost:${config.server.port}/api/health`); + }); + + } catch (error) { + logger.error('Failed to initialize application:', error); + process.exit(1); + } +} + +// Graceful shutdown +process.on('SIGTERM', () => { + logger.info('SIGTERM received. Shutting down gracefully...'); + CronService.stop(); + ViteService.stop(); + process.exit(0); +}); + +process.on('SIGINT', () => { + logger.info('SIGINT received. Shutting down gracefully...'); + CronService.stop(); + ViteService.stop(); + process.exit(0); +}); + +// Handle uncaught exceptions +process.on('uncaughtException', (error) => { + logger.error('Uncaught Exception:', error); + process.exit(1); +}); + +process.on('unhandledRejection', (reason, promise) => { + logger.error('Unhandled Rejection at:', promise, 'reason:', reason); + process.exit(1); +}); + +// Initialize the application +initializeApp(); \ No newline at end of file diff --git a/manage.ts b/manage.ts new file mode 100644 index 0000000..b310996 --- /dev/null +++ b/manage.ts @@ -0,0 +1,331 @@ +#!/usr/bin/env bun + +/** + * Management CLI for NGINX Proxy Manager + * Usage: bun manage.ts [options] + */ + +import { program } from 'commander'; +import { database } from './src/database/index.js'; +import { UserModel } from './src/models/User.js'; +import { ProxyModel } from './src/models/Proxy.js'; +import { CertificateModel } from './src/models/Certificate.js'; +import { SSLService } from './src/services/SSLService.js'; +import { NginxService } from './src/services/NginxService.js'; +import bcrypt from 'bcryptjs'; +import logger from './src/utils/logger.js'; + +program + .name('nginx-proxy-manager') + .description('NGINX Proxy Manager CLI') + .version('1.0.0'); + +// User management commands +const userCmd = program.command('user').description('User management commands'); + +userCmd + .command('create') + .description('Create a new admin user') + .requiredOption('-u, --username ', 'Username') + .requiredOption('-p, --password ', 'Password') + .action(async (options) => { + try { + const hashedPassword = await bcrypt.hash(options.password, 10); + const user = await UserModel.create({ + username: options.username, + password: hashedPassword + }); + console.log(`โœ… User created: ${user.username} (ID: ${user.id})`); + } catch (error: any) { + console.error(`โŒ Failed to create user: ${error.message}`); + } finally { + await database.close(); + process.exit(); + } + }); + +userCmd + .command('change-password') + .description('Change user password') + .requiredOption('-u, --username ', 'Username') + .requiredOption('-p, --password ', 'New password') + .action(async (options) => { + try { + const user = await UserModel.findByUsername(options.username); + if (!user) { + console.error(`โŒ User not found: ${options.username}`); + return; + } + + const hashedPassword = await bcrypt.hash(options.password, 10); + await UserModel.updatePassword(user.id!, hashedPassword); + console.log(`โœ… Password updated for user: ${options.username}`); + } catch (error: any) { + console.error(`โŒ Failed to update password: ${error.message}`); + } finally { + await database.close(); + process.exit(); + } + }); + +// Proxy management commands +const proxyCmd = program.command('proxy').description('Proxy management commands'); + +proxyCmd + .command('list') + .description('List all proxies') + .action(async () => { + try { + const proxies = await ProxyModel.findAll(); + console.log(`\n๐Ÿ“‹ Found ${proxies.length} proxy(ies):\n`); + + proxies.forEach(proxy => { + console.log(`๐Ÿ”— ${proxy.domain} โ†’ ${proxy.target}`); + console.log(` SSL: ${proxy.ssl_type}`); + console.log(` ID: ${proxy.id}`); + console.log(` Created: ${proxy.created_at}\n`); + }); + } catch (error: any) { + console.error(`โŒ Failed to list proxies: ${error.message}`); + } finally { + await database.close(); + process.exit(); + } + }); + +proxyCmd + .command('delete') + .description('Delete a proxy by ID') + .requiredOption('-i, --id ', 'Proxy ID') + .action(async (options) => { + try { + const id = parseInt(options.id); + const proxy = await ProxyModel.findById(id); + + if (!proxy) { + console.error(`โŒ Proxy not found with ID: ${id}`); + return; + } + + // Remove NGINX config + await NginxService.removeConfig(proxy.domain); + + // Delete from database + await ProxyModel.delete(id); + + // Reload NGINX + const result = await NginxService.reload(); + if (result.success) { + console.log(`โœ… Proxy deleted: ${proxy.domain}`); + } else { + console.log(`โš ๏ธ Proxy deleted but NGINX reload failed: ${result.output}`); + } + } catch (error: any) { + console.error(`โŒ Failed to delete proxy: ${error.message}`); + } finally { + await database.close(); + process.exit(); + } + }); + +// Certificate management commands +const certCmd = program.command('cert').description('Certificate management commands'); + +certCmd + .command('list') + .description('List all certificates') + .action(async () => { + try { + const certificates = await CertificateModel.findAll(); + console.log(`\n๐Ÿ” Found ${certificates.length} certificate(s):\n`); + + certificates.forEach(cert => { + console.log(`๐Ÿ“œ ${cert.domain}`); + console.log(` Type: ${cert.type}`); + console.log(` Status: ${cert.status}`); + console.log(` Expiry: ${cert.expiry || 'N/A'}`); + console.log(` ID: ${cert.id}\n`); + }); + } catch (error: any) { + console.error(`โŒ Failed to list certificates: ${error.message}`); + } finally { + await database.close(); + process.exit(); + } + }); + +certCmd + .command('renew') + .description('Renew certificates expiring soon') + .option('-d, --days ', 'Days before expiry to renew', '30') + .action(async (options) => { + try { + const days = parseInt(options.days); + console.log(`๐Ÿ” Checking for certificates expiring within ${days} days...`); + + await SSLService.autoRenewCertificates(); + console.log('โœ… Certificate renewal process completed'); + } catch (error: any) { + console.error(`โŒ Certificate renewal failed: ${error.message}`); + } finally { + await database.close(); + process.exit(); + } + }); + +// NGINX management commands +const nginxCmd = program.command('nginx').description('NGINX management commands'); + +nginxCmd + .command('test') + .description('Test NGINX configuration') + .action(async () => { + try { + const result = await NginxService.testConfig(); + if (result.success) { + console.log('โœ… NGINX configuration is valid'); + } else { + console.log('โŒ NGINX configuration test failed:'); + console.log(result.output); + } + } catch (error: any) { + console.error(`โŒ Failed to test NGINX: ${error.message}`); + } finally { + process.exit(); + } + }); + +nginxCmd + .command('reload') + .description('Reload NGINX configuration') + .action(async () => { + try { + const result = await NginxService.reload(); + if (result.success) { + console.log('โœ… NGINX reloaded successfully'); + } else { + console.log('โŒ NGINX reload failed:'); + console.log(result.output); + } + } catch (error: any) { + console.error(`โŒ Failed to reload NGINX: ${error.message}`); + } finally { + process.exit(); + } + }); + +nginxCmd + .command('status') + .description('Get NGINX status') + .action(async () => { + try { + const result = await NginxService.getStatus(); + if (result.success) { + console.log('โœ… NGINX Status:'); + console.log(result.output); + } else { + console.log('โŒ Failed to get NGINX status:'); + console.log(result.output); + } + } catch (error: any) { + console.error(`โŒ Failed to get NGINX status: ${error.message}`); + } finally { + process.exit(); + } + }); + +// Database management commands +const dbCmd = program.command('db').description('Database management commands'); + +dbCmd + .command('init') + .description('Initialize database') + .action(async () => { + try { + console.log('๐Ÿ—„๏ธ Initializing database...'); + // Database initialization happens automatically when imported + console.log('โœ… Database initialized successfully'); + } catch (error: any) { + console.error(`โŒ Database initialization failed: ${error.message}`); + } finally { + await database.close(); + process.exit(); + } + }); + +dbCmd + .command('backup') + .description('Create database backup') + .option('-o, --output ', 'Output file path', `./backups/backup-${new Date().toISOString().split('T')[0]}.db`) + .action(async (options) => { + try { + const fs = await import('fs'); + const path = await import('path'); + + // Ensure backup directory exists + const backupDir = path.dirname(options.output); + if (!fs.existsSync(backupDir)) { + fs.mkdirSync(backupDir, { recursive: true }); + } + + // Copy database file + fs.copyFileSync('./data/proxy_manager.db', options.output); + console.log(`โœ… Database backed up to: ${options.output}`); + } catch (error: any) { + console.error(`โŒ Backup failed: ${error.message}`); + } finally { + await database.close(); + process.exit(); + } + }); + +// Status command +program + .command('status') + .description('Show application status') + .action(async () => { + try { + console.log('๐Ÿ“Š NGINX Proxy Manager Status\n'); + + // Check database + console.log('๐Ÿ—„๏ธ Database:'); + const proxies = await ProxyModel.findAll(); + const certificates = await CertificateModel.findAll(); + console.log(` Proxies: ${proxies.length}`); + console.log(` Certificates: ${certificates.length}`); + + // Check NGINX + console.log('\n๐Ÿ”ง NGINX:'); + const nginxStatus = await NginxService.getStatus(); + if (nginxStatus.success) { + console.log(' Status: โœ… Running'); + console.log(` Version: ${nginxStatus.output.trim()}`); + } else { + console.log(' Status: โŒ Not running or error'); + } + + // Check config + const configTest = await NginxService.testConfig(); + console.log(` Config: ${configTest.success ? 'โœ… Valid' : 'โŒ Invalid'}`); + + // Check expiring certificates + console.log('\n๐Ÿ” Certificates:'); + const expiring = await SSLService.checkExpiringCertificates(30); + console.log(` Expiring soon (30 days): ${expiring.length}`); + + } catch (error: any) { + console.error(`โŒ Failed to get status: ${error.message}`); + } finally { + await database.close(); + process.exit(); + } + }); + +// Parse arguments +program.parse(); + +// If no command provided, show help +if (!process.argv.slice(2).length) { + program.outputHelp(); + process.exit(); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..36a7f5a --- /dev/null +++ b/package.json @@ -0,0 +1,67 @@ +{ + "name": "reverse-proxy", + "version": "1.0.0", + "description": "Custom NGINX Proxy Manager Backend", + "main": "index.ts", + "type": "module", + "private": true, + "scripts": { + "dev": "bun --watch index.ts", + "start": "bun index.ts", + "build": "bun build index.ts --outdir ./dist", + "build:frontend": "vite build", + "build:all": "bun run build:frontend && bun run build", + "preview": "vite preview", + "db:init": "bun src/database/init.ts", + "test": "bun test-api.ts", + "manage": "bun manage.ts", + "backup": "bun manage.ts db backup", + "status": "bun manage.ts status", + "nginx:test": "bun manage.ts nginx test", + "nginx:reload": "bun manage.ts nginx reload", + "cert:renew": "bun manage.ts cert renew" + }, + "dependencies": { + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-slot": "^1.2.3", + "@types/bcryptjs": "^3.0.0", + "bcryptjs": "^3.0.2", + "class-variance-authority": "^0.7.1", + "cloudflare": "^4.3.0", + "clsx": "^2.1.1", + "commander": "^14.0.0", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "helmet": "^7.1.0", + "joi": "^17.11.0", + "jsonwebtoken": "^9.0.2", + "lucide-react": "^0.514.0", + "multer": "^1.4.5-lts.1", + "node-cron": "^3.0.3", + "react-router": "^7.6.2", + "tailwind-merge": "^3.3.1", + "winston": "^3.11.0" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.9", + "@types/bun": "latest", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.5", + "@types/multer": "^1.4.11", + "@types/node-cron": "^3.0.11", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "^4.5.2", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "tailwindcss": "^4.1.9", + "tw-animate-css": "^1.3.4", + "vite": "^6.3.5" + }, + "peerDependencies": { + "typescript": "^5" + } +} \ No newline at end of file diff --git a/src/config/index.ts b/src/config/index.ts new file mode 100644 index 0000000..a8ba394 --- /dev/null +++ b/src/config/index.ts @@ -0,0 +1,87 @@ +import dotenv from 'dotenv'; +import path from 'path'; + +// Load environment variables +dotenv.config(); + +interface Config { + server: { + port: number; + env: string; + }; + database: { + path: string; + }; + jwt: { + secret: string; + expiresIn: string; + }; + admin: { + username: string; + password: string; + }; + nginx: { + configPath: string; + binaryPath: string; + }; + ssl: { + method: 'acme.sh' | 'certbot'; + acmeShPath: string; + certbotPath: string; + customCertsPath: string; + }; + logging: { + level: string; + file: string; + }; + cors: { + origin: string; + }; + cloudflare: { + apiToken: string; + apiEmail: string; + active: boolean; + }; +} + +export const config: Config = { + server: { + port: parseInt(process.env.PORT || '3000', 10), + env: process.env.NODE_ENV || 'development', + }, + database: { + path: process.env.DATABASE_PATH || './data/proxy_manager.db', + }, + jwt: { + secret: process.env.JWT_SECRET || 'default-secret-change-me', + expiresIn: process.env.JWT_EXPIRES_IN || '24h', + }, + admin: { + username: process.env.ADMIN_USERNAME || 'admin', + password: process.env.ADMIN_PASSWORD || 'admin123', + }, + nginx: { + configPath: process.env.NGINX_CONFIG_PATH || '/etc/nginx/conf.d', + binaryPath: process.env.NGINX_BINARY_PATH || '/usr/sbin/nginx', + }, + ssl: { + method: (process.env.SSL_METHOD as 'acme.sh' | 'certbot') || 'acme.sh', + acmeShPath: process.env.ACME_SH_PATH || '/root/.acme.sh', + certbotPath: process.env.CERTBOT_PATH || '/usr/bin/certbot', + customCertsPath: process.env.CUSTOM_CERTS_PATH || './certs', + }, + logging: { + level: process.env.LOG_LEVEL || 'info', + file: process.env.LOG_FILE || './logs/app.log', + }, + cors: { + origin: process.env.CORS_ORIGIN || 'http://localhost:3001', + }, + cloudflare: { + apiToken: process.env.CLOUDFLARE_API_TOKEN || '', + apiEmail: process.env.CLOUDFLARE_API_EMAIL || '', + active: process.env.CLOUDFLARE_API_TOKEN && process.env.CLOUDFLARE_API_EMAIL ? true : false, + }, +}; + +export default config; diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts new file mode 100644 index 0000000..2eca892 --- /dev/null +++ b/src/controllers/AuthController.ts @@ -0,0 +1,187 @@ +import { Request, Response } from 'express'; +import bcrypt from 'bcryptjs'; +import { UserModel } from '../models/User.js'; +import { generateToken } from '../middleware/auth.js'; +import { ApiResponse } from '../types/index.js'; +import logger from '../utils/logger.js'; + +export class AuthController { + /** + * Login user + */ + static async login(req: Request, res: Response): Promise { + try { + const { username, password } = req.body; + + // Find user by username + const user = await UserModel.findByUsername(username); + if (!user) { + res.status(401).json({ + success: false, + message: 'Invalid credentials' + } as ApiResponse); + return; + } + + // Verify password + const isValidPassword = await bcrypt.compare(password, user.password); + if (!isValidPassword) { + logger.warn('Failed login attempt', { username }); + res.status(401).json({ + success: false, + message: 'Invalid credentials' + } as ApiResponse); + return; + } + + // Generate JWT token + const token = generateToken({ + id: user.id!, + username: user.username + }); + + logger.info('Successful login', { username }); + res.json({ + success: true, + data: { + token, + user: { + id: user.id, + username: user.username + } + }, + message: 'Login successful' + } as ApiResponse); + + } catch (error) { + logger.error('Login error:', error); + res.status(500).json({ + success: false, + message: 'Internal server error' + } as ApiResponse); + } + } + + /** + * Get current user info + */ + static async me(req: Request, res: Response): Promise { + try { + if (!req.user) { + res.status(401).json({ + success: false, + message: 'User not authenticated' + } as ApiResponse); + return; + } + + const user = await UserModel.findById(req.user.id); + if (!user) { + res.status(404).json({ + success: false, + message: 'User not found' + } as ApiResponse); + return; + } + + res.json({ + success: true, + data: { + id: user.id, + username: user.username, + created_at: user.created_at + } + } as ApiResponse); + + } catch (error) { + logger.error('Get user info error:', error); + res.status(500).json({ + success: false, + message: 'Internal server error' + } as ApiResponse); + } + } + + /** + * Change password + */ + static async changePassword(req: Request, res: Response): Promise { + try { + if (!req.user) { + res.status(401).json({ + success: false, + message: 'User not authenticated' + } as ApiResponse); + return; + } + + const { currentPassword, newPassword } = req.body; + + if (!currentPassword || !newPassword) { + res.status(400).json({ + success: false, + message: 'Current password and new password are required' + } as ApiResponse); + return; + } + + // Find user + const user = await UserModel.findById(req.user.id); + if (!user) { + res.status(404).json({ + success: false, + message: 'User not found' + } as ApiResponse); + return; + } + + // Verify current password + const isValidPassword = await bcrypt.compare(currentPassword, user.password); + if (!isValidPassword) { + res.status(401).json({ + success: false, + message: 'Current password is incorrect' + } as ApiResponse); + return; + } + + // Hash new password + const hashedNewPassword = await bcrypt.hash(newPassword, 10); + + // Update password + const success = await UserModel.updatePassword(req.user.id, hashedNewPassword); + if (!success) { + res.status(500).json({ + success: false, + message: 'Failed to update password' + } as ApiResponse); + return; + } + + logger.info('Password changed successfully', { userId: req.user.id }); + res.json({ + success: true, + message: 'Password changed successfully' + } as ApiResponse); + + } catch (error) { + logger.error('Change password error:', error); + res.status(500).json({ + success: false, + message: 'Internal server error' + } as ApiResponse); + } + } + + /** + * Logout (client-side token removal) + */ + static async logout(req: Request, res: Response): Promise { + // Since we're using JWT, logout is handled client-side by removing the token + // We just return a success response + res.json({ + success: true, + message: 'Logout successful' + } as ApiResponse); + } +} diff --git a/src/controllers/CertificateController.ts b/src/controllers/CertificateController.ts new file mode 100644 index 0000000..c60d005 --- /dev/null +++ b/src/controllers/CertificateController.ts @@ -0,0 +1,306 @@ +import { Request, Response } from 'express'; +import multer from 'multer'; +import { SSLService } from '../services/SSLService.js'; +import { CertificateModel } from '../models/Certificate.js'; +import { ApiResponse, Certificate } from '../types/index.js'; +import logger from '../utils/logger.js'; + +// Configure multer for file uploads +const upload = multer({ storage: multer.memoryStorage() }); + +export class CertificateController { + /** + * Get all certificates + */ + static async getAllCertificates(req: Request, res: Response): Promise { + try { + const certificates = await CertificateModel.findAll(); + + res.json({ + success: true, + data: certificates + } as ApiResponse); + + } catch (error) { + logger.error('Get all certificates error:', error); + res.status(500).json({ + success: false, + message: 'Failed to fetch certificates' + } as ApiResponse); + } + } + + /** + * Get certificate by ID + */ + static async getCertificateById(req: Request, res: Response): Promise { + try { + const id = parseInt(req.params.id); + if (isNaN(id)) { + res.status(400).json({ + success: false, + message: 'Invalid certificate ID' + } as ApiResponse); + return; + } + + const certificate = await CertificateModel.findById(id); + if (!certificate) { + res.status(404).json({ + success: false, + message: 'Certificate not found' + } as ApiResponse); + return; + } + + res.json({ + success: true, + data: certificate + } as ApiResponse); + + } catch (error) { + logger.error('Get certificate by ID error:', error); + res.status(500).json({ + success: false, + message: 'Failed to fetch certificate' + } as ApiResponse); + } + } + + /** + * Request Let's Encrypt certificate + */ + static async requestLetsEncrypt(req: Request, res: Response): Promise { + try { + const { domain } = req.body; + + if (!domain) { + res.status(400).json({ + success: false, + message: 'Domain is required' + } as ApiResponse); + return; + } + + // Check if certificate already exists + const existingCert = await CertificateModel.findByDomain(domain); + if (existingCert && existingCert.status === 'active') { + res.status(400).json({ + success: false, + message: 'Active certificate already exists for this domain' + } as ApiResponse); + return; + } + + const certificate = await SSLService.requestLetsEncryptCert(domain); + + res.status(201).json({ + success: true, + data: certificate, + message: 'Let\'s Encrypt certificate requested successfully' + } as ApiResponse); + + } catch (error: any) { + logger.error('Request Let\'s Encrypt certificate error:', error); + res.status(400).json({ + success: false, + message: error.message || 'Failed to request Let\'s Encrypt certificate' + } as ApiResponse); + } + } + + /** + * Upload custom certificate + */ + static uploadCustomCert = [ + upload.fields([ + { name: 'certificate', maxCount: 1 }, + { name: 'privateKey', maxCount: 1 } + ]), + async (req: Request, res: Response): Promise => { + try { + const { domain } = req.body; + const files = req.files as { [fieldname: string]: Express.Multer.File[] }; + + if (!domain) { + res.status(400).json({ + success: false, + message: 'Domain is required' + } as ApiResponse); + return; + } + + if (!files.certificate || !files.privateKey) { + res.status(400).json({ + success: false, + message: 'Both certificate and private key files are required' + } as ApiResponse); + return; + } + + const certContent = files.certificate[0].buffer.toString(); + const keyContent = files.privateKey[0].buffer.toString(); + + // Check if certificate already exists + const existingCert = await CertificateModel.findByDomain(domain); + if (existingCert && existingCert.status === 'active') { + res.status(400).json({ + success: false, + message: 'Active certificate already exists for this domain' + } as ApiResponse); + return; + } + + const certificate = await SSLService.uploadCustomCert(domain, certContent, keyContent); + + res.status(201).json({ + success: true, + data: certificate, + message: 'Custom certificate uploaded successfully' + } as ApiResponse); + + } catch (error: any) { + logger.error('Upload custom certificate error:', error); + res.status(400).json({ + success: false, + message: error.message || 'Failed to upload custom certificate' + } as ApiResponse); + } + } + ]; + + /** + * Renew certificate + */ + static async renewCertificate(req: Request, res: Response): Promise { + try { + const id = parseInt(req.params.id); + if (isNaN(id)) { + res.status(400).json({ + success: false, + message: 'Invalid certificate ID' + } as ApiResponse); + return; + } + + const certificate = await CertificateModel.findById(id); + if (!certificate) { + res.status(404).json({ + success: false, + message: 'Certificate not found' + } as ApiResponse); + return; + } + + if (certificate.type !== 'letsencrypt') { + res.status(400).json({ + success: false, + message: 'Only Let\'s Encrypt certificates can be renewed' + } as ApiResponse); + return; + } + + const renewedCert = await SSLService.renewCertificate(certificate.domain); + + res.json({ + success: true, + data: renewedCert, + message: 'Certificate renewed successfully' + } as ApiResponse); + + } catch (error: any) { + logger.error('Renew certificate error:', error); + res.status(400).json({ + success: false, + message: error.message || 'Failed to renew certificate' + } as ApiResponse); + } + } + + /** + * Delete certificate + */ + static async deleteCertificate(req: Request, res: Response): Promise { + try { + const id = parseInt(req.params.id); + if (isNaN(id)) { + res.status(400).json({ + success: false, + message: 'Invalid certificate ID' + } as ApiResponse); + return; + } + + const certificate = await CertificateModel.findById(id); + if (!certificate) { + res.status(404).json({ + success: false, + message: 'Certificate not found' + } as ApiResponse); + return; + } + + // Remove certificate files + await SSLService.removeCertificate(certificate); + + // Delete from database + await CertificateModel.delete(id); + + res.json({ + success: true, + message: 'Certificate deleted successfully' + } as ApiResponse); + + } catch (error: any) { + logger.error('Delete certificate error:', error); + res.status(500).json({ + success: false, + message: error.message || 'Failed to delete certificate' + } as ApiResponse); + } + } + + /** + * Check expiring certificates + */ + static async getExpiringCertificates(req: Request, res: Response): Promise { + try { + const days = parseInt(req.query.days as string) || 30; + const certificates = await SSLService.checkExpiringCertificates(days); + + res.json({ + success: true, + data: certificates, + message: `Found ${certificates.length} certificates expiring within ${days} days` + } as ApiResponse); + + } catch (error) { + logger.error('Get expiring certificates error:', error); + res.status(500).json({ + success: false, + message: 'Failed to check expiring certificates' + } as ApiResponse); + } + } + + /** + * Auto-renew certificates + */ + static async autoRenewCertificates(req: Request, res: Response): Promise { + try { + await SSLService.autoRenewCertificates(); + + res.json({ + success: true, + message: 'Auto-renewal process completed' + } as ApiResponse); + + } catch (error) { + logger.error('Auto-renew certificates error:', error); + res.status(500).json({ + success: false, + message: 'Auto-renewal process failed' + } as ApiResponse); + } + } +} diff --git a/src/controllers/CloudflareController.ts b/src/controllers/CloudflareController.ts new file mode 100644 index 0000000..03796a2 --- /dev/null +++ b/src/controllers/CloudflareController.ts @@ -0,0 +1,61 @@ +import { type Request, type Response } from 'express'; +import { CloudflareService } from '../services/CloudflareService.js'; +import Cloudflare from 'cloudflare'; +import { type ApiResponse } from '../types/index.js'; +import logger from '../utils/logger.js'; + +export class CloudflareController { + /** + * Get all zones + * This endpoint retrieves all Cloudflare zones configured in the system. + */ + static async getZones(req: Request, res: Response): Promise { + try { + const cf = CloudflareService.getInstance(); + const zones = await cf.getZones(); + + res.json({ + success: true, + data: zones + } as ApiResponse); + + } catch (error) { + logger.error('Get cloudflare zones error:', error); + res.status(500).json({ + success: false, + message: 'Failed to fetch cloudflare zones' + } as ApiResponse); + } + } + + /** + * Get proxy by ID + */ + static async getRecords(req: Request, res: Response): Promise { + try { + const zoneId = req.params.id as string; + const cf = CloudflareService.getInstance(); + const records = await cf.getRecords(zoneId); + + if (!records) { + res.status(404).json({ + success: false, + message: 'Zone not found or no records available' + } as ApiResponse); + return; + } + + res.json({ + success: true, + data: records + } as ApiResponse); + + } catch (error) { + logger.error('Get cloudflare zone records error:', error); + res.status(500).json({ + success: false, + message: 'Failed to fetch cloudflare zone records' + } as ApiResponse); + } + } +} diff --git a/src/controllers/ProxyController.ts b/src/controllers/ProxyController.ts new file mode 100644 index 0000000..f223708 --- /dev/null +++ b/src/controllers/ProxyController.ts @@ -0,0 +1,243 @@ +import { Request, Response } from 'express'; +import { ProxyService } from '../services/ProxyService.js'; +import { ApiResponse, Proxy } from '../types/index.js'; +import logger from '../utils/logger.js'; + +export class ProxyController { + /** + * Get all proxies + */ + static async getAllProxies(req: Request, res: Response): Promise { + try { + const proxies = await ProxyService.getAllProxies(); + + res.json({ + success: true, + data: proxies + } as ApiResponse); + + } catch (error) { + logger.error('Get all proxies error:', error); + res.status(500).json({ + success: false, + message: 'Failed to fetch proxies' + } as ApiResponse); + } + } + + /** + * Get proxy by ID + */ + static async getProxyById(req: Request, res: Response): Promise { + try { + const id = parseInt(req.params.id); + if (isNaN(id)) { + res.status(400).json({ + success: false, + message: 'Invalid proxy ID' + } as ApiResponse); + return; + } + + const proxy = await ProxyService.getProxyById(id); + if (!proxy) { + res.status(404).json({ + success: false, + message: 'Proxy not found' + } as ApiResponse); + return; + } + + res.json({ + success: true, + data: proxy + } as ApiResponse); + + } catch (error) { + logger.error('Get proxy by ID error:', error); + res.status(500).json({ + success: false, + message: 'Failed to fetch proxy' + } as ApiResponse); + } + } + + /** + * Create new proxy + */ + static async createProxy(req: Request, res: Response): Promise { + try { + const proxyData = req.body; + + // Set default options if not provided + if (!proxyData.options) { + proxyData.options = { + redirect_http_to_https: false, + custom_headers: {}, + path_forwarding: {}, + enable_websockets: false, + client_max_body_size: '1m' + }; + } + + const proxy = await ProxyService.createProxy(proxyData); + + res.status(201).json({ + success: true, + data: proxy, + message: 'Proxy created successfully' + } as ApiResponse); + + } catch (error: any) { + logger.error('Create proxy error:', error); + res.status(400).json({ + success: false, + message: error.message || 'Failed to create proxy' + } as ApiResponse); + } + } + + /** + * Update proxy + */ + static async updateProxy(req: Request, res: Response): Promise { + try { + const id = parseInt(req.params.id); + if (isNaN(id)) { + res.status(400).json({ + success: false, + message: 'Invalid proxy ID' + } as ApiResponse); + return; + } + + const updateData = req.body; + const proxy = await ProxyService.updateProxy(id, updateData); + + res.json({ + success: true, + data: proxy, + message: 'Proxy updated successfully' + } as ApiResponse); + + } catch (error: any) { + logger.error('Update proxy error:', error); + + if (error.message === 'Proxy not found') { + res.status(404).json({ + success: false, + message: error.message + } as ApiResponse); + } else { + res.status(400).json({ + success: false, + message: error.message || 'Failed to update proxy' + } as ApiResponse); + } + } + } + + /** + * Delete proxy + */ + static async deleteProxy(req: Request, res: Response): Promise { + try { + const id = parseInt(req.params.id); + if (isNaN(id)) { + res.status(400).json({ + success: false, + message: 'Invalid proxy ID' + } as ApiResponse); + return; + } + + await ProxyService.deleteProxy(id); + + res.json({ + success: true, + message: 'Proxy deleted successfully' + } as ApiResponse); + + } catch (error: any) { + logger.error('Delete proxy error:', error); + + if (error.message === 'Proxy not found') { + res.status(404).json({ + success: false, + message: error.message + } as ApiResponse); + } else { + res.status(500).json({ + success: false, + message: error.message || 'Failed to delete proxy' + } as ApiResponse); + } + } + } + + /** + * Test NGINX configuration + */ + static async testNginx(req: Request, res: Response): Promise { + try { + const result = await ProxyService.testNginxConfig(); + + res.json({ + success: result.success, + data: { output: result.output }, + message: result.success ? 'NGINX configuration is valid' : 'NGINX configuration test failed' + } as ApiResponse); + + } catch (error) { + logger.error('Test NGINX error:', error); + res.status(500).json({ + success: false, + message: 'Failed to test NGINX configuration' + } as ApiResponse); + } + } + + /** + * Reload NGINX + */ + static async reloadNginx(req: Request, res: Response): Promise { + try { + const result = await ProxyService.reloadNginx(); + + res.json({ + success: result.success, + data: { output: result.output }, + message: result.success ? 'NGINX reloaded successfully' : 'NGINX reload failed' + } as ApiResponse); + + } catch (error) { + logger.error('Reload NGINX error:', error); + res.status(500).json({ + success: false, + message: 'Failed to reload NGINX' + } as ApiResponse); + } + } + + /** + * Get NGINX status + */ + static async getNginxStatus(req: Request, res: Response): Promise { + try { + const result = await ProxyService.getNginxStatus(); + + res.json({ + success: result.success, + data: { output: result.output }, + message: result.success ? 'NGINX status retrieved' : 'Failed to get NGINX status' + } as ApiResponse); + + } catch (error) { + logger.error('Get NGINX status error:', error); + res.status(500).json({ + success: false, + message: 'Failed to get NGINX status' + } as ApiResponse); + } + } +} diff --git a/src/database/index.ts b/src/database/index.ts new file mode 100644 index 0000000..ea11dd8 --- /dev/null +++ b/src/database/index.ts @@ -0,0 +1,133 @@ +import { Database } from 'bun:sqlite'; +import { config } from '../config/index.js'; +import logger from '../utils/logger.js'; +import path from 'path'; +import fs from 'fs'; +import bcrypt from 'bcryptjs'; + +class DatabaseManager { + private db: Database | null = null; + constructor() { + this.initSync(); + } + + private initSync(): void { + try { + // Ensure the database directory exists + const dbDir = path.dirname(config.database.path); + if (!fs.existsSync(dbDir)) { + fs.mkdirSync(dbDir, { recursive: true }); + } + + // Create database connection + this.db = new Database(config.database.path); + logger.info('Connected to SQLite database'); + + // Enable foreign keys and WAL mode for better performance + this.db.exec('PRAGMA foreign_keys = ON'); + this.db.exec('PRAGMA journal_mode = WAL'); + this.db.exec('PRAGMA synchronous = NORMAL'); + this.db.exec('PRAGMA cache_size = 1000'); + this.db.exec('PRAGMA temp_store = memory'); // Create tables + this.createTables(); + this.createDefaultAdmin(); + } catch (error) { + logger.error('Database initialization failed:', error); + throw error; + } + } + private createTables(): void { + if (!this.db) { + throw new Error('Database not initialized'); + } + + const queries = [ + // Users table + `CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + )`, + + // Proxies table + `CREATE TABLE IF NOT EXISTS proxies ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + domain TEXT UNIQUE NOT NULL, + target TEXT NOT NULL, + ssl_type TEXT NOT NULL DEFAULT 'none', + cert_path TEXT, + key_path TEXT, + options TEXT NOT NULL DEFAULT '{}', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + )`, + + // Certificates table + `CREATE TABLE IF NOT EXISTS certificates ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + domain TEXT UNIQUE NOT NULL, + type TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + path TEXT NOT NULL, + key_path TEXT, + expiry DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + )` + ]; + + try { + queries.forEach((query) => { + this.db!.exec(query); + }); + logger.info('Database tables created successfully'); + } catch (error) { + logger.error('Error creating tables:', error); + throw error; + } + } private createDefaultAdmin(): void { + if (!this.db) { + throw new Error('Database not initialized'); + } + + try { + // Check if admin user already exists + const stmt = this.db.prepare('SELECT id FROM users WHERE username = ?'); + const existingUser = stmt.get(config.admin.username); if (!existingUser) { + // Create default admin user + const hashedPassword = bcrypt.hashSync(config.admin.password, 10); + + const insertStmt = this.db.prepare('INSERT INTO users (username, password) VALUES (?, ?)'); + insertStmt.run(config.admin.username, hashedPassword); + + logger.info('Default admin user created'); + } else { + logger.info('Admin user already exists'); + } + } catch (error) { + logger.error('Error creating default admin:', error); + throw error; + } + } + getDb(): Database { + if (!this.db) { + throw new Error('Database not initialized'); + } + return this.db; + } + + close(): Promise { + return new Promise((resolve) => { + if (this.db) { + this.db.close(); + logger.info('Database connection closed'); + } + resolve(); + }); + } +} + +export const database = new DatabaseManager(); +export default database; diff --git a/src/database/init.ts b/src/database/init.ts new file mode 100644 index 0000000..e4b9ba0 --- /dev/null +++ b/src/database/init.ts @@ -0,0 +1,27 @@ +#!/usr/bin/env bun +import { database } from './index.js'; +import logger from '../utils/logger.js'; + +function initializeDatabase() { + try { + logger.info('Initializing database...'); + + // The database is initialized synchronously when imported + // No need to wait since it's already complete + + logger.info('Database initialized successfully!'); + logger.info('Default admin credentials:'); + logger.info('Username: admin'); + logger.info('Password: admin123'); + logger.info(''); + logger.info('โš ๏ธ Please change the default password after first login!'); + + database.close(); + process.exit(0); + } catch (error) { + logger.error('Database initialization failed:', error); + process.exit(1); + } +} + +initializeDatabase(); diff --git a/src/database/sequelize.ts b/src/database/sequelize.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts new file mode 100644 index 0000000..be847ac --- /dev/null +++ b/src/middleware/auth.ts @@ -0,0 +1,47 @@ +import { Request, Response, NextFunction } from 'express'; +import jwt from 'jsonwebtoken'; +import { config } from '../config/index.js'; +import { AuthPayload } from '../types/index.js'; +import logger from '../utils/logger.js'; + +// Extend Express Request type to include user +declare global { + namespace Express { + interface Request { + user?: AuthPayload; + } + } +} + +export const authenticateToken = (req: Request, res: Response, next: NextFunction): void => { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN + + if (!token) { + res.status(401).json({ + success: false, + message: 'Access token required' + }); + return; + } + + jwt.verify(token, config.jwt.secret, (err, decoded) => { + if (err) { + logger.warn('Invalid token attempt:', { error: err.message }); + res.status(403).json({ + success: false, + message: 'Invalid or expired token' + }); + return; + } + + req.user = decoded as AuthPayload; + next(); + }); +}; + +export const generateToken = (payload: AuthPayload): string => { + return jwt.sign(payload, config.jwt.secret, { + expiresIn: config.jwt.expiresIn + }); +}; diff --git a/src/middleware/validation.ts b/src/middleware/validation.ts new file mode 100644 index 0000000..89b253d --- /dev/null +++ b/src/middleware/validation.ts @@ -0,0 +1,74 @@ +import { Request, Response, NextFunction } from 'express'; +import Joi from 'joi'; +import logger from '../utils/logger.js'; + +export const validateRequest = (schema: Joi.ObjectSchema) => { + return (req: Request, res: Response, next: NextFunction): void => { + const { error } = schema.validate(req.body); + + if (error) { + logger.warn('Validation error:', { + error: error.details[0].message, + path: req.path, + body: req.body + }); + + res.status(400).json({ + success: false, + message: 'Validation error', + error: error.details[0].message + }); + return; + } + + next(); + }; +}; + +// Validation schemas +export const schemas = { + login: Joi.object({ + username: Joi.string().required(), + password: Joi.string().required() + }), + + proxy: Joi.object({ + domain: Joi.string().domain().required(), + target: Joi.string().uri().required(), + ssl_type: Joi.string().valid('letsencrypt', 'custom', 'none').default('none'), + cert_path: Joi.string().optional(), + key_path: Joi.string().optional(), + options: Joi.object({ + redirect_http_to_https: Joi.boolean().default(false), + custom_headers: Joi.object().pattern(Joi.string(), Joi.string()).default({}), + path_forwarding: Joi.object().pattern(Joi.string(), Joi.string()).default({}), + enable_websockets: Joi.boolean().default(false), + client_max_body_size: Joi.string().default('1m') + }).default({}) + }), + + proxyUpdate: Joi.object({ + domain: Joi.string().domain().optional(), + target: Joi.string().uri().optional(), + ssl_type: Joi.string().valid('letsencrypt', 'custom', 'none').optional(), + cert_path: Joi.string().optional(), + key_path: Joi.string().optional(), + options: Joi.object({ + redirect_http_to_https: Joi.boolean().optional(), + custom_headers: Joi.object().pattern(Joi.string(), Joi.string()).optional(), + path_forwarding: Joi.object().pattern(Joi.string(), Joi.string()).optional(), + enable_websockets: Joi.boolean().optional(), + client_max_body_size: Joi.string().optional() + }).optional() + }), + + certificate: Joi.object({ + domain: Joi.string().domain().required(), + type: Joi.string().valid('letsencrypt', 'custom').required() + }), + + changePassword: Joi.object({ + currentPassword: Joi.string().required(), + newPassword: Joi.string().min(6).required() + }) +}; diff --git a/src/models/Certificate.ts b/src/models/Certificate.ts new file mode 100644 index 0000000..b060de1 --- /dev/null +++ b/src/models/Certificate.ts @@ -0,0 +1,142 @@ +import { database } from '../database/index.js'; +import { Certificate } from '../types/index.js'; +import logger from '../utils/logger.js'; + +export class CertificateModel { + static async findAll(): Promise { + try { + const db = database.getDb(); + const stmt = db.prepare('SELECT * FROM certificates ORDER BY created_at DESC'); + return stmt.all() as Certificate[]; + } catch (error) { + logger.error('Error fetching all certificates:', error); + throw error; + } + } + + static async findById(id: number): Promise { + try { + const db = database.getDb(); + const stmt = db.prepare('SELECT * FROM certificates WHERE id = ?'); + const row = stmt.get(id) as Certificate | undefined; + return row || null; + } catch (error) { + logger.error('Error finding certificate by ID:', error); + throw error; + } + } + + static async findByDomain(domain: string): Promise { + try { + const db = database.getDb(); + const stmt = db.prepare('SELECT * FROM certificates WHERE domain = ?'); + const row = stmt.get(domain) as Certificate | undefined; + return row || null; + } catch (error) { + logger.error('Error finding certificate by domain:', error); + throw error; + } + } + + static async findExpiringSoon(days: number = 30): Promise { + try { + const db = database.getDb(); + const expiryDate = new Date(); + expiryDate.setDate(expiryDate.getDate() + days); + + const stmt = db.prepare('SELECT * FROM certificates WHERE expiry <= ? AND status = "active"'); + return stmt.all(expiryDate.toISOString()) as Certificate[]; + } catch (error) { + logger.error('Error finding expiring certificates:', error); + throw error; + } + } + + static async create(certData: Omit): Promise { + try { + const db = database.getDb(); + const stmt = db.prepare(` + INSERT INTO certificates (domain, type, status, path, key_path, expiry) + VALUES (?, ?, ?, ?, ?, ?) RETURNING * + `); + + const result = stmt.get( + certData.domain, + certData.type, + certData.status, + certData.path, + certData.key_path || null, + certData.expiry || null + ) as Certificate; + + return result; + } catch (error) { + logger.error('Error creating certificate:', error); + throw error; + } + } + + static async update(id: number, certData: Partial): Promise { + try { + const db = database.getDb(); + const fields: string[] = []; + const values: any[] = []; + + if (certData.domain !== undefined) { + fields.push('domain = ?'); + values.push(certData.domain); + } + if (certData.type !== undefined) { + fields.push('type = ?'); + values.push(certData.type); + } + if (certData.status !== undefined) { + fields.push('status = ?'); + values.push(certData.status); + } + if (certData.path !== undefined) { + fields.push('path = ?'); + values.push(certData.path); + } + if (certData.key_path !== undefined) { + fields.push('key_path = ?'); + values.push(certData.key_path); + } + if (certData.expiry !== undefined) { + fields.push('expiry = ?'); + values.push(certData.expiry); + } + + if (fields.length === 0) { + return null; + } + + fields.push('updated_at = CURRENT_TIMESTAMP'); + values.push(id); + + const stmt = db.prepare(`UPDATE certificates SET ${fields.join(', ')} WHERE id = ?`); + const result = stmt.run(...values); + + if (result.changes === 0) { + return null; + } + + return CertificateModel.findById(id); + } catch (error) { + logger.error('Error updating certificate:', error); + throw error; + } + } + + static async delete(id: number): Promise { + try { + const db = database.getDb(); + const stmt = db.prepare('DELETE FROM certificates WHERE id = ?'); + const result = stmt.run(id); + return result.changes > 0; + } catch (error) { + logger.error('Error deleting certificate:', error); + throw error; + } + } +} diff --git a/src/models/Proxy.ts b/src/models/Proxy.ts new file mode 100644 index 0000000..f0e1997 --- /dev/null +++ b/src/models/Proxy.ts @@ -0,0 +1,145 @@ +import { database } from '../database/index.js'; +import { Proxy, ProxyOptions } from '../types/index.js'; +import logger from '../utils/logger.js'; + +export class ProxyModel { + static async findAll(): Promise { + try { + const db = database.getDb(); + const stmt = db.prepare('SELECT * FROM proxies ORDER BY created_at DESC'); + const rows = stmt.all() as any[]; + + const proxies = rows.map(row => ({ + ...row, + options: JSON.parse(row.options) + })); + return proxies; + } catch (error) { + logger.error('Error fetching all proxies:', error); + throw error; + } + } + + static async findById(id: number): Promise { + try { + const db = database.getDb(); + const stmt = db.prepare('SELECT * FROM proxies WHERE id = ?'); + const row = stmt.get(id) as any; + + if (row) { + row.options = JSON.parse(row.options); + } + return row || null; + } catch (error) { + logger.error('Error finding proxy by ID:', error); + throw error; + } + } + + static async findByDomain(domain: string): Promise { + try { + const db = database.getDb(); + const stmt = db.prepare('SELECT * FROM proxies WHERE domain = ?'); + const row = stmt.get(domain) as any; + + if (row) { + row.options = JSON.parse(row.options); + } + return row || null; + } catch (error) { + logger.error('Error finding proxy by domain:', error); + throw error; + } + } + + static async create(proxyData: Omit): Promise { + try { + const db = database.getDb(); + const optionsJson = JSON.stringify(proxyData.options); + + const stmt = db.prepare(` + INSERT INTO proxies (domain, target, ssl_type, cert_path, key_path, options) + VALUES (?, ?, ?, ?, ?, ?) RETURNING * + `); + + const result = stmt.get( + proxyData.domain, + proxyData.target, + proxyData.ssl_type, + proxyData.cert_path || null, + proxyData.key_path || null, + optionsJson + ) as any; + + result.options = JSON.parse(result.options); + return result; + } catch (error) { + logger.error('Error creating proxy:', error); + throw error; + } + } + + static async update(id: number, proxyData: Partial): Promise { + try { + const db = database.getDb(); + const fields: string[] = []; + const values: any[] = []; + + if (proxyData.domain !== undefined) { + fields.push('domain = ?'); + values.push(proxyData.domain); + } + if (proxyData.target !== undefined) { + fields.push('target = ?'); + values.push(proxyData.target); + } + if (proxyData.ssl_type !== undefined) { + fields.push('ssl_type = ?'); + values.push(proxyData.ssl_type); + } + if (proxyData.cert_path !== undefined) { + fields.push('cert_path = ?'); + values.push(proxyData.cert_path); + } + if (proxyData.key_path !== undefined) { + fields.push('key_path = ?'); + values.push(proxyData.key_path); + } + if (proxyData.options !== undefined) { + fields.push('options = ?'); + values.push(JSON.stringify(proxyData.options)); + } + + if (fields.length === 0) { + return null; + } + + fields.push('updated_at = CURRENT_TIMESTAMP'); + values.push(id); + + const stmt = db.prepare(`UPDATE proxies SET ${fields.join(', ')} WHERE id = ?`); + const result = stmt.run(...values); + + if (result.changes === 0) { + return null; + } + + return ProxyModel.findById(id); + } catch (error) { + logger.error('Error updating proxy:', error); + throw error; + } + } + + static async delete(id: number): Promise { + try { + const db = database.getDb(); + const stmt = db.prepare('DELETE FROM proxies WHERE id = ?'); + const result = stmt.run(id); + return result.changes > 0; + } catch (error) { + logger.error('Error deleting proxy:', error); + throw error; + } + } +} diff --git a/src/models/User.ts b/src/models/User.ts new file mode 100644 index 0000000..ffb8aa2 --- /dev/null +++ b/src/models/User.ts @@ -0,0 +1,53 @@ +import { database } from '../database/index.js'; +import { User } from '../types/index.js'; +import logger from '../utils/logger.js'; + +export class UserModel { + static async findByUsername(username: string): Promise { + try { + const db = database.getDb(); + const stmt = db.prepare('SELECT * FROM users WHERE username = ?'); + const row = stmt.get(username) as User | undefined; + return row || null; + } catch (error) { + logger.error('Error finding user by username:', error); + throw error; + } + } + + static async findById(id: number): Promise { + try { + const db = database.getDb(); + const stmt = db.prepare('SELECT * FROM users WHERE id = ?'); + const row = stmt.get(id) as User | undefined; + return row || null; + } catch (error) { + logger.error('Error finding user by ID:', error); + throw error; + } + } + + static async create(userData: Omit): Promise { + try { + const db = database.getDb(); + const stmt = db.prepare('INSERT INTO users (username, password) VALUES (?, ?) RETURNING *'); + const result = stmt.get(userData.username, userData.password) as User; + return result; + } catch (error) { + logger.error('Error creating user:', error); + throw error; + } + } + + static async updatePassword(id: number, hashedPassword: string): Promise { + try { + const db = database.getDb(); + const stmt = db.prepare('UPDATE users SET password = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?'); + const result = stmt.run(hashedPassword, id); + return result.changes > 0; + } catch (error) { + logger.error('Error updating user password:', error); + throw error; + } + } +} diff --git a/src/routes/auth.ts b/src/routes/auth.ts new file mode 100644 index 0000000..a225bf3 --- /dev/null +++ b/src/routes/auth.ts @@ -0,0 +1,16 @@ +import { Router } from 'express'; +import { AuthController } from '../controllers/AuthController.js'; +import { validateRequest, schemas } from '../middleware/validation.js'; +import { authenticateToken } from '../middleware/auth.js'; + +const router = Router(); + +// Public routes +router.post('/login', validateRequest(schemas.login), AuthController.login); +router.post('/logout', AuthController.logout); + +// Protected routes +router.get('/me', authenticateToken, AuthController.me); +router.post('/change-password', authenticateToken, validateRequest(schemas.changePassword), AuthController.changePassword); + +export default router; diff --git a/src/routes/certificates.ts b/src/routes/certificates.ts new file mode 100644 index 0000000..dc19feb --- /dev/null +++ b/src/routes/certificates.ts @@ -0,0 +1,25 @@ +import { Router } from 'express'; +import { CertificateController } from '../controllers/CertificateController.js'; +import { validateRequest, schemas } from '../middleware/validation.js'; +import { authenticateToken } from '../middleware/auth.js'; + +const router = Router(); + +// All certificate routes require authentication +router.use(authenticateToken); + +// Certificate CRUD routes +router.get('/', CertificateController.getAllCertificates); +router.get('/:id', CertificateController.getCertificateById); +router.delete('/:id', CertificateController.deleteCertificate); + +// Certificate management routes +router.post('/letsencrypt', validateRequest(schemas.certificate), CertificateController.requestLetsEncrypt); +router.post('/custom', CertificateController.uploadCustomCert); +router.post('/:id/renew', CertificateController.renewCertificate); + +// Certificate monitoring routes +router.get('/expiring/check', CertificateController.getExpiringCertificates); +router.post('/expiring/renew', CertificateController.autoRenewCertificates); + +export default router; diff --git a/src/routes/cloudflare.ts b/src/routes/cloudflare.ts new file mode 100644 index 0000000..2edbc03 --- /dev/null +++ b/src/routes/cloudflare.ts @@ -0,0 +1,18 @@ +import { Router } from 'express'; +import { CloudflareController } from '../controllers/CloudflareController.js'; +import { validateRequest, schemas } from '../middleware/validation.js'; +import { authenticateToken } from '../middleware/auth.js'; + +const router = Router(); + +// All cloudflare routes require authentication +router.use(authenticateToken); + +// Cloudflare CRUD routes +router.get('/', (req, res) => { + res.json({ success: true, message: 'Cloudflare API routes', timestamp: new Date().toISOString() }); +}); +router.get('/zones', CloudflareController.getZones); +router.get('/zones/:id/records', CloudflareController.getRecords); + +export default router; diff --git a/src/routes/index.ts b/src/routes/index.ts new file mode 100644 index 0000000..199744e --- /dev/null +++ b/src/routes/index.ts @@ -0,0 +1,24 @@ +import { Router } from 'express'; +import authRoutes from './auth.js'; +import proxyRoutes from './proxies.js'; +import certificateRoutes from './certificates.js'; +import cloudflareRoutes from './cloudflare.js'; + +const router = Router(); + +// API routes +router.use('/api/auth', authRoutes); +router.use('/api/proxies', proxyRoutes); +router.use('/api/certificates', certificateRoutes); +router.use('/api/cloudflare', cloudflareRoutes); + +// Health check endpoint +router.get('/api/health', (req, res) => { + res.json({ + success: true, + message: 'NGINX Proxy Manager API is running', + timestamp: new Date().toISOString() + }); +}); + +export default router; diff --git a/src/routes/proxies.ts b/src/routes/proxies.ts new file mode 100644 index 0000000..0f60fab --- /dev/null +++ b/src/routes/proxies.ts @@ -0,0 +1,23 @@ +import { Router } from 'express'; +import { ProxyController } from '../controllers/ProxyController.js'; +import { validateRequest, schemas } from '../middleware/validation.js'; +import { authenticateToken } from '../middleware/auth.js'; + +const router = Router(); + +// All proxy routes require authentication +router.use(authenticateToken); + +// Proxy CRUD routes +router.get('/', ProxyController.getAllProxies); +router.get('/:id', ProxyController.getProxyById); +router.post('/', validateRequest(schemas.proxy), ProxyController.createProxy); +router.put('/:id', validateRequest(schemas.proxyUpdate), ProxyController.updateProxy); +router.delete('/:id', ProxyController.deleteProxy); + +// NGINX management routes +router.post('/nginx/test', ProxyController.testNginx); +router.post('/nginx/reload', ProxyController.reloadNginx); +router.get('/nginx/status', ProxyController.getNginxStatus); + +export default router; diff --git a/src/services/CloudflareService.ts b/src/services/CloudflareService.ts new file mode 100644 index 0000000..eb57a6e --- /dev/null +++ b/src/services/CloudflareService.ts @@ -0,0 +1,42 @@ +import Cloudflare from 'cloudflare'; +import logger from '../utils/logger.js'; +import fs from 'fs'; + +export class CloudflareService { + private cf: Cloudflare | null = null; + public static instance: CloudflareService; + private constructor() { + // Private constructor to enforce singleton pattern + } + + public static getInstance(): CloudflareService { + if (!CloudflareService.instance) { + CloudflareService.instance = new CloudflareService(); + } + return CloudflareService.instance; + } + + + init(options: { apiToken: string; active: boolean }): void { + if (options.active) { + this.cf = new Cloudflare({ + apiToken: options.apiToken + }); + logger.info('Cloudflare service initialized.'); + } + } + + async getZones() { + if (!this.cf) return console.error('Cloudflare service is not initialized.'); + const res = await this.cf.zones.list(); + return res.result + } + + async getRecords(zoneId: string) { + if (!this.cf) return console.error('Cloudflare service is not initialized.'); + const data = await this.cf.dns.records.list({ zone_id: zoneId }) as any; + return data.body.result + } + + +} \ No newline at end of file diff --git a/src/services/CronService.ts b/src/services/CronService.ts new file mode 100644 index 0000000..61508e8 --- /dev/null +++ b/src/services/CronService.ts @@ -0,0 +1,68 @@ +import cron from 'node-cron'; +import { SSLService } from './SSLService.js'; +import logger from '../utils/logger.js'; + +export class CronService { + private static jobs: Map = new Map(); + + /** + * Initialize all cron jobs + */ + static init(): void { + this.startCertificateRenewalJob(); + logger.info('Cron service initialized'); + } + + /** + * Start certificate renewal job + * Runs daily at 2:00 AM to check and renew expiring certificates + */ + private static startCertificateRenewalJob(): void { + const task = cron.schedule('0 2 * * *', async () => { + logger.info('Running automatic certificate renewal check'); + try { + await SSLService.autoRenewCertificates(); + logger.info('Automatic certificate renewal check completed'); + } catch (error) { + logger.error('Automatic certificate renewal check failed:', error); + } + }, { + scheduled: false, + timezone: 'UTC' + }); + + this.jobs.set('certificate-renewal', task); + task.start(); + logger.info('Certificate renewal cron job started (daily at 2:00 AM UTC)'); + } + + /** + * Stop all cron jobs + */ + static stop(): void { + this.jobs.forEach((task, name) => { + task.stop(); + logger.info(`Stopped cron job: ${name}`); + }); + this.jobs.clear(); + } + + /** + * Get status of all cron jobs + */ + static getStatus(): { [key: string]: boolean } { + const status: { [key: string]: boolean } = {}; + this.jobs.forEach((task, name) => { + status[name] = task.getStatus() === 'scheduled'; + }); + return status; + } + + /** + * Manually trigger certificate renewal + */ + static async triggerCertificateRenewal(): Promise { + logger.info('Manually triggering certificate renewal'); + await SSLService.autoRenewCertificates(); + } +} diff --git a/src/services/NginxService.ts b/src/services/NginxService.ts new file mode 100644 index 0000000..ad6a7b3 --- /dev/null +++ b/src/services/NginxService.ts @@ -0,0 +1,236 @@ +import fs from 'fs'; +import path from 'path'; +import { exec } from 'child_process'; +import { promisify } from 'util'; +import { config } from '../config/index.js'; +import { NginxConfigOptions } from '../types/index.js'; +import logger from '../utils/logger.js'; + +const execAsync = promisify(exec); + +export class NginxService { + private static configPath = config.nginx.configPath; + private static nginxBinary = config.nginx.binaryPath; + + /** + * Generate NGINX configuration for a proxy + */ + static generateConfig(options: NginxConfigOptions): string { + const { + domain, + target, + sslEnabled, + certPath, + keyPath, + redirectHttpToHttps, + customHeaders, + pathForwarding, + enableWebsockets, + clientMaxBodySize + } = options; + + let config = ''; + + // HTTP server block (always present) + config += `server { + listen 80; + server_name ${domain}; + client_max_body_size ${clientMaxBodySize}; + +`; + + // Add custom headers + Object.entries(customHeaders).forEach(([name, value]) => { + config += ` add_header "${name}" "${value}";\n`; + }); + + if (redirectHttpToHttps && sslEnabled) { + // Redirect HTTP to HTTPS + config += ` return 301 https://$server_name$request_uri; +} + +`; + } else { + // HTTP proxy configuration + config += this.generateProxyConfig(target, pathForwarding, enableWebsockets); + config += `} + +`; + } + + // HTTPS server block (if SSL is enabled) + if (sslEnabled && certPath && keyPath) { + config += `server { + listen 443 ssl http2; + server_name ${domain}; + client_max_body_size ${clientMaxBodySize}; + + ssl_certificate ${certPath}; + ssl_certificate_key ${keyPath}; + + # SSL configuration + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384; + ssl_prefer_server_ciphers off; + ssl_session_timeout 1d; + ssl_session_cache shared:SSL:50m; + ssl_stapling on; + ssl_stapling_verify on; + +`; + + // Add custom headers + Object.entries(customHeaders).forEach(([name, value]) => { + config += ` add_header "${name}" "${value}";\n`; + }); + + config += this.generateProxyConfig(target, pathForwarding, enableWebsockets); + config += `} + +`; + } + + return config; + } + + /** + * Generate proxy configuration block + */ + private static generateProxyConfig(target: string, pathForwarding: Record, enableWebsockets: boolean): string { + let config = ''; + + // Path forwarding + if (Object.keys(pathForwarding).length > 0) { + Object.entries(pathForwarding).forEach(([path, upstream]) => { + config += ` location ${path} { + proxy_pass ${upstream}; + 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; +`; + + if (enableWebsockets) { + config += ` proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; +`; + } + + config += ` } + +`; + }); + } else { + // Default location + config += ` location / { + proxy_pass ${target}; + 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; +`; + + if (enableWebsockets) { + config += ` proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; +`; + } + + config += ` } + +`; + } + + return config; + } + + /** + * Write configuration file to disk + */ + static async writeConfig(domain: string, configContent: string): Promise { + try { + // Ensure config directory exists + if (!fs.existsSync(this.configPath)) { + fs.mkdirSync(this.configPath, { recursive: true }); + } + + const configFile = path.join(this.configPath, `${domain}.conf`); + fs.writeFileSync(configFile, configContent); + + logger.info(`NGINX config written for ${domain}`, { configFile }); + } catch (error) { + logger.error(`Failed to write NGINX config for ${domain}:`, error); + throw error; + } + } + + /** + * Remove configuration file + */ + static async removeConfig(domain: string): Promise { + try { + const configFile = path.join(this.configPath, `${domain}.conf`); + + if (fs.existsSync(configFile)) { + fs.unlinkSync(configFile); + logger.info(`NGINX config removed for ${domain}`, { configFile }); + } + } catch (error) { + logger.error(`Failed to remove NGINX config for ${domain}:`, error); + throw error; + } + } + + /** + * Test NGINX configuration + */ + static async testConfig(): Promise<{ success: boolean; output: string }> { + try { + const { stdout, stderr } = await execAsync(`${this.nginxBinary} -t`); + logger.info('NGINX config test passed'); + return { success: true, output: stdout + stderr }; + } catch (error: any) { + logger.error('NGINX config test failed:', error); + return { success: false, output: error.stdout + error.stderr }; + } + } + + /** + * Reload NGINX + */ + static async reload(): Promise<{ success: boolean; output: string }> { + try { + // First test the config + const testResult = await this.testConfig(); + if (!testResult.success) { + return testResult; + } + + // Reload NGINX + const { stdout, stderr } = await execAsync(`${this.nginxBinary} -s reload`); + logger.info('NGINX reloaded successfully'); + return { success: true, output: stdout + stderr }; + } catch (error: any) { + logger.error('NGINX reload failed:', error); + return { success: false, output: error.stdout + error.stderr }; + } + } + + /** + * Get NGINX status + */ + static async getStatus(): Promise<{ success: boolean; output: string }> { + try { + const { stdout, stderr } = await execAsync('nginx -v'); + return { success: true, output: stdout + stderr }; + } catch (error: any) { + return { success: false, output: error.stdout + error.stderr }; + } + } +} diff --git a/src/services/ProxyService.ts b/src/services/ProxyService.ts new file mode 100644 index 0000000..c8837cc --- /dev/null +++ b/src/services/ProxyService.ts @@ -0,0 +1,239 @@ +import { Proxy } from '../types/index.js'; +import { ProxyModel } from '../models/Proxy.js'; +import { CertificateModel } from '../models/Certificate.js'; +import { NginxService } from './NginxService.js'; +import { SSLService } from './SSLService.js'; +import logger from '../utils/logger.js'; + +export class ProxyService { + /** + * Create a new proxy entry + */ + static async createProxy(proxyData: Omit): Promise { + try { + logger.info(`Creating proxy for ${proxyData.domain}`); + + // Check if proxy already exists + const existingProxy = await ProxyModel.findByDomain(proxyData.domain); + if (existingProxy) { + throw new Error(`Proxy for domain ${proxyData.domain} already exists`); + } + + // Create proxy in database + const proxy = await ProxyModel.create(proxyData); + + // Handle SSL certificate if needed + if (proxy.ssl_type === 'letsencrypt') { + try { + const certificate = await SSLService.requestLetsEncryptCert(proxy.domain); + proxy.cert_path = certificate.path; + proxy.key_path = certificate.key_path; + + // Update proxy with certificate paths + await ProxyModel.update(proxy.id!, { + cert_path: certificate.path, + key_path: certificate.key_path + }); + } catch (sslError) { + logger.error(`SSL certificate creation failed for ${proxy.domain}:`, sslError); + // Continue with proxy creation but without SSL + proxy.ssl_type = 'none'; + await ProxyModel.update(proxy.id!, { ssl_type: 'none' }); + } + } + + // Generate and write NGINX configuration + await this.updateNginxConfig(proxy); + + // Reload NGINX + const reloadResult = await NginxService.reload(); + if (!reloadResult.success) { + // Rollback: remove proxy and config + await ProxyModel.delete(proxy.id!); + await NginxService.removeConfig(proxy.domain); + throw new Error(`NGINX reload failed: ${reloadResult.output}`); + } + + logger.info(`Proxy successfully created for ${proxy.domain}`); + return proxy; + } catch (error) { + logger.error(`Failed to create proxy for ${proxyData.domain}:`, error); + throw error; + } + } + + /** + * Update an existing proxy + */ + static async updateProxy(id: number, updateData: Partial): Promise { + try { + const existingProxy = await ProxyModel.findById(id); + if (!existingProxy) { + throw new Error('Proxy not found'); + } + + logger.info(`Updating proxy for ${existingProxy.domain}`); + + // Handle SSL type changes + if (updateData.ssl_type && updateData.ssl_type !== existingProxy.ssl_type) { + if (updateData.ssl_type === 'letsencrypt') { + try { + const certificate = await SSLService.requestLetsEncryptCert(existingProxy.domain); + updateData.cert_path = certificate.path; + updateData.key_path = certificate.key_path; + } catch (sslError) { + logger.error(`SSL certificate creation failed:`, sslError); + throw sslError; + } + } else if (updateData.ssl_type === 'none') { + updateData.cert_path = null; + updateData.key_path = null; + } + } + + // Update proxy in database + const updatedProxy = await ProxyModel.update(id, updateData); + if (!updatedProxy) { + throw new Error('Failed to update proxy'); + } + + // Update NGINX configuration + await this.updateNginxConfig(updatedProxy); + + // Reload NGINX + const reloadResult = await NginxService.reload(); + if (!reloadResult.success) { + throw new Error(`NGINX reload failed: ${reloadResult.output}`); + } + + logger.info(`Proxy successfully updated for ${updatedProxy.domain}`); + return updatedProxy; + } catch (error) { + logger.error(`Failed to update proxy ${id}:`, error); + throw error; + } + } + + /** + * Delete a proxy + */ + static async deleteProxy(id: number): Promise { + try { + const proxy = await ProxyModel.findById(id); + if (!proxy) { + throw new Error('Proxy not found'); + } + + logger.info(`Deleting proxy for ${proxy.domain}`); + + // Remove NGINX configuration + await NginxService.removeConfig(proxy.domain); + + // Remove certificate if it's a Let's Encrypt cert + if (proxy.ssl_type === 'letsencrypt') { + const certificate = await CertificateModel.findByDomain(proxy.domain); + if (certificate) { + await SSLService.removeCertificate(certificate); + await CertificateModel.delete(certificate.id!); + } + } + + // Delete proxy from database + await ProxyModel.delete(id); + + // Reload NGINX + const reloadResult = await NginxService.reload(); + if (!reloadResult.success) { + logger.warn(`NGINX reload failed after deleting proxy: ${reloadResult.output}`); + } + + logger.info(`Proxy successfully deleted for ${proxy.domain}`); + } catch (error) { + logger.error(`Failed to delete proxy ${id}:`, error); + throw error; + } + } + + /** + * Get all proxies + */ + static async getAllProxies(): Promise { + try { + return await ProxyModel.findAll(); + } catch (error) { + logger.error('Failed to get all proxies:', error); + throw error; + } + } + + /** + * Get a single proxy by ID + */ + static async getProxyById(id: number): Promise { + try { + return await ProxyModel.findById(id); + } catch (error) { + logger.error(`Failed to get proxy ${id}:`, error); + throw error; + } + } + + /** + * Update NGINX configuration for a proxy + */ + private static async updateNginxConfig(proxy: Proxy): Promise { + const sslEnabled = proxy.ssl_type !== 'none' && proxy.cert_path && proxy.key_path; + + const configOptions = { + domain: proxy.domain, + target: proxy.target, + sslEnabled, + certPath: proxy.cert_path, + keyPath: proxy.key_path, + redirectHttpToHttps: proxy.options.redirect_http_to_https, + customHeaders: proxy.options.custom_headers, + pathForwarding: proxy.options.path_forwarding, + enableWebsockets: proxy.options.enable_websockets, + clientMaxBodySize: proxy.options.client_max_body_size + }; + + const configContent = NginxService.generateConfig(configOptions); + await NginxService.writeConfig(proxy.domain, configContent); + } + + /** + * Test NGINX configuration + */ + static async testNginxConfig(): Promise<{ success: boolean; output: string }> { + try { + return await NginxService.testConfig(); + } catch (error) { + logger.error('Failed to test NGINX config:', error); + throw error; + } + } + + /** + * Reload NGINX + */ + static async reloadNginx(): Promise<{ success: boolean; output: string }> { + try { + return await NginxService.reload(); + } catch (error) { + logger.error('Failed to reload NGINX:', error); + throw error; + } + } + + /** + * Get NGINX status + */ + static async getNginxStatus(): Promise<{ success: boolean; output: string }> { + try { + return await NginxService.getStatus(); + } catch (error) { + logger.error('Failed to get NGINX status:', error); + throw error; + } + } +} diff --git a/src/services/SSLService.ts b/src/services/SSLService.ts new file mode 100644 index 0000000..c32fb24 --- /dev/null +++ b/src/services/SSLService.ts @@ -0,0 +1,293 @@ +import fs from 'fs'; +import path from 'path'; +import { exec } from 'child_process'; +import { promisify } from 'util'; +import { config } from '../config/index.js'; +import { Certificate } from '../types/index.js'; +import { CertificateModel } from '../models/Certificate.js'; +import logger from '../utils/logger.js'; + +const execAsync = promisify(exec); + +export class SSLService { + private static customCertsPath = config.ssl.customCertsPath; + + /** + * Request a new Let's Encrypt certificate using acme.sh + */ + static async requestLetsEncryptCert(domain: string): Promise { + try { + logger.info(`Requesting Let's Encrypt certificate for ${domain}`); + + let certPath: string; + let keyPath: string; + let command: string; + + if (config.ssl.method === 'acme.sh') { + command = `${config.ssl.acmeShPath}/acme.sh --issue -d ${domain} --standalone`; + certPath = `${config.ssl.acmeShPath}/${domain}/${domain}.cer`; + keyPath = `${config.ssl.acmeShPath}/${domain}/${domain}.key`; + } else { + // certbot + command = `${config.ssl.certbotPath} certonly --standalone -d ${domain} --non-interactive --agree-tos --email admin@${domain}`; + certPath = `/etc/letsencrypt/live/${domain}/fullchain.pem`; + keyPath = `/etc/letsencrypt/live/${domain}/privkey.pem`; + } + + const { stdout, stderr } = await execAsync(command); + logger.info(`Certificate request output: ${stdout + stderr}`); + + // Verify certificate files exist + if (!fs.existsSync(certPath) || !fs.existsSync(keyPath)) { + throw new Error('Certificate files not found after issuance'); + } + + // Get certificate expiry date + const certInfo = await this.getCertificateInfo(certPath); + + // Save certificate info to database + const certificate = await CertificateModel.create({ + domain, + type: 'letsencrypt', + status: 'active', + path: certPath, + key_path: keyPath, + expiry: certInfo.expiry + }); + + logger.info(`Let's Encrypt certificate successfully issued for ${domain}`); + return certificate; + } catch (error) { + logger.error(`Failed to request Let's Encrypt certificate for ${domain}:`, error); + + // Update database with failed status + try { + await CertificateModel.create({ + domain, + type: 'letsencrypt', + status: 'failed', + path: '', + }); + } catch (dbError) { + logger.error('Failed to save certificate failure status:', dbError); + } + + throw error; + } + } + + /** + * Upload and store custom certificate + */ + static async uploadCustomCert( + domain: string, + certContent: string, + keyContent: string + ): Promise { + try { + logger.info(`Uploading custom certificate for ${domain}`); + + // Ensure custom certs directory exists + if (!fs.existsSync(this.customCertsPath)) { + fs.mkdirSync(this.customCertsPath, { recursive: true }); + } + + const certPath = path.join(this.customCertsPath, `${domain}.crt`); + const keyPath = path.join(this.customCertsPath, `${domain}.key`); + + // Write certificate files + fs.writeFileSync(certPath, certContent); + fs.writeFileSync(keyPath, keyContent); + + // Set proper permissions (readable only by root) + fs.chmodSync(certPath, 0o600); + fs.chmodSync(keyPath, 0o600); + + // Validate certificate + await this.validateCertificate(certPath, keyPath); + + // Get certificate info + const certInfo = await this.getCertificateInfo(certPath); + + // Save certificate info to database + const certificate = await CertificateModel.create({ + domain, + type: 'custom', + status: 'active', + path: certPath, + key_path: keyPath, + expiry: certInfo.expiry + }); + + logger.info(`Custom certificate successfully uploaded for ${domain}`); + return certificate; + } catch (error) { + logger.error(`Failed to upload custom certificate for ${domain}:`, error); + throw error; + } + } + + /** + * Renew Let's Encrypt certificate + */ + static async renewCertificate(domain: string): Promise { + try { + logger.info(`Renewing certificate for ${domain}`); + + let command: string; + + if (config.ssl.method === 'acme.sh') { + command = `${config.ssl.acmeShPath}/acme.sh --renew -d ${domain}`; + } else { + command = `${config.ssl.certbotPath} renew --cert-name ${domain}`; + } + + const { stdout, stderr } = await execAsync(command); + logger.info(`Certificate renewal output: ${stdout + stderr}`); + + // Get updated certificate info + const existingCert = await CertificateModel.findByDomain(domain); + if (!existingCert) { + throw new Error('Certificate not found in database'); + } + + const certInfo = await this.getCertificateInfo(existingCert.path); + + // Update certificate in database + const updatedCert = await CertificateModel.update(existingCert.id!, { + status: 'active', + expiry: certInfo.expiry + }); + + if (!updatedCert) { + throw new Error('Failed to update certificate in database'); + } + + logger.info(`Certificate successfully renewed for ${domain}`); + return updatedCert; + } catch (error) { + logger.error(`Failed to renew certificate for ${domain}:`, error); + throw error; + } + } + + /** + * Get certificate information + */ + static async getCertificateInfo(certPath: string): Promise<{ expiry: string; subject: string }> { + try { + const command = `openssl x509 -in ${certPath} -noout -enddate -subject`; + const { stdout } = await execAsync(command); + + const lines = stdout.trim().split('\n'); + const endDateLine = lines.find(line => line.startsWith('notAfter=')); + const subjectLine = lines.find(line => line.startsWith('subject=')); + + if (!endDateLine) { + throw new Error('Could not parse certificate expiry date'); + } + + const expiryStr = endDateLine.replace('notAfter=', ''); + const expiry = new Date(expiryStr).toISOString(); + const subject = subjectLine?.replace('subject=', '') || ''; + + return { expiry, subject }; + } catch (error) { + logger.error(`Failed to get certificate info for ${certPath}:`, error); + throw error; + } + } + + /** + * Validate certificate and key pair + */ + static async validateCertificate(certPath: string, keyPath: string): Promise { + try { + // Check if certificate and key match + const certCommand = `openssl x509 -noout -modulus -in ${certPath} | openssl md5`; + const keyCommand = `openssl rsa -noout -modulus -in ${keyPath} | openssl md5`; + + const [{ stdout: certHash }, { stdout: keyHash }] = await Promise.all([ + execAsync(certCommand), + execAsync(keyCommand) + ]); + + if (certHash.trim() !== keyHash.trim()) { + throw new Error('Certificate and private key do not match'); + } + + logger.info('Certificate validation successful'); + } catch (error) { + logger.error('Certificate validation failed:', error); + throw error; + } + } + + /** + * Remove certificate files + */ + static async removeCertificate(certificate: Certificate): Promise { + try { + if (certificate.type === 'custom') { + // Remove custom certificate files + if (fs.existsSync(certificate.path)) { + fs.unlinkSync(certificate.path); + } + if (certificate.key_path && fs.existsSync(certificate.key_path)) { + fs.unlinkSync(certificate.key_path); + } + } else if (certificate.type === 'letsencrypt') { + // For Let's Encrypt, we might want to revoke the certificate + if (config.ssl.method === 'acme.sh') { + const command = `${config.ssl.acmeShPath}/acme.sh --revoke -d ${certificate.domain}`; + await execAsync(command); + } + } + + logger.info(`Certificate files removed for ${certificate.domain}`); + } catch (error) { + logger.error(`Failed to remove certificate files for ${certificate.domain}:`, error); + throw error; + } + } + + /** + * Check for expiring certificates + */ + static async checkExpiringCertificates(days: number = 30): Promise { + try { + const expiringCerts = await CertificateModel.findExpiringSoon(days); + + if (expiringCerts.length > 0) { + logger.warn(`Found ${expiringCerts.length} certificates expiring within ${days} days`); + } + + return expiringCerts; + } catch (error) { + logger.error('Failed to check for expiring certificates:', error); + throw error; + } + } + + /** + * Auto-renew expiring certificates + */ + static async autoRenewCertificates(): Promise { + try { + const expiringCerts = await this.checkExpiringCertificates(30); + + for (const cert of expiringCerts) { + if (cert.type === 'letsencrypt') { + try { + await this.renewCertificate(cert.domain); + logger.info(`Auto-renewed certificate for ${cert.domain}`); + } catch (error) { + logger.error(`Failed to auto-renew certificate for ${cert.domain}:`, error); + } + } + } + } catch (error) { + logger.error('Auto-renewal process failed:', error); + } + } +} diff --git a/src/services/ViteService.ts b/src/services/ViteService.ts new file mode 100644 index 0000000..4729239 --- /dev/null +++ b/src/services/ViteService.ts @@ -0,0 +1,115 @@ +import express from 'express'; +import fs from 'fs/promises'; +import react from '@vitejs/plugin-react'; +import tailwindcss from '@tailwindcss/vite'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import logger from '../utils/logger.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +export class ViteService { + private static server: any = null; + + static async setupMiddleware(app: express.Application, isProduction: boolean = false, base: string = '/') { + try { + const webRoot = path.resolve(__dirname, '../web'); + const distDir = path.resolve(__dirname, '../../dist/web'); + + if (!isProduction) { + // Add CSP middleware for development + app.use((req, res, next) => { + res.setHeader('Content-Security-Policy', + "default-src 'self'; " + + "script-src 'self' 'unsafe-inline' 'unsafe-eval'; " + + "connect-src 'self' ws: wss: http: https:; " + + "style-src 'self' 'unsafe-inline';" + ); + next(); + }); + + // Create vite server + const vite = await import('vite'); + + const viteServer = await vite.createServer({ + plugins: [react(), tailwindcss()], + root: webRoot, + server: { + middlewareMode: true + }, + appType: 'spa', + base, + build: { + outDir: distDir, + emptyOutDir: true, + }, + resolve: { + alias: { + '@': webRoot, + }, + }, + }); + + app.use(base, viteServer.middlewares); + this.server = viteServer; + + logger.info(`๐ŸŽจ Vite dev middleware setup on ${base}`); + } else { + // Production mode - serve static files + const publicDir = distDir; + + // Serve static assets + app.use(base, express.static(publicDir)); + + logger.info(`๐Ÿ“ฆ Static files served from ${publicDir} on ${base}`); + } + + // SPA fallback middleware - works for both dev and prod, but only for non-API routes + app.use((req, res, next) => { + // Skip API routes and static assets + if (req.path.startsWith('/api/') || req.path.includes('.')) { + return next(); + } + + if (!isProduction) { + // In development, let Vite handle SPA routing + return next(); + } + + // In production, serve index.html for SPA routes + const indexPath = path.join(distDir, 'index.html'); + fs.readFile(indexPath, 'utf-8') + .then(index => { + res.set('content-type', 'text/html'); + res.send(index); + }) + .catch(error => { + logger.error('Failed to serve index.html:', error); + res.status(404).send('Frontend not built. Please run build first.'); + }); + }); + + } catch (error) { + logger.error('Failed to setup Vite middleware:', error); + throw error; + } + } + + static async stop() { + if (this.server) { + await this.server.close(); + logger.info('Vite server stopped'); + this.server = null; + } + } + + static isRunning() { + return this.server !== null; + } + + // Legacy method for backward compatibility + static async startDevServer(port: number = 3001) { + logger.warn('startDevServer is deprecated. Use setupMiddleware instead.'); + return null; + } +} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..8b86aa3 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,64 @@ +export interface Proxy { + id?: number; + domain: string; + target: string; + ssl_type: 'letsencrypt' | 'custom' | 'none'; + cert_path?: string; + key_path?: string; + options: ProxyOptions; + created_at?: string; + updated_at?: string; +} + +export interface ProxyOptions { + redirect_http_to_https: boolean; + custom_headers: Record; + path_forwarding: Record; + enable_websockets: boolean; + client_max_body_size: string; +} + +export interface Certificate { + id?: number; + domain: string; + type: 'letsencrypt' | 'custom'; + status: 'active' | 'expired' | 'pending' | 'failed'; + path: string; + key_path?: string; + expiry?: string; + created_at?: string; + updated_at?: string; +} + +export interface User { + id?: number; + username: string; + password: string; + created_at?: string; + updated_at?: string; +} + +export interface AuthPayload { + id: number; + username: string; +} + +export interface ApiResponse { + success: boolean; + data?: T; + message?: string; + error?: string; +} + +export interface NginxConfigOptions { + domain: string; + target: string; + sslEnabled: boolean; + certPath?: string; + keyPath?: string; + redirectHttpToHttps: boolean; + customHeaders: Record; + pathForwarding: Record; + enableWebsockets: boolean; + clientMaxBodySize: string; +} diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 0000000..a09845d --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,34 @@ +import winston from 'winston'; +import { config } from '../config/index.js'; + +// Create logger instance +const logger = winston.createLogger({ + level: config.logging.level, + format: winston.format.combine( + winston.format.timestamp(), + winston.format.errors({ stack: true }), + winston.format.json() + ), + defaultMeta: { service: 'nginx-proxy-manager' }, + transports: [ + // Write all logs with importance level of `error` or less to `error.log` + new winston.transports.File({ + filename: config.logging.file.replace('.log', '-error.log'), + level: 'error' + }), + // Write all logs with importance level of `info` or less to `combined.log` + new winston.transports.File({ filename: config.logging.file }), + ], +}); + +// If we're not in production, log to the console with a simple format +if (config.server.env !== 'production') { + logger.add(new winston.transports.Console({ + format: winston.format.combine( + winston.format.colorize(), + winston.format.simple() + ) + })); +} + +export default logger; diff --git a/src/web/App.tsx b/src/web/App.tsx new file mode 100644 index 0000000..7c11165 --- /dev/null +++ b/src/web/App.tsx @@ -0,0 +1,45 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Shield, FileKey, TrainFrontTunnelIcon as Tunnel } from "lucide-react" + +export default function Home() { + return ( +
+
+

Dashboard

+

Manage your proxies, certificates, and tunnels.

+
+
+ + + Active Proxies + + + +
12
+

4 added this week

+
+
+ + + Valid Certificates + + + +
8
+

2 expiring soon

+
+
+ + + Active Tunnels + + + +
5
+

1 with high traffic

+
+
+
+
+ ) +} diff --git a/src/web/components/layout.tsx b/src/web/components/layout.tsx new file mode 100644 index 0000000..e67a9fd --- /dev/null +++ b/src/web/components/layout.tsx @@ -0,0 +1,96 @@ +import type React from "react" + +import { Link, Outlet, useLocation } from "react-router" +import { Shield, FileKey, TrainFrontTunnelIcon as Tunnel, Home, Menu } from "lucide-react" +import { Button } from "@/components/ui/button" +import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet" +import { cn } from "@/lib/utils" + +const navItems = [ + { + title: "Dashboard", + href: "/", + icon: Home, + }, + { + title: "Proxies", + href: "/proxies", + icon: Shield, + }, + { + title: "Certificates", + href: "/certificates", + icon: FileKey, + }, + { + title: "Tunnels", + href: "/tunnels", + icon: Tunnel, + }, +] + +export default function DashboardLayout() { + //const pathname = usePathname() + const location = useLocation(); + const pathname = location.pathname; + + return ( +
+
+ + + + + + + + +
+ + + Admin Dashboard + +
+
+
+ +
+ +
+
+
+ ) +} diff --git a/src/web/components/ui/badge.tsx b/src/web/components/ui/badge.tsx new file mode 100644 index 0000000..047c137 --- /dev/null +++ b/src/web/components/ui/badge.tsx @@ -0,0 +1,30 @@ +import type * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + warning: "border-transparent bg-amber-500 text-white hover:bg-amber-500/80", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +) + +export interface BadgeProps extends React.HTMLAttributes, VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return
+} + +export { Badge, badgeVariants } diff --git a/src/web/components/ui/button.tsx b/src/web/components/ui/button.tsx new file mode 100644 index 0000000..a2df8dc --- /dev/null +++ b/src/web/components/ui/button.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", + destructive: + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/src/web/components/ui/card.tsx b/src/web/components/ui/card.tsx new file mode 100644 index 0000000..d05bbc6 --- /dev/null +++ b/src/web/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/src/web/components/ui/dropdown-menu.tsx b/src/web/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..0d6741b --- /dev/null +++ b/src/web/components/ui/dropdown-menu.tsx @@ -0,0 +1,255 @@ +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function DropdownMenu({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean + variant?: "default" | "destructive" +}) { + return ( + + ) +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + ) +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + {children} + + + ) +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +} diff --git a/src/web/components/ui/sheet.tsx b/src/web/components/ui/sheet.tsx new file mode 100644 index 0000000..6906f5b --- /dev/null +++ b/src/web/components/ui/sheet.tsx @@ -0,0 +1,137 @@ +import * as React from "react" +import * as SheetPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Sheet({ ...props }: React.ComponentProps) { + return +} + +function SheetTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function SheetClose({ + ...props +}: React.ComponentProps) { + return +} + +function SheetPortal({ + ...props +}: React.ComponentProps) { + return +} + +function SheetOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SheetContent({ + className, + children, + side = "right", + ...props +}: React.ComponentProps & { + side?: "top" | "right" | "bottom" | "left" +}) { + return ( + + + + {children} + + + Close + + + + ) +} + +function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function SheetTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SheetDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Sheet, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +} diff --git a/src/web/components/ui/table.tsx b/src/web/components/ui/table.tsx new file mode 100644 index 0000000..5513a5c --- /dev/null +++ b/src/web/components/ui/table.tsx @@ -0,0 +1,114 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Table({ className, ...props }: React.ComponentProps<"table">) { + return ( +
+ + + ) +} + +function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { + return ( + + ) +} + +function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { + return ( + + ) +} + +function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { + return ( + tr]:last:border-b-0", + className + )} + {...props} + /> + ) +} + +function TableRow({ className, ...props }: React.ComponentProps<"tr">) { + return ( + + ) +} + +function TableHead({ className, ...props }: React.ComponentProps<"th">) { + return ( +
[role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> + ) +} + +function TableCell({ className, ...props }: React.ComponentProps<"td">) { + return ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> + ) +} + +function TableCaption({ + className, + ...props +}: React.ComponentProps<"caption">) { + return ( +
+ ) +} + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/src/web/index.css b/src/web/index.css new file mode 100644 index 0000000..4004786 --- /dev/null +++ b/src/web/index.css @@ -0,0 +1,59 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 240 10% 3.9%; + --card: 0 0% 100%; + --card-foreground: 240 10% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 240 10% 3.9%; + --primary: 240 5.9% 10%; + --primary-foreground: 0 0% 98%; + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + --muted: 240 4.8% 95.9%; + --muted-foreground: 240 3.8% 46.1%; + --accent: 240 4.8% 95.9%; + --accent-foreground: 240 5.9% 10%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 240 5.9% 90%; + --input: 240 5.9% 90%; + --ring: 240 5.9% 10%; + --radius: 0.5rem; + } + + .dark { + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + --ring: 240 4.9% 83.9%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/src/web/index.html b/src/web/index.html new file mode 100644 index 0000000..3fb37e2 --- /dev/null +++ b/src/web/index.html @@ -0,0 +1,13 @@ + + + + + + + NGINX Proxy Manager + + +
+ + + diff --git a/src/web/lib/utils.ts b/src/web/lib/utils.ts new file mode 100644 index 0000000..bd0c391 --- /dev/null +++ b/src/web/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/src/web/main.tsx b/src/web/main.tsx new file mode 100644 index 0000000..f895774 --- /dev/null +++ b/src/web/main.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +import { createBrowserRouter, RouterProvider } from 'react-router'; + +import DashboardLayout from './components/layout'; +import HomePage from './pages/home'; +import CertificatesPage from './pages/certificates'; + +const router = createBrowserRouter([ + { + path: "/", + element: , + children: [ + { + index: true, + element: , + }, + { + path: "/certificates", + element: , + } + ] + }, +]) + +ReactDOM.createRoot(document.getElementById('root')!).render( + +); diff --git a/src/web/pages/certificates.tsx b/src/web/pages/certificates.tsx new file mode 100644 index 0000000..cc035af --- /dev/null +++ b/src/web/pages/certificates.tsx @@ -0,0 +1,111 @@ +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Badge } from "@/components/ui/badge" +import { Plus, MoreHorizontal } from "lucide-react" +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" + +const certificates = [ + { + id: "cert-1", + domain: "example.com", + issuer: "Let's Encrypt", + expires: "2024-12-15", + status: "valid", + }, + { + id: "cert-2", + domain: "api.example.com", + issuer: "Let's Encrypt", + expires: "2024-11-20", + status: "valid", + }, + { + id: "cert-3", + domain: "admin.example.com", + issuer: "Let's Encrypt", + expires: "2024-07-05", + status: "expiring-soon", + }, + { + id: "cert-4", + domain: "media.example.com", + issuer: "Let's Encrypt", + expires: "2024-10-30", + status: "valid", + }, + { + id: "cert-5", + domain: "auth.example.com", + issuer: "Let's Encrypt", + expires: "2024-06-15", + status: "expiring-soon", + }, +] + +export default function CertificatesPage() { + return ( +
+
+
+

Certificates

+

Manage your SSL certificates.

+
+ +
+ + + SSL Certificates + A list of all your SSL certificates and their status. + + + + + + Domain + Issuer + Expires + Status + + + + + {certificates.map((cert) => ( + + {cert.domain} + {cert.issuer} + {cert.expires} + + + {cert.status === "expiring-soon" ? "Expiring Soon" : cert.status} + + + + + + + + + View Details + Renew + Revoke + + + + + ))} + +
+
+
+
+ ) +} diff --git a/src/web/pages/home.tsx b/src/web/pages/home.tsx new file mode 100644 index 0000000..7c11165 --- /dev/null +++ b/src/web/pages/home.tsx @@ -0,0 +1,45 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Shield, FileKey, TrainFrontTunnelIcon as Tunnel } from "lucide-react" + +export default function Home() { + return ( +
+
+

Dashboard

+

Manage your proxies, certificates, and tunnels.

+
+
+ + + Active Proxies + + + +
12
+

4 added this week

+
+
+ + + Valid Certificates + + + +
8
+

2 expiring soon

+
+
+ + + Active Tunnels + + + +
5
+

1 with high traffic

+
+
+
+
+ ) +} diff --git a/tailwind.config.ts b/tailwind.config.ts new file mode 100644 index 0000000..23a1196 --- /dev/null +++ b/tailwind.config.ts @@ -0,0 +1,81 @@ +import type { Config } from "tailwindcss" + +const config = { + darkMode: ["class"], + content: [ + "./pages/**/*.{ts,tsx}", + "./components/**/*.{ts,tsx}", + "./app/**/*.{ts,tsx}", + "./src/**/*.{ts,tsx}", + "*.{js,ts,jsx,tsx,mdx}", + ], + prefix: "", + theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + keyframes: { + "accordion-down": { + from: { height: "0" }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: "0" }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + }, + }, + }, + plugins: [require("tailwindcss-animate")], +} satisfies Config + +export default config diff --git a/test-api.ts b/test-api.ts new file mode 100644 index 0000000..0d8674d --- /dev/null +++ b/test-api.ts @@ -0,0 +1,156 @@ +#!/usr/bin/env bun + +/** + * Simple API test script for the NGINX Proxy Manager Backend + */ + +const API_BASE = 'http://10.0.0.122:3000/api'; + +interface TestResult { + name: string; + success: boolean; + message: string; + data?: any; +} + +class APITestSuite { + private token: string = ''; + private results: TestResult[] = []; + + async runTests(): Promise { + console.log('๐Ÿงช Running API Test Suite...\n'); + + // Test authentication + await this.testHealthCheck(); + await this.testLogin(); + await this.testMe(); + + // Test proxy management (requires auth) + if (this.token) { + await this.testGetProxies(); + await this.testNginxStatus(); + } + + // Print results + this.printResults(); + } + + private async testHealthCheck(): Promise { + try { + const response = await fetch(`${API_BASE}/health`); + const data = await response.json(); + + this.addResult('Health Check', response.ok && data.success, data.message || 'Failed', data); + } catch (error: any) { + this.addResult('Health Check', false, error.message); + } + } + + private async testLogin(): Promise { + try { + const response = await fetch(`${API_BASE}/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + username: 'admin', + password: 'admin123' + }) + }); + + const data = await response.json(); + + if (response.ok && data.success && data.data?.token) { + this.token = data.data.token; + this.addResult('Login', true, 'Successfully logged in', { user: data.data.user }); + } else { + this.addResult('Login', false, data.message || 'Login failed', data); + } + } catch (error: any) { + this.addResult('Login', false, error.message); + } + } + + private async testMe(): Promise { + if (!this.token) { + this.addResult('Get Current User', false, 'No token available'); + return; + } + + try { + const response = await fetch(`${API_BASE}/auth/me`, { + headers: { 'Authorization': `Bearer ${this.token}` } + }); + + const data = await response.json(); + + this.addResult('Get Current User', response.ok && data.success, data.message || 'Failed', data.data); + } catch (error: any) { + this.addResult('Get Current User', false, error.message); + } + } + + private async testGetProxies(): Promise { + try { + const response = await fetch(`${API_BASE}/proxies`, { + headers: { 'Authorization': `Bearer ${this.token}` } + }); + + const data = await response.json(); + + this.addResult('Get Proxies', response.ok && data.success, `Found ${data.data?.length || 0} proxies`, data.data); + } catch (error: any) { + this.addResult('Get Proxies', false, error.message); + } + } + + private async testNginxStatus(): Promise { + try { + const response = await fetch(`${API_BASE}/proxies/nginx/status`, { + headers: { 'Authorization': `Bearer ${this.token}` } + }); + + const data = await response.json(); + + this.addResult('NGINX Status', response.ok, data.message || 'Failed', data.data); + } catch (error: any) { + this.addResult('NGINX Status', false, error.message); + } + } + + private addResult(name: string, success: boolean, message: string, data?: any): void { + this.results.push({ name, success, message, data }); + } + + private printResults(): void { + console.log('\n๐Ÿ“Š Test Results:'); + console.log('='.repeat(50)); + + const passed = this.results.filter(r => r.success).length; + const total = this.results.length; + + this.results.forEach(result => { + const status = result.success ? 'โœ… PASS' : 'โŒ FAIL'; + console.log(`${status} ${result.name}: ${result.message}`); + + if (result.data && typeof result.data === 'object') { + console.log(` Data:`, JSON.stringify(result.data, null, 2).split('\n').slice(0, 3).join('\n')); + } + }); + + console.log('='.repeat(50)); + console.log(`๐Ÿ“ˆ Results: ${passed}/${total} tests passed`); + + if (passed === total) { + console.log('๐ŸŽ‰ All tests passed! API is working correctly.'); + } else { + console.log('โš ๏ธ Some tests failed. Check the logs for details.'); + } + } +} + +// Run tests +const testSuite = new APITestSuite(); +testSuite.runTests().catch(error => { + console.error('โŒ Test suite failed:', error); + process.exit(1); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f7b7216 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["esnext", "dom"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false, + + "baseUrl": ".", + "paths": { + "@/*": ["./src/web/*"] + } + } +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..fb0b24c --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import tailwindcss from '@tailwindcss/vite'; +import path from 'path'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react(), tailwindcss()], + root: './src/web', + build: { + outDir: '../../dist/web', + emptyOutDir: true, + }, + resolve: { + alias: { '@': path.resolve(__dirname, './src/web') }, + }, +}); \ No newline at end of file