Compare commits
12 Commits
ddb548bf21
...
3812e6c3ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
3812e6c3ad
|
|||
| faf6cdf091 | |||
| 0131c0f5d0 | |||
| 1cd5ba54d4 | |||
| fa02987225 | |||
| ab80f02804 | |||
| 01efe34a4a | |||
| 78d69d1f39 | |||
| a332079405 | |||
| a98ffeb41e | |||
| 1d7523490c | |||
| 810577ccae |
38
.dockerignore
Normal file
38
.dockerignore
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.gitattributes
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yml
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
*.egg-info/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -160,5 +160,5 @@ cython_debug/
|
|||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
hysteria2_venv/
|
hysteria2_venv/
|
||||||
.vscode/
|
# .vscode/
|
||||||
!.vscode/settings.json
|
# !.vscode/settings.json
|
||||||
|
|||||||
172
.vscode/settings.json
vendored
Normal file
172
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
{
|
||||||
|
"workbench.colorTheme": "Default Dark+",
|
||||||
|
"workbench.colorCustomizations": {
|
||||||
|
"editor.background": "#252b3d",
|
||||||
|
"editor.foreground": "#dde1eb",
|
||||||
|
"activityBar.background": "#2f3648",
|
||||||
|
"activityBar.foreground": "#FF6B35",
|
||||||
|
"activityBar.activeBorder": "#FF6B35",
|
||||||
|
"activityBar.inactiveForeground": "#a8afc0",
|
||||||
|
"sideBar.background": "#2f3648",
|
||||||
|
"sideBar.foreground": "#dde1eb",
|
||||||
|
"sideBar.border": "#3a4158",
|
||||||
|
"sideBarTitle.foreground": "#FFA500",
|
||||||
|
"statusBar.background": "#2f3648",
|
||||||
|
"statusBar.foreground": "#58C4DC",
|
||||||
|
"statusBar.noFolderBackground": "#2f3648",
|
||||||
|
"statusBar.debuggingBackground": "#FF6347",
|
||||||
|
"statusBar.debuggingForeground": "#ffffff",
|
||||||
|
"titleBar.activeBackground": "#252b3d",
|
||||||
|
"titleBar.activeForeground": "#dde1eb",
|
||||||
|
"titleBar.inactiveBackground": "#252b3d",
|
||||||
|
"titleBar.inactiveForeground": "#a8afc0",
|
||||||
|
"titleBar.border": "#3a4158",
|
||||||
|
"editorGroupHeader.tabsBackground": "#252b3d",
|
||||||
|
"editorGroupHeader.tabsBorder": "#3a4158",
|
||||||
|
"tab.activeBackground": "#2f3648",
|
||||||
|
"tab.activeForeground": "#FFA500",
|
||||||
|
"tab.inactiveBackground": "#252b3d",
|
||||||
|
"tab.inactiveForeground": "#a8afc0",
|
||||||
|
"tab.border": "#3a4158",
|
||||||
|
"tab.activeBorder": "#FF6B35",
|
||||||
|
"tab.activeBorderTop": "#FF6B35",
|
||||||
|
"editorLineNumber.foreground": "#8891a4",
|
||||||
|
"editorLineNumber.activeForeground": "#58C4DC",
|
||||||
|
"editorCursor.foreground": "#FFA500",
|
||||||
|
"editor.selectionBackground": "#4a6589",
|
||||||
|
"editor.selectionHighlightBackground": "#4a658960",
|
||||||
|
"editor.lineHighlightBackground": "#2f364860",
|
||||||
|
"editorBracketMatch.background": "#4a658960",
|
||||||
|
"editorBracketMatch.border": "#FFA500",
|
||||||
|
"editorWidget.background": "#2f3648",
|
||||||
|
"editorSuggestWidget.background": "#2f3648",
|
||||||
|
"editorSuggestWidget.selectedBackground": "#4a6589",
|
||||||
|
"editorSuggestWidget.highlightForeground": "#FFA500",
|
||||||
|
"input.background": "#252b3d",
|
||||||
|
"input.border": "#3a4158",
|
||||||
|
"input.foreground": "#dde1eb",
|
||||||
|
"inputOption.activeBorder": "#58C4DC",
|
||||||
|
"dropdown.background": "#2f3648",
|
||||||
|
"dropdown.border": "#3a4158",
|
||||||
|
"button.background": "#3FB950",
|
||||||
|
"button.foreground": "#ffffff",
|
||||||
|
"button.hoverBackground": "#4ec55e",
|
||||||
|
"list.activeSelectionBackground": "#4a6589",
|
||||||
|
"list.activeSelectionForeground": "#ffffff",
|
||||||
|
"list.inactiveSelectionBackground": "#4a658960",
|
||||||
|
"list.hoverBackground": "#4a658950",
|
||||||
|
"list.focusBackground": "#4a6589",
|
||||||
|
"terminal.foreground": "#dde1eb",
|
||||||
|
"terminal.ansiGreen": "#3FB950",
|
||||||
|
"terminal.ansiYellow": "#D29922",
|
||||||
|
"terminal.ansiBlue": "#58A6FF",
|
||||||
|
"terminal.ansiMagenta": "#BC8CFF",
|
||||||
|
"terminal.ansiCyan": "#39C5CF",
|
||||||
|
"terminal.ansiRed": "#FF6347",
|
||||||
|
"gitDecoration.modifiedResourceForeground": "#D29922",
|
||||||
|
"gitDecoration.addedResourceForeground": "#3FB950",
|
||||||
|
"gitDecoration.deletedResourceForeground": "#FF6347",
|
||||||
|
"gitDecoration.untrackedResourceForeground": "#39C5CF",
|
||||||
|
"badge.background": "#FF6B35",
|
||||||
|
"badge.foreground": "#ffffff",
|
||||||
|
"notifications.background": "#2f3648",
|
||||||
|
"notifications.foreground": "#dde1eb",
|
||||||
|
"notifications.border": "#3a4158",
|
||||||
|
"notificationLink.foreground": "#58A6FF",
|
||||||
|
"panel.background": "#252b3d",
|
||||||
|
"panel.border": "#3a4158",
|
||||||
|
"panelTitle.activeForeground": "#FFA500",
|
||||||
|
"panelTitle.inactiveForeground": "#a8afc0",
|
||||||
|
"panelTitle.activeBorder": "#FF6B35"
|
||||||
|
},
|
||||||
|
"editor.tokenColorCustomizations": {
|
||||||
|
"textMateRules": [
|
||||||
|
{
|
||||||
|
"scope": "comment",
|
||||||
|
"settings": {
|
||||||
|
"foreground": "#8891a4",
|
||||||
|
"fontStyle": "italic"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": ["keyword", "storage.type", "storage.modifier"],
|
||||||
|
"settings": {
|
||||||
|
"foreground": "#FF6347"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": ["string", "string.quoted"],
|
||||||
|
"settings": {
|
||||||
|
"foreground": "#3FB950"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": ["constant.numeric", "constant.language"],
|
||||||
|
"settings": {
|
||||||
|
"foreground": "#BC8CFF"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": ["variable", "support.variable"],
|
||||||
|
"settings": {
|
||||||
|
"foreground": "#58A6FF"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": ["entity.name.function", "support.function"],
|
||||||
|
"settings": {
|
||||||
|
"foreground": "#D29922"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": ["entity.name.type", "entity.name.class", "support.class"],
|
||||||
|
"settings": {
|
||||||
|
"foreground": "#39C5CF"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": ["entity.name.tag"],
|
||||||
|
"settings": {
|
||||||
|
"foreground": "#3FB950"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": ["entity.other.attribute-name"],
|
||||||
|
"settings": {
|
||||||
|
"foreground": "#39C5CF"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": ["support.type.property-name"],
|
||||||
|
"settings": {
|
||||||
|
"foreground": "#58A6FF"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": ["meta.import", "meta.export"],
|
||||||
|
"settings": {
|
||||||
|
"foreground": "#BC8CFF"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": ["punctuation.definition.tag"],
|
||||||
|
"settings": {
|
||||||
|
"foreground": "#a8afc0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"editor.fontFamily": "'JetBrains Mono', 'Fira Code', 'Cascadia Code', Consolas, 'Courier New', monospace",
|
||||||
|
"editor.fontSize": 13.5,
|
||||||
|
"editor.lineHeight": 1.6,
|
||||||
|
"editor.fontLigatures": true,
|
||||||
|
"editor.fontWeight": "400",
|
||||||
|
"editor.letterSpacing": 0.5,
|
||||||
|
"editor.cursorBlinking": "expand",
|
||||||
|
"editor.cursorSmoothCaretAnimation": "on",
|
||||||
|
"editor.smoothScrolling": true,
|
||||||
|
"workbench.list.smoothScrolling": true,
|
||||||
|
"editor.minimap.enabled": false,
|
||||||
|
"editor.bracketPairColorization.enabled": true,
|
||||||
|
"editor.guides.bracketPairs": "active"
|
||||||
|
}
|
||||||
55
Dockerfile
Normal file
55
Dockerfile
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
FROM ubuntu:22.04
|
||||||
|
|
||||||
|
# Prevent interactive prompts
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
python3 \
|
||||||
|
python3-pip \
|
||||||
|
python3-venv \
|
||||||
|
curl \
|
||||||
|
wget \
|
||||||
|
jq \
|
||||||
|
pwgen \
|
||||||
|
bc \
|
||||||
|
zip \
|
||||||
|
unzip \
|
||||||
|
lsof \
|
||||||
|
gnupg \
|
||||||
|
lsb-release \
|
||||||
|
systemctl \
|
||||||
|
iptables \
|
||||||
|
iproute2 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Create working directory
|
||||||
|
WORKDIR /etc/hysteria
|
||||||
|
|
||||||
|
# Copy requirements first (for better caching)
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# Create and activate virtual environment, install dependencies
|
||||||
|
RUN python3 -m venv /etc/hysteria/hysteria2_venv && \
|
||||||
|
/etc/hysteria/hysteria2_venv/bin/pip install --no-cache-dir --upgrade pip && \
|
||||||
|
/etc/hysteria/hysteria2_venv/bin/pip install --no-cache-dir -r requirements.txt && \
|
||||||
|
/etc/hysteria/hysteria2_venv/bin/pip install --no-cache-dir fastapi uvicorn python-multipart
|
||||||
|
|
||||||
|
# Copy application files
|
||||||
|
COPY . /etc/hysteria/
|
||||||
|
|
||||||
|
# Make scripts executable
|
||||||
|
RUN find /etc/hysteria/core/scripts -type f -name "*.sh" -exec chmod +x {} \; && \
|
||||||
|
find /etc/hysteria/core/scripts -type f -name "*.py" -exec chmod +x {} \; && \
|
||||||
|
chmod +x /etc/hysteria/menu.sh
|
||||||
|
|
||||||
|
# Expose ports
|
||||||
|
EXPOSE 8000 443 80
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:8000/health || exit 1
|
||||||
|
|
||||||
|
# Start FastAPI server
|
||||||
|
CMD ["/etc/hysteria/hysteria2_venv/bin/uvicorn", "core.api:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
|
||||||
271
README-DOCKER.md
Normal file
271
README-DOCKER.md
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
# Blitz Panel - Docker Setup
|
||||||
|
|
||||||
|
## 📋 Prerequisites
|
||||||
|
|
||||||
|
- Docker Engine 20.10+
|
||||||
|
- Docker Compose 2.0+
|
||||||
|
- At least 2GB RAM
|
||||||
|
- Ports available: 8000 (API), 443 (Hysteria2), 80 (HTTP)
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### 1. Prepare Your Files
|
||||||
|
|
||||||
|
Make sure your directory structure looks like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
Blitz/
|
||||||
|
├── core/
|
||||||
|
│ └── scripts/
|
||||||
|
│ ├── auth/
|
||||||
|
│ ├── db/
|
||||||
|
│ ├── hysteria2/
|
||||||
|
│ └── ... (other script folders)
|
||||||
|
├── api.py
|
||||||
|
├── cli_api.py
|
||||||
|
├── requirements.txt
|
||||||
|
├── Dockerfile
|
||||||
|
├── docker-compose.yml
|
||||||
|
└── .dockerignore
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Build and Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build and start containers
|
||||||
|
docker-compose up -d --build
|
||||||
|
|
||||||
|
# Check logs
|
||||||
|
docker-compose logs -f blitz-api
|
||||||
|
|
||||||
|
# Check if services are running
|
||||||
|
docker-compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Test the API
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Health check
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
|
||||||
|
# Get API docs (interactive)
|
||||||
|
open http://localhost:8000/docs
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 API Endpoints
|
||||||
|
|
||||||
|
### User Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all users
|
||||||
|
curl http://localhost:8000/api/v1/users
|
||||||
|
|
||||||
|
# Get specific user
|
||||||
|
curl http://localhost:8000/api/v1/users/john
|
||||||
|
|
||||||
|
# Create user
|
||||||
|
curl -X POST http://localhost:8000/api/v1/users \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"username": "john",
|
||||||
|
"traffic_limit": 100,
|
||||||
|
"expiration_days": 30,
|
||||||
|
"unlimited": false
|
||||||
|
}'
|
||||||
|
|
||||||
|
# Edit user
|
||||||
|
curl -X PUT http://localhost:8000/api/v1/users/john \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"new_traffic_limit": 200,
|
||||||
|
"renew_creation_date": true
|
||||||
|
}'
|
||||||
|
|
||||||
|
# Delete user
|
||||||
|
curl -X DELETE http://localhost:8000/api/v1/users/john
|
||||||
|
|
||||||
|
# Get user URI
|
||||||
|
curl http://localhost:8000/api/v1/users/john/uri?qrcode=false
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hysteria2 Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install Hysteria2
|
||||||
|
curl -X POST http://localhost:8000/api/v1/hysteria2/install \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"port": 443,
|
||||||
|
"sni": "example.com"
|
||||||
|
}'
|
||||||
|
|
||||||
|
# Restart Hysteria2
|
||||||
|
curl -X POST http://localhost:8000/api/v1/hysteria2/restart
|
||||||
|
|
||||||
|
# Get current port
|
||||||
|
curl http://localhost:8000/api/v1/hysteria2/config/port
|
||||||
|
|
||||||
|
# Change port
|
||||||
|
curl -X PUT http://localhost:8000/api/v1/hysteria2/config/port \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"port": 8443}'
|
||||||
|
|
||||||
|
# Enable obfuscation
|
||||||
|
curl -X POST http://localhost:8000/api/v1/hysteria2/obfs/enable
|
||||||
|
```
|
||||||
|
|
||||||
|
### Traffic & Stats
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Get traffic status
|
||||||
|
curl http://localhost:8000/api/v1/traffic/status
|
||||||
|
|
||||||
|
# Get server info
|
||||||
|
curl http://localhost:8000/api/v1/info/server
|
||||||
|
|
||||||
|
# Get services status
|
||||||
|
curl http://localhost:8000/api/v1/info/services
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Development Mode
|
||||||
|
|
||||||
|
To run in development with auto-reload:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run API locally (not in Docker)
|
||||||
|
source /etc/hysteria/hysteria2_venv/bin/activate
|
||||||
|
cd /path/to/Blitz
|
||||||
|
uvicorn api:app --reload --host 0.0.0.0 --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Container won't start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check logs
|
||||||
|
docker-compose logs blitz-api
|
||||||
|
|
||||||
|
# Check MongoDB
|
||||||
|
docker-compose logs mongodb
|
||||||
|
|
||||||
|
# Restart services
|
||||||
|
docker-compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permission issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Fix permissions for scripts
|
||||||
|
docker-compose exec blitz-api bash
|
||||||
|
find /etc/hysteria/core/scripts -type f -name "*.sh" -exec chmod +x {} \;
|
||||||
|
find /etc/hysteria/core/scripts -type f -name "*.py" -exec chmod +x {} \;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Network issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if ports are available
|
||||||
|
sudo netstat -tlnp | grep -E ':(8000|443|80)'
|
||||||
|
|
||||||
|
# If ports are in use, change them in docker-compose.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reset everything
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stop and remove everything
|
||||||
|
docker-compose down -v
|
||||||
|
|
||||||
|
# Remove volumes (WARNING: deletes all data)
|
||||||
|
docker volume rm blitz_mongodb_data blitz_config blitz_certs
|
||||||
|
|
||||||
|
# Rebuild
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Monitoring
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Watch logs in real-time
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Check resource usage
|
||||||
|
docker stats
|
||||||
|
|
||||||
|
# Execute commands in container
|
||||||
|
docker-compose exec blitz-api bash
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔒 Production Deployment
|
||||||
|
|
||||||
|
For production, modify `docker-compose.yml`:
|
||||||
|
|
||||||
|
1. **Add authentication** to the API (JWT, API keys)
|
||||||
|
2. **Use HTTPS** with proper SSL certificates
|
||||||
|
3. **Set up reverse proxy** (nginx/traefik)
|
||||||
|
4. **Configure firewall** rules
|
||||||
|
5. **Enable MongoDB authentication**
|
||||||
|
6. **Set resource limits**
|
||||||
|
|
||||||
|
Example production additions:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# In docker-compose.yml
|
||||||
|
services:
|
||||||
|
blitz-api:
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '2'
|
||||||
|
memory: 2G
|
||||||
|
reservations:
|
||||||
|
cpus: '1'
|
||||||
|
memory: 1G
|
||||||
|
environment:
|
||||||
|
- API_KEY=your-secret-key
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Notes
|
||||||
|
|
||||||
|
- Default API runs on port 8000
|
||||||
|
- MongoDB stores data in named volume `mongodb_data`
|
||||||
|
- Configs persist in `blitz_config` volume
|
||||||
|
- All scripts from `core/` are copied into container
|
||||||
|
- Health check runs every 30 seconds
|
||||||
|
|
||||||
|
## 🤝 Common Tasks
|
||||||
|
|
||||||
|
### Backup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backup MongoDB data
|
||||||
|
docker-compose exec mongodb mongodump --out /data/backup
|
||||||
|
|
||||||
|
# Copy backup out
|
||||||
|
docker cp blitz-mongodb:/data/backup ./backup
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pull latest code
|
||||||
|
git pull
|
||||||
|
|
||||||
|
# Rebuild
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scale
|
||||||
|
|
||||||
|
To run multiple instances (load balancing):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d --scale blitz-api=3
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🆘 Support
|
||||||
|
|
||||||
|
- Check logs: `docker-compose logs -f`
|
||||||
|
- Interactive docs: http://localhost:8000/docs
|
||||||
|
- ReDoc: http://localhost:8000/redoc
|
||||||
89
changelog
89
changelog
@ -1,79 +1,36 @@
|
|||||||
# 📦 ** Release 2.5.0 — ACL & WARP fixes, safer password generation, service controls 🚀**
|
# 🔖 ** release(2.5.1): Telegram UX improvements, web panel access & outbound enhancements 🚀 **
|
||||||
|
|
||||||
*Released: 2025-12-19*
|
*Released: 2026-01-02*
|
||||||
|
|
||||||
## ✨ New Features
|
## ✨ New Features
|
||||||
|
|
||||||
### 🧠 **Core & Service Management**
|
* 🤖 **Telegram Bot**: Added settings menu
|
||||||
|
* 🌐 **Web Panel URL**:
|
||||||
|
|
||||||
* 🔍 Added **Hysteria2 core version detection** and validation
|
* Added service status check before retrieving web panel URL
|
||||||
* 📊 Display **core version** directly in the web panel
|
* New handlers to retrieve and display web panel URL
|
||||||
* 🔁 Added **Restart Hysteria2** button in the web UI with proper fetch handling
|
* 🖥️ **CLI**: Added option to display *only* the web panel URL
|
||||||
* 🔌 New API endpoint to **restart Hysteria2 service** programmatically
|
* 🔀 **Outbounds**: Introduced new `select` outbound type(SingBox)
|
||||||
|
* 👥 **Users**: Added option to display up to **1000 users** in selection dropdown
|
||||||
|
* ⏳ **Expiration**: Added validation for expiration days and improved UI
|
||||||
|
|
||||||
### 🌍 **Geo & ACL Rule Management**
|
### 🐛 Fixes
|
||||||
|
|
||||||
* 🗺️ Enhanced **Geo file update process** with intelligent ACL rule handling
|
* 🛡️ Prevent enabling **Masquerade** when **OBFS** is active
|
||||||
* 🧩 Added **geo rule detection & auto-update** for ACL configurations
|
* ⚙️ Conditionally generate **sing-box OBFS** configuration(SingBox)
|
||||||
* 🧹 Removed redundant `geoip` / `geosite` rejection rules
|
|
||||||
* 🚫 Removed speedtest-related rejection from ACL rules
|
|
||||||
|
|
||||||
### 🛡️ **Security & Password Handling**
|
### ♻️ Refactors & Improvements
|
||||||
|
|
||||||
* 🔐 Replaced `pwgen` with **Python `secrets` module** for secure password generation
|
* 🧭 Introduced settings submenu structure in Telegram bot
|
||||||
* 🔑 Improved password generation for:
|
* 🔗 Removed username from URI fragments
|
||||||
|
* 💾 Streamlined upgrade backup process and removed geo data download
|
||||||
|
|
||||||
* add-user
|
### 🔧 Dependencies
|
||||||
* bulk users
|
|
||||||
* internal password utilities
|
|
||||||
* 🧪 Removed implicit password auto-generation where not required
|
|
||||||
|
|
||||||
### 🧰 **CLI Improvements**
|
* ⬆️ **FastAPI** bumped to `0.128.0`
|
||||||
|
* ⬆️ **psutil** bumped to `7.2.1`
|
||||||
|
* ⬆️ **pillow** bumped to `12.1.0`
|
||||||
|
|
||||||
* 📤 Added `run_cmd_and_stream` for **real-time command output streaming**
|
### ❤️ Credits
|
||||||
* ⚙️ Improved error messages during **Hysteria2 installation**
|
|
||||||
* 🔁 Updated install scripts to return meaningful output messages
|
|
||||||
* 🔒 Replaced `pwgen` with **openssl** for:
|
|
||||||
|
|
||||||
* obfs password generation
|
|
||||||
* normalsub path generation
|
|
||||||
|
|
||||||
### 🌐 **Network & IP Detection**
|
|
||||||
|
|
||||||
* 🌍 Added **ip.sb fallback** for public IP detection to improve reliability
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🐛 Fixes & Improvements
|
|
||||||
|
|
||||||
### 👤 **User & Username Handling**
|
|
||||||
|
|
||||||
* ✅ Allowed **underscores (`_`) in usernames**
|
|
||||||
* 🧼 Improved validation regex and error handling
|
|
||||||
* 🔧 Updated `remove-user` command syntax
|
|
||||||
* 🧠 Improved user handler robustness
|
|
||||||
|
|
||||||
### 🎭 **Masquerade & OBFS**
|
|
||||||
|
|
||||||
* 🚫 Prevented **OBFS usage when masquerade is enabled** to avoid conflicts
|
|
||||||
|
|
||||||
### 🧹 **Scheduler & System Stability**
|
|
||||||
|
|
||||||
* 🔇 Suppressed noisy `systemctl` output during scheduler setup
|
|
||||||
* 🧯 Improved rule-checking logic for WARP configuration status
|
|
||||||
|
|
||||||
### 🧼 **Uninstall & Cleanup**
|
|
||||||
|
|
||||||
* ♻️ Improved WARP uninstall flow:
|
|
||||||
|
|
||||||
* Better config loading & error handling
|
|
||||||
* Automatic **ACL rule reset** after uninstall
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📦 Dependency Updates
|
|
||||||
|
|
||||||
* ⬆️ **FastAPI** → 0.124.4
|
|
||||||
* ⬆️ **Certbot** → 5.2.2
|
|
||||||
* ⬆️ **python-multipart** → 0.0.21
|
|
||||||
|
|
||||||
|
Special thanks to **@MiliAxe** and **@SeyedHashtag** for their valuable contributions 🙌
|
||||||
|
|||||||
52
compose.yml
Normal file
52
compose.yml
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
services:
|
||||||
|
mongodb:
|
||||||
|
image: mongo:8.0
|
||||||
|
container_name: blitz-mongodb
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
MONGO_INITDB_DATABASE: hysteria
|
||||||
|
volumes:
|
||||||
|
- mongodb_data:/data/db
|
||||||
|
networks:
|
||||||
|
- blitz-network
|
||||||
|
healthcheck:
|
||||||
|
test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
blitz-api:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: blitz-api
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
- "443:443"
|
||||||
|
- "80:80"
|
||||||
|
environment:
|
||||||
|
- MONGODB_URI=mongodb://mongodb:27017/hysteria
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
volumes:
|
||||||
|
- ./core:/etc/hysteria/core
|
||||||
|
- blitz_config:/etc/hysteria
|
||||||
|
- blitz_certs:/root/.acme.sh
|
||||||
|
networks:
|
||||||
|
- blitz-network
|
||||||
|
depends_on:
|
||||||
|
mongodb:
|
||||||
|
condition: service_healthy
|
||||||
|
privileged: true
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
- SYS_MODULE
|
||||||
|
|
||||||
|
networks:
|
||||||
|
blitz-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongodb_data:
|
||||||
|
blitz_config:
|
||||||
|
blitz_certs:
|
||||||
686
core/api.py
Normal file
686
core/api.py
Normal file
@ -0,0 +1,686 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, "/etc/hysteria/core/scripts")
|
||||||
|
|
||||||
|
from fastapi import FastAPI, HTTPException, status, Query
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import Optional, List, Dict, Any
|
||||||
|
from datetime import datetime
|
||||||
|
from . import cli_api
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
title="Blitz Panel API",
|
||||||
|
description="REST API for Hysteria2 Panel Management",
|
||||||
|
version="1.0.0",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Pydantic Models
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class UserCreate(BaseModel):
|
||||||
|
username: str = Field(..., min_length=1, max_length=50)
|
||||||
|
traffic_limit: int = Field(..., ge=0, description="Traffic limit in GB")
|
||||||
|
expiration_days: int = Field(..., ge=0)
|
||||||
|
password: Optional[str] = None
|
||||||
|
creation_date: Optional[str] = None
|
||||||
|
unlimited: bool = False
|
||||||
|
note: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class BulkUserCreate(BaseModel):
|
||||||
|
traffic_gb: float = Field(..., ge=0)
|
||||||
|
expiration_days: int = Field(..., ge=0)
|
||||||
|
count: int = Field(..., ge=1, le=1000)
|
||||||
|
prefix: str = Field(..., min_length=1)
|
||||||
|
start_number: int = Field(1, ge=1)
|
||||||
|
unlimited: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class UserEdit(BaseModel):
|
||||||
|
new_username: Optional[str] = None
|
||||||
|
new_password: Optional[str] = None
|
||||||
|
new_traffic_limit: Optional[int] = Field(None, ge=0)
|
||||||
|
new_expiration_days: Optional[int] = Field(None, ge=0)
|
||||||
|
renew_password: bool = False
|
||||||
|
renew_creation_date: bool = False
|
||||||
|
blocked: Optional[bool] = None
|
||||||
|
unlimited_ip: Optional[bool] = None
|
||||||
|
note: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class PortChange(BaseModel):
|
||||||
|
port: int = Field(..., ge=1, le=65535)
|
||||||
|
|
||||||
|
|
||||||
|
class SNIChange(BaseModel):
|
||||||
|
sni: str = Field(..., min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
class IPConfig(BaseModel):
|
||||||
|
ipv4: Optional[str] = None
|
||||||
|
ipv6: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class Hysteria2Install(BaseModel):
|
||||||
|
port: int = Field(..., ge=1, le=65535)
|
||||||
|
sni: str = Field(..., min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
class NodeCreate(BaseModel):
|
||||||
|
name: str
|
||||||
|
ip: str
|
||||||
|
sni: Optional[str] = None
|
||||||
|
pinSHA256: Optional[str] = None
|
||||||
|
port: Optional[int] = Field(None, ge=1, le=65535)
|
||||||
|
obfs: Optional[str] = None
|
||||||
|
insecure: Optional[bool] = False
|
||||||
|
|
||||||
|
|
||||||
|
class TelegramBotConfig(BaseModel):
|
||||||
|
token: str
|
||||||
|
admin_ids: str
|
||||||
|
backup_interval: Optional[int] = Field(None, ge=1)
|
||||||
|
|
||||||
|
|
||||||
|
class WebPanelConfig(BaseModel):
|
||||||
|
domain: str
|
||||||
|
port: int = Field(..., ge=1, le=65535)
|
||||||
|
admin_username: str
|
||||||
|
admin_password: str
|
||||||
|
expiration_minutes: int = Field(30, ge=1)
|
||||||
|
debug: bool = False
|
||||||
|
decoy_path: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class NormalSubConfig(BaseModel):
|
||||||
|
domain: str
|
||||||
|
port: int = Field(..., ge=1, le=65535)
|
||||||
|
|
||||||
|
|
||||||
|
class WARPConfig(BaseModel):
|
||||||
|
all_state: Optional[str] = Field(None, pattern="^(on|off)$")
|
||||||
|
popular_sites_state: Optional[str] = Field(None, pattern="^(on|off)$")
|
||||||
|
domestic_sites_state: Optional[str] = Field(None, pattern="^(on|off)$")
|
||||||
|
block_adult_sites_state: Optional[str] = Field(None, pattern="^(on|off)$")
|
||||||
|
|
||||||
|
|
||||||
|
class IPLimiterConfig(BaseModel):
|
||||||
|
block_duration: Optional[int] = Field(None, ge=1)
|
||||||
|
max_ips: Optional[int] = Field(None, ge=1)
|
||||||
|
|
||||||
|
|
||||||
|
class ExtraProxyConfig(BaseModel):
|
||||||
|
name: str
|
||||||
|
uri: str
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Error Handler
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def handle_api_error(func):
|
||||||
|
"""Decorator to handle CLI API errors"""
|
||||||
|
if asyncio.iscoroutinefunction(func):
|
||||||
|
|
||||||
|
async def async_wrapper(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return await func(*args, **kwargs)
|
||||||
|
except cli_api.InvalidInputError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
except cli_api.ScriptNotFoundError as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Script error: {str(e)}")
|
||||||
|
except cli_api.CommandExecutionError as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500, detail=f"Execution error: {str(e)}"
|
||||||
|
)
|
||||||
|
except cli_api.HysteriaError as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500, detail=f"Unexpected error: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return async_wrapper
|
||||||
|
else:
|
||||||
|
|
||||||
|
def sync_wrapper(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except cli_api.InvalidInputError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
except cli_api.ScriptNotFoundError as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Script error: {str(e)}")
|
||||||
|
except cli_api.CommandExecutionError as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500, detail=f"Execution error: {str(e)}"
|
||||||
|
)
|
||||||
|
except cli_api.HysteriaError as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500, detail=f"Unexpected error: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return sync_wrapper
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Health & Info
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health_check():
|
||||||
|
"""Health check endpoint"""
|
||||||
|
return {"status": "healthy", "timestamp": datetime.now().isoformat()}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/v1/info/server")
|
||||||
|
async def get_server_info():
|
||||||
|
"""Get server information"""
|
||||||
|
info = cli_api.server_info()
|
||||||
|
return {"data": info}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/v1/info/version")
|
||||||
|
async def get_version():
|
||||||
|
"""Get panel version"""
|
||||||
|
version = cli_api.show_version()
|
||||||
|
return {"version": version}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/v1/info/services")
|
||||||
|
async def get_services_status():
|
||||||
|
"""Get status of all services"""
|
||||||
|
status_data = cli_api.get_services_status()
|
||||||
|
return {"services": status_data}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# User Management
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/v1/users")
|
||||||
|
async def list_users():
|
||||||
|
"""List all users"""
|
||||||
|
users = cli_api.list_users()
|
||||||
|
return {"users": users or {}}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/v1/users/{username}")
|
||||||
|
async def get_user(username: str):
|
||||||
|
"""Get user details"""
|
||||||
|
user = cli_api.get_user(username)
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=404, detail=f"User '{username}' not found")
|
||||||
|
return {"user": user}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/users", status_code=status.HTTP_201_CREATED)
|
||||||
|
async def create_user(user: UserCreate):
|
||||||
|
"""Create a new user"""
|
||||||
|
cli_api.add_user(
|
||||||
|
username=user.username,
|
||||||
|
traffic_limit=user.traffic_limit,
|
||||||
|
expiration_days=user.expiration_days,
|
||||||
|
password=user.password,
|
||||||
|
creation_date=user.creation_date,
|
||||||
|
unlimited=user.unlimited,
|
||||||
|
note=user.note,
|
||||||
|
)
|
||||||
|
return {"message": f"User '{user.username}' created successfully"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/users/bulk", status_code=status.HTTP_201_CREATED)
|
||||||
|
async def create_bulk_users(bulk: BulkUserCreate):
|
||||||
|
"""Create multiple users at once"""
|
||||||
|
cli_api.bulk_user_add(
|
||||||
|
traffic_gb=bulk.traffic_gb,
|
||||||
|
expiration_days=bulk.expiration_days,
|
||||||
|
count=bulk.count,
|
||||||
|
prefix=bulk.prefix,
|
||||||
|
start_number=bulk.start_number,
|
||||||
|
unlimited=bulk.unlimited,
|
||||||
|
)
|
||||||
|
return {"message": f"Created {bulk.count} users with prefix '{bulk.prefix}'"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/api/v1/users/{username}")
|
||||||
|
async def edit_user(username: str, user: UserEdit):
|
||||||
|
"""Edit user details"""
|
||||||
|
cli_api.edit_user(
|
||||||
|
username=username,
|
||||||
|
new_username=user.new_username,
|
||||||
|
new_password=user.new_password,
|
||||||
|
new_traffic_limit=user.new_traffic_limit,
|
||||||
|
new_expiration_days=user.new_expiration_days,
|
||||||
|
renew_password=user.renew_password,
|
||||||
|
renew_creation_date=user.renew_creation_date,
|
||||||
|
blocked=user.blocked,
|
||||||
|
unlimited_ip=user.unlimited_ip,
|
||||||
|
note=user.note,
|
||||||
|
)
|
||||||
|
return {"message": f"User '{username}' updated successfully"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/users/{username}/reset")
|
||||||
|
async def reset_user(username: str):
|
||||||
|
"""Reset user traffic and dates"""
|
||||||
|
cli_api.reset_user(username)
|
||||||
|
return {"message": f"User '{username}' reset successfully"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/api/v1/users/{username}")
|
||||||
|
async def delete_user(username: str):
|
||||||
|
"""Delete a user"""
|
||||||
|
cli_api.remove_users([username])
|
||||||
|
return {"message": f"User '{username}' deleted successfully"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/api/v1/users")
|
||||||
|
async def delete_multiple_users(usernames: List[str] = Query(...)):
|
||||||
|
"""Delete multiple users"""
|
||||||
|
cli_api.remove_users(usernames)
|
||||||
|
return {"message": f"Deleted {len(usernames)} users"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/users/{username}/kick")
|
||||||
|
async def kick_user(username: str):
|
||||||
|
"""Kick user (disconnect)"""
|
||||||
|
cli_api.kick_users_by_name([username])
|
||||||
|
return {"message": f"User '{username}' kicked successfully"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/v1/users/{username}/uri")
|
||||||
|
async def get_user_uri(
|
||||||
|
username: str,
|
||||||
|
qrcode: bool = False,
|
||||||
|
ipv: int = 4,
|
||||||
|
all: bool = False,
|
||||||
|
singbox: bool = False,
|
||||||
|
normalsub: bool = False,
|
||||||
|
):
|
||||||
|
"""Get user connection URI"""
|
||||||
|
uri = cli_api.show_user_uri(username, qrcode, ipv, all, singbox, normalsub)
|
||||||
|
return {"uri": uri}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/v1/users/uri/batch")
|
||||||
|
async def get_batch_user_uri(usernames: List[str] = Query(...)):
|
||||||
|
"""Get URIs for multiple users"""
|
||||||
|
uris = cli_api.show_user_uri_json(usernames)
|
||||||
|
return {"uris": uris}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Hysteria2 Core Management
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/hysteria2/install")
|
||||||
|
async def install_hysteria2(config: Hysteria2Install):
|
||||||
|
"""Install Hysteria2"""
|
||||||
|
cli_api.install_hysteria2(port=config.port, sni=config.sni)
|
||||||
|
return {"message": "Hysteria2 installed successfully"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/hysteria2/uninstall")
|
||||||
|
async def uninstall_hysteria2():
|
||||||
|
"""Uninstall Hysteria2"""
|
||||||
|
cli_api.uninstall_hysteria2()
|
||||||
|
return {"message": "Hysteria2 uninstalled successfully"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/hysteria2/update")
|
||||||
|
async def update_hysteria2():
|
||||||
|
"""Update Hysteria2 core"""
|
||||||
|
cli_api.update_hysteria2()
|
||||||
|
return {"message": "Hysteria2 updated successfully"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/hysteria2/restart")
|
||||||
|
async def restart_hysteria2():
|
||||||
|
"""Restart Hysteria2 service"""
|
||||||
|
cli_api.restart_hysteria2()
|
||||||
|
return {"message": "Hysteria2 restarted successfully"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/v1/hysteria2/config/port")
|
||||||
|
async def get_port():
|
||||||
|
"""Get current Hysteria2 port"""
|
||||||
|
port = cli_api.get_hysteria2_port()
|
||||||
|
return {"port": port}
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/api/v1/hysteria2/config/port")
|
||||||
|
async def change_port(config: PortChange):
|
||||||
|
"""Change Hysteria2 port"""
|
||||||
|
cli_api.change_hysteria2_port(config.port)
|
||||||
|
return {"message": f"Port changed to {config.port}"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/v1/hysteria2/config/sni")
|
||||||
|
async def get_sni():
|
||||||
|
"""Get current SNI"""
|
||||||
|
sni = cli_api.get_hysteria2_sni()
|
||||||
|
return {"sni": sni}
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/api/v1/hysteria2/config/sni")
|
||||||
|
async def change_sni(config: SNIChange):
|
||||||
|
"""Change SNI"""
|
||||||
|
cli_api.change_hysteria2_sni(config.sni)
|
||||||
|
return {"message": f"SNI changed to {config.sni}"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/hysteria2/obfs/enable")
|
||||||
|
async def enable_obfs():
|
||||||
|
"""Enable obfuscation"""
|
||||||
|
cli_api.enable_hysteria2_obfs()
|
||||||
|
return {"message": "Obfuscation enabled"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/hysteria2/obfs/disable")
|
||||||
|
async def disable_obfs():
|
||||||
|
"""Disable obfuscation"""
|
||||||
|
cli_api.disable_hysteria2_obfs()
|
||||||
|
return {"message": "Obfuscation disabled"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/v1/hysteria2/obfs/status")
|
||||||
|
async def check_obfs():
|
||||||
|
"""Check obfuscation status"""
|
||||||
|
status_msg = cli_api.check_hysteria2_obfs()
|
||||||
|
return {"status": status_msg}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/hysteria2/masquerade/enable")
|
||||||
|
async def enable_masquerade():
|
||||||
|
"""Enable masquerade"""
|
||||||
|
result = cli_api.enable_hysteria2_masquerade()
|
||||||
|
return {"message": result}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/hysteria2/masquerade/disable")
|
||||||
|
async def disable_masquerade():
|
||||||
|
"""Disable masquerade"""
|
||||||
|
result = cli_api.disable_hysteria2_masquerade()
|
||||||
|
return {"message": result}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/v1/hysteria2/masquerade/status")
|
||||||
|
async def get_masquerade_status():
|
||||||
|
"""Get masquerade status"""
|
||||||
|
status_msg = cli_api.get_hysteria2_masquerade_status()
|
||||||
|
return {"status": status_msg}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Traffic & Statistics
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/v1/traffic/status")
|
||||||
|
async def get_traffic_status():
|
||||||
|
"""Get traffic status for all users"""
|
||||||
|
data = cli_api.traffic_status(no_gui=True, display_output=False)
|
||||||
|
return {"traffic": data}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# IP Configuration
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/v1/config/ip")
|
||||||
|
async def get_ip_addresses():
|
||||||
|
"""Get configured IP addresses"""
|
||||||
|
ipv4, ipv6 = cli_api.get_ip_address()
|
||||||
|
return {"ipv4": ipv4, "ipv6": ipv6}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/config/ip")
|
||||||
|
async def add_ip_addresses():
|
||||||
|
"""Auto-detect and add IP addresses"""
|
||||||
|
cli_api.add_ip_address()
|
||||||
|
return {"message": "IP addresses added"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/api/v1/config/ip")
|
||||||
|
async def edit_ip_addresses(config: IPConfig):
|
||||||
|
"""Edit IP addresses"""
|
||||||
|
cli_api.edit_ip_address(ipv4=config.ipv4, ipv6=config.ipv6)
|
||||||
|
return {"message": "IP addresses updated"}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Advanced Features
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/advanced/tcp-brutal/install")
|
||||||
|
async def install_tcp_brutal():
|
||||||
|
"""Install TCP Brutal"""
|
||||||
|
cli_api.install_tcp_brutal()
|
||||||
|
return {"message": "TCP Brutal installed"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/advanced/warp/install")
|
||||||
|
async def install_warp():
|
||||||
|
"""Install WARP"""
|
||||||
|
cli_api.install_warp()
|
||||||
|
return {"message": "WARP installed"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/advanced/warp/uninstall")
|
||||||
|
async def uninstall_warp():
|
||||||
|
"""Uninstall WARP"""
|
||||||
|
cli_api.uninstall_warp()
|
||||||
|
return {"message": "WARP uninstalled"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/api/v1/advanced/warp/configure")
|
||||||
|
async def configure_warp(config: WARPConfig):
|
||||||
|
"""Configure WARP settings"""
|
||||||
|
cli_api.configure_warp(
|
||||||
|
all_state=config.all_state,
|
||||||
|
popular_sites_state=config.popular_sites_state,
|
||||||
|
domestic_sites_state=config.domestic_sites_state,
|
||||||
|
block_adult_sites_state=config.block_adult_sites_state,
|
||||||
|
)
|
||||||
|
return {"message": "WARP configured"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/v1/advanced/warp/status")
|
||||||
|
async def get_warp_status():
|
||||||
|
"""Get WARP status"""
|
||||||
|
status_data = cli_api.warp_status()
|
||||||
|
return {"status": status_data}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Telegram Bot
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/services/telegram/start")
|
||||||
|
async def start_telegram(config: TelegramBotConfig):
|
||||||
|
"""Start Telegram bot"""
|
||||||
|
cli_api.start_telegram_bot(
|
||||||
|
token=config.token,
|
||||||
|
adminid=config.admin_ids,
|
||||||
|
backup_interval=config.backup_interval,
|
||||||
|
)
|
||||||
|
return {"message": "Telegram bot started"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/services/telegram/stop")
|
||||||
|
async def stop_telegram():
|
||||||
|
"""Stop Telegram bot"""
|
||||||
|
cli_api.stop_telegram_bot()
|
||||||
|
return {"message": "Telegram bot stopped"}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# WebPanel
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/services/webpanel/start")
|
||||||
|
async def start_webpanel(config: WebPanelConfig):
|
||||||
|
"""Start WebPanel"""
|
||||||
|
cli_api.start_webpanel(
|
||||||
|
domain=config.domain,
|
||||||
|
port=config.port,
|
||||||
|
admin_username=config.admin_username,
|
||||||
|
admin_password=config.admin_password,
|
||||||
|
expiration_minutes=config.expiration_minutes,
|
||||||
|
debug=config.debug,
|
||||||
|
decoy_path=config.decoy_path,
|
||||||
|
)
|
||||||
|
return {"message": "WebPanel started"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/services/webpanel/stop")
|
||||||
|
async def stop_webpanel():
|
||||||
|
"""Stop WebPanel"""
|
||||||
|
cli_api.stop_webpanel()
|
||||||
|
return {"message": "WebPanel stopped"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/v1/services/webpanel/url")
|
||||||
|
async def get_webpanel_url():
|
||||||
|
"""Get WebPanel URL"""
|
||||||
|
url = cli_api.get_webpanel_url()
|
||||||
|
return {"url": url}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Normal-Sub
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/services/normalsub/start")
|
||||||
|
async def start_normalsub(config: NormalSubConfig):
|
||||||
|
"""Start Normal-Sub service"""
|
||||||
|
cli_api.start_normalsub(domain=config.domain, port=config.port)
|
||||||
|
return {"message": "Normal-Sub started"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/services/normalsub/stop")
|
||||||
|
async def stop_normalsub():
|
||||||
|
"""Stop Normal-Sub service"""
|
||||||
|
cli_api.stop_normalsub()
|
||||||
|
return {"message": "Normal-Sub stopped"}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# IP Limiter
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/services/ip-limiter/start")
|
||||||
|
async def start_ip_limiter():
|
||||||
|
"""Start IP limiter service"""
|
||||||
|
cli_api.start_ip_limiter()
|
||||||
|
return {"message": "IP limiter started"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/services/ip-limiter/stop")
|
||||||
|
async def stop_ip_limiter():
|
||||||
|
"""Stop IP limiter service"""
|
||||||
|
cli_api.stop_ip_limiter()
|
||||||
|
return {"message": "IP limiter stopped"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/api/v1/services/ip-limiter/config")
|
||||||
|
async def config_ip_limiter(config: IPLimiterConfig):
|
||||||
|
"""Configure IP limiter"""
|
||||||
|
cli_api.config_ip_limiter(
|
||||||
|
block_duration=config.block_duration, max_ips=config.max_ips
|
||||||
|
)
|
||||||
|
return {"message": "IP limiter configured"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/v1/services/ip-limiter/config")
|
||||||
|
async def get_ip_limiter_config():
|
||||||
|
"""Get IP limiter configuration"""
|
||||||
|
config = cli_api.get_ip_limiter_config()
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Nodes Management
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/nodes")
|
||||||
|
async def add_node(node: NodeCreate):
|
||||||
|
"""Add external node"""
|
||||||
|
result = cli_api.add_node(
|
||||||
|
name=node.name,
|
||||||
|
ip=node.ip,
|
||||||
|
sni=node.sni,
|
||||||
|
pinSHA256=node.pinSHA256,
|
||||||
|
port=node.port,
|
||||||
|
obfs=node.obfs,
|
||||||
|
insecure=node.insecure,
|
||||||
|
)
|
||||||
|
return {"message": result}
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/api/v1/nodes/{name}")
|
||||||
|
async def delete_node(name: str):
|
||||||
|
"""Delete node"""
|
||||||
|
result = cli_api.delete_node(name)
|
||||||
|
return {"message": result}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/v1/nodes")
|
||||||
|
async def list_nodes():
|
||||||
|
"""List all nodes"""
|
||||||
|
result = cli_api.list_nodes()
|
||||||
|
return {"nodes": result}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Geo Updates
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/geo/update/{country}")
|
||||||
|
async def update_geo(country: str):
|
||||||
|
"""Update geo files for country (iran, china, russia)"""
|
||||||
|
cli_api.update_geo(country)
|
||||||
|
return {"message": f"Geo files updated for {country}"}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Backup & Restore
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/backup")
|
||||||
|
async def create_backup():
|
||||||
|
"""Create backup"""
|
||||||
|
cli_api.backup_hysteria2()
|
||||||
|
return {"message": "Backup created successfully"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/restore")
|
||||||
|
async def restore_backup(backup_file_path: str):
|
||||||
|
"""Restore from backup"""
|
||||||
|
cli_api.restore_hysteria2(backup_file_path)
|
||||||
|
return {"message": "Restored successfully"}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
832
core/cli_api.py
832
core/cli_api.py
File diff suppressed because it is too large
Load Diff
@ -180,10 +180,10 @@ $(function () {
|
|||||||
$("#userTable tbody tr.user-main-row").each(function () {
|
$("#userTable tbody tr.user-main-row").each(function () {
|
||||||
let showRow;
|
let showRow;
|
||||||
switch (filter) {
|
switch (filter) {
|
||||||
case "on-hold": showRow = $(this).find("td:eq(3) i").hasClass("text-warning"); break;
|
case "on-hold": showRow = $(this).find("td.status-cell i").hasClass("text-warning"); break;
|
||||||
case "online": showRow = $(this).find("td:eq(3) i").hasClass("text-success"); break;
|
case "online": showRow = $(this).find("td.status-cell i").hasClass("text-success"); break;
|
||||||
case "enable": showRow = $(this).find("td:eq(8) i").hasClass("text-success"); break;
|
case "enable": showRow = $(this).find("td.enable-cell i").hasClass("text-success"); break;
|
||||||
case "disable": showRow = $(this).find("td:eq(8) i").hasClass("text-danger"); break;
|
case "disable": showRow = $(this).find("td.enable-cell i").hasClass("text-danger"); break;
|
||||||
default: showRow = true;
|
default: showRow = true;
|
||||||
}
|
}
|
||||||
$(this).toggle(showRow).find(".user-checkbox").prop("checked", false);
|
$(this).toggle(showRow).find(".user-checkbox").prop("checked", false);
|
||||||
@ -278,10 +278,11 @@ $(function () {
|
|||||||
const dataRow = $(event.relatedTarget).closest("tr.user-main-row");
|
const dataRow = $(event.relatedTarget).closest("tr.user-main-row");
|
||||||
const url = GET_USER_URL_TEMPLATE.replace('U', user);
|
const url = GET_USER_URL_TEMPLATE.replace('U', user);
|
||||||
|
|
||||||
const trafficText = dataRow.find("td:eq(4)").text();
|
const trafficText = dataRow.find("td.traffic-cell").text();
|
||||||
const expiryText = dataRow.find("td:eq(6)").text();
|
const usageDaysText = dataRow.find("td.usage-days-cell").text();
|
||||||
|
const expiryText = usageDaysText.split('/')[1] || "0";
|
||||||
const note = dataRow.data('note');
|
const note = dataRow.data('note');
|
||||||
const statusText = dataRow.find("td:eq(3)").text().trim();
|
const statusText = dataRow.find("td.status-cell").text().trim();
|
||||||
|
|
||||||
$('#editPasswordError').text('');
|
$('#editPasswordError').text('');
|
||||||
$('#editExpirationDaysError').text('');
|
$('#editExpirationDaysError').text('');
|
||||||
@ -298,7 +299,7 @@ $(function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$("#editNote").val(note || '');
|
$("#editNote").val(note || '');
|
||||||
$("#editBlocked").prop("checked", !dataRow.find("td:eq(8) i").hasClass("text-success"));
|
$("#editBlocked").prop("checked", !dataRow.find("td.enable-cell i").hasClass("text-success"));
|
||||||
$("#editUnlimitedIp").prop("checked", dataRow.find(".unlimited-ip-cell i").hasClass("text-primary"));
|
$("#editUnlimitedIp").prop("checked", dataRow.find(".unlimited-ip-cell i").hasClass("text-primary"));
|
||||||
|
|
||||||
const passwordInput = $("#editPassword");
|
const passwordInput = $("#editPassword");
|
||||||
|
|||||||
@ -10,6 +10,7 @@ class User(BaseModel):
|
|||||||
expiry_date: str
|
expiry_date: str
|
||||||
expiry_days: str
|
expiry_days: str
|
||||||
day_usage: str
|
day_usage: str
|
||||||
|
usage_days_display: str
|
||||||
enable: bool
|
enable: bool
|
||||||
unlimited_ip: bool
|
unlimited_ip: bool
|
||||||
online_count: int = 0
|
online_count: int = 0
|
||||||
@ -88,6 +89,7 @@ class User(BaseModel):
|
|||||||
percentage = (used_bytes / quota_bytes) * 100
|
percentage = (used_bytes / quota_bytes) * 100
|
||||||
|
|
||||||
traffic_used_display = f"{used_formatted}/{quota_formatted} ({percentage:.1f}%)"
|
traffic_used_display = f"{used_formatted}/{quota_formatted} ({percentage:.1f}%)"
|
||||||
|
usage_days_display = f"{day_usage}/{display_expiry_days}"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'username': user_data['username'],
|
'username': user_data['username'],
|
||||||
@ -97,6 +99,7 @@ class User(BaseModel):
|
|||||||
'expiry_date': display_expiry_date,
|
'expiry_date': display_expiry_date,
|
||||||
'expiry_days': display_expiry_days,
|
'expiry_days': display_expiry_days,
|
||||||
'day_usage': day_usage,
|
'day_usage': day_usage,
|
||||||
|
'usage_days_display': usage_days_display,
|
||||||
'enable': not user_data.get('blocked', False),
|
'enable': not user_data.get('blocked', False),
|
||||||
'unlimited_ip': user_data.get('unlimited_user', False),
|
'unlimited_ip': user_data.get('unlimited_user', False),
|
||||||
'online_count': user_data.get('online_count', 0),
|
'online_count': user_data.get('online_count', 0),
|
||||||
|
|||||||
@ -128,9 +128,8 @@
|
|||||||
<th>Username</th>
|
<th>Username</th>
|
||||||
<th class="d-none d-md-table-cell">Status</th>
|
<th class="d-none d-md-table-cell">Status</th>
|
||||||
<th class="d-none d-md-table-cell">Traffic Usage</th>
|
<th class="d-none d-md-table-cell">Traffic Usage</th>
|
||||||
<th class="d-none d-md-table-cell text-nowrap">Expiry Date</th>
|
|
||||||
<th class="d-none d-md-table-cell text-nowrap">Expiry Days</th>
|
|
||||||
<th class="d-none d-md-table-cell text-nowrap">Day Usage</th>
|
<th class="d-none d-md-table-cell text-nowrap">Day Usage</th>
|
||||||
|
<th class="d-none d-md-table-cell text-nowrap">Expiry Date</th>
|
||||||
<th class="d-none d-md-table-cell">Enable</th>
|
<th class="d-none d-md-table-cell">Enable</th>
|
||||||
<th class="d-none d-md-table-cell">Note</th>
|
<th class="d-none d-md-table-cell">Note</th>
|
||||||
<th class="d-none d-md-table-cell text-nowrap requires-iplimit-service" style="display: none;">Unlimited IP</th>
|
<th class="d-none d-md-table-cell text-nowrap requires-iplimit-service" style="display: none;">Unlimited IP</th>
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>{{ loop.index + (offset if offset is defined else 0) }}</td>
|
<td>{{ loop.index + (offset if offset is defined else 0) }}</td>
|
||||||
<td data-username="{{ user.username }}">{{ user.username }}</td>
|
<td data-username="{{ user.username }}">{{ user.username }}</td>
|
||||||
<td class="d-none d-md-table-cell">
|
<td class="d-none d-md-table-cell status-cell">
|
||||||
{% if user.status == "Online" %}
|
{% if user.status == "Online" %}
|
||||||
<i class="fas fa-circle text-success"></i> Online
|
<i class="fas fa-circle text-success"></i> Online
|
||||||
{% if user.online_count and user.online_count > 0 %}
|
{% if user.online_count and user.online_count > 0 %}
|
||||||
@ -28,11 +28,10 @@
|
|||||||
<i class="fas fa-circle text-danger"></i> {{ user.status }}
|
<i class="fas fa-circle text-danger"></i> {{ user.status }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="d-none d-md-table-cell">{{ user.traffic_used }}</td>
|
<td class="d-none d-md-table-cell traffic-cell">{{ user.traffic_used }}</td>
|
||||||
<td class="d-none d-md-table-cell">{{ user.expiry_date }}</td>
|
<td class="d-none d-md-table-cell usage-days-cell">{{ user.usage_days_display }}</td>
|
||||||
<td class="d-none d-md-table-cell">{{ user.expiry_days }}</td>
|
<td class="d-none d-md-table-cell expiry-date-cell">{{ user.expiry_date }}</td>
|
||||||
<td class="d-none d-md-table-cell">{{ user.day_usage }}</td>
|
<td class="d-none d-md-table-cell enable-cell">
|
||||||
<td class="d-none d-md-table-cell">
|
|
||||||
{% if user.enable %}
|
{% if user.enable %}
|
||||||
<i class="fas fa-check-circle text-success"></i>
|
<i class="fas fa-check-circle text-success"></i>
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -100,9 +99,8 @@
|
|||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p><strong>Traffic Usage:</strong> <span>{{ user.traffic_used }}</span></p>
|
<p><strong>Traffic Usage:</strong> <span>{{ user.traffic_used }}</span></p>
|
||||||
|
<p><strong>Day Usage:</strong> <span>{{ user.usage_days_display }}</span></p>
|
||||||
<p><strong>Expiry Date:</strong> <span>{{ user.expiry_date }}</span></p>
|
<p><strong>Expiry Date:</strong> <span>{{ user.expiry_date }}</span></p>
|
||||||
<p><strong>Expiry Days:</strong> <span>{{ user.expiry_days }}</span></p>
|
|
||||||
<p><strong>Day Usage:</strong> <span>{{ user.day_usage }}</span></p>
|
|
||||||
<p><strong>Note:</strong> <span>{{ user.note or 'N/A' }}</span></p>
|
<p><strong>Note:</strong> <span>{{ user.note or 'N/A' }}</span></p>
|
||||||
<p><strong>Enable:</strong>
|
<p><strong>Enable:</strong>
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
# Core async & network
|
# Core async & network
|
||||||
aiohttp==3.13.2
|
aiohttp==3.13.3
|
||||||
aiofiles==25.1.0
|
aiofiles==25.1.0
|
||||||
|
|
||||||
# CLI tools
|
# CLI tools
|
||||||
@ -16,7 +16,7 @@ typing_extensions==4.15.0
|
|||||||
pyTelegramBotAPI==4.29.1
|
pyTelegramBotAPI==4.29.1
|
||||||
qrcode==8.2
|
qrcode==8.2
|
||||||
pypng==0.20220715.0
|
pypng==0.20220715.0
|
||||||
pillow==12.0.0
|
pillow==12.1.0
|
||||||
|
|
||||||
# Cache / misc
|
# Cache / misc
|
||||||
propcache==0.4.1
|
propcache==0.4.1
|
||||||
@ -38,3 +38,21 @@ annotated-types==0.7.0
|
|||||||
|
|
||||||
# SSL / ACME management
|
# SSL / ACME management
|
||||||
certbot==5.2.2
|
certbot==5.2.2
|
||||||
|
|
||||||
|
# Core dependencies (from your existing project)
|
||||||
|
pymongo
|
||||||
|
python-dotenv
|
||||||
|
qrcode[pil]
|
||||||
|
Pillow
|
||||||
|
|
||||||
|
# FastAPI and server
|
||||||
|
fastapi
|
||||||
|
uvicorn[standard]
|
||||||
|
python-multipart
|
||||||
|
pydantic
|
||||||
|
|
||||||
|
# Additional utilities
|
||||||
|
requests
|
||||||
|
cryptography
|
||||||
|
asyncio
|
||||||
|
requests
|
||||||
|
|||||||
Reference in New Issue
Block a user