main Initial commit
This commit is contained in:
commit
ac7df91600
65 changed files with 8957 additions and 0 deletions
38
.env.example
Normal file
38
.env.example
Normal 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
34
.gitignore
vendored
Normal 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
276
DEPLOYMENT.md
Normal 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
64
Dockerfile
Normal 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
226
PROJECT_SUMMARY.md
Normal 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
310
README.md
Normal 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
213
arfire_dns.json
Normal 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
828
bun.lock
Normal 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
859
cloudflare_zones.json
Normal 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
21
components.json
Normal 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
BIN
data/proxy_manager.db
Normal file
Binary file not shown.
BIN
data/proxy_manager.db-shm
Normal file
BIN
data/proxy_manager.db-shm
Normal file
Binary file not shown.
0
data/proxy_manager.db-wal
Normal file
0
data/proxy_manager.db-wal
Normal file
72
docker-compose.yml
Normal file
72
docker-compose.yml
Normal 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
91
docker/nginx.conf
Normal 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
15
docker/start.sh
Normal 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
37
docker/supervisord.conf
Normal 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
1261
hcwsone_dns.json
Normal file
File diff suppressed because it is too large
Load diff
134
index.ts
Normal file
134
index.ts
Normal 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
331
manage.ts
Normal 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
67
package.json
Normal 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
87
src/config/index.ts
Normal 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;
|
||||
187
src/controllers/AuthController.ts
Normal file
187
src/controllers/AuthController.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
306
src/controllers/CertificateController.ts
Normal file
306
src/controllers/CertificateController.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
61
src/controllers/CloudflareController.ts
Normal file
61
src/controllers/CloudflareController.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
243
src/controllers/ProxyController.ts
Normal file
243
src/controllers/ProxyController.ts
Normal 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
133
src/database/index.ts
Normal 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
27
src/database/init.ts
Normal 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();
|
||||
0
src/database/sequelize.ts
Normal file
0
src/database/sequelize.ts
Normal file
47
src/middleware/auth.ts
Normal file
47
src/middleware/auth.ts
Normal 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
|
||||
});
|
||||
};
|
||||
74
src/middleware/validation.ts
Normal file
74
src/middleware/validation.ts
Normal 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
142
src/models/Certificate.ts
Normal 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
145
src/models/Proxy.ts
Normal 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
53
src/models/User.ts
Normal 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
16
src/routes/auth.ts
Normal 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;
|
||||
25
src/routes/certificates.ts
Normal file
25
src/routes/certificates.ts
Normal 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
18
src/routes/cloudflare.ts
Normal 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
24
src/routes/index.ts
Normal 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
23
src/routes/proxies.ts
Normal 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;
|
||||
42
src/services/CloudflareService.ts
Normal file
42
src/services/CloudflareService.ts
Normal 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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
68
src/services/CronService.ts
Normal file
68
src/services/CronService.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
236
src/services/NginxService.ts
Normal file
236
src/services/NginxService.ts
Normal 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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
239
src/services/ProxyService.ts
Normal file
239
src/services/ProxyService.ts
Normal 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
293
src/services/SSLService.ts
Normal 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
115
src/services/ViteService.ts
Normal 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
64
src/types/index.ts
Normal 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
34
src/utils/logger.ts
Normal 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
45
src/web/App.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
96
src/web/components/layout.tsx
Normal file
96
src/web/components/layout.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
30
src/web/components/ui/badge.tsx
Normal file
30
src/web/components/ui/badge.tsx
Normal 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 }
|
||||
59
src/web/components/ui/button.tsx
Normal file
59
src/web/components/ui/button.tsx
Normal 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 }
|
||||
92
src/web/components/ui/card.tsx
Normal file
92
src/web/components/ui/card.tsx
Normal 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,
|
||||
}
|
||||
255
src/web/components/ui/dropdown-menu.tsx
Normal file
255
src/web/components/ui/dropdown-menu.tsx
Normal 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,
|
||||
}
|
||||
137
src/web/components/ui/sheet.tsx
Normal file
137
src/web/components/ui/sheet.tsx
Normal 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,
|
||||
}
|
||||
114
src/web/components/ui/table.tsx
Normal file
114
src/web/components/ui/table.tsx
Normal 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
59
src/web/index.css
Normal 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
13
src/web/index.html
Normal 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
6
src/web/lib/utils.ts
Normal 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
29
src/web/main.tsx
Normal 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} />
|
||||
);
|
||||
111
src/web/pages/certificates.tsx
Normal file
111
src/web/pages/certificates.tsx
Normal 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
45
src/web/pages/home.tsx
Normal 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
81
tailwind.config.ts
Normal 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
156
test-api.ts
Normal 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
33
tsconfig.json
Normal 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
17
vite.config.ts
Normal 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') },
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue