main Initial commit

This commit is contained in:
hunternick87 2025-06-12 01:33:06 -04:00
commit ac7df91600
65 changed files with 8957 additions and 0 deletions

38
.env.example Normal file
View file

@ -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

34
.gitignore vendored Normal file
View file

@ -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

276
DEPLOYMENT.md Normal file
View file

@ -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 <repository-url>
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 <repository-url>
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 <<EOF
[Unit]
Description=NGINX Proxy Manager API
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/path/to/reverse-proxy
ExecStart=/root/.bun/bin/bun index.ts
Restart=always
RestartSec=5
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.target
EOF
```
5. **Start Service**
```bash
sudo systemctl daemon-reload
sudo systemctl enable nginx-proxy-manager
sudo systemctl start nginx-proxy-manager
```
## 🔒 Security Hardening
### 1. Firewall Configuration
```bash
# Allow only necessary ports
sudo ufw allow 22/tcp # SSH
sudo ufw allow 80/tcp # HTTP
sudo ufw allow 443/tcp # HTTPS
sudo ufw enable
```
### 2. SSL/TLS Configuration
- Use strong SSL ciphers (already configured)
- Enable HTTP/2 (configured in NGINX)
- Use HSTS headers for enhanced security
### 3. Rate Limiting
- API requests: 10 req/sec (configured)
- Login attempts: 1 req/sec (configured)
- Customize in `docker/nginx.conf` if needed
### 4. Access Control
- Change default admin credentials immediately
- Use strong JWT secrets
- Consider IP whitelisting for admin access
## 📊 Monitoring and Maintenance
### 1. Log Monitoring
```bash
# Application logs
docker-compose logs -f nginx-proxy-manager
# NGINX logs
docker-compose exec nginx-proxy-manager tail -f /var/log/nginx/access.log
docker-compose exec nginx-proxy-manager tail -f /var/log/nginx/error.log
```
### 2. Health Checks
```bash
# API health
curl http://localhost:3000/api/health
# NGINX status
curl -I http://your-domain.com
```
### 3. Database Backup
```bash
# Manual backup
docker-compose exec nginx-proxy-manager cp /app/data/proxy_manager.db /app/backups/
# Automated backup is included in docker-compose.yml
```
### 4. Certificate Monitoring
- Certificates are automatically renewed 30 days before expiry
- Check certificate status via API: `/api/certificates/expiring/check`
- Force renewal: `/api/certificates/expiring/renew`
## 🔄 Updates and Maintenance
### 1. Update Application
```bash
# Pull latest changes
git pull origin main
# Rebuild and restart
docker-compose down
docker-compose build --no-cache
docker-compose up -d
```
### 2. Database Migration
```bash
# Backup database before updates
docker-compose exec nginx-proxy-manager cp /app/data/proxy_manager.db /app/backups/backup-$(date +%Y%m%d).db
# Run initialization (handles schema updates)
docker-compose exec nginx-proxy-manager bun src/database/init.ts
```
## 🐛 Troubleshooting
### Common Issues
1. **Port Already in Use**
```bash
# Check what's using the port
sudo netstat -tulpn | grep :80
sudo netstat -tulpn | grep :443
# Stop conflicting services
sudo systemctl stop apache2 # if Apache is running
```
2. **Permission Denied for NGINX Config**
```bash
# Fix permissions
sudo chown -R root:root /etc/nginx/conf.d/
sudo chmod 644 /etc/nginx/conf.d/*.conf
```
3. **SSL Certificate Issues**
```bash
# Check acme.sh logs
docker-compose exec nginx-proxy-manager cat /root/.acme.sh/acme.sh.log
# Manual certificate request
docker-compose exec nginx-proxy-manager /root/.acme.sh/acme.sh --issue -d yourdomain.com --standalone
```
4. **Database Locked**
```bash
# Stop application
docker-compose stop nginx-proxy-manager
# Remove lock file
docker-compose exec nginx-proxy-manager rm -f /app/data/proxy_manager.db-wal /app/data/proxy_manager.db-shm
# Restart
docker-compose start nginx-proxy-manager
```
### Log Analysis
```bash
# Search for errors
docker-compose logs nginx-proxy-manager | grep -i error
# Monitor in real-time
docker-compose logs -f --tail=100 nginx-proxy-manager
```
## 📞 Support
1. Check application logs first
2. Verify NGINX configuration with `nginx -t`
3. Test API endpoints manually
4. Check certificate expiry dates
5. Review firewall and DNS settings
For persistent issues, create a detailed bug report with:
- Error messages and logs
- Configuration details
- Steps to reproduce
- Environment information

64
Dockerfile Normal file
View file

@ -0,0 +1,64 @@
# Use official Ubuntu as base image
FROM ubuntu:22.04
# Set environment variables
ENV DEBIAN_FRONTEND=noninteractive
ENV NODE_ENV=production
# Install system dependencies
RUN apt-get update && apt-get install -y \
nginx \
curl \
wget \
unzip \
openssl \
cron \
supervisor \
&& rm -rf /var/lib/apt/lists/*
# Install Bun
RUN curl -fsSL https://bun.sh/install | bash
ENV PATH="/root/.bun/bin:$PATH"
# Install acme.sh
RUN curl -fsSL https://get.acme.sh | sh -s email=admin@example.com
ENV PATH="/root/.acme.sh:$PATH"
# Create app directory
WORKDIR /app
# Copy package files
COPY package.json bun.lock ./
# Install dependencies
RUN bun install --production
# Copy application code
COPY . .
# Create necessary directories
RUN mkdir -p /app/logs /app/data /app/certs /app/nginx
RUN mkdir -p /etc/nginx/conf.d
# Set permissions
RUN chmod +x /app/src/database/init.ts
# Copy nginx configuration
COPY docker/nginx.conf /etc/nginx/nginx.conf
# Copy supervisor configuration
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# Create startup script
COPY docker/start.sh /start.sh
RUN chmod +x /start.sh
# Expose ports
EXPOSE 80 443 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/api/health || exit 1
# Start services
CMD ["/start.sh"]

226
PROJECT_SUMMARY.md Normal file
View file

@ -0,0 +1,226 @@
# 🎉 Project Summary: Custom NGINX Proxy Manager Backend
## ✅ What We've Built
You now have a **complete, production-ready backend** for managing NGINX reverse proxies with automatic SSL certificate management! Here's what's included:
### 🏗️ Core Features Implemented
**✅ Proxy Management API**
- Full CRUD operations for proxy entries
- Domain to target URL mapping
- HTTP/HTTPS support with automatic redirects
- Custom headers configuration
- Path-based forwarding
- WebSocket support
- Configurable client max body size
**✅ SSL Certificate Management**
- Automatic Let's Encrypt certificate issuance via acme.sh/certbot
- Custom certificate upload support
- Automatic certificate renewal (30 days before expiry)
- Certificate expiry monitoring
- Certificate validation and verification
**✅ NGINX Integration**
- Dynamic configuration generation
- Configuration testing before reload
- Automatic NGINX reload after changes
- Error handling and rollback capabilities
- Rate limiting and security headers
**✅ Security & Authentication**
- JWT-based authentication
- Password hashing with bcrypt
- CORS protection with configurable origins
- Helmet security headers
- Request validation with Joi schemas
- Rate limiting for API and login endpoints
**✅ Database & Storage**
- SQLite database with proper schema
- Models for users, proxies, and certificates
- Automatic database initialization
- Backup utilities
**✅ Monitoring & Automation**
- Comprehensive logging with Winston
- Automatic certificate renewal cron job
- Health check endpoints
- Management CLI for administrative tasks
### 📁 Project Structure
```
reverse-proxy/
├── 🔧 src/
│ ├── config/ # Environment configuration
│ ├── controllers/ # API request handlers
│ ├── database/ # Database setup and initialization
│ ├── middleware/ # Authentication and validation
│ ├── models/ # Database models (User, Proxy, Certificate)
│ ├── routes/ # API routes definition
│ ├── services/ # Business logic (NGINX, SSL, Proxy, Cron)
│ ├── types/ # TypeScript type definitions
│ └── utils/ # Utility functions (logging)
├── 🐳 docker/ # Docker configuration files
├── 📊 data/ # SQLite database storage
├── 📝 logs/ # Application logs
├── 🔐 certs/ # Custom SSL certificates
├── ⚙️ nginx/ # Generated NGINX configurations
├── 📋 index.ts # Main application entry point
├── 🛠️ manage.ts # Management CLI tool
├── 🧪 test-api.ts # API testing script
├── 🐳 Dockerfile # Docker image definition
├── 🐳 docker-compose.yml # Docker Compose configuration
├── 📖 README.md # Comprehensive documentation
├── 🚀 DEPLOYMENT.md # Deployment guide
└── ⚙️ package.json # Project dependencies and scripts
```
### 🛠️ Available Commands
**Development:**
```bash
bun run dev # Start development server with hot reload
bun run start # Start production server
bun run test # Run API tests
```
**Database Management:**
```bash
bun run db:init # Initialize database
bun run backup # Create database backup
```
**NGINX Management:**
```bash
bun run nginx:test # Test NGINX configuration
bun run nginx:reload # Reload NGINX configuration
```
**Certificate Management:**
```bash
bun run cert:renew # Renew expiring certificates
```
**CLI Management:**
```bash
bun run manage # Show CLI help
bun run status # Show application status
```
### 🔄 API Endpoints
**Authentication:**
- `POST /api/auth/login` - User login
- `GET /api/auth/me` - Get current user
- `POST /api/auth/change-password` - Change password
- `POST /api/auth/logout` - Logout
**Proxy Management:**
- `GET /api/proxies` - List all proxies
- `GET /api/proxies/:id` - Get proxy by ID
- `POST /api/proxies` - Create new proxy
- `PUT /api/proxies/:id` - Update proxy
- `DELETE /api/proxies/:id` - Delete proxy
**NGINX Management:**
- `POST /api/proxies/nginx/test` - Test NGINX config
- `POST /api/proxies/nginx/reload` - Reload NGINX
- `GET /api/proxies/nginx/status` - Get NGINX status
**Certificate Management:**
- `GET /api/certificates` - List all certificates
- `GET /api/certificates/:id` - Get certificate by ID
- `POST /api/certificates/letsencrypt` - Request Let's Encrypt cert
- `POST /api/certificates/custom` - Upload custom certificate
- `POST /api/certificates/:id/renew` - Renew certificate
- `DELETE /api/certificates/:id` - Delete certificate
- `GET /api/certificates/expiring/check` - Check expiring certs
- `POST /api/certificates/expiring/renew` - Auto-renew expiring certs
**System:**
- `GET /api/health` - Health check endpoint
### 🚀 Deployment Options
**1. Docker (Recommended):**
```bash
docker-compose up -d
```
**2. Native Installation:**
```bash
bun install
bun run db:init
bun run start
```
**3. Production with SSL:**
- Full Docker setup with NGINX proxy
- Automatic certificate management
- Rate limiting and security headers
- Backup automation
### ⚡ Testing Results
**All tests passed!** The API is fully functional:
- Health check endpoint working
- Authentication system operational
- Database operations successful
- Proxy management ready
- Certificate management prepared
### 🔒 Security Features
- **JWT Authentication** with configurable expiration
- **Password hashing** with bcrypt (10 rounds)
- **CORS protection** with configurable origins
- **Rate limiting**: 10 req/sec for API, 1 req/sec for login
- **Input validation** with Joi schemas
- **Security headers** via Helmet
- **SSL/TLS configuration** with modern ciphers
- **File permissions** properly set for certificates
### 📊 Monitoring & Maintenance
- **Comprehensive logging** with Winston (JSON format)
- **Automatic certificate renewal** (daily cron job)
- **Health check endpoints** for monitoring
- **Database backup utilities**
- **Management CLI** for administrative tasks
- **Error handling** with rollback capabilities
### 🔧 Next Steps
1. **Deploy** using Docker Compose or native installation
2. **Change default credentials** immediately
3. **Configure environment** variables for your setup
4. **Set up monitoring** and log aggregation
5. **Create your first proxy** via the API
6. **Test SSL certificate** issuance
7. **Set up backups** and monitoring alerts
### 📚 Documentation
- `README.md` - Complete usage guide and API documentation
- `DEPLOYMENT.md` - Detailed deployment instructions
- Environment variables documented in `.env.example`
- TypeScript types provide inline documentation
- Comprehensive error messages and logging
## 🎯 Production Readiness
This backend is **production-ready** with:
- ✅ Proper error handling and logging
- ✅ Security best practices implemented
- ✅ Automatic SSL certificate management
- ✅ Database migrations and initialization
- ✅ Docker containerization
- ✅ Health checks and monitoring
- ✅ Backup and recovery procedures
- ✅ CLI management tools
- ✅ Comprehensive documentation
**You now have a robust, secure, and scalable NGINX proxy manager backend that can handle production workloads!** 🚀

310
README.md Normal file
View file

@ -0,0 +1,310 @@
# 🔄 Custom NGINX Proxy Manager Backend
A modern, lightweight backend for managing NGINX reverse proxies with automatic SSL certificate management.
## 🧱 Tech Stack
- **Node.js** with **Bun** runtime
- **Express.js** for API routing
- **SQLite** for data storage
- **NGINX** for reverse proxying
- **Let's Encrypt** (via acme.sh/certbot) for automatic TLS certificates
- **JWT** for authentication
- **TypeScript** for type safety
## 🚀 Features
### 🔧 Proxy Management
- ✅ Create, read, update, delete reverse proxy entries
- ✅ Domain to target URL mapping
- ✅ HTTP/HTTPS support with automatic redirects
- ✅ Custom headers configuration
- ✅ Path-based forwarding
- ✅ WebSocket support
- ✅ Configurable client max body size
### 🔒 SSL Certificate Management
- ✅ Automatic Let's Encrypt certificate issuance
- ✅ Custom certificate upload support
- ✅ Automatic certificate renewal
- ✅ Certificate expiry monitoring
- ✅ Certificate validation
### 🔐 Security
- ✅ JWT-based authentication
- ✅ Password hashing with bcrypt
- ✅ CORS protection
- ✅ Helmet security headers
- ✅ Request validation with Joi
### 🗄️ Database
- ✅ SQLite database with proper schema
- ✅ Models for users, proxies, and certificates
- ✅ Automatic database initialization
### 🔁 NGINX Integration
- ✅ Dynamic configuration generation
- ✅ Configuration testing before reload
- ✅ Automatic NGINX reload
- ✅ Error handling and rollback
## 📁 Project Structure
```
reverse-proxy/
├── src/
│ ├── config/ # Configuration management
│ ├── controllers/ # Request handlers
│ ├── database/ # Database setup and initialization
│ ├── middleware/ # Express middleware (auth, validation)
│ ├── models/ # Database models
│ ├── routes/ # API routes
│ ├── services/ # Business logic
│ ├── types/ # TypeScript type definitions
│ └── utils/ # Utility functions
├── logs/ # Application logs
├── nginx/ # NGINX configurations
├── certs/ # Custom SSL certificates
├── data/ # SQLite database
└── index.ts # Application entry point
```
## 🛠️ Installation
### Prerequisites
- **Bun** runtime installed
- **NGINX** installed and running
- **acme.sh** or **certbot** for Let's Encrypt certificates
- Proper permissions for NGINX config management
### Setup
1. **Clone and Install Dependencies**
```bash
git clone <repository-url>
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 <token>
```
### Proxy Management
#### Get All Proxies
```http
GET /api/proxies
Authorization: Bearer <token>
```
#### Create Proxy
```http
POST /api/proxies
Authorization: Bearer <token>
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 <token>
Content-Type: application/json
{
"target": "http://localhost:9000",
"options": {
"redirect_http_to_https": false
}
}
```
#### Delete Proxy
```http
DELETE /api/proxies/:id
Authorization: Bearer <token>
```
### Certificate Management
#### Request Let's Encrypt Certificate
```http
POST /api/certificates/letsencrypt
Authorization: Bearer <token>
Content-Type: application/json
{
"domain": "example.com"
}
```
#### Upload Custom Certificate
```http
POST /api/certificates/custom
Authorization: Bearer <token>
Content-Type: multipart/form-data
{
"domain": "example.com",
"certificate": <file>,
"privateKey": <file>
}
```
#### Get Expiring Certificates
```http
GET /api/certificates/expiring/check?days=30
Authorization: Bearer <token>
```
### NGINX Management
#### Test NGINX Configuration
```http
POST /api/proxies/nginx/test
Authorization: Bearer <token>
```
#### Reload NGINX
```http
POST /api/proxies/nginx/reload
Authorization: Bearer <token>
```
## 🔄 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.

213
arfire_dns.json Normal file
View file

@ -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
}
}

828
bun.lock Normal file
View file

@ -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=="],
}
}

859
cloudflare_zones.json Normal file
View file

@ -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
}
}

21
components.json Normal file
View file

@ -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"
}

BIN
data/proxy_manager.db Normal file

Binary file not shown.

BIN
data/proxy_manager.db-shm Normal file

Binary file not shown.

View file

72
docker-compose.yml Normal file
View file

@ -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

91
docker/nginx.conf Normal file
View file

@ -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;
}

15
docker/start.sh Normal file
View file

@ -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

37
docker/supervisord.conf Normal file
View file

@ -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

1261
hcwsone_dns.json Normal file

File diff suppressed because it is too large Load diff

134
index.ts Normal file
View file

@ -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();

331
manage.ts Normal file
View file

@ -0,0 +1,331 @@
#!/usr/bin/env bun
/**
* Management CLI for NGINX Proxy Manager
* Usage: bun manage.ts <command> [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>', 'Username')
.requiredOption('-p, --password <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>', 'Username')
.requiredOption('-p, --password <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 <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>', '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 <path>', '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();
}

67
package.json Normal file
View file

@ -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"
}
}

87
src/config/index.ts Normal file
View file

@ -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;

View file

@ -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<void> {
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<void> {
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<void> {
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<void> {
// 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);
}
}

View file

@ -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<void> {
try {
const certificates = await CertificateModel.findAll();
res.json({
success: true,
data: certificates
} as ApiResponse<Certificate[]>);
} 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<void> {
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<Certificate>);
} 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<void> {
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<Certificate>);
} 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<void> => {
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<Certificate>);
} 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<void> {
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<Certificate>);
} 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<void> {
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<void> {
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<Certificate[]>);
} 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<void> {
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);
}
}
}

View file

@ -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<void> {
try {
const cf = CloudflareService.getInstance();
const zones = await cf.getZones();
res.json({
success: true,
data: zones
} as ApiResponse<Cloudflare.Zones[]>);
} 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<void> {
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<Cloudflare.DNS.Records[]>);
} 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);
}
}
}

View file

@ -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<void> {
try {
const proxies = await ProxyService.getAllProxies();
res.json({
success: true,
data: proxies
} as ApiResponse<Proxy[]>);
} 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<void> {
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<Proxy>);
} 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<void> {
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<Proxy>);
} 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<void> {
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<Proxy>);
} 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<void> {
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<void> {
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<void> {
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<void> {
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);
}
}
}

133
src/database/index.ts Normal file
View file

@ -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<void> {
return new Promise((resolve) => {
if (this.db) {
this.db.close();
logger.info('Database connection closed');
}
resolve();
});
}
}
export const database = new DatabaseManager();
export default database;

27
src/database/init.ts Normal file
View file

@ -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();

View file

47
src/middleware/auth.ts Normal file
View file

@ -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
});
};

View file

@ -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()
})
};

142
src/models/Certificate.ts Normal file
View file

@ -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<Certificate[]> {
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<Certificate | null> {
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<Certificate | null> {
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<Certificate[]> {
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<Certificate, 'id' | 'created_at' | 'updated_at'>): Promise<Certificate> {
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<Certificate>): Promise<Certificate | null> {
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<boolean> {
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;
}
}
}

145
src/models/Proxy.ts Normal file
View file

@ -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<Proxy[]> {
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<Proxy | null> {
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<Proxy | null> {
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<Proxy, 'id' | 'created_at' | 'updated_at'>): Promise<Proxy> {
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<Proxy>): Promise<Proxy | null> {
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<boolean> {
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;
}
}
}

53
src/models/User.ts Normal file
View file

@ -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<User | null> {
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<User | null> {
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<User, 'id' | 'created_at' | 'updated_at'>): Promise<User> {
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<boolean> {
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;
}
}
}

16
src/routes/auth.ts Normal file
View file

@ -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;

View file

@ -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;

18
src/routes/cloudflare.ts Normal file
View file

@ -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;

24
src/routes/index.ts Normal file
View file

@ -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;

23
src/routes/proxies.ts Normal file
View file

@ -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;

View file

@ -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
}
}

View file

@ -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<string, cron.ScheduledTask> = 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<void> {
logger.info('Manually triggering certificate renewal');
await SSLService.autoRenewCertificates();
}
}

View file

@ -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<string, string>, 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<void> {
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<void> {
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 };
}
}
}

View file

@ -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<Proxy, 'id' | 'created_at' | 'updated_at'>): Promise<Proxy> {
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<Proxy>): Promise<Proxy> {
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<void> {
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<Proxy[]> {
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<Proxy | null> {
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<void> {
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;
}
}
}

293
src/services/SSLService.ts Normal file
View file

@ -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<Certificate> {
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<Certificate> {
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<Certificate> {
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<void> {
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<void> {
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<Certificate[]> {
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<void> {
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);
}
}
}

115
src/services/ViteService.ts Normal file
View file

@ -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;
}
}

64
src/types/index.ts Normal file
View file

@ -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<string, string>;
path_forwarding: Record<string, string>;
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<T = any> {
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<string, string>;
pathForwarding: Record<string, string>;
enableWebsockets: boolean;
clientMaxBodySize: string;
}

34
src/utils/logger.ts Normal file
View file

@ -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;

45
src/web/App.tsx Normal file
View file

@ -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 (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold tracking-tight">Dashboard</h1>
<p className="text-muted-foreground">Manage your proxies, certificates, and tunnels.</p>
</div>
<div className="grid gap-4 md:grid-cols-3">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Active Proxies</CardTitle>
<Shield className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">12</div>
<p className="text-xs text-muted-foreground">4 added this week</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Valid Certificates</CardTitle>
<FileKey className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">8</div>
<p className="text-xs text-muted-foreground">2 expiring soon</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Active Tunnels</CardTitle>
<Tunnel className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">5</div>
<p className="text-xs text-muted-foreground">1 with high traffic</p>
</CardContent>
</Card>
</div>
</div>
)
}

View file

@ -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 (
<div className="flex min-h-screen flex-col text-foreground">
<header className="sticky top-0 z-30 flex h-14 items-center gap-4 border-b bg-background px-4 sm:px-6">
<Sheet>
<SheetTrigger asChild>
<Button variant="outline" size="icon" className="md:hidden">
<Menu className="h-5 w-5" />
<span className="sr-only">Toggle navigation menu</span>
</Button>
</SheetTrigger>
<SheetContent side="left" className="w-72">
<nav className="grid gap-2 text-lg font-medium">
{navItems.map((item, index) => (
<Link
key={index}
to={item.href}
className={cn(
"flex items-center gap-2 rounded-lg px-3 py-2 text-muted-foreground hover:text-foreground",
pathname === item.href && "bg-muted text-foreground",
)}
>
<item.icon className="h-5 w-5" />
{item.title}
</Link>
))}
</nav>
</SheetContent>
</Sheet>
<div className="flex items-center gap-2">
<Link to="/" className="flex items-center gap-2 font-semibold">
<Shield className="h-6 w-6" />
<span>Admin Dashboard</span>
</Link>
</div>
</header>
<div className="flex flex-1">
<aside className="hidden w-64 border-r bg-muted/40 md:block">
<nav className="grid gap-2 p-4 text-sm">
{navItems.map((item, index) => (
<Link
key={index}
to={item.href}
className={cn(
"flex items-center gap-3 rounded-lg px-3 py-2 text-muted-foreground transition-all hover:text-foreground",
pathname === item.href && "bg-muted text-foreground",
)}
>
<item.icon className="h-4 w-4" />
{item.title}
</Link>
))}
</nav>
</aside>
<main className="flex-1 p-4 md:p-6">
<Outlet />
</main>
</div>
</div>
)
}

View file

@ -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<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
}
export { Badge, badgeVariants }

View file

@ -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<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View file

@ -0,0 +1,92 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View file

@ -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<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}

View file

@ -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<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />
}
function SheetTrigger({
...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
}
function SheetClose({
...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
}
function SheetPortal({
...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
}
function SheetOverlay({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function SheetContent({
className,
children,
side = "right",
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left"
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot="sheet-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
side === "right" &&
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
side === "left" &&
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
side === "top" &&
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
className
)}
{...props}
>
{children}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<XIcon className="size-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
)
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
)
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
}
function SheetTitle({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
)
}
function SheetDescription({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}

View file

@ -0,0 +1,114 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
)
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b", className)}
{...props}
/>
)
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
)
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
)
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className
)}
{...props}
/>
)
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
data-slot="table-head"
className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCaption({
className,
...props
}: React.ComponentProps<"caption">) {
return (
<caption
data-slot="table-caption"
className={cn("text-muted-foreground mt-4 text-sm", className)}
{...props}
/>
)
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

59
src/web/index.css Normal file
View file

@ -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;
}
}

13
src/web/index.html Normal file
View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NGINX Proxy Manager</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

6
src/web/lib/utils.ts Normal file
View file

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

29
src/web/main.tsx Normal file
View file

@ -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: <DashboardLayout />,
children: [
{
index: true,
element: <HomePage />,
},
{
path: "/certificates",
element: <CertificatesPage />,
}
]
},
])
ReactDOM.createRoot(document.getElementById('root')!).render(
<RouterProvider router={router} />
);

View file

@ -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 (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">Certificates</h1>
<p className="text-muted-foreground">Manage your SSL certificates.</p>
</div>
<Button>
<Plus className="mr-2 h-4 w-4" />
Create Certificate
</Button>
</div>
<Card>
<CardHeader>
<CardTitle>SSL Certificates</CardTitle>
<CardDescription>A list of all your SSL certificates and their status.</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Domain</TableHead>
<TableHead>Issuer</TableHead>
<TableHead>Expires</TableHead>
<TableHead>Status</TableHead>
<TableHead className="w-[80px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{certificates.map((cert) => (
<TableRow key={cert.id}>
<TableCell className="font-medium">{cert.domain}</TableCell>
<TableCell>{cert.issuer}</TableCell>
<TableCell>{cert.expires}</TableCell>
<TableCell>
<Badge
variant={ cert.status === "valid" ? "default" : cert.status === "expiring-soon" ? "warning" : "destructive" }
>
{cert.status === "expiring-soon" ? "Expiring Soon" : cert.status}
</Badge>
</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">Open menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>View Details</DropdownMenuItem>
<DropdownMenuItem>Renew</DropdownMenuItem>
<DropdownMenuItem className="text-destructive">Revoke</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
)
}

45
src/web/pages/home.tsx Normal file
View file

@ -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 (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold tracking-tight">Dashboard</h1>
<p className="text-muted-foreground">Manage your proxies, certificates, and tunnels.</p>
</div>
<div className="grid gap-4 md:grid-cols-3">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Active Proxies</CardTitle>
<Shield className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">12</div>
<p className="text-xs text-muted-foreground">4 added this week</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Valid Certificates</CardTitle>
<FileKey className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">8</div>
<p className="text-xs text-muted-foreground">2 expiring soon</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Active Tunnels</CardTitle>
<Tunnel className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">5</div>
<p className="text-xs text-muted-foreground">1 with high traffic</p>
</CardContent>
</Card>
</div>
</div>
)
}

81
tailwind.config.ts Normal file
View file

@ -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

156
test-api.ts Normal file
View file

@ -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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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);
});

33
tsconfig.json Normal file
View file

@ -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/*"]
}
}
}

17
vite.config.ts Normal file
View file

@ -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') },
},
});