Compare commits
25 Commits
d395240dce
...
nginx
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4262da7c9 | ||
|
|
024430754c | ||
|
|
d17ed79e29 | ||
|
|
21eb63659a | ||
|
|
2ea24a98cd | ||
|
|
2f0e9ffdf9 | ||
|
|
b7afa9736b | ||
|
|
c879bbaed0 | ||
|
|
a39dbdd613 | ||
|
|
cedb411536 | ||
|
|
361e0bc459 | ||
|
|
1e08fa45a1 | ||
|
|
48f1bfbcad | ||
|
|
ef17abfe6b | ||
|
|
fc4c8a7474 | ||
|
|
3829d98e91 | ||
|
|
88e24f8fec | ||
|
|
87709bab4d | ||
|
|
0dfeb0ef7f | ||
|
|
4a9616a0f7 | ||
|
|
328edebe3c | ||
|
|
ff14e8defb | ||
|
|
8d52c0338f | ||
|
|
3921a09c4e | ||
|
|
8e43f2bd42 |
@@ -52,6 +52,4 @@ PLAYER_AUTH.md
|
|||||||
PROGRESS.md
|
PROGRESS.md
|
||||||
README.md
|
README.md
|
||||||
|
|
||||||
# Config templates
|
|
||||||
player_config_template.ini
|
|
||||||
player_auth_module.py
|
|
||||||
|
|||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -13,6 +13,9 @@ ENV/
|
|||||||
instance/
|
instance/
|
||||||
.webassets-cache
|
.webassets-cache
|
||||||
|
|
||||||
|
# Persistent data folder (containers, database, uploads)
|
||||||
|
data/
|
||||||
|
|
||||||
# IDEs
|
# IDEs
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
@@ -52,3 +55,7 @@ htmlcov/
|
|||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
|
|
||||||
|
#data
|
||||||
|
data/
|
||||||
|
|
||||||
|
|||||||
16
Dockerfile
16
Dockerfile
@@ -4,16 +4,19 @@ FROM python:3.13-slim
|
|||||||
# Set working directory
|
# Set working directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install system dependencies
|
# Install system dependencies including LibreOffice for PPTX conversion
|
||||||
# Note: LibreOffice is excluded from the base image to reduce size (~500MB)
|
RUN apt-get update && \
|
||||||
# It can be installed on-demand via the Admin Panel → System Dependencies
|
apt-get install -y --no-install-recommends \
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
poppler-utils \
|
poppler-utils \
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
libmagic1 \
|
libmagic1 \
|
||||||
sudo \
|
sudo \
|
||||||
fonts-noto-color-emoji \
|
fonts-noto-color-emoji \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
libreoffice-core \
|
||||||
|
libreoffice-impress \
|
||||||
|
libreoffice-writer \
|
||||||
|
&& apt-get clean && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy requirements first for better caching
|
# Copy requirements first for better caching
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
@@ -28,9 +31,6 @@ COPY . .
|
|||||||
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||||
RUN chmod +x /docker-entrypoint.sh
|
RUN chmod +x /docker-entrypoint.sh
|
||||||
|
|
||||||
# Create directories for uploads and database
|
|
||||||
RUN mkdir -p app/static/uploads instance
|
|
||||||
|
|
||||||
# Set environment variables
|
# Set environment variables
|
||||||
ENV FLASK_APP=app.app:create_app
|
ENV FLASK_APP=app.app:create_app
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|||||||
487
QUICK_DEPLOYMENT.md
Normal file
487
QUICK_DEPLOYMENT.md
Normal file
@@ -0,0 +1,487 @@
|
|||||||
|
# DigiServer v2 - Quick Deployment Guide
|
||||||
|
|
||||||
|
## 📋 Overview
|
||||||
|
|
||||||
|
DigiServer is deployed using Docker Compose with the following architecture:
|
||||||
|
|
||||||
|
```
|
||||||
|
Internet (User)
|
||||||
|
↓
|
||||||
|
Nginx Reverse Proxy (Port 80/443)
|
||||||
|
↓
|
||||||
|
Internal Docker Network
|
||||||
|
↓
|
||||||
|
Flask App (Gunicorn on Port 5000)
|
||||||
|
↓
|
||||||
|
SQLite Database
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Quick Start (One Command)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /srv/digiserver-v2
|
||||||
|
bash deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
1. ✅ Start Docker containers
|
||||||
|
2. ✅ Initialize database
|
||||||
|
3. ✅ Run migrations
|
||||||
|
4. ✅ Configure HTTPS
|
||||||
|
5. ✅ Display access information
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Container Architecture
|
||||||
|
|
||||||
|
### **Container 1: digiserver-app (Flask)**
|
||||||
|
- **Image**: Built from Dockerfile (Python 3.13)
|
||||||
|
- **Port**: 5000 (internal only)
|
||||||
|
- **Volumes**:
|
||||||
|
- `./data:/app` - Persistent application data
|
||||||
|
- `./data/instance:/app/instance` - Database & configuration
|
||||||
|
- `./data/uploads:/app/app/static/uploads` - User uploads
|
||||||
|
- **Startup**: Automatically initializes database on first run
|
||||||
|
- **Health Check**: Every 30 seconds
|
||||||
|
|
||||||
|
### **Container 2: nginx (Reverse Proxy)**
|
||||||
|
- **Image**: nginx:alpine
|
||||||
|
- **Ports**: 80 & 443 (exposed to internet)
|
||||||
|
- **Volumes**:
|
||||||
|
- `nginx.conf` - Main configuration
|
||||||
|
- `./data/nginx-ssl/` - SSL certificates
|
||||||
|
- `./data/nginx-logs/` - Access/error logs
|
||||||
|
- `./data/certbot/` - Let's Encrypt challenges
|
||||||
|
- **Startup**: Waits for Flask app to start
|
||||||
|
- **Health Check**: Every 30 seconds
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Deployment Steps Explained
|
||||||
|
|
||||||
|
### **Step 1: Start Containers**
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
- Builds Flask app image (if needed)
|
||||||
|
- Starts both containers
|
||||||
|
- Waits for containers to be healthy
|
||||||
|
|
||||||
|
### **Step 2: Database Initialization** (docker-entrypoint.sh)
|
||||||
|
When Flask container starts:
|
||||||
|
1. Create required directories
|
||||||
|
2. Check if database exists
|
||||||
|
3. If NOT exists:
|
||||||
|
- Initialize SQLite database (dashboard.db)
|
||||||
|
- Create admin user from environment variables
|
||||||
|
4. Start Gunicorn server (4 workers, 120s timeout)
|
||||||
|
|
||||||
|
### **Step 3: Run Migrations**
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T digiserver-app python /app/migrations/[migration_name].py
|
||||||
|
```
|
||||||
|
Applied migrations:
|
||||||
|
- `add_https_config_table.py` - HTTPS settings
|
||||||
|
- `add_player_user_table.py` - Player user management
|
||||||
|
- `add_email_to_https_config.py` - Email configuration
|
||||||
|
- `migrate_player_user_global.py` - Global settings
|
||||||
|
|
||||||
|
### **Step 4: Configure HTTPS**
|
||||||
|
- SSL certificates stored in `./data/nginx-ssl/`
|
||||||
|
- Pre-generated self-signed certs for development
|
||||||
|
- Ready for Let's Encrypt integration
|
||||||
|
|
||||||
|
### **Step 5: Reverse Proxy Routing** (nginx.conf)
|
||||||
|
```
|
||||||
|
HTTP (80):
|
||||||
|
• Redirect all traffic to HTTPS
|
||||||
|
• Allow ACME challenges for Let's Encrypt
|
||||||
|
|
||||||
|
HTTPS (443):
|
||||||
|
• TLS 1.2+, HTTP/2 enabled
|
||||||
|
• Proxy all requests to Flask app
|
||||||
|
• Security headers added
|
||||||
|
• Gzip compression enabled
|
||||||
|
• Max upload size: 2GB
|
||||||
|
• Proxy timeout: 300s
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Step 6: ProxyFix Middleware** (app/app.py)
|
||||||
|
Extracts real client information from Nginx headers:
|
||||||
|
- `X-Forwarded-For` → Real client IP
|
||||||
|
- `X-Forwarded-Proto` → Protocol (http/https)
|
||||||
|
- `X-Forwarded-Host` → Original hostname
|
||||||
|
- `X-Forwarded-Port` → Original port
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 Directory Structure & Persistence
|
||||||
|
|
||||||
|
```
|
||||||
|
/srv/digiserver-v2/
|
||||||
|
├── app/ (Flask application code)
|
||||||
|
├── data/ (PERSISTENT - mounted as Docker volume)
|
||||||
|
│ ├── app/ (Copy of app/ for container)
|
||||||
|
│ ├── instance/ (dashboard.db - SQLite database)
|
||||||
|
│ ├── uploads/ (User uploaded files)
|
||||||
|
│ ├── nginx-ssl/ (SSL certificates)
|
||||||
|
│ ├── nginx-logs/ (Nginx logs)
|
||||||
|
│ └── certbot/ (Let's Encrypt challenges)
|
||||||
|
├── migrations/ (Database schema updates)
|
||||||
|
├── docker-compose.yml (Container orchestration)
|
||||||
|
├── Dockerfile (Flask app image definition)
|
||||||
|
├── nginx.conf (Reverse proxy configuration)
|
||||||
|
└── docker-entrypoint.sh (Container startup script)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Default Credentials
|
||||||
|
|
||||||
|
```
|
||||||
|
Username: admin
|
||||||
|
Password: admin123
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ **CHANGE IMMEDIATELY IN PRODUCTION!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 Access Points
|
||||||
|
|
||||||
|
After deployment, access the app at:
|
||||||
|
- `https://localhost` (if deployed locally)
|
||||||
|
- `https://192.168.0.121` (if deployed on server)
|
||||||
|
- `https://<DOMAIN>` (if DNS configured)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Environment Variables (Optional)
|
||||||
|
|
||||||
|
Create `.env` file in project root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Network Configuration
|
||||||
|
HOSTNAME=digiserver
|
||||||
|
DOMAIN=digiserver.example.com
|
||||||
|
IP_ADDRESS=192.168.0.121
|
||||||
|
|
||||||
|
# SSL/HTTPS
|
||||||
|
EMAIL=admin@example.com
|
||||||
|
|
||||||
|
# Flask Configuration
|
||||||
|
SECRET_KEY=your-secret-key-here
|
||||||
|
FLASK_ENV=production
|
||||||
|
|
||||||
|
# Admin User
|
||||||
|
ADMIN_USERNAME=admin
|
||||||
|
ADMIN_PASSWORD=admin123
|
||||||
|
```
|
||||||
|
|
||||||
|
Then start with:
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Common Commands
|
||||||
|
|
||||||
|
### **Start/Stop Containers**
|
||||||
|
```bash
|
||||||
|
# Start containers
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Stop containers
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Restart containers
|
||||||
|
docker-compose restart
|
||||||
|
|
||||||
|
# Restart specific container
|
||||||
|
docker-compose restart digiserver-app
|
||||||
|
docker-compose restart nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### **View Logs**
|
||||||
|
```bash
|
||||||
|
# All containers
|
||||||
|
docker-compose logs
|
||||||
|
|
||||||
|
# Follow logs (real-time)
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Specific container
|
||||||
|
docker-compose logs -f digiserver-app
|
||||||
|
docker-compose logs -f nginx
|
||||||
|
|
||||||
|
# Show last 50 lines
|
||||||
|
docker-compose logs --tail=50 digiserver-app
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Container Status**
|
||||||
|
```bash
|
||||||
|
# Show running containers
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
# Show container details
|
||||||
|
docker-compose ps -a
|
||||||
|
|
||||||
|
# Check container health
|
||||||
|
docker ps --format="table {{.Names}}\t{{.Status}}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Database Operations**
|
||||||
|
```bash
|
||||||
|
# Access database shell
|
||||||
|
docker-compose exec digiserver-app sqlite3 /app/instance/dashboard.db
|
||||||
|
|
||||||
|
# Backup database
|
||||||
|
docker-compose exec digiserver-app cp /app/instance/dashboard.db /app/instance/dashboard.db.backup
|
||||||
|
|
||||||
|
# Restore database
|
||||||
|
docker-compose exec digiserver-app cp /app/instance/dashboard.db.backup /app/instance/dashboard.db
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Nginx Operations**
|
||||||
|
```bash
|
||||||
|
# Validate Nginx configuration
|
||||||
|
docker exec digiserver-nginx nginx -t
|
||||||
|
|
||||||
|
# Reload Nginx (without restart)
|
||||||
|
docker exec digiserver-nginx nginx -s reload
|
||||||
|
|
||||||
|
# View Nginx logs
|
||||||
|
docker-compose logs -f nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 SSL Certificate Management
|
||||||
|
|
||||||
|
### **Generate New Self-Signed Certificate**
|
||||||
|
```bash
|
||||||
|
bash generate_nginx_certs.sh 192.168.0.121 365
|
||||||
|
docker-compose restart nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- `192.168.0.121` - Domain/IP for certificate
|
||||||
|
- `365` - Certificate validity in days
|
||||||
|
|
||||||
|
### **Set Up Let's Encrypt (Production)**
|
||||||
|
1. Update `DOMAIN` and `EMAIL` in environment
|
||||||
|
2. Modify `nginx.conf` to enable certbot challenges
|
||||||
|
3. Run certbot:
|
||||||
|
```bash
|
||||||
|
docker run --rm -v $(pwd)/data/certbot:/etc/letsencrypt \
|
||||||
|
-v $(pwd)/data/nginx-logs:/var/log/letsencrypt \
|
||||||
|
certbot/certbot certonly --webroot \
|
||||||
|
-w /var/www/certbot \
|
||||||
|
-d yourdomain.com \
|
||||||
|
-m your-email@example.com \
|
||||||
|
--agree-tos
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### **Containers not starting?**
|
||||||
|
```bash
|
||||||
|
# Check docker-compose logs
|
||||||
|
docker-compose logs
|
||||||
|
|
||||||
|
# Check system resources
|
||||||
|
docker stats
|
||||||
|
|
||||||
|
# Restart Docker daemon
|
||||||
|
sudo systemctl restart docker
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Application not responding?**
|
||||||
|
```bash
|
||||||
|
# Check app container health
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
# View app logs
|
||||||
|
docker-compose logs -f digiserver-app
|
||||||
|
|
||||||
|
# Test Flask directly
|
||||||
|
docker-compose exec digiserver-app curl http://localhost:5000/
|
||||||
|
```
|
||||||
|
|
||||||
|
### **HTTPS not working?**
|
||||||
|
```bash
|
||||||
|
# Verify Nginx config
|
||||||
|
docker exec digiserver-nginx nginx -t
|
||||||
|
|
||||||
|
# Check SSL certificates exist
|
||||||
|
ls -la ./data/nginx-ssl/
|
||||||
|
|
||||||
|
# View Nginx error logs
|
||||||
|
docker-compose logs nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Database issues?**
|
||||||
|
```bash
|
||||||
|
# Check database file exists
|
||||||
|
ls -la ./data/instance/dashboard.db
|
||||||
|
|
||||||
|
# Verify database permissions
|
||||||
|
docker-compose exec digiserver-app ls -la /app/instance/
|
||||||
|
|
||||||
|
# Check database tables
|
||||||
|
docker-compose exec digiserver-app sqlite3 /app/instance/dashboard.db ".tables"
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Port already in use?**
|
||||||
|
```bash
|
||||||
|
# Find process using port 80
|
||||||
|
sudo lsof -i :80
|
||||||
|
|
||||||
|
# Find process using port 443
|
||||||
|
sudo lsof -i :443
|
||||||
|
|
||||||
|
# Kill process (if needed)
|
||||||
|
sudo kill -9 <PID>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Health Checks
|
||||||
|
|
||||||
|
Both containers have health checks:
|
||||||
|
|
||||||
|
**Flask App**: Pings `http://localhost:5000/` every 30 seconds
|
||||||
|
**Nginx**: Pings `http://localhost:80/` every 30 seconds
|
||||||
|
|
||||||
|
Check health status:
|
||||||
|
```bash
|
||||||
|
docker-compose ps
|
||||||
|
# Look for "Up (healthy)" status
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Database Backup & Restore
|
||||||
|
|
||||||
|
### **Backup**
|
||||||
|
```bash
|
||||||
|
# Create backup
|
||||||
|
docker-compose exec digiserver-app cp /app/instance/dashboard.db /app/instance/dashboard.backup.db
|
||||||
|
|
||||||
|
# Download to local machine
|
||||||
|
cp ./data/instance/dashboard.backup.db ./backup/
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Restore**
|
||||||
|
```bash
|
||||||
|
# Stop containers
|
||||||
|
docker-compose stop
|
||||||
|
|
||||||
|
# Restore database
|
||||||
|
cp ./backup/dashboard.backup.db ./data/instance/dashboard.db
|
||||||
|
|
||||||
|
# Start containers
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 Performance Tuning
|
||||||
|
|
||||||
|
### **Gunicorn Workers** (docker-entrypoint.sh)
|
||||||
|
```bash
|
||||||
|
# Default: 4 workers
|
||||||
|
# Formula: (2 × CPU_count) + 1
|
||||||
|
# For 4-core CPU: 9 workers
|
||||||
|
|
||||||
|
# Modify docker-entrypoint.sh:
|
||||||
|
gunicorn --workers 9 ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Nginx Worker Processes** (nginx.conf)
|
||||||
|
```nginx
|
||||||
|
# Default: auto (CPU count)
|
||||||
|
worker_processes auto;
|
||||||
|
|
||||||
|
# Or specify manually:
|
||||||
|
worker_processes 4;
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Upload Timeout** (nginx.conf)
|
||||||
|
```nginx
|
||||||
|
# Default: 300s
|
||||||
|
proxy_connect_timeout 300s;
|
||||||
|
proxy_send_timeout 300s;
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Security Checklist
|
||||||
|
|
||||||
|
- [ ] Change admin password immediately
|
||||||
|
- [ ] Set strong `SECRET_KEY` environment variable
|
||||||
|
- [ ] Enable firewall rules (allow only ports 80, 443)
|
||||||
|
- [ ] Set up HTTPS with Let's Encrypt
|
||||||
|
- [ ] Configure regular database backups
|
||||||
|
- [ ] Review Nginx security headers
|
||||||
|
- [ ] Update Flask dependencies regularly
|
||||||
|
- [ ] Monitor container logs for errors
|
||||||
|
- [ ] Restrict admin panel access (IP whitelist optional)
|
||||||
|
- [ ] Enable Flask debug mode only in development
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Support & Documentation
|
||||||
|
|
||||||
|
- **Nginx Setup**: See `NGINX_SETUP_QUICK.md`
|
||||||
|
- **ProxyFix Configuration**: See `PROXY_FIX_SETUP.md`
|
||||||
|
- **Deployment Commands**: See `DEPLOYMENT_COMMANDS.md`
|
||||||
|
- **Issue Troubleshooting**: Check `old_code_documentation/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Deployment Checklist
|
||||||
|
|
||||||
|
- [ ] Docker & Docker Compose installed
|
||||||
|
- [ ] Running from `/srv/digiserver-v2` directory
|
||||||
|
- [ ] Environment variables configured (optional)
|
||||||
|
- [ ] Port 80/443 available
|
||||||
|
- [ ] Sufficient disk space (min 5GB)
|
||||||
|
- [ ] Sufficient RAM (min 2GB free)
|
||||||
|
- [ ] Network connectivity verified
|
||||||
|
- [ ] SSL certificates generated or obtained
|
||||||
|
- [ ] Admin credentials changed (production)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Next Steps After Deployment
|
||||||
|
|
||||||
|
1. **Access Web Interface**
|
||||||
|
- Login with admin credentials
|
||||||
|
- Change password immediately
|
||||||
|
|
||||||
|
2. **Configure Application**
|
||||||
|
- Set up players
|
||||||
|
- Upload content
|
||||||
|
- Configure groups & permissions
|
||||||
|
|
||||||
|
3. **Production Hardening**
|
||||||
|
- Enable Let's Encrypt HTTPS
|
||||||
|
- Configure firewall rules
|
||||||
|
- Set up database backups
|
||||||
|
- Monitor logs
|
||||||
|
|
||||||
|
4. **Optional Enhancements**
|
||||||
|
- Set up custom domain
|
||||||
|
- Configure email notifications
|
||||||
|
- Install optional dependencies (LibreOffice, etc.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated**: January 15, 2026
|
||||||
@@ -4,6 +4,7 @@ Modern Flask application with blueprint architecture
|
|||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
from flask import Flask, render_template
|
from flask import Flask, render_template
|
||||||
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
from app.config import DevelopmentConfig, ProductionConfig, TestingConfig
|
from app.config import DevelopmentConfig, ProductionConfig, TestingConfig
|
||||||
@@ -37,6 +38,10 @@ def create_app(config_name=None):
|
|||||||
|
|
||||||
app.config.from_object(config)
|
app.config.from_object(config)
|
||||||
|
|
||||||
|
# Apply ProxyFix middleware for reverse proxy (Nginx/Caddy)
|
||||||
|
# This ensures proper handling of X-Forwarded-* headers
|
||||||
|
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1)
|
||||||
|
|
||||||
# Initialize extensions
|
# Initialize extensions
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
bcrypt.init_app(app)
|
bcrypt.init_app(app)
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ from datetime import datetime
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from app.extensions import db, bcrypt
|
from app.extensions import db, bcrypt
|
||||||
from app.models import User, Player, Group, Content, ServerLog, Playlist
|
from app.models import User, Player, Group, Content, ServerLog, Playlist, HTTPSConfig
|
||||||
from app.utils.logger import log_action
|
from app.utils.logger import log_action
|
||||||
|
from app.utils.caddy_manager import CaddyConfigGenerator
|
||||||
|
from app.utils.nginx_config_reader import get_nginx_status
|
||||||
|
|
||||||
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
|
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
|
||||||
|
|
||||||
@@ -31,7 +33,6 @@ def admin_required(f):
|
|||||||
|
|
||||||
@admin_bp.route('/')
|
@admin_bp.route('/')
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
|
||||||
def admin_panel():
|
def admin_panel():
|
||||||
"""Display admin panel with system overview."""
|
"""Display admin panel with system overview."""
|
||||||
try:
|
try:
|
||||||
@@ -351,7 +352,6 @@ def system_info():
|
|||||||
|
|
||||||
@admin_bp.route('/leftover-media')
|
@admin_bp.route('/leftover-media')
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
|
||||||
def leftover_media():
|
def leftover_media():
|
||||||
"""Display leftover media files not assigned to any playlist."""
|
"""Display leftover media files not assigned to any playlist."""
|
||||||
from app.models.playlist import playlist_content
|
from app.models.playlist import playlist_content
|
||||||
@@ -374,12 +374,15 @@ def leftover_media():
|
|||||||
leftover_pdfs = [c for c in leftover_content if c.content_type == 'pdf']
|
leftover_pdfs = [c for c in leftover_content if c.content_type == 'pdf']
|
||||||
leftover_pptx = [c for c in leftover_content if c.content_type == 'pptx']
|
leftover_pptx = [c for c in leftover_content if c.content_type == 'pptx']
|
||||||
|
|
||||||
# Calculate storage
|
# Calculate storage (handle None values)
|
||||||
total_leftover_size = sum(c.file_size for c in leftover_content)
|
def safe_file_size(content_list):
|
||||||
images_size = sum(c.file_size for c in leftover_images)
|
return sum(c.file_size or 0 for c in content_list)
|
||||||
videos_size = sum(c.file_size for c in leftover_videos)
|
|
||||||
pdfs_size = sum(c.file_size for c in leftover_pdfs)
|
total_leftover_size = safe_file_size(leftover_content)
|
||||||
pptx_size = sum(c.file_size for c in leftover_pptx)
|
images_size = safe_file_size(leftover_images)
|
||||||
|
videos_size = safe_file_size(leftover_videos)
|
||||||
|
pdfs_size = safe_file_size(leftover_pdfs)
|
||||||
|
pptx_size = safe_file_size(leftover_pptx)
|
||||||
|
|
||||||
return render_template('admin/leftover_media.html',
|
return render_template('admin/leftover_media.html',
|
||||||
leftover_images=leftover_images,
|
leftover_images=leftover_images,
|
||||||
@@ -401,7 +404,6 @@ def leftover_media():
|
|||||||
|
|
||||||
@admin_bp.route('/delete-leftover-images', methods=['POST'])
|
@admin_bp.route('/delete-leftover-images', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
|
||||||
def delete_leftover_images():
|
def delete_leftover_images():
|
||||||
"""Delete all leftover images that are not part of any playlist"""
|
"""Delete all leftover images that are not part of any playlist"""
|
||||||
from app.models.playlist import playlist_content
|
from app.models.playlist import playlist_content
|
||||||
@@ -426,6 +428,16 @@ def delete_leftover_images():
|
|||||||
if os.path.exists(file_path):
|
if os.path.exists(file_path):
|
||||||
os.remove(file_path)
|
os.remove(file_path)
|
||||||
|
|
||||||
|
# Delete edited media archive folder if it exists
|
||||||
|
import shutil
|
||||||
|
edited_media_dir = os.path.join(current_app.config['UPLOAD_FOLDER'], 'edited_media', str(content.id))
|
||||||
|
if os.path.exists(edited_media_dir):
|
||||||
|
shutil.rmtree(edited_media_dir)
|
||||||
|
|
||||||
|
# Delete associated player edit records first
|
||||||
|
from app.models.player_edit import PlayerEdit
|
||||||
|
PlayerEdit.query.filter_by(content_id=content.id).delete()
|
||||||
|
|
||||||
# Delete database record
|
# Delete database record
|
||||||
db.session.delete(content)
|
db.session.delete(content)
|
||||||
deleted_count += 1
|
deleted_count += 1
|
||||||
@@ -447,7 +459,6 @@ def delete_leftover_images():
|
|||||||
|
|
||||||
@admin_bp.route('/delete-leftover-videos', methods=['POST'])
|
@admin_bp.route('/delete-leftover-videos', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
|
||||||
def delete_leftover_videos():
|
def delete_leftover_videos():
|
||||||
"""Delete all leftover videos that are not part of any playlist"""
|
"""Delete all leftover videos that are not part of any playlist"""
|
||||||
from app.models.playlist import playlist_content
|
from app.models.playlist import playlist_content
|
||||||
@@ -472,6 +483,16 @@ def delete_leftover_videos():
|
|||||||
if os.path.exists(file_path):
|
if os.path.exists(file_path):
|
||||||
os.remove(file_path)
|
os.remove(file_path)
|
||||||
|
|
||||||
|
# Delete edited media archive folder if it exists
|
||||||
|
import shutil
|
||||||
|
edited_media_dir = os.path.join(current_app.config['UPLOAD_FOLDER'], 'edited_media', str(content.id))
|
||||||
|
if os.path.exists(edited_media_dir):
|
||||||
|
shutil.rmtree(edited_media_dir)
|
||||||
|
|
||||||
|
# Delete associated player edit records first
|
||||||
|
from app.models.player_edit import PlayerEdit
|
||||||
|
PlayerEdit.query.filter_by(content_id=content.id).delete()
|
||||||
|
|
||||||
# Delete database record
|
# Delete database record
|
||||||
db.session.delete(content)
|
db.session.delete(content)
|
||||||
deleted_count += 1
|
deleted_count += 1
|
||||||
@@ -493,7 +514,6 @@ def delete_leftover_videos():
|
|||||||
|
|
||||||
@admin_bp.route('/delete-single-leftover/<int:content_id>', methods=['POST'])
|
@admin_bp.route('/delete-single-leftover/<int:content_id>', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
|
||||||
def delete_single_leftover(content_id):
|
def delete_single_leftover(content_id):
|
||||||
"""Delete a single leftover content file"""
|
"""Delete a single leftover content file"""
|
||||||
try:
|
try:
|
||||||
@@ -505,6 +525,16 @@ def delete_single_leftover(content_id):
|
|||||||
if os.path.exists(file_path):
|
if os.path.exists(file_path):
|
||||||
os.remove(file_path)
|
os.remove(file_path)
|
||||||
|
|
||||||
|
# Delete edited media archive folder if it exists
|
||||||
|
import shutil
|
||||||
|
edited_media_dir = os.path.join(current_app.config['UPLOAD_FOLDER'], 'edited_media', str(content.id))
|
||||||
|
if os.path.exists(edited_media_dir):
|
||||||
|
shutil.rmtree(edited_media_dir)
|
||||||
|
|
||||||
|
# Delete associated player edit records first
|
||||||
|
from app.models.player_edit import PlayerEdit
|
||||||
|
PlayerEdit.query.filter_by(content_id=content.id).delete()
|
||||||
|
|
||||||
# Delete database record
|
# Delete database record
|
||||||
db.session.delete(content)
|
db.session.delete(content)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@@ -742,3 +772,246 @@ def upload_login_logo():
|
|||||||
flash(f'Error uploading logo: {str(e)}', 'danger')
|
flash(f'Error uploading logo: {str(e)}', 'danger')
|
||||||
|
|
||||||
return redirect(url_for('admin.customize_logos'))
|
return redirect(url_for('admin.customize_logos'))
|
||||||
|
|
||||||
|
|
||||||
|
@admin_bp.route('/editing-users')
|
||||||
|
@login_required
|
||||||
|
def manage_editing_users():
|
||||||
|
"""Display and manage users that edit images on players."""
|
||||||
|
try:
|
||||||
|
from app.models.player_user import PlayerUser
|
||||||
|
from app.models.player_edit import PlayerEdit
|
||||||
|
|
||||||
|
# Get all editing users
|
||||||
|
users = PlayerUser.query.order_by(PlayerUser.created_at.desc()).all()
|
||||||
|
|
||||||
|
# Get edit counts for each user
|
||||||
|
user_stats = {}
|
||||||
|
for user in users:
|
||||||
|
edit_count = PlayerEdit.query.filter_by(user=user.user_code).count()
|
||||||
|
user_stats[user.user_code] = edit_count
|
||||||
|
|
||||||
|
return render_template('admin/editing_users.html',
|
||||||
|
users=users,
|
||||||
|
user_stats=user_stats)
|
||||||
|
except Exception as e:
|
||||||
|
log_action('error', f'Error loading editing users: {str(e)}')
|
||||||
|
flash('Error loading editing users.', 'danger')
|
||||||
|
return redirect(url_for('admin.admin_panel'))
|
||||||
|
|
||||||
|
|
||||||
|
@admin_bp.route('/editing-users/<int:user_id>/update', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def update_editing_user(user_id: int):
|
||||||
|
"""Update editing user name."""
|
||||||
|
try:
|
||||||
|
from app.models.player_user import PlayerUser
|
||||||
|
|
||||||
|
user = PlayerUser.query.get_or_404(user_id)
|
||||||
|
user_name = request.form.get('user_name', '').strip()
|
||||||
|
|
||||||
|
user.user_name = user_name if user_name else None
|
||||||
|
user.updated_at = datetime.utcnow()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
log_action('info', f'Updated editing user {user.user_code} name to: {user_name or "None"}')
|
||||||
|
flash('User name updated successfully!', 'success')
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
log_action('error', f'Error updating editing user: {str(e)}')
|
||||||
|
flash(f'Error updating user: {str(e)}', 'danger')
|
||||||
|
|
||||||
|
return redirect(url_for('admin.manage_editing_users'))
|
||||||
|
|
||||||
|
|
||||||
|
@admin_bp.route('/editing-users/<int:user_id>/delete', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def delete_editing_user(user_id: int):
|
||||||
|
"""Delete editing user."""
|
||||||
|
try:
|
||||||
|
from app.models.player_user import PlayerUser
|
||||||
|
|
||||||
|
user = PlayerUser.query.get_or_404(user_id)
|
||||||
|
user_code = user.user_code
|
||||||
|
|
||||||
|
db.session.delete(user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
log_action('info', f'Deleted editing user: {user_code}')
|
||||||
|
flash('User deleted successfully!', 'success')
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
log_action('error', f'Error deleting editing user: {str(e)}')
|
||||||
|
flash(f'Error deleting user: {str(e)}', 'danger')
|
||||||
|
|
||||||
|
return redirect(url_for('admin.manage_editing_users'))
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# HTTPS Configuration Management Routes
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@admin_bp.route('/https-config', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def https_config():
|
||||||
|
"""Display HTTPS configuration management page."""
|
||||||
|
try:
|
||||||
|
config = HTTPSConfig.get_config()
|
||||||
|
|
||||||
|
# Detect actual current HTTPS status
|
||||||
|
# Check if current connection is HTTPS
|
||||||
|
is_https_active = request.scheme == 'https' or request.headers.get('X-Forwarded-Proto') == 'https'
|
||||||
|
current_host = request.host.split(':')[0] # Remove port if present
|
||||||
|
|
||||||
|
# If HTTPS is active but database shows disabled, sync it
|
||||||
|
if is_https_active and config and not config.https_enabled:
|
||||||
|
# Update database to reflect actual HTTPS status
|
||||||
|
config.https_enabled = True
|
||||||
|
db.session.commit()
|
||||||
|
log_action('info', f'HTTPS status auto-corrected to enabled (detected from request)')
|
||||||
|
|
||||||
|
# Get Nginx configuration status
|
||||||
|
nginx_status = get_nginx_status()
|
||||||
|
|
||||||
|
return render_template('admin/https_config.html',
|
||||||
|
config=config,
|
||||||
|
is_https_active=is_https_active,
|
||||||
|
current_host=current_host,
|
||||||
|
nginx_status=nginx_status)
|
||||||
|
except Exception as e:
|
||||||
|
log_action('error', f'Error loading HTTPS config page: {str(e)}')
|
||||||
|
flash('Error loading HTTPS configuration page.', 'danger')
|
||||||
|
return redirect(url_for('admin.admin_panel'))
|
||||||
|
|
||||||
|
|
||||||
|
@admin_bp.route('/https-config/update', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def update_https_config():
|
||||||
|
"""Update HTTPS configuration."""
|
||||||
|
try:
|
||||||
|
https_enabled = request.form.get('https_enabled') == 'on'
|
||||||
|
hostname = request.form.get('hostname', '').strip()
|
||||||
|
domain = request.form.get('domain', '').strip()
|
||||||
|
ip_address = request.form.get('ip_address', '').strip()
|
||||||
|
email = request.form.get('email', '').strip()
|
||||||
|
port = request.form.get('port', '443').strip()
|
||||||
|
|
||||||
|
# Validation
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
if https_enabled:
|
||||||
|
if not hostname:
|
||||||
|
errors.append('Hostname is required when HTTPS is enabled.')
|
||||||
|
if not domain:
|
||||||
|
errors.append('Domain name is required when HTTPS is enabled.')
|
||||||
|
if not ip_address:
|
||||||
|
errors.append('IP address is required when HTTPS is enabled.')
|
||||||
|
if not email:
|
||||||
|
errors.append('Email address is required when HTTPS is enabled.')
|
||||||
|
|
||||||
|
# Validate domain format (basic)
|
||||||
|
if domain and '.' not in domain:
|
||||||
|
errors.append('Please enter a valid domain name (e.g., example.com).')
|
||||||
|
|
||||||
|
# Validate IP format (basic)
|
||||||
|
if ip_address:
|
||||||
|
ip_parts = ip_address.split('.')
|
||||||
|
if len(ip_parts) != 4:
|
||||||
|
errors.append('Please enter a valid IPv4 address (e.g., 10.76.152.164).')
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
for part in ip_parts:
|
||||||
|
num = int(part)
|
||||||
|
if num < 0 or num > 255:
|
||||||
|
raise ValueError()
|
||||||
|
except ValueError:
|
||||||
|
errors.append('Please enter a valid IPv4 address.')
|
||||||
|
|
||||||
|
# Validate email format (basic)
|
||||||
|
if email and '@' not in email:
|
||||||
|
errors.append('Please enter a valid email address.')
|
||||||
|
|
||||||
|
# Validate port
|
||||||
|
try:
|
||||||
|
port_num = int(port)
|
||||||
|
if port_num < 1 or port_num > 65535:
|
||||||
|
errors.append('Port must be between 1 and 65535.')
|
||||||
|
port = port_num
|
||||||
|
except ValueError:
|
||||||
|
errors.append('Port must be a valid number.')
|
||||||
|
else:
|
||||||
|
port = 443
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
for error in errors:
|
||||||
|
flash(error, 'warning')
|
||||||
|
return redirect(url_for('admin.https_config'))
|
||||||
|
|
||||||
|
# Update configuration
|
||||||
|
config = HTTPSConfig.create_or_update(
|
||||||
|
https_enabled=https_enabled,
|
||||||
|
hostname=hostname if https_enabled else None,
|
||||||
|
domain=domain if https_enabled else None,
|
||||||
|
ip_address=ip_address if https_enabled else None,
|
||||||
|
email=email if https_enabled else None,
|
||||||
|
port=port if https_enabled else 443,
|
||||||
|
updated_by=current_user.username
|
||||||
|
)
|
||||||
|
|
||||||
|
# Generate and update Caddyfile
|
||||||
|
try:
|
||||||
|
caddyfile_content = CaddyConfigGenerator.generate_caddyfile(config)
|
||||||
|
if CaddyConfigGenerator.write_caddyfile(caddyfile_content):
|
||||||
|
# Reload Caddy configuration
|
||||||
|
if CaddyConfigGenerator.reload_caddy():
|
||||||
|
caddy_status = '✅ Caddy configuration updated successfully!'
|
||||||
|
log_action('info', f'Caddy configuration reloaded by {current_user.username}')
|
||||||
|
else:
|
||||||
|
caddy_status = '⚠️ Caddyfile updated but reload failed. Please restart containers.'
|
||||||
|
log_action('warning', f'Caddy reload failed for {current_user.username}')
|
||||||
|
else:
|
||||||
|
caddy_status = '⚠️ Configuration saved but Caddyfile update failed.'
|
||||||
|
log_action('warning', f'Caddyfile write failed for {current_user.username}')
|
||||||
|
except Exception as caddy_error:
|
||||||
|
caddy_status = f'⚠️ Configuration saved but Caddy update failed: {str(caddy_error)}'
|
||||||
|
log_action('error', f'Caddy update error: {str(caddy_error)}')
|
||||||
|
|
||||||
|
if https_enabled:
|
||||||
|
log_action('info', f'HTTPS enabled by {current_user.username}: domain={domain}, hostname={hostname}, ip={ip_address}, email={email}')
|
||||||
|
flash(f'✅ HTTPS configuration saved successfully!\n{caddy_status}\nServer available at https://{domain}', 'success')
|
||||||
|
else:
|
||||||
|
log_action('info', f'HTTPS disabled by {current_user.username}')
|
||||||
|
flash(f'✅ HTTPS has been disabled. Server running on HTTP only.\n{caddy_status}', 'success')
|
||||||
|
|
||||||
|
return redirect(url_for('admin.https_config'))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
log_action('error', f'Error updating HTTPS config: {str(e)}')
|
||||||
|
flash(f'Error updating HTTPS configuration: {str(e)}', 'danger')
|
||||||
|
return redirect(url_for('admin.https_config'))
|
||||||
|
|
||||||
|
|
||||||
|
@admin_bp.route('/https-config/status')
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def https_config_status():
|
||||||
|
"""Get current HTTPS configuration status as JSON."""
|
||||||
|
try:
|
||||||
|
config = HTTPSConfig.get_config()
|
||||||
|
|
||||||
|
if config:
|
||||||
|
return jsonify(config.to_dict())
|
||||||
|
else:
|
||||||
|
return jsonify({
|
||||||
|
'https_enabled': False,
|
||||||
|
'hostname': None,
|
||||||
|
'domain': None,
|
||||||
|
'ip_address': None,
|
||||||
|
'port': 443,
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
log_action('error', f'Error getting HTTPS status: {str(e)}')
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ def health_check():
|
|||||||
|
|
||||||
|
|
||||||
@api_bp.route('/auth/player', methods=['POST'])
|
@api_bp.route('/auth/player', methods=['POST'])
|
||||||
@rate_limit(max_requests=10, window=60)
|
@rate_limit(max_requests=120, window=60)
|
||||||
def authenticate_player():
|
def authenticate_player():
|
||||||
"""Authenticate a player and return auth code and configuration.
|
"""Authenticate a player and return auth code and configuration.
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ def authenticate_player():
|
|||||||
|
|
||||||
|
|
||||||
@api_bp.route('/auth/verify', methods=['POST'])
|
@api_bp.route('/auth/verify', methods=['POST'])
|
||||||
@rate_limit(max_requests=30, window=60)
|
@rate_limit(max_requests=300, window=60)
|
||||||
def verify_auth_code():
|
def verify_auth_code():
|
||||||
"""Verify an auth code and return player information.
|
"""Verify an auth code and return player information.
|
||||||
|
|
||||||
@@ -293,7 +293,7 @@ def get_playlist_by_quickconnect():
|
|||||||
|
|
||||||
|
|
||||||
@api_bp.route('/playlists/<int:player_id>', methods=['GET'])
|
@api_bp.route('/playlists/<int:player_id>', methods=['GET'])
|
||||||
@rate_limit(max_requests=30, window=60)
|
@rate_limit(max_requests=300, window=60)
|
||||||
@verify_player_auth
|
@verify_player_auth
|
||||||
def get_player_playlist(player_id: int):
|
def get_player_playlist(player_id: int):
|
||||||
"""Get playlist for a specific player.
|
"""Get playlist for a specific player.
|
||||||
@@ -401,14 +401,15 @@ def get_cached_playlist(player_id: int) -> List[Dict]:
|
|||||||
'duration': content._playlist_duration or content.duration or 10,
|
'duration': content._playlist_duration or content.duration or 10,
|
||||||
'position': content._playlist_position or idx,
|
'position': content._playlist_position or idx,
|
||||||
'url': content_url, # Full URL for downloads
|
'url': content_url, # Full URL for downloads
|
||||||
'description': content.description
|
'description': content.description,
|
||||||
|
'edit_on_player': getattr(content, '_playlist_edit_on_player_enabled', False)
|
||||||
})
|
})
|
||||||
|
|
||||||
return playlist_data
|
return playlist_data
|
||||||
|
|
||||||
|
|
||||||
@api_bp.route('/player-feedback', methods=['POST'])
|
@api_bp.route('/player-feedback', methods=['POST'])
|
||||||
@rate_limit(max_requests=100, window=60)
|
@rate_limit(max_requests=600, window=60)
|
||||||
def receive_player_feedback():
|
def receive_player_feedback():
|
||||||
"""Receive feedback/status updates from players (Kivy player compatible).
|
"""Receive feedback/status updates from players (Kivy player compatible).
|
||||||
|
|
||||||
@@ -427,15 +428,13 @@ def receive_player_feedback():
|
|||||||
data = request.json
|
data = request.json
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
|
log_action('warning', 'Player feedback received with no data')
|
||||||
return jsonify({'error': 'No data provided'}), 400
|
return jsonify({'error': 'No data provided'}), 400
|
||||||
|
|
||||||
player_name = data.get('player_name')
|
player_name = data.get('player_name')
|
||||||
hostname = data.get('hostname') # Also accept hostname
|
hostname = data.get('hostname') # Also accept hostname
|
||||||
quickconnect_code = data.get('quickconnect_code')
|
quickconnect_code = data.get('quickconnect_code')
|
||||||
|
|
||||||
if (not player_name and not hostname) or not quickconnect_code:
|
|
||||||
return jsonify({'error': 'player_name/hostname and quickconnect_code required'}), 400
|
|
||||||
|
|
||||||
# Find player by hostname first (more reliable), then by name
|
# Find player by hostname first (more reliable), then by name
|
||||||
player = None
|
player = None
|
||||||
if hostname:
|
if hostname:
|
||||||
@@ -443,12 +442,35 @@ def receive_player_feedback():
|
|||||||
if not player and player_name:
|
if not player and player_name:
|
||||||
player = Player.query.filter_by(name=player_name).first()
|
player = Player.query.filter_by(name=player_name).first()
|
||||||
|
|
||||||
|
# If player not found and no credentials provided, try to infer from IP and recent auth
|
||||||
|
if not player and (not quickconnect_code or (not player_name and not hostname)):
|
||||||
|
# Try to find player by recent authentication from same IP
|
||||||
|
client_ip = request.remote_addr
|
||||||
|
# Look for players with matching IP in recent activity (last 5 minutes)
|
||||||
|
recent_time = datetime.utcnow() - timedelta(minutes=5)
|
||||||
|
possible_player = Player.query.filter(
|
||||||
|
Player.last_seen >= recent_time
|
||||||
|
).order_by(Player.last_seen.desc()).first()
|
||||||
|
|
||||||
|
if possible_player:
|
||||||
|
player = possible_player
|
||||||
|
log_action('info', f'Inferred player feedback from {player.name} ({player.hostname}) based on recent activity')
|
||||||
|
|
||||||
|
# Still require quickconnect validation if provided
|
||||||
|
if not player:
|
||||||
|
if not player_name and not hostname:
|
||||||
|
log_action('warning', f'Player feedback missing required fields. Data: {data}')
|
||||||
|
return jsonify({'error': 'player_name/hostname and quickconnect_code required'}), 400
|
||||||
|
else:
|
||||||
|
log_action('warning', f'Player feedback from unknown player: {player_name or hostname}')
|
||||||
|
return jsonify({'error': 'Player not found'}), 404
|
||||||
|
|
||||||
if not player:
|
if not player:
|
||||||
log_action('warning', f'Player feedback from unknown player: {player_name or hostname}')
|
log_action('warning', f'Player feedback from unknown player: {player_name or hostname}')
|
||||||
return jsonify({'error': 'Player not found'}), 404
|
return jsonify({'error': 'Player not found'}), 404
|
||||||
|
|
||||||
# Validate quickconnect code (using bcrypt verification)
|
# Validate quickconnect code if provided (using bcrypt verification)
|
||||||
if not player.check_quickconnect_code(quickconnect_code):
|
if quickconnect_code and not player.check_quickconnect_code(quickconnect_code):
|
||||||
log_action('warning', f'Invalid quickconnect in feedback from: {player.name} ({player.hostname})')
|
log_action('warning', f'Invalid quickconnect in feedback from: {player.name} ({player.hostname})')
|
||||||
return jsonify({'error': 'Invalid quickconnect code'}), 403
|
return jsonify({'error': 'Invalid quickconnect code'}), 403
|
||||||
|
|
||||||
@@ -678,6 +700,154 @@ def get_logs():
|
|||||||
return jsonify({'error': 'Internal server error'}), 500
|
return jsonify({'error': 'Internal server error'}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@api_bp.route('/player-edit-media', methods=['POST'])
|
||||||
|
@rate_limit(max_requests=60, window=60)
|
||||||
|
@verify_player_auth
|
||||||
|
def receive_edited_media():
|
||||||
|
"""Receive edited media from player.
|
||||||
|
|
||||||
|
Expected multipart/form-data:
|
||||||
|
- image_file: The edited image file
|
||||||
|
- metadata: JSON string with metadata
|
||||||
|
|
||||||
|
Metadata JSON structure:
|
||||||
|
{
|
||||||
|
"time_of_modification": "ISO timestamp",
|
||||||
|
"original_name": "original_file.jpg",
|
||||||
|
"new_name": "original_file_v1.jpg",
|
||||||
|
"version": 1,
|
||||||
|
"user": "player_user"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
player = request.player
|
||||||
|
|
||||||
|
# Check if file is present
|
||||||
|
if 'image_file' not in request.files:
|
||||||
|
return jsonify({'error': 'No image file provided'}), 400
|
||||||
|
|
||||||
|
file = request.files['image_file']
|
||||||
|
if file.filename == '':
|
||||||
|
return jsonify({'error': 'No file selected'}), 400
|
||||||
|
|
||||||
|
# Get metadata
|
||||||
|
import json
|
||||||
|
metadata_str = request.form.get('metadata')
|
||||||
|
if not metadata_str:
|
||||||
|
return jsonify({'error': 'No metadata provided'}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
metadata = json.loads(metadata_str)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return jsonify({'error': 'Invalid metadata JSON'}), 400
|
||||||
|
|
||||||
|
# Validate required metadata fields
|
||||||
|
required_fields = ['time_of_modification', 'original_name', 'new_name', 'version']
|
||||||
|
for field in required_fields:
|
||||||
|
if field not in metadata:
|
||||||
|
return jsonify({'error': f'Missing required field: {field}'}), 400
|
||||||
|
|
||||||
|
# Import required modules
|
||||||
|
import os
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
from app.models.player_edit import PlayerEdit
|
||||||
|
|
||||||
|
# Find the original content by filename
|
||||||
|
original_name = metadata['original_name']
|
||||||
|
content = Content.query.filter_by(filename=original_name).first()
|
||||||
|
|
||||||
|
if not content:
|
||||||
|
log_action('warning', f'Player {player.name} tried to edit non-existent content: {original_name}')
|
||||||
|
return jsonify({'error': f'Original content not found: {original_name}'}), 404
|
||||||
|
|
||||||
|
# Create versioned folder structure: edited_media/<content_id>/
|
||||||
|
base_upload_dir = os.path.join(current_app.root_path, 'static', 'uploads')
|
||||||
|
edited_media_dir = os.path.join(base_upload_dir, 'edited_media', str(content.id))
|
||||||
|
os.makedirs(edited_media_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Save the edited file with version suffix
|
||||||
|
version = metadata['version']
|
||||||
|
new_filename = metadata['new_name']
|
||||||
|
edited_file_path = os.path.join(edited_media_dir, new_filename)
|
||||||
|
file.save(edited_file_path)
|
||||||
|
|
||||||
|
# Save metadata JSON file
|
||||||
|
metadata_filename = f"{os.path.splitext(new_filename)[0]}_metadata.json"
|
||||||
|
metadata_path = os.path.join(edited_media_dir, metadata_filename)
|
||||||
|
with open(metadata_path, 'w') as f:
|
||||||
|
json.dump(metadata, f, indent=2)
|
||||||
|
|
||||||
|
# Update the content record to reference the edited version path
|
||||||
|
# Keep original filename unchanged, point to edited_media folder
|
||||||
|
old_filename = content.filename
|
||||||
|
content.filename = f"edited_media/{content.id}/{new_filename}"
|
||||||
|
|
||||||
|
# Create edit record
|
||||||
|
time_of_mod = None
|
||||||
|
if metadata.get('time_of_modification'):
|
||||||
|
try:
|
||||||
|
time_of_mod = datetime.fromisoformat(metadata['time_of_modification'].replace('Z', '+00:00'))
|
||||||
|
except:
|
||||||
|
time_of_mod = datetime.utcnow()
|
||||||
|
|
||||||
|
# Auto-create PlayerUser record if user code is provided
|
||||||
|
user_code = metadata.get('user_card_data')
|
||||||
|
log_action('debug', f'Metadata user code: {user_code}')
|
||||||
|
if user_code:
|
||||||
|
from app.models.player_user import PlayerUser
|
||||||
|
existing_user = PlayerUser.query.filter_by(user_code=user_code).first()
|
||||||
|
if not existing_user:
|
||||||
|
new_user = PlayerUser(user_code=user_code)
|
||||||
|
db.session.add(new_user)
|
||||||
|
log_action('info', f'Auto-created PlayerUser record for code: {user_code}')
|
||||||
|
else:
|
||||||
|
log_action('debug', f'PlayerUser already exists for code: {user_code}')
|
||||||
|
else:
|
||||||
|
log_action('debug', 'No user code in metadata')
|
||||||
|
|
||||||
|
edit_record = PlayerEdit(
|
||||||
|
player_id=player.id,
|
||||||
|
content_id=content.id,
|
||||||
|
original_name=original_name,
|
||||||
|
new_name=new_filename,
|
||||||
|
version=version,
|
||||||
|
user=user_code,
|
||||||
|
time_of_modification=time_of_mod,
|
||||||
|
metadata_path=metadata_path,
|
||||||
|
edited_file_path=edited_file_path
|
||||||
|
)
|
||||||
|
db.session.add(edit_record)
|
||||||
|
|
||||||
|
# Update playlist version to force player refresh
|
||||||
|
if player.playlist_id:
|
||||||
|
from app.models.playlist import Playlist
|
||||||
|
playlist = db.session.get(Playlist, player.playlist_id)
|
||||||
|
if playlist:
|
||||||
|
playlist.version += 1
|
||||||
|
|
||||||
|
# Clear playlist cache
|
||||||
|
cache.delete_memoized(get_cached_playlist, player.id)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
log_action('info', f'Player {player.name} uploaded edited media: {old_filename} -> {new_filename} (v{version})')
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'message': 'Edited media received and processed',
|
||||||
|
'edit_id': edit_record.id,
|
||||||
|
'version': version,
|
||||||
|
'old_filename': old_filename,
|
||||||
|
'new_filename': new_filename,
|
||||||
|
'new_playlist_version': playlist.version if player.playlist_id and playlist else None
|
||||||
|
}), 200
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
log_action('error', f'Error receiving edited media: {str(e)}')
|
||||||
|
return jsonify({'error': 'Internal server error'}), 500
|
||||||
|
|
||||||
|
|
||||||
@api_bp.errorhandler(404)
|
@api_bp.errorhandler(404)
|
||||||
def api_not_found(error):
|
def api_not_found(error):
|
||||||
"""Handle 404 errors in API."""
|
"""Handle 404 errors in API."""
|
||||||
|
|||||||
@@ -39,8 +39,14 @@ def content_list():
|
|||||||
@login_required
|
@login_required
|
||||||
def media_library():
|
def media_library():
|
||||||
"""View all media files in the library."""
|
"""View all media files in the library."""
|
||||||
|
from app.models.player_edit import PlayerEdit
|
||||||
|
|
||||||
media_files = Content.query.order_by(Content.uploaded_at.desc()).all()
|
media_files = Content.query.order_by(Content.uploaded_at.desc()).all()
|
||||||
|
|
||||||
|
# Add edit count to each media item
|
||||||
|
for media in media_files:
|
||||||
|
media.edit_count = PlayerEdit.query.filter_by(content_id=media.id).count()
|
||||||
|
|
||||||
# Group by content type
|
# Group by content type
|
||||||
images = [m for m in media_files if m.content_type == 'image']
|
images = [m for m in media_files if m.content_type == 'image']
|
||||||
videos = [m for m in media_files if m.content_type == 'video']
|
videos = [m for m in media_files if m.content_type == 'video']
|
||||||
@@ -74,6 +80,21 @@ def delete_media(media_id: int):
|
|||||||
os.remove(file_path)
|
os.remove(file_path)
|
||||||
log_action('info', f'Deleted physical file: {filename}')
|
log_action('info', f'Deleted physical file: {filename}')
|
||||||
|
|
||||||
|
# Delete edited media archive folder if it exists
|
||||||
|
import shutil
|
||||||
|
edited_media_dir = os.path.join(current_app.config['UPLOAD_FOLDER'], 'edited_media', str(media.id))
|
||||||
|
if os.path.exists(edited_media_dir):
|
||||||
|
shutil.rmtree(edited_media_dir)
|
||||||
|
log_action('info', f'Deleted edited media archive for content ID {media.id}')
|
||||||
|
|
||||||
|
# Delete associated player edit records first
|
||||||
|
from app.models.player_edit import PlayerEdit
|
||||||
|
edit_records = PlayerEdit.query.filter_by(content_id=media.id).all()
|
||||||
|
if edit_records:
|
||||||
|
for edit in edit_records:
|
||||||
|
db.session.delete(edit)
|
||||||
|
log_action('info', f'Deleted {len(edit_records)} edit record(s) for content: {filename}')
|
||||||
|
|
||||||
# Remove from all playlists (this will cascade properly)
|
# Remove from all playlists (this will cascade properly)
|
||||||
db.session.delete(media)
|
db.session.delete(media)
|
||||||
|
|
||||||
@@ -396,6 +417,47 @@ def update_playlist_content_muted(playlist_id: int, content_id: int):
|
|||||||
return jsonify({'success': False, 'message': str(e)}), 500
|
return jsonify({'success': False, 'message': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@content_bp.route('/playlist/<int:playlist_id>/update-edit-enabled/<int:content_id>', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def update_playlist_content_edit_enabled(playlist_id: int, content_id: int):
|
||||||
|
"""Update content edit_on_player_enabled setting in playlist."""
|
||||||
|
playlist = Playlist.query.get_or_404(playlist_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
content = Content.query.get_or_404(content_id)
|
||||||
|
edit_enabled = request.form.get('edit_enabled', 'false').lower() == 'true'
|
||||||
|
|
||||||
|
from app.models.playlist import playlist_content
|
||||||
|
from sqlalchemy import update
|
||||||
|
|
||||||
|
# Update edit_on_player_enabled in association table
|
||||||
|
stmt = update(playlist_content).where(
|
||||||
|
(playlist_content.c.playlist_id == playlist_id) &
|
||||||
|
(playlist_content.c.content_id == content_id)
|
||||||
|
).values(edit_on_player_enabled=edit_enabled)
|
||||||
|
db.session.execute(stmt)
|
||||||
|
|
||||||
|
# Increment playlist version
|
||||||
|
playlist.increment_version()
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
cache.clear()
|
||||||
|
|
||||||
|
log_action('info', f'Updated edit_on_player_enabled={edit_enabled} for "{content.filename}" in playlist "{playlist.name}"')
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'message': 'Edit setting updated',
|
||||||
|
'edit_enabled': edit_enabled,
|
||||||
|
'version': playlist.version
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
log_action('error', f'Error updating edit setting: {str(e)}')
|
||||||
|
return jsonify({'success': False, 'message': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
@content_bp.route('/upload-media-page')
|
@content_bp.route('/upload-media-page')
|
||||||
@login_required
|
@login_required
|
||||||
def upload_media_page():
|
def upload_media_page():
|
||||||
@@ -527,7 +589,7 @@ def process_pdf_file(filepath: str, filename: str) -> tuple[bool, str]:
|
|||||||
|
|
||||||
|
|
||||||
def process_file_in_background(app, filepath: str, filename: str, file_ext: str,
|
def process_file_in_background(app, filepath: str, filename: str, file_ext: str,
|
||||||
duration: int, playlist_id: Optional[int], task_id: str):
|
duration: int, playlist_id: Optional[int], task_id: str, edit_on_player_enabled: bool = False):
|
||||||
"""Process large files (PDF, PPTX, Video) in background thread."""
|
"""Process large files (PDF, PPTX, Video) in background thread."""
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
try:
|
try:
|
||||||
@@ -573,7 +635,8 @@ def process_file_in_background(app, filepath: str, filename: str, file_ext: str,
|
|||||||
playlist_id=playlist_id,
|
playlist_id=playlist_id,
|
||||||
content_id=page_content.id,
|
content_id=page_content.id,
|
||||||
position=max_position,
|
position=max_position,
|
||||||
duration=duration
|
duration=duration,
|
||||||
|
edit_on_player_enabled=edit_on_player_enabled
|
||||||
)
|
)
|
||||||
db.session.execute(stmt)
|
db.session.execute(stmt)
|
||||||
|
|
||||||
@@ -629,7 +692,8 @@ def process_file_in_background(app, filepath: str, filename: str, file_ext: str,
|
|||||||
playlist_id=playlist_id,
|
playlist_id=playlist_id,
|
||||||
content_id=slide_content.id,
|
content_id=slide_content.id,
|
||||||
position=max_position,
|
position=max_position,
|
||||||
duration=duration
|
duration=duration,
|
||||||
|
edit_on_player_enabled=edit_on_player_enabled
|
||||||
)
|
)
|
||||||
db.session.execute(stmt)
|
db.session.execute(stmt)
|
||||||
|
|
||||||
@@ -677,7 +741,8 @@ def process_file_in_background(app, filepath: str, filename: str, file_ext: str,
|
|||||||
playlist_id=playlist_id,
|
playlist_id=playlist_id,
|
||||||
content_id=content.id,
|
content_id=content.id,
|
||||||
position=max_position + 1,
|
position=max_position + 1,
|
||||||
duration=duration
|
duration=duration,
|
||||||
|
edit_on_player_enabled=edit_on_player_enabled
|
||||||
)
|
)
|
||||||
db.session.execute(stmt)
|
db.session.execute(stmt)
|
||||||
playlist.version += 1
|
playlist.version += 1
|
||||||
@@ -949,6 +1014,7 @@ def upload_media():
|
|||||||
content_type = request.form.get('content_type', 'image')
|
content_type = request.form.get('content_type', 'image')
|
||||||
duration = request.form.get('duration', type=int, default=10)
|
duration = request.form.get('duration', type=int, default=10)
|
||||||
playlist_id = request.form.get('playlist_id', type=int)
|
playlist_id = request.form.get('playlist_id', type=int)
|
||||||
|
edit_on_player_enabled = request.form.get('edit_on_player_enabled', '0') == '1'
|
||||||
|
|
||||||
if not files or files[0].filename == '':
|
if not files or files[0].filename == '':
|
||||||
flash('No files provided.', 'warning')
|
flash('No files provided.', 'warning')
|
||||||
@@ -991,7 +1057,7 @@ def upload_media():
|
|||||||
|
|
||||||
thread = threading.Thread(
|
thread = threading.Thread(
|
||||||
target=process_file_in_background,
|
target=process_file_in_background,
|
||||||
args=(current_app._get_current_object(), filepath, filename, file_ext, duration, playlist_id, task_id)
|
args=(current_app._get_current_object(), filepath, filename, file_ext, duration, playlist_id, task_id, edit_on_player_enabled)
|
||||||
)
|
)
|
||||||
thread.daemon = True
|
thread.daemon = True
|
||||||
thread.start()
|
thread.start()
|
||||||
@@ -1054,7 +1120,8 @@ def upload_media():
|
|||||||
playlist_id=playlist_id,
|
playlist_id=playlist_id,
|
||||||
content_id=page_content.id,
|
content_id=page_content.id,
|
||||||
position=max_position,
|
position=max_position,
|
||||||
duration=duration
|
duration=duration,
|
||||||
|
edit_on_player_enabled=edit_on_player_enabled
|
||||||
)
|
)
|
||||||
db.session.execute(stmt)
|
db.session.execute(stmt)
|
||||||
|
|
||||||
@@ -1113,7 +1180,8 @@ def upload_media():
|
|||||||
playlist_id=playlist_id,
|
playlist_id=playlist_id,
|
||||||
content_id=slide_content.id,
|
content_id=slide_content.id,
|
||||||
position=max_position,
|
position=max_position,
|
||||||
duration=duration
|
duration=duration,
|
||||||
|
edit_on_player_enabled=edit_on_player_enabled
|
||||||
)
|
)
|
||||||
db.session.execute(stmt)
|
db.session.execute(stmt)
|
||||||
|
|
||||||
@@ -1165,7 +1233,8 @@ def upload_media():
|
|||||||
playlist_id=playlist_id,
|
playlist_id=playlist_id,
|
||||||
content_id=content.id,
|
content_id=content.id,
|
||||||
position=max_position + 1,
|
position=max_position + 1,
|
||||||
duration=duration
|
duration=duration,
|
||||||
|
edit_on_player_enabled=edit_on_player_enabled
|
||||||
)
|
)
|
||||||
db.session.execute(stmt)
|
db.session.execute(stmt)
|
||||||
|
|
||||||
|
|||||||
@@ -315,6 +315,13 @@ def manage_player(player_id: int):
|
|||||||
.limit(20)\
|
.limit(20)\
|
||||||
.all()
|
.all()
|
||||||
|
|
||||||
|
# Get edited media history from player
|
||||||
|
from app.models.player_edit import PlayerEdit
|
||||||
|
edited_media = PlayerEdit.query.filter_by(player_id=player_id)\
|
||||||
|
.order_by(PlayerEdit.created_at.desc())\
|
||||||
|
.limit(20)\
|
||||||
|
.all()
|
||||||
|
|
||||||
# Get player status
|
# Get player status
|
||||||
status_info = get_player_status_info(player_id)
|
status_info = get_player_status_info(player_id)
|
||||||
|
|
||||||
@@ -323,9 +330,54 @@ def manage_player(player_id: int):
|
|||||||
playlists=playlists,
|
playlists=playlists,
|
||||||
current_playlist=current_playlist,
|
current_playlist=current_playlist,
|
||||||
recent_logs=recent_logs,
|
recent_logs=recent_logs,
|
||||||
|
edited_media=edited_media,
|
||||||
status_info=status_info)
|
status_info=status_info)
|
||||||
|
|
||||||
|
|
||||||
|
@players_bp.route('/<int:player_id>/edited-media')
|
||||||
|
@login_required
|
||||||
|
def edited_media(player_id: int):
|
||||||
|
"""Display all edited media files from this player."""
|
||||||
|
try:
|
||||||
|
player = Player.query.get_or_404(player_id)
|
||||||
|
|
||||||
|
# Get all edited media history from player
|
||||||
|
from app.models.player_edit import PlayerEdit
|
||||||
|
from app.models.player_user import PlayerUser
|
||||||
|
|
||||||
|
edited_media = PlayerEdit.query.filter_by(player_id=player_id)\
|
||||||
|
.order_by(PlayerEdit.created_at.desc())\
|
||||||
|
.all()
|
||||||
|
|
||||||
|
# Get original content files for each edited media
|
||||||
|
content_files = {}
|
||||||
|
for edit in edited_media:
|
||||||
|
if edit.content_id not in content_files:
|
||||||
|
content = Content.query.get(edit.content_id)
|
||||||
|
if content:
|
||||||
|
content_files[edit.content_id] = content
|
||||||
|
|
||||||
|
# Get user mappings for display names
|
||||||
|
user_mappings = {}
|
||||||
|
for edit in edited_media:
|
||||||
|
if edit.user and edit.user not in user_mappings:
|
||||||
|
player_user = PlayerUser.query.filter_by(user_code=edit.user).first()
|
||||||
|
if player_user:
|
||||||
|
user_mappings[edit.user] = player_user.user_name or edit.user
|
||||||
|
else:
|
||||||
|
user_mappings[edit.user] = edit.user
|
||||||
|
|
||||||
|
return render_template('players/edited_media.html',
|
||||||
|
player=player,
|
||||||
|
edited_media=edited_media,
|
||||||
|
content_files=content_files,
|
||||||
|
user_mappings=user_mappings)
|
||||||
|
except Exception as e:
|
||||||
|
log_action('error', f'Error loading edited media for player {player_id}: {str(e)}')
|
||||||
|
flash('Error loading edited media.', 'danger')
|
||||||
|
return redirect(url_for('players.manage_player', player_id=player_id))
|
||||||
|
|
||||||
|
|
||||||
@players_bp.route('/<int:player_id>/fullscreen')
|
@players_bp.route('/<int:player_id>/fullscreen')
|
||||||
def player_fullscreen(player_id: int):
|
def player_fullscreen(player_id: int):
|
||||||
"""Display player fullscreen view (no authentication required for players)."""
|
"""Display player fullscreen view (no authentication required for players)."""
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ class Config:
|
|||||||
SESSION_COOKIE_HTTPONLY = True
|
SESSION_COOKIE_HTTPONLY = True
|
||||||
SESSION_COOKIE_SAMESITE = 'Lax'
|
SESSION_COOKIE_SAMESITE = 'Lax'
|
||||||
|
|
||||||
|
# Reverse proxy trust (for Nginx/Caddy with ProxyFix middleware)
|
||||||
|
# These are set by werkzeug.middleware.proxy_fix
|
||||||
|
TRUSTED_PROXIES = os.getenv('TRUSTED_PROXIES', '127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16')
|
||||||
|
PREFERRED_URL_SCHEME = os.getenv('PREFERRED_URL_SCHEME', 'https')
|
||||||
|
|
||||||
# Cache
|
# Cache
|
||||||
SEND_FILE_MAX_AGE_DEFAULT = 300 # 5 minutes for static files
|
SEND_FILE_MAX_AGE_DEFAULT = 300 # 5 minutes for static files
|
||||||
|
|
||||||
@@ -80,19 +85,13 @@ class ProductionConfig(Config):
|
|||||||
f'sqlite:///{os.path.join(_basedir, "instance", "dashboard.db")}'
|
f'sqlite:///{os.path.join(_basedir, "instance", "dashboard.db")}'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Redis Cache
|
# Cache - use simple cache instead of Redis
|
||||||
CACHE_TYPE = 'redis'
|
CACHE_TYPE = 'simple'
|
||||||
CACHE_REDIS_HOST = os.getenv('REDIS_HOST', 'redis')
|
|
||||||
CACHE_REDIS_PORT = int(os.getenv('REDIS_PORT', 6379))
|
|
||||||
CACHE_REDIS_DB = 0
|
|
||||||
CACHE_DEFAULT_TIMEOUT = 300
|
CACHE_DEFAULT_TIMEOUT = 300
|
||||||
|
|
||||||
# Security
|
# Security
|
||||||
SESSION_COOKIE_SECURE = True
|
SESSION_COOKIE_SECURE = True
|
||||||
WTF_CSRF_ENABLED = True
|
WTF_CSRF_ENABLED = True
|
||||||
|
|
||||||
# Rate Limiting
|
|
||||||
RATELIMIT_STORAGE_URL = f"redis://{os.getenv('REDIS_HOST', 'redis')}:6379/1"
|
|
||||||
|
|
||||||
|
|
||||||
class TestingConfig(Config):
|
class TestingConfig(Config):
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ from app.models.playlist import Playlist, playlist_content
|
|||||||
from app.models.content import Content
|
from app.models.content import Content
|
||||||
from app.models.server_log import ServerLog
|
from app.models.server_log import ServerLog
|
||||||
from app.models.player_feedback import PlayerFeedback
|
from app.models.player_feedback import PlayerFeedback
|
||||||
|
from app.models.player_edit import PlayerEdit
|
||||||
|
from app.models.player_user import PlayerUser
|
||||||
|
from app.models.https_config import HTTPSConfig
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'User',
|
'User',
|
||||||
@@ -15,6 +18,9 @@ __all__ = [
|
|||||||
'Content',
|
'Content',
|
||||||
'ServerLog',
|
'ServerLog',
|
||||||
'PlayerFeedback',
|
'PlayerFeedback',
|
||||||
|
'PlayerEdit',
|
||||||
|
'PlayerUser',
|
||||||
|
'HTTPSConfig',
|
||||||
'group_content',
|
'group_content',
|
||||||
'playlist_content',
|
'playlist_content',
|
||||||
]
|
]
|
||||||
|
|||||||
104
app/models/https_config.py
Normal file
104
app/models/https_config.py
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
"""HTTPS Configuration model."""
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from app.extensions import db
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPSConfig(db.Model):
|
||||||
|
"""HTTPS configuration model for managing secure connections.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
id: Primary key
|
||||||
|
https_enabled: Whether HTTPS is enabled
|
||||||
|
hostname: Server hostname (e.g., 'digiserver')
|
||||||
|
domain: Full domain name (e.g., 'digiserver.sibiusb.harting.intra')
|
||||||
|
ip_address: IP address for direct access
|
||||||
|
email: Email address for SSL certificate notifications
|
||||||
|
port: HTTPS port (default 443)
|
||||||
|
created_at: Creation timestamp
|
||||||
|
updated_at: Last update timestamp
|
||||||
|
updated_by: User who made the last update
|
||||||
|
"""
|
||||||
|
__tablename__ = 'https_config'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
https_enabled = db.Column(db.Boolean, default=False, nullable=False)
|
||||||
|
hostname = db.Column(db.String(255), nullable=True)
|
||||||
|
domain = db.Column(db.String(255), nullable=True)
|
||||||
|
ip_address = db.Column(db.String(45), nullable=True) # Support IPv6
|
||||||
|
email = db.Column(db.String(255), nullable=True)
|
||||||
|
port = db.Column(db.Integer, default=443, nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow,
|
||||||
|
onupdate=datetime.utcnow, nullable=False)
|
||||||
|
updated_by = db.Column(db.String(255), nullable=True)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
"""String representation of HTTPSConfig."""
|
||||||
|
status = 'ENABLED' if self.https_enabled else 'DISABLED'
|
||||||
|
return f'<HTTPSConfig [{status}] {self.domain or "N/A"}>'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_config(cls) -> Optional['HTTPSConfig']:
|
||||||
|
"""Get the current HTTPS configuration.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HTTPSConfig instance or None if not configured
|
||||||
|
"""
|
||||||
|
return cls.query.first()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_or_update(cls, https_enabled: bool, hostname: str = None,
|
||||||
|
domain: str = None, ip_address: str = None,
|
||||||
|
email: str = None, port: int = 443,
|
||||||
|
updated_by: str = None) -> 'HTTPSConfig':
|
||||||
|
"""Create or update HTTPS configuration.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
https_enabled: Whether HTTPS is enabled
|
||||||
|
hostname: Server hostname
|
||||||
|
domain: Full domain name
|
||||||
|
ip_address: IP address
|
||||||
|
email: Email for SSL certificates
|
||||||
|
port: HTTPS port
|
||||||
|
updated_by: Username of who made the update
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HTTPSConfig instance
|
||||||
|
"""
|
||||||
|
config = cls.get_config()
|
||||||
|
if not config:
|
||||||
|
config = cls()
|
||||||
|
|
||||||
|
config.https_enabled = https_enabled
|
||||||
|
config.hostname = hostname
|
||||||
|
config.domain = domain
|
||||||
|
config.ip_address = ip_address
|
||||||
|
config.email = email
|
||||||
|
config.port = port
|
||||||
|
config.updated_by = updated_by
|
||||||
|
config.updated_at = datetime.utcnow()
|
||||||
|
|
||||||
|
db.session.add(config)
|
||||||
|
db.session.commit()
|
||||||
|
return config
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
"""Convert configuration to dictionary.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary representation of config
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'https_enabled': self.https_enabled,
|
||||||
|
'hostname': self.hostname,
|
||||||
|
'domain': self.domain,
|
||||||
|
'ip_address': self.ip_address,
|
||||||
|
'email': self.email,
|
||||||
|
'port': self.port,
|
||||||
|
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||||
|
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
||||||
|
'updated_by': self.updated_by,
|
||||||
|
}
|
||||||
60
app/models/player_edit.py
Normal file
60
app/models/player_edit.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
"""Player edit model for tracking media edited on players."""
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from app.extensions import db
|
||||||
|
|
||||||
|
|
||||||
|
class PlayerEdit(db.Model):
|
||||||
|
"""Player edit model for tracking media files edited on player devices.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
id: Primary key
|
||||||
|
player_id: Foreign key to player
|
||||||
|
content_id: Foreign key to content that was edited
|
||||||
|
original_name: Original filename
|
||||||
|
new_name: New filename after editing
|
||||||
|
version: Edit version number (v1, v2, etc.)
|
||||||
|
user: User who made the edit (from player)
|
||||||
|
time_of_modification: When the edit was made
|
||||||
|
metadata_path: Path to the metadata JSON file
|
||||||
|
edited_file_path: Path to the edited file
|
||||||
|
created_at: Record creation timestamp
|
||||||
|
"""
|
||||||
|
__tablename__ = 'player_edit'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
player_id = db.Column(db.Integer, db.ForeignKey('player.id', ondelete='CASCADE'), nullable=False, index=True)
|
||||||
|
content_id = db.Column(db.Integer, db.ForeignKey('content.id', ondelete='CASCADE'), nullable=False, index=True)
|
||||||
|
original_name = db.Column(db.String(255), nullable=False)
|
||||||
|
new_name = db.Column(db.String(255), nullable=False)
|
||||||
|
version = db.Column(db.Integer, default=1, nullable=False)
|
||||||
|
user = db.Column(db.String(255), nullable=True)
|
||||||
|
time_of_modification = db.Column(db.DateTime, nullable=True)
|
||||||
|
metadata_path = db.Column(db.String(512), nullable=True)
|
||||||
|
edited_file_path = db.Column(db.String(512), nullable=False)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False, index=True)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
player = db.relationship('Player', backref=db.backref('edits', lazy='dynamic', cascade='all, delete-orphan'))
|
||||||
|
content = db.relationship('Content', backref=db.backref('edits', lazy='dynamic', cascade='all, delete-orphan'))
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
"""String representation of PlayerEdit."""
|
||||||
|
return f'<PlayerEdit {self.original_name} v{self.version} by {self.user or "unknown"}>'
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
"""Convert to dictionary for API responses."""
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'player_id': self.player_id,
|
||||||
|
'player_name': self.player.name if self.player else None,
|
||||||
|
'content_id': self.content_id,
|
||||||
|
'original_name': self.original_name,
|
||||||
|
'new_name': self.new_name,
|
||||||
|
'version': self.version,
|
||||||
|
'user': self.user,
|
||||||
|
'time_of_modification': self.time_of_modification.isoformat() if self.time_of_modification else None,
|
||||||
|
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||||
|
'edited_file_path': self.edited_file_path
|
||||||
|
}
|
||||||
37
app/models/player_user.py
Normal file
37
app/models/player_user.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
"""Player user model for managing user codes and names."""
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from app.extensions import db
|
||||||
|
|
||||||
|
|
||||||
|
class PlayerUser(db.Model):
|
||||||
|
"""Player user model for managing user codes and names globally.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
id: Primary key
|
||||||
|
user_code: User code received from player (unique)
|
||||||
|
user_name: Display name for the user
|
||||||
|
created_at: Record creation timestamp
|
||||||
|
updated_at: Record update timestamp
|
||||||
|
"""
|
||||||
|
__tablename__ = 'player_user'
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
user_code = db.Column(db.String(255), nullable=False, unique=True, index=True)
|
||||||
|
user_name = db.Column(db.String(255), nullable=True)
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
"""String representation of PlayerUser."""
|
||||||
|
return f'<PlayerUser {self.user_code} -> {self.user_name or "Unnamed"}>'
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
"""Convert to dictionary for API responses."""
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'user_code': self.user_code,
|
||||||
|
'user_name': self.user_name,
|
||||||
|
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||||
|
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
||||||
|
}
|
||||||
@@ -11,7 +11,8 @@ playlist_content = db.Table('playlist_content',
|
|||||||
db.Column('content_id', db.Integer, db.ForeignKey('content.id', ondelete='CASCADE'), primary_key=True),
|
db.Column('content_id', db.Integer, db.ForeignKey('content.id', ondelete='CASCADE'), primary_key=True),
|
||||||
db.Column('position', db.Integer, default=0),
|
db.Column('position', db.Integer, default=0),
|
||||||
db.Column('duration', db.Integer, default=10),
|
db.Column('duration', db.Integer, default=10),
|
||||||
db.Column('muted', db.Boolean, default=True)
|
db.Column('muted', db.Boolean, default=True),
|
||||||
|
db.Column('edit_on_player_enabled', db.Boolean, default=False)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -78,7 +79,8 @@ class Playlist(db.Model):
|
|||||||
stmt = select(playlist_content.c.content_id,
|
stmt = select(playlist_content.c.content_id,
|
||||||
playlist_content.c.position,
|
playlist_content.c.position,
|
||||||
playlist_content.c.duration,
|
playlist_content.c.duration,
|
||||||
playlist_content.c.muted).where(
|
playlist_content.c.muted,
|
||||||
|
playlist_content.c.edit_on_player_enabled).where(
|
||||||
playlist_content.c.playlist_id == self.id
|
playlist_content.c.playlist_id == self.id
|
||||||
).order_by(playlist_content.c.position)
|
).order_by(playlist_content.c.position)
|
||||||
|
|
||||||
@@ -91,6 +93,7 @@ class Playlist(db.Model):
|
|||||||
content._playlist_position = row.position
|
content._playlist_position = row.position
|
||||||
content._playlist_duration = row.duration
|
content._playlist_duration = row.duration
|
||||||
content._playlist_muted = row.muted if len(row) > 3 else True
|
content._playlist_muted = row.muted if len(row) > 3 else True
|
||||||
|
content._playlist_edit_on_player_enabled = row.edit_on_player_enabled if len(row) > 4 else False
|
||||||
ordered_content.append(content)
|
ordered_content.append(content)
|
||||||
|
|
||||||
return ordered_content
|
return ordered_content
|
||||||
|
|||||||
@@ -48,7 +48,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- User Management Card -->
|
{% if current_user.is_admin %}
|
||||||
|
<!-- User Management Card (Admin Only) -->
|
||||||
<div class="card management-card">
|
<div class="card management-card">
|
||||||
<h2>👥 User Management</h2>
|
<h2>👥 User Management</h2>
|
||||||
<p>Manage application users, roles and permissions</p>
|
<p>Manage application users, roles and permissions</p>
|
||||||
@@ -58,6 +59,18 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Editing Users Card -->
|
||||||
|
<div class="card management-card">
|
||||||
|
<h2>✏️ Editing Users</h2>
|
||||||
|
<p>Manage user codes from players that edit images on-screen</p>
|
||||||
|
<div class="card-actions">
|
||||||
|
<a href="{{ url_for('admin.manage_editing_users') }}" class="btn btn-primary">
|
||||||
|
Manage Editing Users
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Leftover Media Management Card -->
|
<!-- Leftover Media Management Card -->
|
||||||
<div class="card management-card">
|
<div class="card management-card">
|
||||||
@@ -70,7 +83,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- System Dependencies Card -->
|
{% if current_user.is_admin %}
|
||||||
|
<!-- System Dependencies Card (Admin Only) -->
|
||||||
<div class="card management-card" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
|
<div class="card management-card" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
|
||||||
<h2>🔧 System Dependencies</h2>
|
<h2>🔧 System Dependencies</h2>
|
||||||
<p>Check and install required software dependencies</p>
|
<p>Check and install required software dependencies</p>
|
||||||
@@ -81,7 +95,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Logo Customization Card -->
|
<!-- Logo Customization Card (Admin Only) -->
|
||||||
<div class="card management-card" style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);">
|
<div class="card management-card" style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);">
|
||||||
<h2>🎨 Logo Customization</h2>
|
<h2>🎨 Logo Customization</h2>
|
||||||
<p>Upload custom logos for header and login page</p>
|
<p>Upload custom logos for header and login page</p>
|
||||||
@@ -92,6 +106,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- HTTPS Configuration Card (Admin Only) -->
|
||||||
|
<div class="card management-card" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
|
||||||
|
<h2>🔒 HTTPS Configuration</h2>
|
||||||
|
<p>Manage SSL/HTTPS settings, domain, and access points</p>
|
||||||
|
<div class="card-actions">
|
||||||
|
<a href="{{ url_for('admin.https_config') }}" class="btn btn-primary">
|
||||||
|
Configure HTTPS
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Quick Actions Card -->
|
<!-- Quick Actions Card -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>⚡ Quick Actions</h2>
|
<h2>⚡ Quick Actions</h2>
|
||||||
|
|||||||
105
app/templates/admin/editing_users.html
Normal file
105
app/templates/admin/editing_users.html
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Manage Editing Users{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div style="margin-bottom: 2rem;">
|
||||||
|
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem;">
|
||||||
|
<div style="display: flex; align-items: center; gap: 1rem;">
|
||||||
|
<a href="{{ url_for('admin.admin_panel') }}"
|
||||||
|
class="btn"
|
||||||
|
style="background: #6c757d; color: white; padding: 0.5rem 1rem; text-decoration: none; border-radius: 6px;">
|
||||||
|
← Back to Admin
|
||||||
|
</a>
|
||||||
|
<h1 style="margin: 0;">👤 Manage Editing Users</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p style="color: #6c757d;">Manage users who edit images on players. User codes are automatically created from player metadata.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if users %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 style="margin: 0;">Editing Users ({{ users|length }})</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body" style="padding: 0;">
|
||||||
|
<table class="table" style="margin: 0;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 30%;">User Code</th>
|
||||||
|
<th style="width: 30%;">Display Name</th>
|
||||||
|
<th style="width: 15%;">Edits Count</th>
|
||||||
|
<th style="width: 15%;">Created</th>
|
||||||
|
<th style="width: 10%;">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for user in users %}
|
||||||
|
<tr>
|
||||||
|
<td style="font-family: monospace; font-weight: 600;">{{ user.user_code }}</td>
|
||||||
|
<td>
|
||||||
|
<form method="POST" action="{{ url_for('admin.update_editing_user', user_id=user.id) }}" style="display: flex; gap: 0.5rem; align-items: center;">
|
||||||
|
<input type="text"
|
||||||
|
name="user_name"
|
||||||
|
value="{{ user.user_name or '' }}"
|
||||||
|
placeholder="Enter display name"
|
||||||
|
class="form-control"
|
||||||
|
style="flex: 1;">
|
||||||
|
<button type="submit" class="btn btn-sm btn-primary">Save</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge badge-info">{{ user_stats.get(user.user_code, 0) }} edits</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ user.created_at | localtime('%Y-%m-%d %H:%M') }}</td>
|
||||||
|
<td>
|
||||||
|
<form method="POST"
|
||||||
|
action="{{ url_for('admin.delete_editing_user', user_id=user.id) }}"
|
||||||
|
style="display: inline;"
|
||||||
|
onsubmit="return confirm('Are you sure you want to delete this user? This will not delete their edit history.');">
|
||||||
|
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="card" style="text-align: center; padding: 4rem 2rem;">
|
||||||
|
<div style="font-size: 4rem; margin-bottom: 1rem; opacity: 0.5;">👤</div>
|
||||||
|
<h2 style="color: #6c757d; margin-bottom: 1rem;">No Editing Users Yet</h2>
|
||||||
|
<p style="color: #6c757d; font-size: 1.1rem;">
|
||||||
|
User codes will appear here automatically when players edit media files.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body.dark-mode .table {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .table thead th {
|
||||||
|
background: #2d3748;
|
||||||
|
color: #e2e8f0;
|
||||||
|
border-color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .table tbody tr {
|
||||||
|
border-color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .table tbody tr:hover {
|
||||||
|
background: #2d3748;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .form-control {
|
||||||
|
background: #2d3748;
|
||||||
|
color: #e2e8f0;
|
||||||
|
border-color: #4a5568;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
654
app/templates/admin/https_config.html
Normal file
654
app/templates/admin/https_config.html
Normal file
@@ -0,0 +1,654 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}HTTPS Configuration - DigiServer v2{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="page-header">
|
||||||
|
<a href="{{ url_for('admin.admin_panel') }}" class="back-link">← Back to Admin Panel</a>
|
||||||
|
<h1>🔒 HTTPS Configuration</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="https-config-container">
|
||||||
|
<!-- Status Display -->
|
||||||
|
<div class="card status-card">
|
||||||
|
<h2>Current Status</h2>
|
||||||
|
|
||||||
|
<!-- Real-time HTTPS detection -->
|
||||||
|
<div class="status-detection">
|
||||||
|
<p class="detection-info">
|
||||||
|
<strong>🔍 Detected Connection:</strong>
|
||||||
|
{% if is_https_active %}
|
||||||
|
<span class="badge badge-success">🔒 HTTPS ({{ request.scheme.upper() }})</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge badge-warning">🔓 HTTP</span>
|
||||||
|
{% endif %}
|
||||||
|
<br>
|
||||||
|
<small>Current host: <code>{{ current_host }}</code> via {{ request.host }}</small>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if config and config.https_enabled %}
|
||||||
|
<div class="status-enabled">
|
||||||
|
<span class="status-badge">✅ HTTPS ENABLED</span>
|
||||||
|
<div class="status-details">
|
||||||
|
<p><strong>Domain:</strong> {{ config.domain }}</p>
|
||||||
|
<p><strong>Hostname:</strong> {{ config.hostname }}</p>
|
||||||
|
<p><strong>Email:</strong> {{ config.email }}</p>
|
||||||
|
<p><strong>IP Address:</strong> {{ config.ip_address }}</p>
|
||||||
|
<p><strong>Port:</strong> {{ config.port }}</p>
|
||||||
|
<p><strong>Access URL:</strong> <code>https://{{ config.domain }}</code></p>
|
||||||
|
<p><strong>Last Updated:</strong> {{ config.updated_at.strftime('%Y-%m-%d %H:%M:%S') }} by {{ config.updated_by }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="status-disabled">
|
||||||
|
<span class="status-badge-inactive">⚠️ HTTPS DISABLED</span>
|
||||||
|
{% if is_https_active %}
|
||||||
|
<p style="color: #156b2e; background: #d1f0e0; padding: 10px; border-radius: 4px; margin: 10px 0;">
|
||||||
|
✅ <strong>Note:</strong> You are currently accessing this page via HTTPS, but the configuration shows as disabled.
|
||||||
|
This configuration will be automatically updated. Please refresh the page.
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<p>The application is currently running on HTTP only (port 80)</p>
|
||||||
|
<p>Enable HTTPS below to secure your application.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Configuration Form -->
|
||||||
|
<div class="card config-card">
|
||||||
|
<h2>Configure HTTPS Settings</h2>
|
||||||
|
<p class="info-text">
|
||||||
|
💡 <strong>Workflow:</strong> First, the app runs on HTTP (port 80). After you configure the HTTPS settings below,
|
||||||
|
the application will be available over HTTPS (port 443) using the domain and hostname you specify.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form method="POST" action="{{ url_for('admin.update_https_config') }}" class="https-form">
|
||||||
|
<!-- Enable HTTPS Toggle -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="toggle-label">
|
||||||
|
<input type="checkbox" name="https_enabled" id="https_enabled"
|
||||||
|
{% if config and config.https_enabled %}checked{% endif %}
|
||||||
|
class="toggle-input">
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
<span class="toggle-text">Enable HTTPS</span>
|
||||||
|
</label>
|
||||||
|
<p class="form-hint">Check this box to enable HTTPS/SSL for your application</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hostname Field -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="hostname">Hostname <span class="required">*</span></label>
|
||||||
|
<input type="text" id="hostname" name="hostname"
|
||||||
|
value="{{ config.hostname or 'digiserver' }}"
|
||||||
|
placeholder="e.g., digiserver"
|
||||||
|
class="form-input"
|
||||||
|
required>
|
||||||
|
<p class="form-hint">Short name for your server (e.g., 'digiserver')</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Domain Field -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="domain">Full Domain Name <span class="required">*</span></label>
|
||||||
|
<input type="text" id="domain" name="domain"
|
||||||
|
value="{{ config.domain or 'digiserver.sibiusb.harting.intra' }}"
|
||||||
|
placeholder="e.g., digiserver.sibiusb.harting.intra"
|
||||||
|
class="form-input"
|
||||||
|
required>
|
||||||
|
<p class="form-hint">Complete domain name (e.g., digiserver.sibiusb.harting.intra)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- IP Address Field -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="ip_address">IP Address <span class="required">*</span></label>
|
||||||
|
<input type="text" id="ip_address" name="ip_address"
|
||||||
|
value="{{ config.ip_address or '10.76.152.164' }}"
|
||||||
|
placeholder="e.g., 10.76.152.164"
|
||||||
|
class="form-input"
|
||||||
|
required>
|
||||||
|
<p class="form-hint">Server's IP address for direct access (e.g., 10.76.152.164)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Email Field -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email Address <span class="required">*</span></label>
|
||||||
|
<input type="email" id="email" name="email"
|
||||||
|
value="{{ config.email or '' }}"
|
||||||
|
placeholder="e.g., admin@example.com"
|
||||||
|
class="form-input"
|
||||||
|
required>
|
||||||
|
<p class="form-hint">Email address for SSL certificate notifications and Let's Encrypt communications</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Port Field -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="port">HTTPS Port</label>
|
||||||
|
<input type="number" id="port" name="port"
|
||||||
|
value="{{ config.port or 443 }}"
|
||||||
|
placeholder="443"
|
||||||
|
min="1" max="65535"
|
||||||
|
class="form-input">
|
||||||
|
<p class="form-hint">Port for HTTPS connections (default: 443)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Preview Section -->
|
||||||
|
<div class="preview-section">
|
||||||
|
<h3>Access Points After Configuration:</h3>
|
||||||
|
<ul class="access-points">
|
||||||
|
<li>
|
||||||
|
<strong>HTTPS (Recommended):</strong>
|
||||||
|
<code>https://<span id="preview-domain">digiserver.sibiusb.harting.intra</span></code>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>HTTP (Fallback):</strong>
|
||||||
|
<code>http://<span id="preview-ip">10.76.152.164</span></code>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Form Actions -->
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg">
|
||||||
|
💾 Save HTTPS Configuration
|
||||||
|
</button>
|
||||||
|
<a href="{{ url_for('admin.admin_panel') }}" class="btn btn-secondary">
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Nginx Status Card -->
|
||||||
|
<div class="card nginx-status-card">
|
||||||
|
<h2>🔧 Nginx Reverse Proxy Status</h2>
|
||||||
|
{% if nginx_status.available %}
|
||||||
|
<div class="nginx-status-content">
|
||||||
|
<div class="status-item">
|
||||||
|
<strong>Status:</strong>
|
||||||
|
<span class="badge badge-success">✅ Nginx Configured</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-item">
|
||||||
|
<strong>Configuration Path:</strong>
|
||||||
|
<code>{{ nginx_status.path }}</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if nginx_status.ssl_enabled %}
|
||||||
|
<div class="status-item">
|
||||||
|
<strong>SSL/TLS:</strong>
|
||||||
|
<span class="badge badge-success">🔒 Enabled</span>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="status-item">
|
||||||
|
<strong>SSL/TLS:</strong>
|
||||||
|
<span class="badge badge-warning">⚠️ Not Configured</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if nginx_status.http_ports %}
|
||||||
|
<div class="status-item">
|
||||||
|
<strong>HTTP Ports:</strong>
|
||||||
|
<code>{{ nginx_status.http_ports|join(', ') }}</code>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if nginx_status.https_ports %}
|
||||||
|
<div class="status-item">
|
||||||
|
<strong>HTTPS Ports:</strong>
|
||||||
|
<code>{{ nginx_status.https_ports|join(', ') }}</code>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if nginx_status.server_names %}
|
||||||
|
<div class="status-item">
|
||||||
|
<strong>Server Names:</strong>
|
||||||
|
{% for name in nginx_status.server_names %}
|
||||||
|
<code>{{ name }}</code>{% if not loop.last %}<br>{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if nginx_status.upstream_servers %}
|
||||||
|
<div class="status-item">
|
||||||
|
<strong>Upstream Servers:</strong>
|
||||||
|
{% for server in nginx_status.upstream_servers %}
|
||||||
|
<code>{{ server }}</code>{% if not loop.last %}<br>{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if nginx_status.ssl_protocols %}
|
||||||
|
<div class="status-item">
|
||||||
|
<strong>SSL Protocols:</strong>
|
||||||
|
<code>{{ nginx_status.ssl_protocols|join(', ') }}</code>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if nginx_status.client_max_body_size %}
|
||||||
|
<div class="status-item">
|
||||||
|
<strong>Max Body Size:</strong>
|
||||||
|
<code>{{ nginx_status.client_max_body_size }}</code>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if nginx_status.gzip_enabled %}
|
||||||
|
<div class="status-item">
|
||||||
|
<strong>Gzip Compression:</strong>
|
||||||
|
<span class="badge badge-success">✅ Enabled</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="status-disabled">
|
||||||
|
<p>⚠️ <strong>Nginx configuration not accessible</strong></p>
|
||||||
|
<p>Error: {{ nginx_status.error|default('Unknown error') }}</p>
|
||||||
|
<p style="font-size: 12px; color: #666;">Path checked: {{ nginx_status.path }}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Information Section -->
|
||||||
|
<div class="card info-card">
|
||||||
|
<h2>ℹ️ Important Information</h2>
|
||||||
|
<div class="info-sections">
|
||||||
|
<div class="info-section">
|
||||||
|
<h3>📝 Before You Start</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Ensure your DNS is configured to resolve the domain to your server</li>
|
||||||
|
<li>Verify the IP address matches your server's actual network interface</li>
|
||||||
|
<li>Check that ports 80, 443, and 443/UDP are open for traffic</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="info-section">
|
||||||
|
<h3>🔐 HTTPS Setup</h3>
|
||||||
|
<ul>
|
||||||
|
<li>SSL certificates are automatically managed by Caddy</li>
|
||||||
|
<li>Certificates are obtained from Let's Encrypt</li>
|
||||||
|
<li>Automatic renewal is handled by the system</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="info-section">
|
||||||
|
<h3>✅ After Configuration</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Your app will restart with the new settings</li>
|
||||||
|
<li>Both HTTP and HTTPS access points will be available</li>
|
||||||
|
<li>HTTP requests will be redirected to HTTPS</li>
|
||||||
|
<li>Check the status above for current configuration</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.https-config-container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #0066cc;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-detection {
|
||||||
|
background: #f0f7ff;
|
||||||
|
border-left: 4px solid #0066cc;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detection-info {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-success {
|
||||||
|
background: #d1f0e0;
|
||||||
|
color: #156b2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-warning {
|
||||||
|
background: #fff3cd;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-card {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
border-left: 5px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-enabled {
|
||||||
|
background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
border-left: 5px solid #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-disabled {
|
||||||
|
background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
border-left: 5px solid #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge-inactive {
|
||||||
|
display: inline-block;
|
||||||
|
background: #ffc107;
|
||||||
|
color: #333;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-details {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-details p {
|
||||||
|
margin: 8px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-details code {
|
||||||
|
background: rgba(0,0,0,0.1);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-card {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-text {
|
||||||
|
background: #e7f3ff;
|
||||||
|
border-left: 4px solid #0066cc;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.https-form {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #0066cc;
|
||||||
|
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-hint {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toggle Switch Styling */
|
||||||
|
.toggle-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-slider {
|
||||||
|
display: inline-block;
|
||||||
|
width: 50px;
|
||||||
|
height: 28px;
|
||||||
|
background: #ccc;
|
||||||
|
border-radius: 14px;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-slider::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-input:checked + .toggle-slider {
|
||||||
|
background: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-input:checked + .toggle-slider::after {
|
||||||
|
left: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-text {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-section {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 2px dashed #0066cc;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 25px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-section h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #0066cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.access-points {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.access-points li {
|
||||||
|
padding: 10px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border-left: 4px solid #0066cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.access-points code {
|
||||||
|
background: #e7f3ff;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
color: #0066cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 30px;
|
||||||
|
padding-top: 25px;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-lg {
|
||||||
|
padding: 12px 30px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card {
|
||||||
|
background: linear-gradient(135deg, #e7f3ff 0%, #f0f7ff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card h2 {
|
||||||
|
color: #0066cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nginx-status-card {
|
||||||
|
background: linear-gradient(135deg, #f0f7ff 0%, #e7f3ff 100%);
|
||||||
|
border-left: 5px solid #0066cc;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nginx-status-card h2 {
|
||||||
|
color: #0066cc;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nginx-status-content {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
padding: 12px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-left: 3px solid #0066cc;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item strong {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 150px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item code {
|
||||||
|
background: #f0f7ff;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
color: #0066cc;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-sections {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section h3 {
|
||||||
|
color: #0066cc;
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section ul {
|
||||||
|
padding-left: 20px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section li {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.https-config-container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-sections {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-lg {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Update preview in real-time
|
||||||
|
document.getElementById('domain').addEventListener('input', function() {
|
||||||
|
document.getElementById('preview-domain').textContent = this.value || 'digiserver.sibiusb.harting.intra';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('ip_address').addEventListener('input', function() {
|
||||||
|
document.getElementById('preview-ip').textContent = this.value || '10.76.152.164';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load initial preview
|
||||||
|
document.getElementById('preview-domain').textContent = document.getElementById('domain').value || 'digiserver.sibiusb.harting.intra';
|
||||||
|
document.getElementById('preview-ip').textContent = document.getElementById('ip_address').value || '10.76.152.164';
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -376,7 +376,7 @@
|
|||||||
<header>
|
<header>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>
|
<h1>
|
||||||
<img src="{{ url_for('static', filename='uploads/header_logo.png') }}" alt="DigiServer" style="height: 32px; width: auto;" onerror="this.src='{{ url_for('static', filename='icons/monitor.svg') }}'; this.style.filter='brightness(0) invert(1)'; this.style.width='28px'; this.style.height='28px';">
|
<img src="{{ url_for('static', filename='uploads/header_logo.png?v=1') }}" alt="DigiServer" style="height: 32px; width: auto; margin-right: 8px;" onerror="this.style.display='none';" onload="this.style.display='inline';">
|
||||||
DigiServer
|
DigiServer
|
||||||
</h1>
|
</h1>
|
||||||
<nav>
|
<nav>
|
||||||
@@ -393,9 +393,7 @@
|
|||||||
<img src="{{ url_for('static', filename='icons/playlist.svg') }}" alt="">
|
<img src="{{ url_for('static', filename='icons/playlist.svg') }}" alt="">
|
||||||
Playlists
|
Playlists
|
||||||
</a>
|
</a>
|
||||||
{% if current_user.is_admin %}
|
<a href="{{ url_for('admin.admin_panel') }}">Admin</a>
|
||||||
<a href="{{ url_for('admin.admin_panel') }}">Admin</a>
|
|
||||||
{% endif %}
|
|
||||||
<a href="{{ url_for('auth.logout') }}">Logout ({{ current_user.username }})</a>
|
<a href="{{ url_for('auth.logout') }}">Logout ({{ current_user.username }})</a>
|
||||||
<button class="dark-mode-toggle" onclick="toggleDarkMode()" title="Toggle Dark Mode">
|
<button class="dark-mode-toggle" onclick="toggleDarkMode()" title="Toggle Dark Mode">
|
||||||
<img id="theme-icon" src="{{ url_for('static', filename='icons/moon.svg') }}" alt="Toggle theme">
|
<img id="theme-icon" src="{{ url_for('static', filename='icons/moon.svg') }}" alt="Toggle theme">
|
||||||
|
|||||||
@@ -211,6 +211,7 @@
|
|||||||
<th style="width: 100px;">Type</th>
|
<th style="width: 100px;">Type</th>
|
||||||
<th style="width: 100px;">Duration</th>
|
<th style="width: 100px;">Duration</th>
|
||||||
<th style="width: 80px;">Audio</th>
|
<th style="width: 80px;">Audio</th>
|
||||||
|
<th style="width: 80px;">Edit</th>
|
||||||
<th style="width: 100px;">Actions</th>
|
<th style="width: 100px;">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -247,6 +248,23 @@
|
|||||||
<span style="color: #999;">—</span>
|
<span style="color: #999;">—</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if content.content_type in ['image', 'pdf'] %}
|
||||||
|
<label class="audio-toggle">
|
||||||
|
<input type="checkbox"
|
||||||
|
class="edit-checkbox"
|
||||||
|
data-content-id="{{ content.id }}"
|
||||||
|
{{ 'checked' if content._playlist_edit_on_player_enabled else '' }}
|
||||||
|
onchange="toggleEdit({{ content.id }}, this.checked)">
|
||||||
|
<span class="audio-label">
|
||||||
|
<span class="audio-on">✏️</span>
|
||||||
|
<span class="audio-off">🔒</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
{% else %}
|
||||||
|
<span style="color: #999;">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<form method="POST"
|
<form method="POST"
|
||||||
action="{{ url_for('content.remove_content_from_playlist', playlist_id=playlist.id, content_id=content.id) }}"
|
action="{{ url_for('content.remove_content_from_playlist', playlist_id=playlist.id, content_id=content.id) }}"
|
||||||
@@ -427,6 +445,37 @@ function toggleAudio(contentId, enabled) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleEdit(contentId, enabled) {
|
||||||
|
const playlistId = {{ playlist.id }};
|
||||||
|
const url = `/content/playlist/${playlistId}/update-edit-enabled/${contentId}`;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('edit_enabled', enabled ? 'true' : 'false');
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
console.log('Edit setting updated:', enabled ? 'Enabled' : 'Disabled');
|
||||||
|
} else {
|
||||||
|
alert('Error updating edit setting: ' + data.message);
|
||||||
|
// Revert checkbox on error
|
||||||
|
const checkbox = document.querySelector(`.edit-checkbox[data-content-id="${contentId}"]`);
|
||||||
|
if (checkbox) checkbox.checked = !enabled;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Error updating edit setting');
|
||||||
|
// Revert checkbox on error
|
||||||
|
const checkbox = document.querySelector(`.edit-checkbox[data-content-id="${contentId}"]`);
|
||||||
|
if (checkbox) checkbox.checked = !enabled;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function toggleSelectAll(checkbox) {
|
function toggleSelectAll(checkbox) {
|
||||||
const checkboxes = document.querySelectorAll('.content-checkbox');
|
const checkboxes = document.querySelectorAll('.content-checkbox');
|
||||||
checkboxes.forEach(cb => {
|
checkboxes.forEach(cb => {
|
||||||
|
|||||||
@@ -273,7 +273,7 @@
|
|||||||
<div class="media-grid">
|
<div class="media-grid">
|
||||||
{% for media in images %}
|
{% for media in images %}
|
||||||
<div class="media-card">
|
<div class="media-card">
|
||||||
<button class="delete-btn" onclick="confirmDelete({{ media.id }}, '{{ media.filename }}', {{ media.playlists.count() }})" title="Delete">🗑️</button>
|
<button class="delete-btn" onclick="confirmDelete({{ media.id }}, '{{ media.filename }}', {{ media.playlists.count() }}, {{ media.edit_count }})" title="Delete">🗑️</button>
|
||||||
<div class="media-thumbnail">
|
<div class="media-thumbnail">
|
||||||
<img src="{{ url_for('static', filename='uploads/' + media.filename) }}"
|
<img src="{{ url_for('static', filename='uploads/' + media.filename) }}"
|
||||||
alt="{{ media.filename }}"
|
alt="{{ media.filename }}"
|
||||||
@@ -304,7 +304,7 @@
|
|||||||
<div class="media-grid">
|
<div class="media-grid">
|
||||||
{% for media in videos %}
|
{% for media in videos %}
|
||||||
<div class="media-card">
|
<div class="media-card">
|
||||||
<button class="delete-btn" onclick="confirmDelete({{ media.id }}, '{{ media.filename }}', {{ media.playlists.count() }})" title="Delete">🗑️</button>
|
<button class="delete-btn" onclick="confirmDelete({{ media.id }}, '{{ media.filename }}', {{ media.playlists.count() }}, {{ media.edit_count }})" title="Delete">🗑️</button>
|
||||||
<div class="media-thumbnail">
|
<div class="media-thumbnail">
|
||||||
<span class="media-icon">🎥</span>
|
<span class="media-icon">🎥</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -333,7 +333,7 @@
|
|||||||
<div class="media-grid">
|
<div class="media-grid">
|
||||||
{% for media in pdfs %}
|
{% for media in pdfs %}
|
||||||
<div class="media-card">
|
<div class="media-card">
|
||||||
<button class="delete-btn" onclick="confirmDelete({{ media.id }}, '{{ media.filename }}', {{ media.playlists.count() }})" title="Delete">🗑️</button>
|
<button class="delete-btn" onclick="confirmDelete({{ media.id }}, '{{ media.filename }}', {{ media.playlists.count() }}, {{ media.edit_count }})" title="Delete">🗑️</button>
|
||||||
<div class="media-thumbnail">
|
<div class="media-thumbnail">
|
||||||
<span class="media-icon">📄</span>
|
<span class="media-icon">📄</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -362,7 +362,7 @@
|
|||||||
<div class="media-grid">
|
<div class="media-grid">
|
||||||
{% for media in presentations %}
|
{% for media in presentations %}
|
||||||
<div class="media-card">
|
<div class="media-card">
|
||||||
<button class="delete-btn" onclick="confirmDelete({{ media.id }}, '{{ media.filename }}', {{ media.playlists.count() }})" title="Delete">🗑️</button>
|
<button class="delete-btn" onclick="confirmDelete({{ media.id }}, '{{ media.filename }}', {{ media.playlists.count() }}, {{ media.edit_count }})" title="Delete">🗑️</button>
|
||||||
<div class="media-thumbnail">
|
<div class="media-thumbnail">
|
||||||
<span class="media-icon">📊</span>
|
<span class="media-icon">📊</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -391,7 +391,7 @@
|
|||||||
<div class="media-grid">
|
<div class="media-grid">
|
||||||
{% for media in others %}
|
{% for media in others %}
|
||||||
<div class="media-card">
|
<div class="media-card">
|
||||||
<button class="delete-btn" onclick="confirmDelete({{ media.id }}, '{{ media.filename }}', {{ media.playlists.count() }})" title="Delete">🗑️</button>
|
<button class="delete-btn" onclick="confirmDelete({{ media.id }}, '{{ media.filename }}', {{ media.playlists.count() }}, {{ media.edit_count }})" title="Delete">🗑️</button>
|
||||||
<div class="media-thumbnail">
|
<div class="media-thumbnail">
|
||||||
<span class="media-icon">📁</span>
|
<span class="media-icon">📁</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -424,28 +424,45 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Delete Confirmation Modal -->
|
<!-- Delete Confirmation Modal -->
|
||||||
<div id="deleteModal" style="display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.5);">
|
<div id="deleteModal" class="delete-modal">
|
||||||
<div style="background-color: #fefefe; margin: 10% auto; padding: 30px; border-radius: 12px; width: 90%; max-width: 500px; box-shadow: 0 4px 20px rgba(0,0,0,0.3);">
|
<div class="delete-modal-content">
|
||||||
<h2 style="margin-top: 0; color: #dc3545; display: flex; align-items: center; gap: 0.5rem;">
|
<div class="delete-modal-header">
|
||||||
<span style="font-size: 2rem;">⚠️</span>
|
<span class="delete-icon">⚠️</span>
|
||||||
Confirm Delete
|
<h2>Confirm Delete</h2>
|
||||||
</h2>
|
|
||||||
<p style="font-size: 1.1rem; margin: 1.5rem 0;">
|
|
||||||
Are you sure you want to delete <strong id="deleteFilename"></strong>?
|
|
||||||
</p>
|
|
||||||
<div id="playlistWarning" style="display: none; background: #fff3cd; border-left: 4px solid #ffc107; padding: 12px; margin: 1rem 0; border-radius: 4px;">
|
|
||||||
<strong style="color: #856404;">⚠️ Warning:</strong>
|
|
||||||
<p style="margin: 0.5rem 0 0 0; color: #856404;">This file is used in <strong id="playlistCount"></strong> playlist(s). Deleting it will remove it from all playlists and increment their version numbers.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<p style="color: #dc3545; margin: 1rem 0;">
|
|
||||||
<strong>This action cannot be undone!</strong>
|
<div class="delete-modal-body">
|
||||||
</p>
|
<p class="delete-question">
|
||||||
<div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 2rem;">
|
Are you sure you want to delete <strong id="deleteFilename" class="delete-filename"></strong>?
|
||||||
<button onclick="closeDeleteModal()" class="btn" style="background: #6c757d;">
|
</p>
|
||||||
|
|
||||||
|
<div id="playlistWarning" class="warning-box warning-playlist">
|
||||||
|
<div class="warning-header">
|
||||||
|
<span>⚠️</span>
|
||||||
|
<strong>Playlist Warning</strong>
|
||||||
|
</div>
|
||||||
|
<p>This file is used in <strong id="playlistCount"></strong> playlist(s). Deleting it will remove it from all playlists and increment their version numbers.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="editWarning" class="warning-box warning-edit">
|
||||||
|
<div class="warning-header">
|
||||||
|
<span>✏️</span>
|
||||||
|
<strong>Edited Versions</strong>
|
||||||
|
</div>
|
||||||
|
<p>This file has <strong id="editCount"></strong> edited version(s) from player devices. All edited versions and their metadata will also be permanently deleted.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="delete-final-warning">
|
||||||
|
<strong>⚠️ This action cannot be undone!</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="delete-modal-footer">
|
||||||
|
<button onclick="closeDeleteModal()" class="btn btn-cancel">
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<form id="deleteForm" method="POST" style="margin: 0;">
|
<form id="deleteForm" method="POST">
|
||||||
<button type="submit" class="btn" style="background: #dc3545;">
|
<button type="submit" class="btn btn-delete">
|
||||||
Yes, Delete File
|
Yes, Delete File
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -454,52 +471,294 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body.dark-mode #deleteModal {
|
/* Delete Modal - Light Mode Styles */
|
||||||
|
.delete-modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
animation: fadeIn 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-modal-content {
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
||||||
|
margin: 8% auto;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 16px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 550px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||||
|
animation: slideDown 0.3s ease-out;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideDown {
|
||||||
|
from {
|
||||||
|
transform: translateY(-50px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-modal-header {
|
||||||
|
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 24px 30px;
|
||||||
|
border-radius: 16px 16px 0 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-modal-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-modal-body {
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-question {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
color: #2d3748;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-filename {
|
||||||
|
color: #dc3545;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-box {
|
||||||
|
display: none;
|
||||||
|
padding: 16px;
|
||||||
|
margin: 16px 0;
|
||||||
|
border-radius: 10px;
|
||||||
|
border-left: 4px solid;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-box:hover {
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-header span {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-header strong {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-box p {
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-playlist {
|
||||||
|
background: linear-gradient(135deg, #fff8e1 0%, #fff3cd 100%);
|
||||||
|
border-color: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-playlist .warning-header,
|
||||||
|
.warning-playlist p {
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-edit {
|
||||||
|
background: linear-gradient(135deg, #f3e8ff 0%, #e9d5ff 100%);
|
||||||
|
border-color: #7c3aed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-edit .warning-header,
|
||||||
|
.warning-edit p {
|
||||||
|
color: #5b21b6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-final-warning {
|
||||||
|
background: linear-gradient(135deg, #fee 0%, #fdd 100%);
|
||||||
|
border: 2px solid #dc3545;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin: 20px 0 0 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-final-warning strong {
|
||||||
|
color: #dc3545;
|
||||||
|
font-size: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-modal-footer {
|
||||||
|
padding: 20px 30px;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
background: rgba(0, 0, 0, 0.02);
|
||||||
|
border-radius: 0 0 16px 16px;
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-modal-footer form {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
background: linear-gradient(135deg, #6c757d 0%, #5a6268 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel:hover {
|
||||||
|
background: linear-gradient(135deg, #5a6268 0%, #4e555b 100%);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete {
|
||||||
|
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 12px 28px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 2px 6px rgba(220, 53, 69, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete:hover {
|
||||||
|
background: linear-gradient(135deg, #c82333 0%, #bd2130 100%);
|
||||||
|
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.5);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Delete Modal - Dark Mode Styles */
|
||||||
|
body.dark-mode .delete-modal {
|
||||||
background-color: rgba(0, 0, 0, 0.8);
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.dark-mode #deleteModal > div {
|
body.dark-mode .delete-modal-content {
|
||||||
background-color: #1a202c;
|
background: linear-gradient(135deg, #1a202c 0%, #2d3748 100%);
|
||||||
border: 1px solid #4a5568;
|
border: 1px solid #4a5568;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .delete-modal-header {
|
||||||
|
background: linear-gradient(135deg, #b91c1c 0%, #991b1b 100%);
|
||||||
|
box-shadow: 0 4px 12px rgba(185, 28, 28, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .delete-question {
|
||||||
color: #e2e8f0;
|
color: #e2e8f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.dark-mode #deleteModal h2 {
|
body.dark-mode .delete-filename {
|
||||||
color: #f87171;
|
color: #f87171;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.dark-mode #deleteModal p {
|
body.dark-mode .warning-playlist {
|
||||||
color: #e2e8f0;
|
background: linear-gradient(135deg, #422006 0%, #713f12 100%);
|
||||||
|
border-color: #fbbf24;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.dark-mode #deleteModal strong {
|
body.dark-mode .warning-playlist .warning-header,
|
||||||
color: #fbbf24;
|
body.dark-mode .warning-playlist p {
|
||||||
|
color: #fde68a;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.dark-mode #playlistWarning {
|
body.dark-mode .warning-edit {
|
||||||
background: #4a3800;
|
background: linear-gradient(135deg, #3b0764 0%, #581c87 100%);
|
||||||
border-left-color: #ffc107;
|
border-color: #a78bfa;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.dark-mode #playlistWarning strong,
|
body.dark-mode .warning-edit .warning-header,
|
||||||
body.dark-mode #playlistWarning p {
|
body.dark-mode .warning-edit p {
|
||||||
color: #fbbf24;
|
color: #ddd6fe;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.dark-mode #deleteForm button {
|
body.dark-mode .delete-final-warning {
|
||||||
background: #dc3545 !important;
|
background: linear-gradient(135deg, #450a0a 0%, #7f1d1d 100%);
|
||||||
color: white !important;
|
border-color: #f87171;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.dark-mode #deleteForm button:hover {
|
body.dark-mode .delete-final-warning strong {
|
||||||
background: #a02834 !important;
|
color: #fca5a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .delete-modal-footer {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-top: 1px solid #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .btn-cancel {
|
||||||
|
background: linear-gradient(135deg, #374151 0%, #1f2937 100%);
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .btn-cancel:hover {
|
||||||
|
background: linear-gradient(135deg, #4b5563 0%, #374151 100%);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .btn-delete {
|
||||||
|
background: linear-gradient(135deg, #b91c1c 0%, #991b1b 100%);
|
||||||
|
box-shadow: 0 2px 6px rgba(185, 28, 28, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .btn-delete:hover {
|
||||||
|
background: linear-gradient(135deg, #991b1b 0%, #7f1d1d 100%);
|
||||||
|
box-shadow: 0 4px 12px rgba(185, 28, 28, 0.6);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let deleteMediaId = null;
|
let deleteMediaId = null;
|
||||||
|
|
||||||
function confirmDelete(mediaId, filename, playlistCount) {
|
function confirmDelete(mediaId, filename, playlistCount, editCount) {
|
||||||
deleteMediaId = mediaId;
|
deleteMediaId = mediaId;
|
||||||
document.getElementById('deleteFilename').textContent = filename;
|
document.getElementById('deleteFilename').textContent = filename;
|
||||||
document.getElementById('deleteForm').action = "{{ url_for('content.delete_media', media_id=0) }}".replace('/0', '/' + mediaId);
|
document.getElementById('deleteForm').action = "{{ url_for('content.delete_media', media_id=0) }}".replace('/0', '/' + mediaId);
|
||||||
@@ -512,6 +771,14 @@ function confirmDelete(mediaId, filename, playlistCount) {
|
|||||||
document.getElementById('playlistWarning').style.display = 'none';
|
document.getElementById('playlistWarning').style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show edit versions warning if file has been edited
|
||||||
|
if (editCount > 0) {
|
||||||
|
document.getElementById('editWarning').style.display = 'block';
|
||||||
|
document.getElementById('editCount').textContent = editCount;
|
||||||
|
} else {
|
||||||
|
document.getElementById('editWarning').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('deleteModal').style.display = 'block';
|
document.getElementById('deleteModal').style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -296,6 +296,17 @@
|
|||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label style="display: flex; align-items: center; cursor: pointer; user-select: none;">
|
||||||
|
<input type="checkbox" name="edit_on_player_enabled" id="edit_on_player_enabled"
|
||||||
|
value="1" style="margin-right: 8px; width: 18px; height: 18px; cursor: pointer;">
|
||||||
|
<span>Allow editing on player (PDF, Images, PPTX)</span>
|
||||||
|
</label>
|
||||||
|
<small style="color: #6c757d; display: block; margin-top: 4px; font-size: 11px;">
|
||||||
|
✏️ Enable local editing of this media on the player device
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Upload Button -->
|
<!-- Upload Button -->
|
||||||
<button type="submit" class="btn-upload" id="upload-btn" disabled style="display: flex; align-items: center; justify-content: center; gap: 0.5rem; margin-top: 20px; padding: 12px 30px;">
|
<button type="submit" class="btn-upload" id="upload-btn" disabled style="display: flex; align-items: center; justify-content: center; gap: 0.5rem; margin-top: 20px; padding: 12px 30px;">
|
||||||
<img src="{{ url_for('static', filename='icons/upload.svg') }}" alt="" style="width: 18px; height: 18px; filter: brightness(0) invert(1);">
|
<img src="{{ url_for('static', filename='icons/upload.svg') }}" alt="" style="width: 18px; height: 18px; filter: brightness(0) invert(1);">
|
||||||
|
|||||||
525
app/templates/players/edited_media.html
Normal file
525
app/templates/players/edited_media.html
Normal file
@@ -0,0 +1,525 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Edited Media - {{ player.name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<style>
|
||||||
|
.expandable-card {
|
||||||
|
width: 100%;
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
||||||
|
border: 2px solid #cbd5e1;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandable-card:hover {
|
||||||
|
border-color: #7c3aed;
|
||||||
|
box-shadow: 0 4px 16px rgba(124, 58, 237, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandable-card.expanded {
|
||||||
|
border-color: #7c3aed;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
padding: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandable-card:not(.expanded) .card-header:hover {
|
||||||
|
background: rgba(124, 58, 237, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a202c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandable-card.expanded .card-header-icon {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandable-card.expanded .card-content {
|
||||||
|
max-height: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
padding: 0 1.5rem 1.5rem 1.5rem;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 350px 1fr;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-area {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-container {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
background: #000;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-container img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-info {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-info-item {
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-info-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-info-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #64748b;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-info-value {
|
||||||
|
color: #1a202c;
|
||||||
|
text-align: right;
|
||||||
|
word-break: break-word;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.versions-area {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.versions-title {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #7c3aed;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.versions-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-item {
|
||||||
|
background: white;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-item:hover {
|
||||||
|
border-color: #7c3aed;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-item.active {
|
||||||
|
border-color: #7c3aed;
|
||||||
|
box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-thumbnail {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
background: #000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-thumbnail img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-label {
|
||||||
|
padding: 0.75rem;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a202c;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-item.active .version-label {
|
||||||
|
background: #7c3aed;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-latest {
|
||||||
|
background: #7c3aed;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-original {
|
||||||
|
background: #10b981;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: linear-gradient(135deg, #7c3aed 0%, #6d28d9 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(124, 58, 237, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode styles */
|
||||||
|
body.dark-mode .expandable-card {
|
||||||
|
background: linear-gradient(135deg, #1a202c 0%, #2d3748 100%);
|
||||||
|
border-color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .expandable-card:hover,
|
||||||
|
body.dark-mode .expandable-card.expanded {
|
||||||
|
border-color: #7c3aed;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .card-header-title {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .expandable-card:not(.expanded) .card-header:hover {
|
||||||
|
background: rgba(124, 58, 237, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .preview-info {
|
||||||
|
background: #1a202c;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .preview-info-item {
|
||||||
|
border-bottom-color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .preview-info-label {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .preview-info-value {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .versions-title {
|
||||||
|
color: #a78bfa;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .version-item {
|
||||||
|
background: #2d3748;
|
||||||
|
border-color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .version-item:hover,
|
||||||
|
body.dark-mode .version-item.active {
|
||||||
|
border-color: #7c3aed;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .version-label {
|
||||||
|
background: #1a202c;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .version-item.active .version-label {
|
||||||
|
background: #7c3aed;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 2rem;">
|
||||||
|
<div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem;">
|
||||||
|
<a href="{{ url_for('players.manage_player', player_id=player.id) }}"
|
||||||
|
class="btn"
|
||||||
|
style="background: #6c757d; color: white; padding: 0.5rem 1rem; text-decoration: none; border-radius: 6px; display: inline-flex; align-items: center; gap: 0.5rem;">
|
||||||
|
← Back to Player
|
||||||
|
</a>
|
||||||
|
<h1 style="margin: 0; display: flex; align-items: center; gap: 0.5rem;">
|
||||||
|
<img src="{{ url_for('static', filename='icons/edit.svg') }}" alt="" style="width: 32px; height: 32px;">
|
||||||
|
Edited Media - {{ player.name }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<p style="color: #6c757d; font-size: 1rem;">Complete history of media files edited on this player</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if edited_media %}
|
||||||
|
{% set edited_by_content = {} %}
|
||||||
|
{% for edit in edited_media %}
|
||||||
|
{% if edit.content_id not in edited_by_content %}
|
||||||
|
{% set _ = edited_by_content.update({edit.content_id: {'original_name': edit.original_name, 'content_id': edit.content_id, 'versions': []}}) %}
|
||||||
|
{% endif %}
|
||||||
|
{% set _ = edited_by_content[edit.content_id]['versions'].append(edit) %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<!-- Expandable Cards -->
|
||||||
|
{% for content_id, data in edited_by_content.items() %}
|
||||||
|
{% set original_content = content_files.get(content_id) %}
|
||||||
|
<div class="expandable-card" id="card-{{ content_id }}" onclick="toggleCard({{ content_id }})">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="card-header-title">
|
||||||
|
<span class="card-header-icon">▶</span>
|
||||||
|
<span>📄 {{ original_content.filename if original_content else data.original_name }}</span>
|
||||||
|
<span style="font-size: 0.9rem; color: #64748b; font-weight: normal;">
|
||||||
|
({{ data.versions|length + 1 }} version{{ 's' if (data.versions|length + 1) > 1 else '' }})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Preview Area (Left Column) -->
|
||||||
|
<div class="preview-area">
|
||||||
|
<div class="preview-container" id="preview-{{ content_id }}">
|
||||||
|
{% set latest = data.versions|sort(attribute='version', reverse=True)|first %}
|
||||||
|
{% if latest.new_name.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.webp')) %}
|
||||||
|
<img src="{{ url_for('static', filename='uploads/edited_media/' ~ latest.content_id ~ '/' ~ latest.new_name) }}"
|
||||||
|
alt="{{ latest.new_name }}"
|
||||||
|
id="preview-img-{{ content_id }}">
|
||||||
|
{% else %}
|
||||||
|
<div style="color: white; text-align: center;">
|
||||||
|
<div style="font-size: 3rem; margin-bottom: 0.5rem;">📄</div>
|
||||||
|
<div>No preview available</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{{ url_for('static', filename='uploads/edited_media/' ~ latest.content_id ~ '/' ~ latest.new_name) }}"
|
||||||
|
download
|
||||||
|
class="download-btn"
|
||||||
|
id="download-btn-{{ content_id }}">
|
||||||
|
💾 Download File
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="preview-info" id="info-{{ content_id }}">
|
||||||
|
<div class="preview-info-item">
|
||||||
|
<span class="preview-info-label">📄 Filename:</span>
|
||||||
|
<span class="preview-info-value" id="info-filename-{{ content_id }}">{{ latest.new_name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="preview-info-item">
|
||||||
|
<span class="preview-info-label">📦 Version:</span>
|
||||||
|
<span class="preview-info-value" id="info-version-{{ content_id }}">v{{ latest.version }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="preview-info-item">
|
||||||
|
<span class="preview-info-label">👤 Edited by:</span>
|
||||||
|
<span class="preview-info-value" id="info-user-{{ content_id }}">{{ user_mappings.get(latest.user, latest.user or 'Unknown') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="preview-info-item">
|
||||||
|
<span class="preview-info-label">🕒 Modified:</span>
|
||||||
|
<span class="preview-info-value" id="info-date-{{ content_id }}">{{ latest.time_of_modification | localtime('%Y-%m-%d %H:%M') if latest.time_of_modification else 'N/A' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="preview-info-item">
|
||||||
|
<span class="preview-info-label">📅 Uploaded:</span>
|
||||||
|
<span class="preview-info-value" id="info-created-{{ content_id }}">{{ latest.created_at | localtime('%Y-%m-%d %H:%M') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Versions Area (Right Column) -->
|
||||||
|
<div class="versions-area">
|
||||||
|
<div class="versions-title">
|
||||||
|
📚 All Versions
|
||||||
|
</div>
|
||||||
|
<div class="versions-grid">
|
||||||
|
{% for edit in data.versions|sort(attribute='version', reverse=True) %}
|
||||||
|
<div class="version-item {% if loop.first %}active{% endif %}"
|
||||||
|
id="version-{{ content_id }}-{{ edit.version }}"
|
||||||
|
onclick="event.stopPropagation(); selectVersion({{ content_id }}, {{ edit.version }}, '{{ edit.new_name }}', '{{ user_mappings.get(edit.user, edit.user or 'Unknown') }}', '{{ edit.time_of_modification | localtime('%Y-%m-%d %H:%M') if edit.time_of_modification else 'N/A' }}', '{{ edit.created_at | localtime('%Y-%m-%d %H:%M') }}', '{{ url_for('static', filename='uploads/edited_media/' ~ edit.content_id ~ '/' ~ edit.new_name) }}')">
|
||||||
|
<div class="version-thumbnail">
|
||||||
|
{% if edit.new_name.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.webp')) %}
|
||||||
|
<img src="{{ url_for('static', filename='uploads/edited_media/' ~ edit.content_id ~ '/' ~ edit.new_name) }}"
|
||||||
|
alt="Version {{ edit.version }}">
|
||||||
|
{% else %}
|
||||||
|
<div style="color: white; font-size: 2rem;">📄</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="version-label">
|
||||||
|
v{{ edit.version }}
|
||||||
|
{% if loop.first %}
|
||||||
|
<span class="version-badge badge-latest">Latest</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<!-- Original File -->
|
||||||
|
{% if original_content %}
|
||||||
|
<div class="version-item"
|
||||||
|
id="version-{{ content_id }}-original"
|
||||||
|
onclick="event.stopPropagation(); selectVersion({{ content_id }}, 'original', '{{ original_content.filename }}', 'System', 'N/A', '{{ original_content.uploaded_at | localtime('%Y-%m-%d %H:%M') }}', '{{ url_for('static', filename='uploads/' ~ original_content.filename) }}')">
|
||||||
|
<div class="version-thumbnail">
|
||||||
|
{% if original_content.filename.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.webp')) %}
|
||||||
|
<img src="{{ url_for('static', filename='uploads/' ~ original_content.filename) }}"
|
||||||
|
alt="Original">
|
||||||
|
{% else %}
|
||||||
|
<div style="color: white; font-size: 2rem;">📄</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="version-label">
|
||||||
|
Original
|
||||||
|
<span class="version-badge badge-original">Source</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<!-- Summary Statistics -->
|
||||||
|
<div class="card" style="margin-top: 2rem; background: linear-gradient(135deg, #7c3aed 0%, #6d28d9 100%); color: white; border-radius: 12px; overflow: hidden;">
|
||||||
|
<div style="padding: 1.5rem; display: flex; justify-content: space-around; align-items: center; flex-wrap: wrap; gap: 2rem;">
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<div style="font-size: 2.5rem; font-weight: bold; margin-bottom: 0.5rem;">{{ edited_by_content|length }}</div>
|
||||||
|
<div style="font-size: 0.9rem; opacity: 0.9;">Total Files Edited</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<div style="font-size: 2.5rem; font-weight: bold; margin-bottom: 0.5rem;">{{ edited_media|length }}</div>
|
||||||
|
<div style="font-size: 0.9rem; opacity: 0.9;">Total Versions</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<div style="font-size: 2.5rem; font-weight: bold; margin-bottom: 0.5rem;">{{ ((edited_media|length / edited_by_content|length) | round(1)) if edited_by_content else 0 }}</div>
|
||||||
|
<div style="font-size: 0.9rem; opacity: 0.9;">Avg Versions per File</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<div class="card" style="text-align: center; padding: 4rem 2rem;">
|
||||||
|
<div style="font-size: 4rem; margin-bottom: 1rem; opacity: 0.5;">📭</div>
|
||||||
|
<h2 style="color: #6c757d; margin-bottom: 1rem;">No Edited Media Yet</h2>
|
||||||
|
<p style="color: #6c757d; font-size: 1.1rem;">
|
||||||
|
This player hasn't edited any media files yet. When the player edits content,<br>
|
||||||
|
all versions will be tracked and displayed here.
|
||||||
|
</p>
|
||||||
|
<a href="{{ url_for('players.manage_player', player_id=player.id) }}"
|
||||||
|
class="btn"
|
||||||
|
style="margin-top: 2rem; display: inline-block; background: #7c3aed; color: white; padding: 0.75rem 1.5rem; text-decoration: none; border-radius: 6px; font-size: 1rem;">
|
||||||
|
← Back to Player Management
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleCard(contentId) {
|
||||||
|
const card = document.getElementById('card-' + contentId);
|
||||||
|
const wasExpanded = card.classList.contains('expanded');
|
||||||
|
|
||||||
|
// Close all cards
|
||||||
|
document.querySelectorAll('.expandable-card').forEach(c => {
|
||||||
|
c.classList.remove('expanded');
|
||||||
|
});
|
||||||
|
|
||||||
|
// If this card wasn't expanded, expand it
|
||||||
|
if (!wasExpanded) {
|
||||||
|
card.classList.add('expanded');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectVersion(contentId, version, filename, user, modifiedDate, createdDate, fileUrl) {
|
||||||
|
// Update preview image
|
||||||
|
const previewImg = document.getElementById('preview-img-' + contentId);
|
||||||
|
if (previewImg) {
|
||||||
|
previewImg.src = fileUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update download button
|
||||||
|
const downloadBtn = document.getElementById('download-btn-' + contentId);
|
||||||
|
if (downloadBtn) {
|
||||||
|
downloadBtn.href = fileUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update metadata
|
||||||
|
document.getElementById('info-filename-' + contentId).textContent = filename;
|
||||||
|
document.getElementById('info-version-' + contentId).textContent = 'v' + version;
|
||||||
|
document.getElementById('info-user-' + contentId).textContent = user;
|
||||||
|
document.getElementById('info-date-' + contentId).textContent = modifiedDate;
|
||||||
|
document.getElementById('info-created-' + contentId).textContent = createdDate;
|
||||||
|
|
||||||
|
// Update active state on version items
|
||||||
|
document.querySelectorAll('.version-item').forEach(item => {
|
||||||
|
if (item.id.startsWith('version-' + contentId + '-')) {
|
||||||
|
item.classList.remove('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.getElementById('version-' + contentId + '-' + version).classList.add('active');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -247,6 +247,33 @@
|
|||||||
color: #a0aec0 !important;
|
color: #a0aec0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Edited Media Cards Dark Mode */
|
||||||
|
body.dark-mode .card[style*="border: 2px solid #7c3aed"] {
|
||||||
|
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%) !important;
|
||||||
|
border-color: #8b5cf6 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .card[style*="border: 2px solid #7c3aed"] h3 {
|
||||||
|
color: #a78bfa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .card[style*="border: 2px solid #7c3aed"] div[style*="background: white"] {
|
||||||
|
background: #374151 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .card[style*="border: 2px solid #7c3aed"] strong {
|
||||||
|
color: #a78bfa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .card[style*="border: 2px solid #7c3aed"] p[style*="color: #475569"],
|
||||||
|
body.dark-mode .card[style*="border: 2px solid #7c3aed"] p[style*="color: #64748b"] {
|
||||||
|
color: #cbd5e1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode div[style*="background: #f8f9fa; border-radius: 8px"] {
|
||||||
|
background: #2d3748 !important;
|
||||||
|
}
|
||||||
|
|
||||||
body.dark-mode .card > div[style*="text-align: center"] p {
|
body.dark-mode .card > div[style*="text-align: center"] p {
|
||||||
color: #a0aec0 !important;
|
color: #a0aec0 !important;
|
||||||
}
|
}
|
||||||
@@ -611,6 +638,111 @@ document.addEventListener('keydown', function(event) {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Edited Media Section - Full Width -->
|
||||||
|
<div class="card" style="margin-top: 2rem;">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;">
|
||||||
|
<h2 style="display: flex; align-items: center; gap: 0.5rem; margin: 0;">
|
||||||
|
<img src="{{ url_for('static', filename='icons/edit.svg') }}" alt="" style="width: 24px; height: 24px;">
|
||||||
|
Edited Media on the Player
|
||||||
|
</h2>
|
||||||
|
{% if edited_media %}
|
||||||
|
<a href="{{ url_for('players.edited_media', player_id=player.id) }}"
|
||||||
|
class="btn"
|
||||||
|
style="background: #7c3aed; color: white; padding: 0.5rem 1rem; text-decoration: none; border-radius: 6px; font-size: 0.9rem; display: inline-flex; align-items: center; gap: 0.5rem; transition: background 0.2s;"
|
||||||
|
onmouseover="this.style.background='#6d28d9'"
|
||||||
|
onmouseout="this.style.background='#7c3aed'">
|
||||||
|
📋 View All Edited Media
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<p style="color: #6c757d; font-size: 0.9rem; margin-top: 0.5rem;">Latest 3 edited files with their most recent versions</p>
|
||||||
|
|
||||||
|
{% if edited_media %}
|
||||||
|
{% set edited_by_content = {} %}
|
||||||
|
{% for edit in edited_media %}
|
||||||
|
{% if edit.content_id not in edited_by_content %}
|
||||||
|
{% set _ = edited_by_content.update({edit.content_id: {'original_name': edit.original_name, 'content_id': edit.content_id, 'latest_version': edit}}) %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;">
|
||||||
|
{% for content_id, data in edited_by_content.items() %}
|
||||||
|
{% if loop.index <= 3 %}
|
||||||
|
<div class="card" style="background: linear-gradient(135deg, #f8f9fa 0%, #fff 100%); border: 2px solid #7c3aed; box-shadow: 0 2px 8px rgba(124, 58, 237, 0.1);">
|
||||||
|
{% set edit = data.latest_version %}
|
||||||
|
|
||||||
|
<!-- Image Preview if it's an image -->
|
||||||
|
{% if edit.new_name.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.webp')) %}
|
||||||
|
<div style="width: 100%; height: 200px; overflow: hidden; border-radius: 8px 8px 0 0; background: #000;">
|
||||||
|
<img src="{{ url_for('static', filename='uploads/edited_media/' ~ edit.content_id ~ '/' ~ edit.new_name) }}"
|
||||||
|
alt="{{ edit.new_name }}"
|
||||||
|
style="width: 100%; height: 100%; object-fit: contain;">
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div style="padding: 1rem;">
|
||||||
|
<h3 style="margin: 0 0 0.75rem 0; color: #7c3aed; font-size: 1rem; display: flex; align-items: center; gap: 0.5rem;">
|
||||||
|
✏️ {{ data.original_name }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div style="padding: 0.75rem; background: white; border-radius: 6px; border-left: 4px solid #7c3aed; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 0.5rem;">
|
||||||
|
<strong style="color: #7c3aed; font-size: 0.95rem;">
|
||||||
|
Version {{ edit.version }}
|
||||||
|
<span style="background: #7c3aed; color: white; padding: 0.15rem 0.5rem; border-radius: 12px; font-size: 0.75rem; margin-left: 0.5rem;">Latest</span>
|
||||||
|
</strong>
|
||||||
|
<small style="color: #6c757d; white-space: nowrap;">
|
||||||
|
{{ edit.created_at | localtime('%m/%d %H:%M') }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="margin: 0.25rem 0; font-size: 0.85rem; color: #475569;">
|
||||||
|
📄 {{ edit.new_name }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% if edit.user %}
|
||||||
|
<p style="margin: 0.25rem 0; font-size: 0.8rem; color: #64748b;">
|
||||||
|
👤 {{ edit.user }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if edit.time_of_modification %}
|
||||||
|
<p style="margin: 0.25rem 0; font-size: 0.8rem; color: #64748b;">
|
||||||
|
🕒 {{ edit.time_of_modification | localtime('%Y-%m-%d %H:%M') }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div style="margin-top: 0.75rem; display: flex; gap: 0.5rem;">
|
||||||
|
<a href="{{ url_for('static', filename='uploads/edited_media/' ~ edit.content_id ~ '/' ~ edit.new_name) }}"
|
||||||
|
target="_blank"
|
||||||
|
style="display: inline-flex; align-items: center; gap: 0.25rem; padding: 0.4rem 0.75rem; background: #7c3aed; color: white; text-decoration: none; border-radius: 4px; font-size: 0.8rem; transition: background 0.2s;"
|
||||||
|
onmouseover="this.style.background='#6d28d9'"
|
||||||
|
onmouseout="this.style.background='#7c3aed'">
|
||||||
|
📥 View File
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('static', filename='uploads/edited_media/' ~ edit.content_id ~ '/' ~ edit.new_name) }}"
|
||||||
|
download
|
||||||
|
style="display: inline-flex; align-items: center; gap: 0.25rem; padding: 0.4rem 0.75rem; background: #64748b; color: white; text-decoration: none; border-radius: 4px; font-size: 0.8rem; transition: background 0.2s;"
|
||||||
|
onmouseover="this.style.background='#475569'"
|
||||||
|
onmouseout="this.style.background='#64748b'">
|
||||||
|
💾 Download
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div style="text-align: center; padding: 3rem; color: #6c757d; background: #f8f9fa; border-radius: 8px; margin-top: 1.5rem;">
|
||||||
|
<p style="font-size: 2rem; margin: 0;">📝</p>
|
||||||
|
<p style="margin: 0.5rem 0 0 0; font-size: 1.1rem; font-weight: 500;">No edited media yet</p>
|
||||||
|
<p style="font-size: 0.9rem; margin: 0.5rem 0 0 0;">Media edits will appear here once the player sends edited files</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Additional Info Section -->
|
<!-- Additional Info Section -->
|
||||||
<div class="card" style="margin-top: 2rem;">
|
<div class="card" style="margin-top: 2rem;">
|
||||||
<h2>ℹ️ Player Information</h2>
|
<h2>ℹ️ Player Information</h2>
|
||||||
|
|||||||
154
app/utils/caddy_manager.py
Normal file
154
app/utils/caddy_manager.py
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
"""Caddy configuration generator and manager."""
|
||||||
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
from app.models.https_config import HTTPSConfig
|
||||||
|
|
||||||
|
|
||||||
|
class CaddyConfigGenerator:
|
||||||
|
"""Generate Caddyfile configuration based on HTTPSConfig."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_caddyfile(config: Optional[HTTPSConfig] = None) -> str:
|
||||||
|
"""Generate complete Caddyfile content.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: HTTPSConfig instance or None
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Complete Caddyfile content as string
|
||||||
|
"""
|
||||||
|
# Get config from database if not provided
|
||||||
|
if config is None:
|
||||||
|
config = HTTPSConfig.get_config()
|
||||||
|
|
||||||
|
# Base configuration
|
||||||
|
email = "admin@localhost"
|
||||||
|
if config and config.email:
|
||||||
|
email = config.email
|
||||||
|
|
||||||
|
base_config = f"""{{
|
||||||
|
# Global options
|
||||||
|
email {email}
|
||||||
|
# Admin API for configuration management (listen on all interfaces)
|
||||||
|
admin 0.0.0.0:2019
|
||||||
|
# Uncomment for testing to avoid rate limits
|
||||||
|
# acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
|
||||||
|
}}
|
||||||
|
|
||||||
|
# Shared reverse proxy configuration
|
||||||
|
(reverse_proxy_config) {{
|
||||||
|
reverse_proxy digiserver-app:5000 {{
|
||||||
|
header_up Host {{host}}
|
||||||
|
header_up X-Real-IP {{remote_host}}
|
||||||
|
header_up X-Forwarded-Proto {{scheme}}
|
||||||
|
|
||||||
|
# Timeouts for large uploads
|
||||||
|
transport http {{
|
||||||
|
read_timeout 300s
|
||||||
|
write_timeout 300s
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
# File upload size limit (2GB)
|
||||||
|
request_body {{
|
||||||
|
max_size 2GB
|
||||||
|
}}
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
header {{
|
||||||
|
X-Frame-Options "SAMEORIGIN"
|
||||||
|
X-Content-Type-Options "nosniff"
|
||||||
|
X-XSS-Protection "1; mode=block"
|
||||||
|
}}
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
log {{
|
||||||
|
output file /var/log/caddy/access.log
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
# Localhost (development/local access)
|
||||||
|
http://localhost {{
|
||||||
|
import reverse_proxy_config
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Add main domain/IP configuration if HTTPS is enabled
|
||||||
|
if config and config.https_enabled and config.domain and config.ip_address:
|
||||||
|
# Internal domain configuration
|
||||||
|
domain_config = f"""
|
||||||
|
# Internal domain (HTTP only - internal use)
|
||||||
|
http://{config.domain} {{
|
||||||
|
import reverse_proxy_config
|
||||||
|
}}
|
||||||
|
|
||||||
|
# Handle IP address access
|
||||||
|
http://{config.ip_address} {{
|
||||||
|
import reverse_proxy_config
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
base_config += domain_config
|
||||||
|
else:
|
||||||
|
# Default fallback configuration
|
||||||
|
base_config += """
|
||||||
|
# Internal domain (HTTP only - internal use)
|
||||||
|
http://digiserver.sibiusb.harting.intra {
|
||||||
|
import reverse_proxy_config
|
||||||
|
}
|
||||||
|
|
||||||
|
# Handle IP address access
|
||||||
|
http://10.76.152.164 {
|
||||||
|
import reverse_proxy_config
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Add catch-all for any other HTTP requests
|
||||||
|
base_config += """
|
||||||
|
# Catch-all for any other HTTP requests
|
||||||
|
http://* {
|
||||||
|
import reverse_proxy_config
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
return base_config
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def write_caddyfile(caddyfile_content: str, path: str = '/app/Caddyfile') -> bool:
|
||||||
|
"""Write Caddyfile to disk.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
caddyfile_content: Content to write
|
||||||
|
path: Path to Caddyfile
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if successful, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(path, 'w') as f:
|
||||||
|
f.write(caddyfile_content)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error writing Caddyfile: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def reload_caddy() -> bool:
|
||||||
|
"""Reload Caddy configuration without restart.
|
||||||
|
|
||||||
|
Note: Caddy monitoring is handled via file watching. After writing the Caddyfile,
|
||||||
|
Caddy should automatically reload. If it doesn't, you may need to restart the
|
||||||
|
Caddy container manually.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if configuration was written successfully (Caddy will auto-reload)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Just verify that Caddy is reachable
|
||||||
|
import urllib.request
|
||||||
|
response = urllib.request.urlopen('http://caddy:2019/config/', timeout=2)
|
||||||
|
return response.status == 200
|
||||||
|
except Exception as e:
|
||||||
|
# Caddy might not be reachable, but Caddyfile was already written
|
||||||
|
# Caddy should reload automatically when it detects file changes
|
||||||
|
print(f"Note: Caddy reload check returned: {str(e)}")
|
||||||
|
return True # Return True anyway since Caddyfile was written
|
||||||
120
app/utils/nginx_config_reader.py
Normal file
120
app/utils/nginx_config_reader.py
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
"""Nginx configuration reader utility."""
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from typing import Dict, List, Optional, Any
|
||||||
|
|
||||||
|
|
||||||
|
class NginxConfigReader:
|
||||||
|
"""Read and parse Nginx configuration files."""
|
||||||
|
|
||||||
|
def __init__(self, config_path: str = '/etc/nginx/nginx.conf'):
|
||||||
|
"""Initialize Nginx config reader."""
|
||||||
|
self.config_path = config_path
|
||||||
|
self.config_content = None
|
||||||
|
self.is_available = os.path.exists(config_path)
|
||||||
|
|
||||||
|
if self.is_available:
|
||||||
|
try:
|
||||||
|
with open(config_path, 'r') as f:
|
||||||
|
self.config_content = f.read()
|
||||||
|
except Exception as e:
|
||||||
|
self.is_available = False
|
||||||
|
self.error = str(e)
|
||||||
|
|
||||||
|
def get_status(self) -> Dict[str, Any]:
|
||||||
|
"""Get Nginx configuration status."""
|
||||||
|
if not self.is_available:
|
||||||
|
return {
|
||||||
|
'available': False,
|
||||||
|
'error': 'Nginx configuration not found',
|
||||||
|
'path': self.config_path
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'available': True,
|
||||||
|
'path': self.config_path,
|
||||||
|
'file_exists': os.path.exists(self.config_path),
|
||||||
|
'ssl_enabled': self._check_ssl_enabled(),
|
||||||
|
'http_ports': self._extract_http_ports(),
|
||||||
|
'https_ports': self._extract_https_ports(),
|
||||||
|
'upstream_servers': self._extract_upstream_servers(),
|
||||||
|
'server_names': self._extract_server_names(),
|
||||||
|
'ssl_protocols': self._extract_ssl_protocols(),
|
||||||
|
'client_max_body_size': self._extract_client_max_body_size(),
|
||||||
|
'gzip_enabled': self._check_gzip_enabled(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _check_ssl_enabled(self) -> bool:
|
||||||
|
"""Check if SSL is enabled."""
|
||||||
|
if not self.config_content:
|
||||||
|
return False
|
||||||
|
return 'ssl_certificate' in self.config_content
|
||||||
|
|
||||||
|
def _extract_http_ports(self) -> List[int]:
|
||||||
|
"""Extract HTTP listening ports."""
|
||||||
|
if not self.config_content:
|
||||||
|
return []
|
||||||
|
pattern = r'listen\s+(\d+)'
|
||||||
|
matches = re.findall(pattern, self.config_content)
|
||||||
|
return sorted(list(set(int(p) for p in matches if int(p) < 1000)))
|
||||||
|
|
||||||
|
def _extract_https_ports(self) -> List[int]:
|
||||||
|
"""Extract HTTPS listening ports."""
|
||||||
|
if not self.config_content:
|
||||||
|
return []
|
||||||
|
pattern = r'listen\s+(\d+).*ssl'
|
||||||
|
matches = re.findall(pattern, self.config_content)
|
||||||
|
return sorted(list(set(int(p) for p in matches)))
|
||||||
|
|
||||||
|
def _extract_upstream_servers(self) -> List[str]:
|
||||||
|
"""Extract upstream servers."""
|
||||||
|
if not self.config_content:
|
||||||
|
return []
|
||||||
|
upstream_match = re.search(r'upstream\s+\w+\s*{([^}]+)}', self.config_content)
|
||||||
|
if upstream_match:
|
||||||
|
upstream_content = upstream_match.group(1)
|
||||||
|
servers = re.findall(r'server\s+([^\s;]+)', upstream_content)
|
||||||
|
return servers
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _extract_server_names(self) -> List[str]:
|
||||||
|
"""Extract server names."""
|
||||||
|
if not self.config_content:
|
||||||
|
return []
|
||||||
|
pattern = r'server_name\s+([^;]+);'
|
||||||
|
matches = re.findall(pattern, self.config_content)
|
||||||
|
result = []
|
||||||
|
for match in matches:
|
||||||
|
names = match.strip().split()
|
||||||
|
result.extend(names)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _extract_ssl_protocols(self) -> List[str]:
|
||||||
|
"""Extract SSL protocols."""
|
||||||
|
if not self.config_content:
|
||||||
|
return []
|
||||||
|
pattern = r'ssl_protocols\s+([^;]+);'
|
||||||
|
match = re.search(pattern, self.config_content)
|
||||||
|
if match:
|
||||||
|
return match.group(1).strip().split()
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _extract_client_max_body_size(self) -> Optional[str]:
|
||||||
|
"""Extract client max body size."""
|
||||||
|
if not self.config_content:
|
||||||
|
return None
|
||||||
|
pattern = r'client_max_body_size\s+([^;]+);'
|
||||||
|
match = re.search(pattern, self.config_content)
|
||||||
|
return match.group(1).strip() if match else None
|
||||||
|
|
||||||
|
def _check_gzip_enabled(self) -> bool:
|
||||||
|
"""Check if gzip is enabled."""
|
||||||
|
if not self.config_content:
|
||||||
|
return False
|
||||||
|
return bool(re.search(r'gzip\s+on\s*;', self.config_content))
|
||||||
|
|
||||||
|
|
||||||
|
def get_nginx_status() -> Dict[str, Any]:
|
||||||
|
"""Get Nginx configuration status."""
|
||||||
|
reader = NginxConfigReader()
|
||||||
|
return reader.get_status()
|
||||||
174
deploy.sh
Executable file
174
deploy.sh
Executable file
@@ -0,0 +1,174 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Automated deployment script for DigiServer on a new PC
|
||||||
|
# Run this script to completely set up DigiServer with all configurations
|
||||||
|
|
||||||
|
set -e # Exit on any error
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
echo -e "${BLUE}╔════════════════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${BLUE}║ DigiServer Automated Deployment ║${NC}"
|
||||||
|
echo -e "${BLUE}╚════════════════════════════════════════════════════════════════╝${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if docker-compose is available
|
||||||
|
if ! command -v docker-compose &> /dev/null; then
|
||||||
|
echo -e "${RED}❌ docker-compose not found!${NC}"
|
||||||
|
echo "Please install docker-compose first"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if we're in the project directory
|
||||||
|
if [ ! -f "docker-compose.yml" ]; then
|
||||||
|
echo -e "${RED}❌ docker-compose.yml not found!${NC}"
|
||||||
|
echo "Please run this script from the digiserver-v2 directory"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# CONFIGURATION VARIABLES
|
||||||
|
# ============================================================================
|
||||||
|
HOSTNAME="${HOSTNAME:-digiserver}"
|
||||||
|
DOMAIN="${DOMAIN:-digiserver.sibiusb.harting.intra}"
|
||||||
|
IP_ADDRESS="${IP_ADDRESS:-10.76.152.164}"
|
||||||
|
EMAIL="${EMAIL:-admin@example.com}"
|
||||||
|
PORT="${PORT:-443}"
|
||||||
|
|
||||||
|
echo -e "${BLUE}Configuration:${NC}"
|
||||||
|
echo " Hostname: $HOSTNAME"
|
||||||
|
echo " Domain: $DOMAIN"
|
||||||
|
echo " IP Address: $IP_ADDRESS"
|
||||||
|
echo " Email: $EMAIL"
|
||||||
|
echo " Port: $PORT"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# STEP 1: Start containers
|
||||||
|
# ============================================================================
|
||||||
|
echo -e "${YELLOW}📦 [1/6] Starting containers...${NC}"
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
echo -e "${YELLOW}⏳ Waiting for containers to be healthy...${NC}"
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
# Verify containers are running
|
||||||
|
if ! docker-compose ps | grep -q "Up"; then
|
||||||
|
echo -e "${RED}❌ Containers failed to start!${NC}"
|
||||||
|
docker-compose logs
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo -e "${GREEN}✅ Containers started successfully${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# STEP 2: Run database migrations
|
||||||
|
# ============================================================================
|
||||||
|
echo -e "${YELLOW}📊 [2/6] Running database migrations...${NC}"
|
||||||
|
|
||||||
|
echo -e " • Creating https_config table..."
|
||||||
|
docker-compose exec -T digiserver-app python /app/migrations/add_https_config_table.py
|
||||||
|
echo -e " • Creating player_user table..."
|
||||||
|
docker-compose exec -T digiserver-app python /app/migrations/add_player_user_table.py
|
||||||
|
echo -e " • Adding email to https_config..."
|
||||||
|
docker-compose exec -T digiserver-app python /app/migrations/add_email_to_https_config.py
|
||||||
|
echo -e " • Migrating player_user global settings..."
|
||||||
|
docker-compose exec -T digiserver-app python /app/migrations/migrate_player_user_global.py
|
||||||
|
|
||||||
|
echo -e "${GREEN}✅ All database migrations completed${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# STEP 3: Configure HTTPS
|
||||||
|
# ============================================================================
|
||||||
|
echo -e "${YELLOW}🔒 [3/6] Configuring HTTPS...${NC}"
|
||||||
|
|
||||||
|
docker-compose exec -T digiserver-app python /app/https_manager.py enable \
|
||||||
|
"$HOSTNAME" \
|
||||||
|
"$DOMAIN" \
|
||||||
|
"$EMAIL" \
|
||||||
|
"$IP_ADDRESS" \
|
||||||
|
"$PORT"
|
||||||
|
|
||||||
|
echo -e "${GREEN}✅ HTTPS configured successfully${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# STEP 4: Verify database setup
|
||||||
|
# ============================================================================
|
||||||
|
echo -e "${YELLOW}🔍 [4/6] Verifying database setup...${NC}"
|
||||||
|
|
||||||
|
docker-compose exec -T digiserver-app python -c "
|
||||||
|
from app.app import create_app
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
with app.app_context():
|
||||||
|
inspector = inspect(app.extensions.db.engine)
|
||||||
|
tables = inspector.get_table_names()
|
||||||
|
print(' Database tables:')
|
||||||
|
for table in sorted(tables):
|
||||||
|
print(f' ✓ {table}')
|
||||||
|
print(f'')
|
||||||
|
print(f' ✅ Total tables: {len(tables)}')
|
||||||
|
" 2>/dev/null || echo " ⚠️ Database verification skipped"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# STEP 5: Verify Caddy configuration
|
||||||
|
# ============================================================================
|
||||||
|
echo -e "${YELLOW}🔧 [5/6] Verifying Caddy configuration...${NC}"
|
||||||
|
|
||||||
|
docker-compose exec -T caddy caddy validate --config /etc/caddy/Caddyfile >/dev/null 2>&1
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo -e " ${GREEN}✅ Caddy configuration is valid${NC}"
|
||||||
|
else
|
||||||
|
echo -e " ${YELLOW}⚠️ Caddy validation skipped${NC}"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# STEP 6: Display summary
|
||||||
|
# ============================================================================
|
||||||
|
echo -e "${YELLOW}📋 [6/6] Displaying configuration summary...${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
docker-compose exec -T digiserver-app python /app/https_manager.py status
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}╔════════════════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${GREEN}║ 🎉 Deployment Complete! ║${NC}"
|
||||||
|
echo -e "${GREEN}╚════════════════════════════════════════════════════════════════╝${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${BLUE}📍 Access Points:${NC}"
|
||||||
|
echo " 🔒 https://$HOSTNAME"
|
||||||
|
echo " 🔒 https://$IP_ADDRESS"
|
||||||
|
echo " 🔒 https://$DOMAIN"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${BLUE}📝 Default Credentials:${NC}"
|
||||||
|
echo " Username: admin"
|
||||||
|
echo " Password: admin123 (⚠️ CHANGE IN PRODUCTION)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${BLUE}📚 Documentation:${NC}"
|
||||||
|
echo " • DEPLOYMENT_COMMANDS.md - Detailed docker exec commands"
|
||||||
|
echo " • HTTPS_CONFIGURATION.md - HTTPS setup details"
|
||||||
|
echo " • setup_https.sh - Manual configuration script"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${YELLOW}Next Steps:${NC}"
|
||||||
|
echo "1. Access the application at one of the URLs above"
|
||||||
|
echo "2. Log in with admin credentials"
|
||||||
|
echo "3. Change the default password immediately"
|
||||||
|
echo "4. Configure your players and content"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo -e "${BLUE}📞 Support:${NC}"
|
||||||
|
echo "For troubleshooting, see DEPLOYMENT_COMMANDS.md section 7"
|
||||||
|
echo ""
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
#version: '3.8'
|
#version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
digiserver:
|
digiserver-app:
|
||||||
build: .
|
build: .
|
||||||
container_name: digiserver-v2
|
container_name: digiserver-v2
|
||||||
ports:
|
# Don't expose directly; use Caddy reverse proxy instead
|
||||||
- "80:5000"
|
expose:
|
||||||
|
- "5000"
|
||||||
volumes:
|
volumes:
|
||||||
- ./instance:/app/instance
|
- ./data:/app
|
||||||
- ./app/static/uploads:/app/app/static/uploads
|
- ./data/instance:/app/instance
|
||||||
|
- ./data/uploads:/app/app/static/uploads
|
||||||
environment:
|
environment:
|
||||||
- FLASK_ENV=production
|
- FLASK_ENV=production
|
||||||
- SECRET_KEY=${SECRET_KEY:-your-secret-key-change-this}
|
- SECRET_KEY=${SECRET_KEY:-your-secret-key-change-this}
|
||||||
@@ -21,14 +23,38 @@ services:
|
|||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 40s
|
start_period: 40s
|
||||||
|
networks:
|
||||||
|
- digiserver-network
|
||||||
|
|
||||||
# Optional: Redis for caching (uncomment if needed)
|
# Nginx reverse proxy with HTTPS support
|
||||||
# redis:
|
nginx:
|
||||||
# image: redis:7-alpine
|
image: nginx:alpine
|
||||||
# container_name: digiserver-redis
|
container_name: digiserver-nginx
|
||||||
# restart: unless-stopped
|
ports:
|
||||||
# volumes:
|
- "80:80"
|
||||||
# - redis-data:/data
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
|
- ./nginx-custom-domains.conf:/etc/nginx/conf.d/custom-domains.conf:rw
|
||||||
|
- ./data/nginx-ssl:/etc/nginx/ssl:ro
|
||||||
|
- ./data/nginx-logs:/var/log/nginx
|
||||||
|
- ./data/certbot:/var/www/certbot:ro # For Let's Encrypt ACME challenges
|
||||||
|
environment:
|
||||||
|
- DOMAIN=${DOMAIN:-localhost}
|
||||||
|
- EMAIL=${EMAIL:-admin@localhost}
|
||||||
|
depends_on:
|
||||||
|
digiserver-app:
|
||||||
|
condition: service_started
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:80/"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
networks:
|
||||||
|
- digiserver-network
|
||||||
|
|
||||||
# volumes:
|
networks:
|
||||||
# redis-data:
|
digiserver-network:
|
||||||
|
driver: bridge
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ mkdir -p /app/instance
|
|||||||
mkdir -p /app/app/static/uploads
|
mkdir -p /app/app/static/uploads
|
||||||
|
|
||||||
# Initialize database if it doesn't exist
|
# Initialize database if it doesn't exist
|
||||||
if [ ! -f /app/instance/digiserver.db ]; then
|
if [ ! -f /app/instance/dashboard.db ]; then
|
||||||
echo "Initializing database..."
|
echo "Initializing database..."
|
||||||
python -c "
|
python -c "
|
||||||
from app.app import create_app
|
from app.app import create_app
|
||||||
from app.extensions import db, bcrypt
|
from app.extensions import db, bcrypt
|
||||||
from app.models import User
|
from app.models import User
|
||||||
|
|
||||||
app = create_app()
|
app = create_app('production')
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
db.create_all()
|
db.create_all()
|
||||||
|
|
||||||
@@ -49,4 +49,4 @@ exec gunicorn \
|
|||||||
--timeout 120 \
|
--timeout 120 \
|
||||||
--access-logfile - \
|
--access-logfile - \
|
||||||
--error-logfile - \
|
--error-logfile - \
|
||||||
"app.app:create_app()"
|
"app.app:create_app('production')"
|
||||||
|
|||||||
25
fix_player_user_schema.py
Normal file
25
fix_player_user_schema.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Fix player_user table schema by dropping and recreating it."""
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, '/app')
|
||||||
|
|
||||||
|
from app.app import create_app
|
||||||
|
from app.extensions import db
|
||||||
|
from app.models.player_user import PlayerUser
|
||||||
|
|
||||||
|
def main():
|
||||||
|
app = create_app('production')
|
||||||
|
with app.app_context():
|
||||||
|
# Drop the old table
|
||||||
|
print("Dropping player_user table...")
|
||||||
|
db.session.execute(db.text('DROP TABLE IF EXISTS player_user'))
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Recreate with new schema
|
||||||
|
print("Creating player_user table with new schema...")
|
||||||
|
PlayerUser.__table__.create(db.engine)
|
||||||
|
|
||||||
|
print("Done! player_user table recreated successfully.")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
30
generate_nginx_certs.sh
Executable file
30
generate_nginx_certs.sh
Executable file
@@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Generate self-signed SSL certificates for Nginx
|
||||||
|
# Usage: ./generate_nginx_certs.sh [domain] [days]
|
||||||
|
|
||||||
|
DOMAIN=${1:-localhost}
|
||||||
|
DAYS=${2:-365}
|
||||||
|
CERT_DIR="./data/nginx-ssl"
|
||||||
|
|
||||||
|
echo "🔐 Generating self-signed SSL certificate for Nginx"
|
||||||
|
echo "Domain: $DOMAIN"
|
||||||
|
echo "Valid for: $DAYS days"
|
||||||
|
echo "Certificate directory: $CERT_DIR"
|
||||||
|
|
||||||
|
# Create directory if it doesnt exist
|
||||||
|
mkdir -p "$CERT_DIR"
|
||||||
|
|
||||||
|
# Generate private key and certificate
|
||||||
|
openssl req -x509 -nodes -days "$DAYS" \
|
||||||
|
-newkey rsa:2048 \
|
||||||
|
-keyout "$CERT_DIR/key.pem" \
|
||||||
|
-out "$CERT_DIR/cert.pem" \
|
||||||
|
-subj "/CN=$DOMAIN/O=DigiServer/C=US"
|
||||||
|
|
||||||
|
# Set proper permissions
|
||||||
|
chmod 644 "$CERT_DIR/cert.pem"
|
||||||
|
chmod 600 "$CERT_DIR/key.pem"
|
||||||
|
|
||||||
|
echo "✅ Certificates generated successfully!"
|
||||||
|
echo "Certificate: $CERT_DIR/cert.pem"
|
||||||
|
echo "Key: $CERT_DIR/key.pem"
|
||||||
29
init-data.sh
Executable file
29
init-data.sh
Executable file
@@ -0,0 +1,29 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Initialize ./data folder with all necessary files for deployment
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🔧 Initializing data folder..."
|
||||||
|
mkdir -p data/{app,instance,uploads}
|
||||||
|
|
||||||
|
echo "📁 Copying app folder..."
|
||||||
|
rm -rf data/app
|
||||||
|
mkdir -p data/app
|
||||||
|
cp -r app/* data/app/
|
||||||
|
|
||||||
|
echo "📋 Copying migrations..."
|
||||||
|
rm -rf data/migrations
|
||||||
|
cp -r migrations data/
|
||||||
|
|
||||||
|
echo "🔧 Copying utility scripts..."
|
||||||
|
cp fix_player_user_schema.py data/
|
||||||
|
|
||||||
|
echo "🔐 Setting permissions..."
|
||||||
|
chmod 755 data/{app,instance,uploads}
|
||||||
|
chmod -R 755 data/app/
|
||||||
|
find data/app -type f \( -name "*.py" -o -name "*.html" -o -name "*.css" -o -name "*.js" \) -exec chmod 644 {} \;
|
||||||
|
chmod 777 data/instance data/uploads
|
||||||
|
|
||||||
|
echo "✅ Data folder initialized successfully!"
|
||||||
|
echo "📊 Data folder contents:"
|
||||||
|
du -sh data/*/
|
||||||
0
install_emoji_fonts.sh
Normal file → Executable file
0
install_emoji_fonts.sh
Normal file → Executable file
33
migrations/add_email_to_https_config.py
Normal file
33
migrations/add_email_to_https_config.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
"""Add email field to https_config table."""
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, '/app')
|
||||||
|
|
||||||
|
from app.app import create_app
|
||||||
|
from app.extensions import db
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
print("Adding email column to https_config table...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check if column already exists
|
||||||
|
inspector = db.inspect(db.engine)
|
||||||
|
columns = [col['name'] for col in inspector.get_columns('https_config')]
|
||||||
|
|
||||||
|
if 'email' not in columns:
|
||||||
|
# Add the column
|
||||||
|
with db.engine.connect() as conn:
|
||||||
|
conn.execute(text('ALTER TABLE https_config ADD COLUMN email VARCHAR(255)'))
|
||||||
|
conn.commit()
|
||||||
|
print("✓ Email column added to https_config table!")
|
||||||
|
else:
|
||||||
|
print("✓ Email column already exists in https_config table.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ℹ️ Note: If table doesn't exist, run add_https_config_table.py first")
|
||||||
|
print(f"Error details: {str(e)}")
|
||||||
|
print("\nAlternatively, you can reset the database:")
|
||||||
|
print(" rm instance/digiserver.db")
|
||||||
|
print(" python migrations/add_https_config_table.py")
|
||||||
14
migrations/add_https_config_table.py
Normal file
14
migrations/add_https_config_table.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
"""Add https_config table for HTTPS configuration management."""
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, '/app')
|
||||||
|
|
||||||
|
from app.app import create_app
|
||||||
|
from app.extensions import db
|
||||||
|
from app.models.https_config import HTTPSConfig
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
print("Creating https_config table...")
|
||||||
|
db.create_all()
|
||||||
|
print("✓ https_config table created successfully!")
|
||||||
14
migrations/add_player_user_table.py
Normal file
14
migrations/add_player_user_table.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
"""Add player_user table for user code mappings."""
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, '/app')
|
||||||
|
|
||||||
|
from app.app import create_app
|
||||||
|
from app.extensions import db
|
||||||
|
from app.models.player_user import PlayerUser
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
print("Creating player_user table...")
|
||||||
|
db.create_all()
|
||||||
|
print("✓ player_user table created successfully!")
|
||||||
24
migrations/migrate_player_user_global.py
Normal file
24
migrations/migrate_player_user_global.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"""Migrate player_user table to remove player_id and make user_code unique globally."""
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, '/app')
|
||||||
|
|
||||||
|
from app.app import create_app
|
||||||
|
from app.extensions import db
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
app = create_app('production')
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
print("Migrating player_user table...")
|
||||||
|
|
||||||
|
# Drop existing table and recreate with new schema
|
||||||
|
db.session.execute(text('DROP TABLE IF EXISTS player_user'))
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Create new table
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
print("✓ player_user table migrated successfully!")
|
||||||
|
print(" - Removed player_id foreign key")
|
||||||
|
print(" - Made user_code unique globally")
|
||||||
|
print(" - user_name is now nullable")
|
||||||
21
nginx-custom-domains.conf
Normal file
21
nginx-custom-domains.conf
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Nginx configuration for custom HTTPS domains
|
||||||
|
# This file will be dynamically generated based on HTTPSConfig database entries
|
||||||
|
# Include this in your nginx.conf with: include /etc/nginx/conf.d/custom-domains.conf;
|
||||||
|
|
||||||
|
# Example entry for custom domain:
|
||||||
|
# server {
|
||||||
|
# listen 443 ssl http2;
|
||||||
|
# listen [::]:443 ssl http2;
|
||||||
|
# server_name digiserver.example.com;
|
||||||
|
#
|
||||||
|
# ssl_certificate /etc/nginx/ssl/custom/cert.pem;
|
||||||
|
# ssl_certificate_key /etc/nginx/ssl/custom/key.pem;
|
||||||
|
#
|
||||||
|
# location / {
|
||||||
|
# proxy_pass http://digiserver_app;
|
||||||
|
# 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;
|
||||||
|
# }
|
||||||
|
# }
|
||||||
117
nginx.conf
Normal file
117
nginx.conf
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
user nginx;
|
||||||
|
worker_processes auto;
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
use epoll;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
tcp_nodelay on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
types_hash_max_size 2048;
|
||||||
|
client_max_body_size 2048M;
|
||||||
|
|
||||||
|
# Gzip compression
|
||||||
|
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/rss+xml;
|
||||||
|
|
||||||
|
# Upstream to Flask application
|
||||||
|
upstream digiserver_app {
|
||||||
|
server digiserver-app:5000;
|
||||||
|
keepalive 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTP Server - redirect to HTTPS
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
listen [::]:80 default_server;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
# Allow ACME challenges for Let's Encrypt
|
||||||
|
location /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/certbot;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Redirect HTTP to HTTPS for non-ACME requests
|
||||||
|
location / {
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTPS Server (with self-signed cert by default)
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2 default_server;
|
||||||
|
listen [::]:443 ssl http2 default_server;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
# SSL certificate paths (will be volume-mounted)
|
||||||
|
ssl_certificate /etc/nginx/ssl/cert.pem;
|
||||||
|
ssl_certificate_key /etc/nginx/ssl/key.pem;
|
||||||
|
|
||||||
|
# SSL Configuration
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
ssl_session_cache shared:SSL:10m;
|
||||||
|
ssl_session_timeout 10m;
|
||||||
|
|
||||||
|
# Security Headers
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||||
|
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
|
||||||
|
|
||||||
|
# Proxy settings
|
||||||
|
location / {
|
||||||
|
proxy_pass http://digiserver_app;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
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 $server_name;
|
||||||
|
|
||||||
|
# Timeouts for large uploads
|
||||||
|
proxy_connect_timeout 300s;
|
||||||
|
proxy_send_timeout 300s;
|
||||||
|
proxy_read_timeout 300s;
|
||||||
|
|
||||||
|
# Buffering
|
||||||
|
proxy_buffering on;
|
||||||
|
proxy_buffer_size 128k;
|
||||||
|
proxy_buffers 4 256k;
|
||||||
|
proxy_busy_buffers_size 256k;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Static files caching
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
proxy_pass http://digiserver_app;
|
||||||
|
proxy_cache_valid 200 60d;
|
||||||
|
expires 60d;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Additional server blocks for custom domains can be included here
|
||||||
|
include /etc/nginx/conf.d/*.conf;
|
||||||
|
}
|
||||||
@@ -5,13 +5,13 @@ FLASK_ENV=development
|
|||||||
# Security
|
# Security
|
||||||
SECRET_KEY=change-this-to-a-random-secret-key
|
SECRET_KEY=change-this-to-a-random-secret-key
|
||||||
|
|
||||||
|
# Domain & SSL (for HTTPS with Caddy)
|
||||||
|
DOMAIN=your-domain.com
|
||||||
|
EMAIL=admin@your-domain.com
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
DATABASE_URL=sqlite:///instance/dev.db
|
DATABASE_URL=sqlite:///instance/dev.db
|
||||||
|
|
||||||
# Redis (for production)
|
|
||||||
REDIS_HOST=redis
|
|
||||||
REDIS_PORT=6379
|
|
||||||
|
|
||||||
# Admin User Credentials (used during initial Docker deployment)
|
# Admin User Credentials (used during initial Docker deployment)
|
||||||
# These credentials are set when the database is first created
|
# These credentials are set when the database is first created
|
||||||
ADMIN_USERNAME=admin
|
ADMIN_USERNAME=admin
|
||||||
295
old_code_documentation/CADDY_DYNAMIC_CONFIG.md
Normal file
295
old_code_documentation/CADDY_DYNAMIC_CONFIG.md
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
# Caddy Dynamic Configuration Management
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The HTTPS configuration system now automatically generates and manages the Caddy configuration in real-time. When an admin updates settings through the admin panel, the Caddyfile is regenerated and reloaded without requiring a full container restart.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### 1. **Configuration Generation**
|
||||||
|
When admin saves HTTPS settings:
|
||||||
|
1. Settings are saved to database (HTTPSConfig table)
|
||||||
|
2. `CaddyConfigGenerator` creates a new Caddyfile based on settings
|
||||||
|
3. Generated Caddyfile is written to disk
|
||||||
|
|
||||||
|
### 2. **Configuration Reload**
|
||||||
|
After Caddyfile is written:
|
||||||
|
1. Caddy reload API is called via `docker-compose exec`
|
||||||
|
2. Caddy validates and applies new configuration
|
||||||
|
3. No downtime - live configuration update
|
||||||
|
|
||||||
|
### 3. **Fallback Configuration**
|
||||||
|
If HTTPS is disabled:
|
||||||
|
1. System uses default hardcoded configuration
|
||||||
|
2. Supports localhost, internal domain, and IP address
|
||||||
|
3. Catch-all configuration for any other requests
|
||||||
|
|
||||||
|
## Files Involved
|
||||||
|
|
||||||
|
### New Files
|
||||||
|
- **`app/utils/caddy_manager.py`** - CaddyConfigGenerator class with:
|
||||||
|
- `generate_caddyfile()` - Generates Caddyfile content
|
||||||
|
- `write_caddyfile()` - Writes to disk
|
||||||
|
- `reload_caddy()` - Reloads via Docker
|
||||||
|
|
||||||
|
### Updated Files
|
||||||
|
- **`app/blueprints/admin.py`** - HTTPS config route now:
|
||||||
|
- Generates new Caddyfile
|
||||||
|
- Writes to disk
|
||||||
|
- Reloads Caddy automatically
|
||||||
|
- Reports status to user
|
||||||
|
|
||||||
|
## Admin Panel Workflow
|
||||||
|
|
||||||
|
### Step 1: User Fills Form
|
||||||
|
```
|
||||||
|
Admin Panel → HTTPS Configuration
|
||||||
|
- Hostname: digiserver
|
||||||
|
- Domain: digiserver.sibiusb.harting.intra
|
||||||
|
- Email: admin@example.com
|
||||||
|
- IP: 10.76.152.164
|
||||||
|
- Port: 443
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Admin Saves Configuration
|
||||||
|
- POST /admin/https-config/update
|
||||||
|
- Settings validated and saved to database
|
||||||
|
- Caddyfile generated dynamically
|
||||||
|
- Caddy reloaded with new configuration
|
||||||
|
|
||||||
|
### Step 3: User Sees Confirmation
|
||||||
|
```
|
||||||
|
✅ HTTPS configuration saved successfully!
|
||||||
|
✅ Caddy configuration updated successfully!
|
||||||
|
Server available at https://digiserver.sibiusb.harting.intra
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Configuration Live
|
||||||
|
- New domain/IP immediately active
|
||||||
|
- No container restart needed
|
||||||
|
- Caddy applying new routes in real-time
|
||||||
|
|
||||||
|
## Generated Caddyfile Structure
|
||||||
|
|
||||||
|
**When HTTPS Enabled:**
|
||||||
|
```caddyfile
|
||||||
|
{
|
||||||
|
email admin@example.com
|
||||||
|
}
|
||||||
|
|
||||||
|
(reverse_proxy_config) {
|
||||||
|
reverse_proxy digiserver-app:5000 { ... }
|
||||||
|
request_body { max_size 2GB }
|
||||||
|
header { ... }
|
||||||
|
log { ... }
|
||||||
|
}
|
||||||
|
|
||||||
|
http://localhost { import reverse_proxy_config }
|
||||||
|
http://digiserver.sibiusb.harting.intra { import reverse_proxy_config }
|
||||||
|
http://10.76.152.164 { import reverse_proxy_config }
|
||||||
|
http://* { import reverse_proxy_config }
|
||||||
|
```
|
||||||
|
|
||||||
|
**When HTTPS Disabled:**
|
||||||
|
```caddyfile
|
||||||
|
{
|
||||||
|
email admin@localhost
|
||||||
|
}
|
||||||
|
|
||||||
|
(reverse_proxy_config) { ... }
|
||||||
|
|
||||||
|
http://localhost { import reverse_proxy_config }
|
||||||
|
http://digiserver.sibiusb.harting.intra { import reverse_proxy_config }
|
||||||
|
http://10.76.152.164 { import reverse_proxy_config }
|
||||||
|
http://* { import reverse_proxy_config }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
### ✅ No Restart Required
|
||||||
|
- Caddyfile changes applied without restarting containers
|
||||||
|
- Caddy reload API handles configuration hot-swap
|
||||||
|
- Zero downtime configuration updates
|
||||||
|
|
||||||
|
### ✅ Dynamic Configuration
|
||||||
|
- Settings in admin panel → Generated Caddyfile
|
||||||
|
- Database is source of truth
|
||||||
|
- Easy to modify in admin UI
|
||||||
|
|
||||||
|
### ✅ Automatic Fallbacks
|
||||||
|
- Catch-all `http://*` handles any host
|
||||||
|
- Always has localhost access
|
||||||
|
- Always has IP address access
|
||||||
|
|
||||||
|
### ✅ User Feedback
|
||||||
|
- Admin sees status of Caddy reload
|
||||||
|
- Error messages if Caddy reload fails
|
||||||
|
- Logging of all changes
|
||||||
|
|
||||||
|
### ✅ Safe Updates
|
||||||
|
- Caddyfile validation before reload
|
||||||
|
- Graceful error handling
|
||||||
|
- Falls back to previous config if reload fails
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
If Caddy reload fails:
|
||||||
|
1. Database still has updated settings
|
||||||
|
2. Old Caddyfile may still be in use
|
||||||
|
3. User sees warning with status
|
||||||
|
4. Admin can manually restart: `docker-compose restart caddy`
|
||||||
|
|
||||||
|
## Admin Panel Status Messages
|
||||||
|
|
||||||
|
### Success (✅)
|
||||||
|
```
|
||||||
|
✅ HTTPS configuration saved successfully!
|
||||||
|
✅ Caddy configuration updated successfully!
|
||||||
|
Server available at https://domain.local
|
||||||
|
```
|
||||||
|
|
||||||
|
### Partial Success (⚠️)
|
||||||
|
```
|
||||||
|
✅ HTTPS configuration saved successfully!
|
||||||
|
⚠️ Caddyfile updated but reload failed. Please restart containers.
|
||||||
|
Server available at https://domain.local
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Saved, Update Failed (⚠️)
|
||||||
|
```
|
||||||
|
⚠️ Configuration saved but Caddy update failed: [error details]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Configuration
|
||||||
|
|
||||||
|
### Check Caddyfile Content
|
||||||
|
```bash
|
||||||
|
cat /srv/digiserver-v2/Caddyfile
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manually Reload Caddy
|
||||||
|
```bash
|
||||||
|
docker-compose exec caddy caddy reload --config /etc/caddy/Caddyfile
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Caddy Status
|
||||||
|
```bash
|
||||||
|
docker-compose logs caddy --tail=20
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Access Points
|
||||||
|
```bash
|
||||||
|
# Test all configured domains/IPs
|
||||||
|
curl http://localhost
|
||||||
|
curl http://digiserver.sibiusb.harting.intra
|
||||||
|
curl http://10.76.152.164
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Database
|
||||||
|
|
||||||
|
Settings stored in `https_config` table:
|
||||||
|
```
|
||||||
|
https_enabled: boolean
|
||||||
|
hostname: string
|
||||||
|
domain: string
|
||||||
|
ip_address: string
|
||||||
|
email: string
|
||||||
|
port: integer
|
||||||
|
updated_at: datetime
|
||||||
|
updated_by: string
|
||||||
|
```
|
||||||
|
|
||||||
|
When admin updates form → Database updated → Caddyfile regenerated → Caddy reloaded
|
||||||
|
|
||||||
|
## Workflow Diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ Admin Panel Form │
|
||||||
|
│ (HTTPS Config) │
|
||||||
|
└──────────┬──────────┘
|
||||||
|
│ Submit
|
||||||
|
↓
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ Validate Input │
|
||||||
|
└──────────┬──────────┘
|
||||||
|
│ Valid
|
||||||
|
↓
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ Save to Database │
|
||||||
|
│ (HTTPSConfig) │
|
||||||
|
└──────────┬──────────┘
|
||||||
|
│ Saved
|
||||||
|
↓
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ Generate Caddyfile │
|
||||||
|
│ (CaddyConfigGen) │
|
||||||
|
└──────────┬──────────┘
|
||||||
|
│ Generated
|
||||||
|
↓
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ Write to Disk │
|
||||||
|
│ (/Caddyfile) │
|
||||||
|
└──────────┬──────────┘
|
||||||
|
│ Written
|
||||||
|
↓
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ Reload Caddy │
|
||||||
|
│ (Docker exec) │
|
||||||
|
└──────────┬──────────┘
|
||||||
|
│ Reloaded
|
||||||
|
↓
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ Show Status to │
|
||||||
|
│ Admin (Success) │
|
||||||
|
└─────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### CaddyConfigGenerator Class
|
||||||
|
|
||||||
|
**generate_caddyfile(config)**
|
||||||
|
- Takes HTTPSConfig from database
|
||||||
|
- Generates complete Caddyfile content
|
||||||
|
- Uses shared reverse proxy configuration template
|
||||||
|
- Returns full Caddyfile as string
|
||||||
|
|
||||||
|
**write_caddyfile(content, path)**
|
||||||
|
- Writes generated content to disk
|
||||||
|
- Path defaults to /srv/digiserver-v2/Caddyfile
|
||||||
|
- Returns True on success, False on error
|
||||||
|
|
||||||
|
**reload_caddy()**
|
||||||
|
- Runs: `docker-compose exec -T caddy caddy reload`
|
||||||
|
- Validates config and applies live
|
||||||
|
- Returns True on success, False on error
|
||||||
|
|
||||||
|
## Advantages Over Manual Configuration
|
||||||
|
|
||||||
|
| Manual | Dynamic |
|
||||||
|
|--------|---------|
|
||||||
|
| Edit Caddyfile manually | Change via admin panel |
|
||||||
|
| Restart container | No restart needed |
|
||||||
|
| Risk of syntax errors | Validated generation |
|
||||||
|
| No audit trail | Logged with username |
|
||||||
|
| Each change is manual | One-time setup |
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
Potential improvements:
|
||||||
|
- Configuration history/backup
|
||||||
|
- Rollback to previous config
|
||||||
|
- Health check after reload
|
||||||
|
- Automatic backup before update
|
||||||
|
- Configuration templates
|
||||||
|
- Multi-domain support
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues:
|
||||||
|
1. Check admin panel messages for Caddy reload status
|
||||||
|
2. Review logs: `docker-compose logs caddy`
|
||||||
|
3. Check Caddyfile: `cat /srv/digiserver-v2/Caddyfile`
|
||||||
|
4. Manual reload: `docker-compose exec caddy caddy reload --config /etc/caddy/Caddyfile`
|
||||||
|
5. Full restart: `docker-compose restart caddy`
|
||||||
75
old_code_documentation/DATA_DEPLOYMENT.md
Normal file
75
old_code_documentation/DATA_DEPLOYMENT.md
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# Data Folder Deployment Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The `./data` folder is the **persistent data storage** for the DigiServer deployment. It is **NOT committed to the repository** but contains all necessary files copied from the repo during deployment.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
data/
|
||||||
|
├── app/ # Complete application code (copied from ./app)
|
||||||
|
├── Caddyfile # Reverse proxy configuration (copied from root)
|
||||||
|
├── instance/ # Flask instance folder (database, configs)
|
||||||
|
├── uploads/ # User file uploads
|
||||||
|
├── caddy-data/ # Caddy SSL certificates and cache
|
||||||
|
└── caddy-config/ # Caddy configuration data
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment Process
|
||||||
|
|
||||||
|
### Step 1: Initialize Data Folder
|
||||||
|
|
||||||
|
Run this script to copy all necessary files from the repository to `./data`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./init-data.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This will:
|
||||||
|
- Create the `./data` directory structure
|
||||||
|
- Copy `./app` folder to `./data/app`
|
||||||
|
- Copy `Caddyfile` to `./data/Caddyfile`
|
||||||
|
- Set proper permissions for all files and folders
|
||||||
|
|
||||||
|
### Step 2: Start Docker Containers
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Run Migrations (First Time Only)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- **./data is NOT in git**: The `./data` folder is listed in `.gitignore` and will not be committed
|
||||||
|
- **All persistent data here**: Database files, uploads, certificates, and configurations are stored in `./data`
|
||||||
|
- **Easy backups**: To backup the entire deployment, backup the `./data` folder
|
||||||
|
- **Easy troubleshooting**: Check the `./data` folder to verify all required files are present
|
||||||
|
- **Updates**: When you pull new changes, run `./init-data.sh` to update app files in `./data`
|
||||||
|
|
||||||
|
## Deployment Checklist
|
||||||
|
|
||||||
|
✓ All volumes in docker-compose.yml point to `./data`
|
||||||
|
✓ `./data` folder contains: app/, Caddyfile, instance/, uploads/, caddy-data/, caddy-config/
|
||||||
|
✓ Files are copied from repository to `./data` via init-data.sh
|
||||||
|
✓ Permissions are correctly set for Docker container user
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Before starting:
|
||||||
|
```bash
|
||||||
|
ls -la data/
|
||||||
|
# Should show: app/, Caddyfile, instance/, uploads/, caddy-data/, caddy-config/
|
||||||
|
```
|
||||||
|
|
||||||
|
After deployment check data folder for:
|
||||||
|
```bash
|
||||||
|
data/instance/*.db # Database files
|
||||||
|
data/uploads/ # User uploads
|
||||||
|
data/caddy-data/*.pem # SSL certificates
|
||||||
|
```
|
||||||
272
old_code_documentation/DEPLOYMENT_COMMANDS.md
Normal file
272
old_code_documentation/DEPLOYMENT_COMMANDS.md
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
# DigiServer Deployment Commands
|
||||||
|
|
||||||
|
This document contains all necessary `docker exec` commands to deploy and configure DigiServer on a new PC with the same settings as the production system.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ensure you're in the project directory
|
||||||
|
cd /path/to/digiserver-v2
|
||||||
|
|
||||||
|
# Start the containers
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1. Database Initialization and Migrations
|
||||||
|
|
||||||
|
### Run all database migrations in sequence:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create https_config table
|
||||||
|
docker-compose exec -T digiserver-app python /app/migrations/add_https_config_table.py
|
||||||
|
|
||||||
|
# Create player_user table
|
||||||
|
docker-compose exec -T digiserver-app python /app/migrations/add_player_user_table.py
|
||||||
|
|
||||||
|
# Add email to https_config table
|
||||||
|
docker-compose exec -T digiserver-app python /app/migrations/add_email_to_https_config.py
|
||||||
|
|
||||||
|
# Migrate player_user global settings
|
||||||
|
docker-compose exec -T digiserver-app python /app/migrations/migrate_player_user_global.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** The `-T` flag prevents Docker from allocating a pseudo-terminal, which is useful for automated deployments.
|
||||||
|
|
||||||
|
## 2. HTTPS Configuration via CLI
|
||||||
|
|
||||||
|
### Check HTTPS Configuration Status:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T digiserver-app python /app/https_manager.py status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Enable HTTPS with Production Settings:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T digiserver-app python /app/https_manager.py enable \
|
||||||
|
digiserver \
|
||||||
|
digiserver.sibiusb.harting.intra \
|
||||||
|
admin@example.com \
|
||||||
|
10.76.152.164 \
|
||||||
|
443
|
||||||
|
```
|
||||||
|
|
||||||
|
### Show Detailed Configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T digiserver-app python /app/https_manager.py show
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Admin User Setup
|
||||||
|
|
||||||
|
### Create/Reset Admin User (if needed):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T digiserver-app python -c "
|
||||||
|
from app.app import create_app
|
||||||
|
from app.models.user import User
|
||||||
|
from app.extensions import db
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
with app.app_context():
|
||||||
|
# Check if admin exists
|
||||||
|
admin = User.query.filter_by(username='admin').first()
|
||||||
|
if admin:
|
||||||
|
print('✅ Admin user already exists')
|
||||||
|
else:
|
||||||
|
# Create new admin user
|
||||||
|
admin = User(username='admin', email='admin@example.com')
|
||||||
|
admin.set_password('admin123') # Change this password!
|
||||||
|
admin.is_admin = True
|
||||||
|
db.session.add(admin)
|
||||||
|
db.session.commit()
|
||||||
|
print('✅ Admin user created with username: admin')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Database Verification
|
||||||
|
|
||||||
|
### Check Database Tables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T digiserver-app python -c "
|
||||||
|
from app.app import create_app
|
||||||
|
from app.extensions import db
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
with app.app_context():
|
||||||
|
inspector = inspect(db.engine)
|
||||||
|
tables = inspector.get_table_names()
|
||||||
|
print('📊 Database Tables:')
|
||||||
|
for table in sorted(tables):
|
||||||
|
print(f' ✓ {table}')
|
||||||
|
print(f'\\n✅ Total tables: {len(tables)}')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check HTTPS Configuration in Database:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T digiserver-app python -c "
|
||||||
|
from app.app import create_app
|
||||||
|
from app.models.https_config import HTTPSConfig
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
with app.app_context():
|
||||||
|
config = HTTPSConfig.get_config()
|
||||||
|
if config:
|
||||||
|
print('✅ HTTPS Configuration Found:')
|
||||||
|
print(f' Status: {\"ENABLED\" if config.https_enabled else \"DISABLED\"}')
|
||||||
|
print(f' Hostname: {config.hostname}')
|
||||||
|
print(f' Domain: {config.domain}')
|
||||||
|
print(f' IP Address: {config.ip_address}')
|
||||||
|
print(f' Port: {config.port}')
|
||||||
|
else:
|
||||||
|
print('⚠️ No HTTPS configuration found')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Health Checks
|
||||||
|
|
||||||
|
### Test Caddy Configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T caddy caddy validate --config /etc/caddy/Caddyfile
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Flask Application Health:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T digiserver-app python -c "
|
||||||
|
import urllib.request
|
||||||
|
try:
|
||||||
|
response = urllib.request.urlopen('http://localhost:5000/health', timeout=5)
|
||||||
|
print('✅ Application is responding')
|
||||||
|
print(f' Status: {response.status}')
|
||||||
|
except Exception as e:
|
||||||
|
print(f'❌ Application health check failed: {e}')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Docker Container Logs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Flask app logs
|
||||||
|
docker-compose logs digiserver-app | tail -50
|
||||||
|
|
||||||
|
# Caddy logs
|
||||||
|
docker-compose logs caddy | tail -50
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Complete Deployment Script
|
||||||
|
|
||||||
|
Create a file called `deploy.sh` to run all steps automatically:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🚀 DigiServer Deployment Script"
|
||||||
|
echo "=================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Change to project directory
|
||||||
|
cd /path/to/digiserver-v2
|
||||||
|
|
||||||
|
# Step 1: Start containers
|
||||||
|
echo "📦 Starting containers..."
|
||||||
|
docker-compose up -d
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# Step 2: Run migrations
|
||||||
|
echo "📊 Running database migrations..."
|
||||||
|
docker-compose exec -T digiserver-app python /app/migrations/add_https_config_table.py
|
||||||
|
docker-compose exec -T digiserver-app python /app/migrations/add_player_user_table.py
|
||||||
|
docker-compose exec -T digiserver-app python /app/migrations/add_email_to_https_config.py
|
||||||
|
docker-compose exec -T digiserver-app python /app/migrations/migrate_player_user_global.py
|
||||||
|
|
||||||
|
# Step 3: Configure HTTPS
|
||||||
|
echo "🔒 Configuring HTTPS..."
|
||||||
|
docker-compose exec -T digiserver-app python /app/https_manager.py enable \
|
||||||
|
digiserver \
|
||||||
|
digiserver.sibiusb.harting.intra \
|
||||||
|
admin@example.com \
|
||||||
|
10.76.152.164 \
|
||||||
|
443
|
||||||
|
|
||||||
|
# Step 4: Verify setup
|
||||||
|
echo "✅ Verifying setup..."
|
||||||
|
docker-compose exec -T digiserver-app python /app/https_manager.py status
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🎉 Deployment Complete!"
|
||||||
|
echo "=================================="
|
||||||
|
echo "Access your application at:"
|
||||||
|
echo " - https://digiserver"
|
||||||
|
echo " - https://10.76.152.164"
|
||||||
|
echo " - https://digiserver.sibiusb.harting.intra"
|
||||||
|
echo ""
|
||||||
|
echo "Login with:"
|
||||||
|
echo " Username: admin"
|
||||||
|
echo " Password: (check your password settings)"
|
||||||
|
```
|
||||||
|
|
||||||
|
Make it executable:
|
||||||
|
```bash
|
||||||
|
chmod +x deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Run it:
|
||||||
|
```bash
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Troubleshooting
|
||||||
|
|
||||||
|
### Restart Services:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Restart all containers
|
||||||
|
docker-compose restart
|
||||||
|
|
||||||
|
# Restart just the app
|
||||||
|
docker-compose restart digiserver-app
|
||||||
|
|
||||||
|
# Restart just Caddy
|
||||||
|
docker-compose restart caddy
|
||||||
|
```
|
||||||
|
|
||||||
|
### View Caddy Configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T caddy cat /etc/caddy/Caddyfile
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test HTTPS Endpoints:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test from host machine (if accessible)
|
||||||
|
curl -k https://digiserver.sibiusb.harting.intra
|
||||||
|
|
||||||
|
# Test from within containers
|
||||||
|
docker-compose exec -T caddy wget --no-check-certificate -qO- https://localhost/ | head -20
|
||||||
|
```
|
||||||
|
|
||||||
|
### Clear Caddy Cache (if certificate issues occur):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker volume rm digiserver-v2_caddy-data
|
||||||
|
docker volume rm digiserver-v2_caddy-config
|
||||||
|
docker-compose restart caddy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- Always use `-T` flag with `docker-compose exec` in automated scripts to prevent TTY issues
|
||||||
|
- Change default passwords (`admin123`) in production environments
|
||||||
|
- Adjust email address in HTTPS configuration as needed
|
||||||
|
- For different network setups, modify the IP address and domain in the enable HTTPS command
|
||||||
|
- Keep database backups before running migrations
|
||||||
|
- Test all three access points after deployment
|
||||||
|
|
||||||
278
old_code_documentation/DEPLOYMENT_INDEX.md
Normal file
278
old_code_documentation/DEPLOYMENT_INDEX.md
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
# 📚 DigiServer Deployment Documentation Index
|
||||||
|
|
||||||
|
Complete documentation for deploying and maintaining DigiServer. Choose your path below:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 I Want to Deploy Now!
|
||||||
|
|
||||||
|
### Quick Start (2 minutes)
|
||||||
|
```bash
|
||||||
|
cd /path/to/digiserver-v2
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
→ See [DEPLOYMENT_README.md](DEPLOYMENT_README.md)
|
||||||
|
|
||||||
|
### Or Step-by-Step Setup
|
||||||
|
```bash
|
||||||
|
./setup_https.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 Documentation Files
|
||||||
|
|
||||||
|
### 1. **[DEPLOYMENT_README.md](DEPLOYMENT_README.md)** ⭐ START HERE
|
||||||
|
- **Size**: 9.4 KB
|
||||||
|
- **Purpose**: Complete deployment guide for beginners
|
||||||
|
- **Contains**:
|
||||||
|
- Quick start instructions
|
||||||
|
- Prerequisites checklist
|
||||||
|
- 3 deployment methods (auto, semi-auto, manual)
|
||||||
|
- Verification procedures
|
||||||
|
- First access setup
|
||||||
|
- Troubleshooting guide
|
||||||
|
- **Read time**: 15-20 minutes
|
||||||
|
|
||||||
|
### 2. **[DOCKER_EXEC_COMMANDS.md](DOCKER_EXEC_COMMANDS.md)** ⭐ REFERENCE GUIDE
|
||||||
|
- **Size**: 7.6 KB
|
||||||
|
- **Purpose**: Quick reference for all docker exec commands
|
||||||
|
- **Contains**:
|
||||||
|
- Database migrations
|
||||||
|
- HTTPS configuration
|
||||||
|
- User management
|
||||||
|
- Database inspection
|
||||||
|
- Health checks
|
||||||
|
- Maintenance commands
|
||||||
|
- Troubleshooting commands
|
||||||
|
- **Use when**: You need a specific command
|
||||||
|
- **Read time**: 5-10 minutes (or search for what you need)
|
||||||
|
|
||||||
|
### 3. **[DEPLOYMENT_COMMANDS.md](DEPLOYMENT_COMMANDS.md)**
|
||||||
|
- **Size**: 6.8 KB
|
||||||
|
- **Purpose**: Detailed deployment command explanations
|
||||||
|
- **Contains**:
|
||||||
|
- Individual command explanations
|
||||||
|
- Complete deployment script template
|
||||||
|
- Health check procedures
|
||||||
|
- Verification steps
|
||||||
|
- Advanced troubleshooting
|
||||||
|
- **Read time**: 20-30 minutes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Executable Scripts
|
||||||
|
|
||||||
|
### 1. **[deploy.sh](deploy.sh)** - Fully Automated
|
||||||
|
- **Size**: 6.7 KB
|
||||||
|
- **Purpose**: One-command deployment
|
||||||
|
- **Does**:
|
||||||
|
1. Starts Docker containers
|
||||||
|
2. Runs all migrations
|
||||||
|
3. Configures HTTPS
|
||||||
|
4. Verifies setup
|
||||||
|
5. Shows access URLs
|
||||||
|
- **Usage**:
|
||||||
|
```bash
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
- **With custom settings**:
|
||||||
|
```bash
|
||||||
|
HOSTNAME=server1 DOMAIN=server1.internal ./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **[setup_https.sh](setup_https.sh)** - Semi-Automated
|
||||||
|
- **Size**: 5.9 KB
|
||||||
|
- **Purpose**: Setup script that works in or outside Docker
|
||||||
|
- **Does**:
|
||||||
|
- Detects environment (Docker container or host)
|
||||||
|
- Runs migrations
|
||||||
|
- Configures HTTPS
|
||||||
|
- Shows status
|
||||||
|
- **Usage**:
|
||||||
|
```bash
|
||||||
|
./setup_https.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Quick Navigation by Task
|
||||||
|
|
||||||
|
### "I need to deploy on a new PC"
|
||||||
|
1. Read: [DEPLOYMENT_README.md](DEPLOYMENT_README.md#prerequisites)
|
||||||
|
2. Run: `./deploy.sh`
|
||||||
|
3. Access: https://digiserver.sibiusb.harting.intra
|
||||||
|
|
||||||
|
### "I need a specific docker exec command"
|
||||||
|
→ Search [DOCKER_EXEC_COMMANDS.md](DOCKER_EXEC_COMMANDS.md)
|
||||||
|
|
||||||
|
### "I want to understand what's being deployed"
|
||||||
|
→ Read [DEPLOYMENT_COMMANDS.md](DEPLOYMENT_COMMANDS.md#prerequisites)
|
||||||
|
|
||||||
|
### "Something went wrong, help!"
|
||||||
|
→ See [DEPLOYMENT_README.md](DEPLOYMENT_README.md#-troubleshooting) or [DOCKER_EXEC_COMMANDS.md](DOCKER_EXEC_COMMANDS.md#-troubleshooting)
|
||||||
|
|
||||||
|
### "I need to configure custom settings"
|
||||||
|
→ Read [DEPLOYMENT_README.md](DEPLOYMENT_README.md#-environment-variables)
|
||||||
|
|
||||||
|
### "I want to manage HTTPS"
|
||||||
|
→ See [DOCKER_EXEC_COMMANDS.md](DOCKER_EXEC_COMMANDS.md#-https-configuration-management)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Deployment Checklist
|
||||||
|
|
||||||
|
- [ ] Docker and Docker Compose installed
|
||||||
|
- [ ] Project files copied to new PC
|
||||||
|
- [ ] Run `./deploy.sh` or `./setup_https.sh`
|
||||||
|
- [ ] Verify with `docker-compose ps`
|
||||||
|
- [ ] Access https://digiserver.sibiusb.harting.intra
|
||||||
|
- [ ] Log in with admin/admin123
|
||||||
|
- [ ] Change default password
|
||||||
|
- [ ] Configure players and content
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔑 Configuration Options
|
||||||
|
|
||||||
|
### Default Settings
|
||||||
|
```
|
||||||
|
Hostname: digiserver
|
||||||
|
Domain: digiserver.sibiusb.harting.intra
|
||||||
|
IP Address: 10.76.152.164
|
||||||
|
Port: 443
|
||||||
|
Email: admin@example.com
|
||||||
|
Username: admin
|
||||||
|
Password: admin123
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customize During Deployment
|
||||||
|
```bash
|
||||||
|
HOSTNAME=myserver \
|
||||||
|
DOMAIN=myserver.internal \
|
||||||
|
IP_ADDRESS=192.168.1.100 \
|
||||||
|
EMAIL=admin@myserver.com \
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆘 Common Tasks
|
||||||
|
|
||||||
|
| Task | Command |
|
||||||
|
|------|---------|
|
||||||
|
| **Start containers** | `docker-compose up -d` |
|
||||||
|
| **Stop containers** | `docker-compose stop` |
|
||||||
|
| **View logs** | `docker-compose logs -f` |
|
||||||
|
| **Check HTTPS status** | `docker-compose exec -T digiserver-app python /app/https_manager.py status` |
|
||||||
|
| **Reset password** | See [DOCKER_EXEC_COMMANDS.md](DOCKER_EXEC_COMMANDS.md#reset-admin-password) |
|
||||||
|
| **View all tables** | See [DOCKER_EXEC_COMMANDS.md](DOCKER_EXEC_COMMANDS.md#list-all-tables) |
|
||||||
|
| **Create admin user** | See [DOCKER_EXEC_COMMANDS.md](DOCKER_EXEC_COMMANDS.md#create-admin-user) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
digiserver-v2/
|
||||||
|
├── DEPLOYMENT_README.md ..................... Main deployment guide
|
||||||
|
├── DOCKER_EXEC_COMMANDS.md ................. Quick reference (BEST FOR COMMANDS)
|
||||||
|
├── DEPLOYMENT_COMMANDS.md .................. Detailed explanations
|
||||||
|
├── deploy.sh ............................. Fully automated deployment
|
||||||
|
├── setup_https.sh ......................... Semi-automated setup
|
||||||
|
├── docker-compose.yml ..................... Docker services config
|
||||||
|
├── Caddyfile .............................. Reverse proxy config
|
||||||
|
├── requirements.txt ....................... Python dependencies
|
||||||
|
│
|
||||||
|
├── app/
|
||||||
|
│ ├── app.py ............................ Flask application
|
||||||
|
│ ├── models/ ........................... Database models
|
||||||
|
│ │ ├── https_config.py
|
||||||
|
│ │ ├── user.py
|
||||||
|
│ │ └── ...
|
||||||
|
│ └── ...
|
||||||
|
│
|
||||||
|
├── migrations/
|
||||||
|
│ ├── add_https_config_table.py
|
||||||
|
│ ├── add_player_user_table.py
|
||||||
|
│ ├── add_email_to_https_config.py
|
||||||
|
│ └── migrate_player_user_global.py
|
||||||
|
│
|
||||||
|
└── old_code_documentation/
|
||||||
|
├── HTTPS_CONFIGURATION.md
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Deployment Methods Comparison
|
||||||
|
|
||||||
|
| Method | Time | Effort | Best For |
|
||||||
|
|--------|------|--------|----------|
|
||||||
|
| `./deploy.sh` | 2-3 min | Click & wait | First-time setup, automation |
|
||||||
|
| `./setup_https.sh` | 3-5 min | Manual review | Learning, step debugging |
|
||||||
|
| Manual commands | 10-15 min | Full control | Advanced users, scripting |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ What Gets Deployed
|
||||||
|
|
||||||
|
✅ Flask web application with admin dashboard
|
||||||
|
✅ HTTPS with self-signed certificates
|
||||||
|
✅ Caddy reverse proxy for routing
|
||||||
|
✅ SQLite database with all tables
|
||||||
|
✅ User management system
|
||||||
|
✅ HTTPS configuration management
|
||||||
|
✅ Player and content management
|
||||||
|
✅ Group and playlist management
|
||||||
|
✅ Admin audit trail
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎓 Learning Path
|
||||||
|
|
||||||
|
1. **Total Beginner?**
|
||||||
|
- Start: [DEPLOYMENT_README.md](DEPLOYMENT_README.md)
|
||||||
|
- Run: `./deploy.sh`
|
||||||
|
- Learn: Browse [DOCKER_EXEC_COMMANDS.md](DOCKER_EXEC_COMMANDS.md) for available commands
|
||||||
|
|
||||||
|
2. **Want to Understand Everything?**
|
||||||
|
- Read: [DEPLOYMENT_README.md](DEPLOYMENT_README.md#-deployment-methods) (all 3 methods)
|
||||||
|
- Study: [DEPLOYMENT_COMMANDS.md](DEPLOYMENT_COMMANDS.md)
|
||||||
|
- Reference: [DOCKER_EXEC_COMMANDS.md](DOCKER_EXEC_COMMANDS.md)
|
||||||
|
|
||||||
|
3. **Need to Troubleshoot?**
|
||||||
|
- Check: [DEPLOYMENT_README.md](DEPLOYMENT_README.md#-troubleshooting)
|
||||||
|
- Or: [DOCKER_EXEC_COMMANDS.md](DOCKER_EXEC_COMMANDS.md#-troubleshooting)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Pro Tips
|
||||||
|
|
||||||
|
1. **Use `-T` flag** in docker-compose exec for scripts (prevents TTY issues)
|
||||||
|
2. **Keep backups** before major changes
|
||||||
|
3. **Check logs often**: `docker-compose logs -f`
|
||||||
|
4. **Use environment variables** for custom deployments
|
||||||
|
5. **Verify after deployment** using health check commands
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 Related Documentation
|
||||||
|
|
||||||
|
- **HTTPS Setup**: `old_code_documentation/HTTPS_CONFIGURATION.md`
|
||||||
|
- **Admin Features**: Check admin panel after login
|
||||||
|
- **API Documentation**: See `old_code_documentation/PLAYER_EDIT_MEDIA_API.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Support
|
||||||
|
|
||||||
|
- Logs: `docker-compose logs digiserver-app`
|
||||||
|
- Health Check: See [DOCKER_EXEC_COMMANDS.md#-health-checks](DOCKER_EXEC_COMMANDS.md#-health-checks)
|
||||||
|
- Troubleshooting: See [DEPLOYMENT_README.md#-troubleshooting](DEPLOYMENT_README.md#-troubleshooting)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Ready? Start with:** `./deploy.sh` 🚀
|
||||||
|
|
||||||
|
Or read [DEPLOYMENT_README.md](DEPLOYMENT_README.md) for the full guide.
|
||||||
433
old_code_documentation/DEPLOYMENT_README.md
Normal file
433
old_code_documentation/DEPLOYMENT_README.md
Normal file
@@ -0,0 +1,433 @@
|
|||||||
|
# DigiServer Deployment Guide
|
||||||
|
|
||||||
|
Complete guide for deploying DigiServer on a new PC with automatic or manual configuration.
|
||||||
|
|
||||||
|
## 📋 Table of Contents
|
||||||
|
|
||||||
|
1. [Quick Start](#quick-start)
|
||||||
|
2. [Prerequisites](#prerequisites)
|
||||||
|
3. [Deployment Methods](#deployment-methods)
|
||||||
|
4. [Verification](#verification)
|
||||||
|
5. [Documentation Files](#documentation-files)
|
||||||
|
6. [Troubleshooting](#troubleshooting)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
The fastest way to deploy DigiServer on a new PC:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clone or copy the project to your new PC
|
||||||
|
cd /path/to/digiserver-v2
|
||||||
|
|
||||||
|
# 2. Run the automated deployment script
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
That's it! The script will:
|
||||||
|
- ✅ Start all Docker containers
|
||||||
|
- ✅ Run all database migrations
|
||||||
|
- ✅ Configure HTTPS with self-signed certificates
|
||||||
|
- ✅ Verify the setup
|
||||||
|
- ✅ Display access URLs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Prerequisites
|
||||||
|
|
||||||
|
Before deploying, ensure you have:
|
||||||
|
|
||||||
|
### 1. Docker & Docker Compose
|
||||||
|
```bash
|
||||||
|
# Check Docker installation
|
||||||
|
docker --version
|
||||||
|
|
||||||
|
# Check Docker Compose installation
|
||||||
|
docker-compose --version
|
||||||
|
```
|
||||||
|
|
||||||
|
If not installed, follow the official guides:
|
||||||
|
- [Docker Installation](https://docs.docker.com/install/)
|
||||||
|
- [Docker Compose Installation](https://docs.docker.com/compose/install/)
|
||||||
|
|
||||||
|
### 2. Project Files
|
||||||
|
```bash
|
||||||
|
# You should have these files in the project directory:
|
||||||
|
ls -la
|
||||||
|
# Caddyfile - Reverse proxy configuration
|
||||||
|
# docker-compose.yml - Docker services definition
|
||||||
|
# setup_https.sh - Manual setup script
|
||||||
|
# deploy.sh - Automated deployment script
|
||||||
|
# requirements.txt - Python dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Sufficient Disk Space
|
||||||
|
- ~2GB for Docker images and volumes
|
||||||
|
- Additional space for your content/uploads
|
||||||
|
|
||||||
|
### 4. Network Access
|
||||||
|
- Ports 80, 443 available (or configure in docker-compose.yml)
|
||||||
|
- Port 2019 for Caddy admin API (internal only)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Deployment Methods
|
||||||
|
|
||||||
|
### Method 1: Fully Automated (Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/digiserver-v2
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
1. Starts Docker containers
|
||||||
|
2. Runs all migrations
|
||||||
|
3. Configures HTTPS
|
||||||
|
4. Verifies setup
|
||||||
|
5. Shows access URLs
|
||||||
|
|
||||||
|
**Configuration variables** (can be customized):
|
||||||
|
```bash
|
||||||
|
# Use environment variables to customize
|
||||||
|
HOSTNAME=digiserver \
|
||||||
|
DOMAIN=digiserver.sibiusb.harting.intra \
|
||||||
|
IP_ADDRESS=10.76.152.164 \
|
||||||
|
EMAIL=admin@example.com \
|
||||||
|
PORT=443 \
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Method 2: Semi-Automated Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/digiserver-v2
|
||||||
|
./setup_https.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
1. Starts containers (if needed)
|
||||||
|
2. Runs all migrations
|
||||||
|
3. Configures HTTPS with production settings
|
||||||
|
4. Shows status
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Method 3: Manual Step-by-Step
|
||||||
|
|
||||||
|
#### Step 1: Start Containers
|
||||||
|
```bash
|
||||||
|
cd /path/to/digiserver-v2
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Wait for containers to be ready (check with `docker-compose ps`).
|
||||||
|
|
||||||
|
#### Step 2: Run Migrations
|
||||||
|
```bash
|
||||||
|
# Migration 1: HTTPS Config
|
||||||
|
docker-compose exec -T digiserver-app python /app/migrations/add_https_config_table.py
|
||||||
|
|
||||||
|
# Migration 2: Player User
|
||||||
|
docker-compose exec -T digiserver-app python /app/migrations/add_player_user_table.py
|
||||||
|
|
||||||
|
# Migration 3: Email
|
||||||
|
docker-compose exec -T digiserver-app python /app/migrations/add_email_to_https_config.py
|
||||||
|
|
||||||
|
# Migration 4: Player User Global
|
||||||
|
docker-compose exec -T digiserver-app python /app/migrations/migrate_player_user_global.py
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 3: Configure HTTPS
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T digiserver-app python /app/https_manager.py enable \
|
||||||
|
digiserver \
|
||||||
|
digiserver.sibiusb.harting.intra \
|
||||||
|
admin@example.com \
|
||||||
|
10.76.152.164 \
|
||||||
|
443
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 4: Verify Status
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T digiserver-app python /app/https_manager.py status
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Verification
|
||||||
|
|
||||||
|
### Check Container Status
|
||||||
|
```bash
|
||||||
|
docker-compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output:
|
||||||
|
```
|
||||||
|
NAME SERVICE STATUS PORTS
|
||||||
|
digiserver-v2 digiserver-app Up (healthy) 5000/tcp
|
||||||
|
digiserver-caddy caddy Up 80, 443, 2019/tcp
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test HTTPS Access
|
||||||
|
```bash
|
||||||
|
# From the same network (if DNS configured)
|
||||||
|
curl -k https://digiserver.sibiusb.harting.intra
|
||||||
|
|
||||||
|
# Or from container
|
||||||
|
docker-compose exec -T caddy wget --no-check-certificate -qO- https://localhost/ | head -10
|
||||||
|
```
|
||||||
|
|
||||||
|
### Expected Response
|
||||||
|
Should show HTML login page with "DigiServer" in the title.
|
||||||
|
|
||||||
|
### Check Database
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T digiserver-app python -c "
|
||||||
|
from app.app import create_app
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
with app.app_context():
|
||||||
|
inspector = inspect(app.extensions.db.engine)
|
||||||
|
tables = inspector.get_table_names()
|
||||||
|
print('Database tables:', len(tables))
|
||||||
|
for t in sorted(tables):
|
||||||
|
print(f' ✓ {t}')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Documentation Files
|
||||||
|
|
||||||
|
### 1. `DOCKER_EXEC_COMMANDS.md` ⭐ **START HERE**
|
||||||
|
Quick reference for all docker exec commands
|
||||||
|
- Database operations
|
||||||
|
- User management
|
||||||
|
- HTTPS configuration
|
||||||
|
- Health checks
|
||||||
|
- Maintenance tasks
|
||||||
|
|
||||||
|
### 2. `DEPLOYMENT_COMMANDS.md`
|
||||||
|
Comprehensive deployment guide
|
||||||
|
- Prerequisites
|
||||||
|
- Each deployment step explained
|
||||||
|
- Complete deployment script template
|
||||||
|
- Troubleshooting section
|
||||||
|
|
||||||
|
### 3. `deploy.sh`
|
||||||
|
Automated deployment script (executable)
|
||||||
|
- Runs all steps automatically
|
||||||
|
- Shows progress with colors
|
||||||
|
- Configurable via environment variables
|
||||||
|
|
||||||
|
### 4. `setup_https.sh`
|
||||||
|
Semi-automated setup script (executable)
|
||||||
|
- Detects if running in Docker or on host
|
||||||
|
- Manual configuration option
|
||||||
|
- Detailed output
|
||||||
|
|
||||||
|
### 5. `Caddyfile`
|
||||||
|
Reverse proxy configuration
|
||||||
|
- HTTPS certificate management
|
||||||
|
- Domain routing
|
||||||
|
- Security headers
|
||||||
|
|
||||||
|
### 6. `docker-compose.yml`
|
||||||
|
Docker services definition
|
||||||
|
- Flask application
|
||||||
|
- Caddy reverse proxy
|
||||||
|
- Volumes and networks
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 First Access
|
||||||
|
|
||||||
|
After deployment:
|
||||||
|
|
||||||
|
1. **Access the application**
|
||||||
|
- https://digiserver.sibiusb.harting.intra
|
||||||
|
- https://10.76.152.164
|
||||||
|
- https://digiserver
|
||||||
|
|
||||||
|
2. **Log in with default credentials**
|
||||||
|
```
|
||||||
|
Username: admin
|
||||||
|
Password: admin123
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **⚠️ IMPORTANT: Change the password immediately**
|
||||||
|
- Click on admin user settings
|
||||||
|
- Change default password to a strong password
|
||||||
|
|
||||||
|
4. **Configure your system**
|
||||||
|
- Set up players
|
||||||
|
- Upload content
|
||||||
|
- Create groups
|
||||||
|
- Configure playlists
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆘 Troubleshooting
|
||||||
|
|
||||||
|
### Containers Won't Start
|
||||||
|
```bash
|
||||||
|
# Check logs
|
||||||
|
docker-compose logs
|
||||||
|
|
||||||
|
# Try rebuilding
|
||||||
|
docker-compose down
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration Fails
|
||||||
|
```bash
|
||||||
|
# Check database connection
|
||||||
|
docker-compose exec -T digiserver-app python -c "
|
||||||
|
from app.app import create_app
|
||||||
|
app = create_app()
|
||||||
|
print('Database OK')
|
||||||
|
"
|
||||||
|
|
||||||
|
# Check if tables already exist
|
||||||
|
docker-compose exec -T digiserver-app python -c "
|
||||||
|
from app.app import create_app
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
app = create_app()
|
||||||
|
with app.app_context():
|
||||||
|
inspector = inspect(app.extensions.db.engine)
|
||||||
|
print('Existing tables:', inspector.get_table_names())
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTTPS Certificate Issues
|
||||||
|
```bash
|
||||||
|
# Clear Caddy certificate cache
|
||||||
|
docker volume rm digiserver-v2_caddy-data
|
||||||
|
docker volume rm digiserver-v2_caddy-config
|
||||||
|
|
||||||
|
# Restart Caddy
|
||||||
|
docker-compose restart caddy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Port 80/443 Already in Use
|
||||||
|
```bash
|
||||||
|
# Find what's using the port
|
||||||
|
lsof -i :80 # For port 80
|
||||||
|
lsof -i :443 # For port 443
|
||||||
|
|
||||||
|
# Stop the conflicting service or change ports in docker-compose.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Can't Access via IP Address
|
||||||
|
```bash
|
||||||
|
# Verify Caddy is listening
|
||||||
|
docker-compose exec -T caddy netstat -tlnp 2>/dev/null | grep -E ':(80|443)'
|
||||||
|
|
||||||
|
# Test from container
|
||||||
|
docker-compose exec -T caddy wget --no-check-certificate -qO- https://localhost/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Corruption
|
||||||
|
```bash
|
||||||
|
# Backup current database
|
||||||
|
docker-compose exec -T digiserver-app cp /app/instance/digiserver.db /app/instance/digiserver.db.backup
|
||||||
|
|
||||||
|
# Reset database (CAUTION: This deletes all data)
|
||||||
|
docker-compose exec -T digiserver-app rm /app/instance/digiserver.db
|
||||||
|
|
||||||
|
# Restart and re-run migrations
|
||||||
|
docker-compose restart digiserver-app
|
||||||
|
./setup_https.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 More Help
|
||||||
|
|
||||||
|
See the detailed documentation files:
|
||||||
|
- **Quick Commands**: `DOCKER_EXEC_COMMANDS.md`
|
||||||
|
- **Full Guide**: `DEPLOYMENT_COMMANDS.md`
|
||||||
|
- **HTTPS Details**: `old_code_documentation/HTTPS_CONFIGURATION.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Deployment on Different PC
|
||||||
|
|
||||||
|
To deploy on a different PC:
|
||||||
|
|
||||||
|
1. **Copy project files** to the new PC (or clone from git)
|
||||||
|
2. **Ensure Docker and Docker Compose are installed**
|
||||||
|
3. **Run deployment script**:
|
||||||
|
```bash
|
||||||
|
cd /path/to/digiserver-v2
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
4. **Access the application** on the new PC at the configured URLs
|
||||||
|
|
||||||
|
All settings will be automatically configured! 🎉
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Environment Variables
|
||||||
|
|
||||||
|
You can customize deployment using environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Customize hostname
|
||||||
|
HOSTNAME=myserver ./deploy.sh
|
||||||
|
|
||||||
|
# Customize domain
|
||||||
|
DOMAIN=myserver.example.com ./deploy.sh
|
||||||
|
|
||||||
|
# Customize IP address
|
||||||
|
IP_ADDRESS=192.168.1.100 ./deploy.sh
|
||||||
|
|
||||||
|
# Customize email
|
||||||
|
EMAIL=admin@myserver.com ./deploy.sh
|
||||||
|
|
||||||
|
# Customize port
|
||||||
|
PORT=8443 ./deploy.sh
|
||||||
|
|
||||||
|
# All together
|
||||||
|
HOSTNAME=server1 \
|
||||||
|
DOMAIN=server1.internal \
|
||||||
|
IP_ADDRESS=192.168.1.100 \
|
||||||
|
EMAIL=admin@server1.com \
|
||||||
|
PORT=443 \
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
✅ Automated HTTPS with self-signed certificates
|
||||||
|
✅ Multi-access (hostname, domain, IP address)
|
||||||
|
✅ Automatic reverse proxy with Caddy
|
||||||
|
✅ Docker containerized (easy deployment)
|
||||||
|
✅ Complete database schema with migrations
|
||||||
|
✅ Admin dashboard for configuration
|
||||||
|
✅ User management
|
||||||
|
✅ Player management
|
||||||
|
✅ Content/Playlist management
|
||||||
|
✅ Group management
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Notes
|
||||||
|
|
||||||
|
- Default SSL certificates are **self-signed** (internal use)
|
||||||
|
- For production with Let's Encrypt, edit the Caddyfile
|
||||||
|
- Keep database backups before major changes
|
||||||
|
- Default credentials are in the code; change them in production
|
||||||
|
- All logs available via `docker-compose logs`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Ready to deploy? Run:** `./deploy.sh` 🚀
|
||||||
|
|
||||||
353
old_code_documentation/DOCKER_EXEC_COMMANDS.md
Normal file
353
old_code_documentation/DOCKER_EXEC_COMMANDS.md
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
# DigiServer Docker Exec Commands - Quick Reference
|
||||||
|
|
||||||
|
Quick reference guide for common `docker exec` commands used in DigiServer deployment and maintenance.
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### Complete Automated Deployment
|
||||||
|
```bash
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Step-by-Step Setup
|
||||||
|
```bash
|
||||||
|
./setup_https.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Database Migrations
|
||||||
|
|
||||||
|
Run migrations in this order:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. HTTPS Configuration table
|
||||||
|
docker-compose exec -T digiserver-app python /app/migrations/add_https_config_table.py
|
||||||
|
|
||||||
|
# 2. Player User table
|
||||||
|
docker-compose exec -T digiserver-app python /app/migrations/add_player_user_table.py
|
||||||
|
|
||||||
|
# 3. Email column for HTTPS config
|
||||||
|
docker-compose exec -T digiserver-app python /app/migrations/add_email_to_https_config.py
|
||||||
|
|
||||||
|
# 4. Player User global migration
|
||||||
|
docker-compose exec -T digiserver-app python /app/migrations/migrate_player_user_global.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 HTTPS Configuration Management
|
||||||
|
|
||||||
|
### Check HTTPS Status
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T digiserver-app python /app/https_manager.py status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Show Detailed Configuration
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T digiserver-app python /app/https_manager.py show
|
||||||
|
```
|
||||||
|
|
||||||
|
### Enable HTTPS (Production Settings)
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T digiserver-app python /app/https_manager.py enable \
|
||||||
|
digiserver \
|
||||||
|
digiserver.sibiusb.harting.intra \
|
||||||
|
admin@example.com \
|
||||||
|
10.76.152.164 \
|
||||||
|
443
|
||||||
|
```
|
||||||
|
|
||||||
|
### Disable HTTPS
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T digiserver-app python /app/https_manager.py disable
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👤 User Management
|
||||||
|
|
||||||
|
### Create Admin User
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T digiserver-app python -c "
|
||||||
|
from app.app import create_app
|
||||||
|
from app.models.user import User
|
||||||
|
from app.extensions import db
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
with app.app_context():
|
||||||
|
admin = User.query.filter_by(username='admin').first()
|
||||||
|
if not admin:
|
||||||
|
admin = User(username='admin', email='admin@example.com')
|
||||||
|
admin.set_password('admin123')
|
||||||
|
admin.is_admin = True
|
||||||
|
db.session.add(admin)
|
||||||
|
db.session.commit()
|
||||||
|
print('✅ Admin user created')
|
||||||
|
else:
|
||||||
|
print('✅ Admin user already exists')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reset Admin Password
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T digiserver-app python -c "
|
||||||
|
from app.app import create_app
|
||||||
|
from app.models.user import User
|
||||||
|
from app.extensions import db
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
with app.app_context():
|
||||||
|
admin = User.query.filter_by(username='admin').first()
|
||||||
|
if admin:
|
||||||
|
admin.set_password('newpassword123')
|
||||||
|
db.session.commit()
|
||||||
|
print('✅ Admin password reset successfully')
|
||||||
|
else:
|
||||||
|
print('❌ Admin user not found')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Database Inspection
|
||||||
|
|
||||||
|
### List All Tables
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T digiserver-app python -c "
|
||||||
|
from app.app import create_app
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
with app.app_context():
|
||||||
|
inspector = inspect(app.extensions.db.engine)
|
||||||
|
tables = inspector.get_table_names()
|
||||||
|
for table in sorted(tables):
|
||||||
|
print(f' ✓ {table}')
|
||||||
|
print(f'Total: {len(tables)} tables')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check HTTPS Configuration Record
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T digiserver-app python -c "
|
||||||
|
from app.app import create_app
|
||||||
|
from app.models.https_config import HTTPSConfig
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
with app.app_context():
|
||||||
|
config = HTTPSConfig.get_config()
|
||||||
|
if config:
|
||||||
|
print('HTTPS Configuration:')
|
||||||
|
print(f' Status: {\"ENABLED\" if config.https_enabled else \"DISABLED\"}')
|
||||||
|
print(f' Hostname: {config.hostname}')
|
||||||
|
print(f' Domain: {config.domain}')
|
||||||
|
print(f' IP: {config.ip_address}')
|
||||||
|
print(f' Port: {config.port}')
|
||||||
|
print(f' Updated: {config.updated_at}')
|
||||||
|
print(f' Updated by: {config.updated_by}')
|
||||||
|
else:
|
||||||
|
print('No configuration found')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Count Users
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T digiserver-app python -c "
|
||||||
|
from app.app import create_app
|
||||||
|
from app.models.user import User
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
with app.app_context():
|
||||||
|
count = User.query.count()
|
||||||
|
print(f'Total users: {count}')
|
||||||
|
admins = User.query.filter_by(is_admin=True).count()
|
||||||
|
print(f'Admin users: {admins}')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Health Checks
|
||||||
|
|
||||||
|
### Check Flask Application
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T digiserver-app python -c "
|
||||||
|
import urllib.request
|
||||||
|
try:
|
||||||
|
response = urllib.request.urlopen('http://localhost:5000/', timeout=5)
|
||||||
|
print(f'✅ Application responding (HTTP {response.status})')
|
||||||
|
except Exception as e:
|
||||||
|
print(f'❌ Application error: {e}')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validate Caddy Configuration
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T caddy caddy validate --config /etc/caddy/Caddyfile
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test HTTPS from Container
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T caddy wget --no-check-certificate -qO- https://localhost/ | head -10
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Maintenance Commands
|
||||||
|
|
||||||
|
### View Caddy Configuration
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T caddy cat /etc/caddy/Caddyfile
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reload Caddy Configuration
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T caddy caddy reload --config /etc/caddy/Caddyfile
|
||||||
|
```
|
||||||
|
|
||||||
|
### View Application Logs (Last 50 lines)
|
||||||
|
```bash
|
||||||
|
docker-compose logs --tail=50 digiserver-app
|
||||||
|
```
|
||||||
|
|
||||||
|
### View Caddy Logs (Last 50 lines)
|
||||||
|
```bash
|
||||||
|
docker-compose logs --tail=50 caddy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Clear All Logs
|
||||||
|
```bash
|
||||||
|
docker-compose logs --clear
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Container Management
|
||||||
|
|
||||||
|
### Restart All Containers
|
||||||
|
```bash
|
||||||
|
docker-compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restart Specific Container
|
||||||
|
```bash
|
||||||
|
# Restart application
|
||||||
|
docker-compose restart digiserver-app
|
||||||
|
|
||||||
|
# Restart Caddy
|
||||||
|
docker-compose restart caddy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stop All Containers
|
||||||
|
```bash
|
||||||
|
docker-compose stop
|
||||||
|
```
|
||||||
|
|
||||||
|
### Start All Containers
|
||||||
|
```bash
|
||||||
|
docker-compose start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Remove Everything (Clean slate)
|
||||||
|
```bash
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Remove Everything Including Volumes (Full cleanup)
|
||||||
|
```bash
|
||||||
|
docker-compose down -v
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Backup and Recovery
|
||||||
|
|
||||||
|
### Backup Database
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T digiserver-app python -c "
|
||||||
|
from app.app import create_app
|
||||||
|
import shutil
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||||
|
backup_name = f'digiserver_{timestamp}.db'
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
# Get database path
|
||||||
|
db_path = app.instance_path + '/digiserver.db'
|
||||||
|
shutil.copy(db_path, f'/app/backups/{backup_name}')
|
||||||
|
print(f'✅ Backup created: {backup_name}')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### List Database Backups
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T digiserver-app ls -lah /app/backups/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚨 Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**Containers won't start:**
|
||||||
|
```bash
|
||||||
|
# Check logs
|
||||||
|
docker-compose logs
|
||||||
|
|
||||||
|
# Try rebuild
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
**Migration fails:**
|
||||||
|
```bash
|
||||||
|
# Check database connection
|
||||||
|
docker-compose exec -T digiserver-app python -c "
|
||||||
|
from app.app import create_app
|
||||||
|
app = create_app()
|
||||||
|
print('✅ Database connection OK')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Certificate issues:**
|
||||||
|
```bash
|
||||||
|
# Clear Caddy cache
|
||||||
|
docker volume rm digiserver-v2_caddy-data
|
||||||
|
docker volume rm digiserver-v2_caddy-config
|
||||||
|
|
||||||
|
# Restart Caddy
|
||||||
|
docker-compose restart caddy
|
||||||
|
```
|
||||||
|
|
||||||
|
**Port conflicts:**
|
||||||
|
```bash
|
||||||
|
# Find what's using port 443
|
||||||
|
lsof -i :443
|
||||||
|
|
||||||
|
# Find what's using port 80
|
||||||
|
lsof -i :80
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Tips and Notes
|
||||||
|
|
||||||
|
- **`-T` flag**: Prevents Docker from allocating a pseudo-terminal (use in scripts)
|
||||||
|
- **No `-T` flag**: Allocates a terminal (use for interactive commands)
|
||||||
|
- **Container name**: `digiserver-app` (Flask application)
|
||||||
|
- **Container name**: `digiserver-caddy` (Reverse proxy)
|
||||||
|
- **Network**: `digiserver-v2_digiserver-network`
|
||||||
|
- **Database**: SQLite at `/app/instance/digiserver.db`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 Related Documentation
|
||||||
|
|
||||||
|
- [DEPLOYMENT_COMMANDS.md](DEPLOYMENT_COMMANDS.md) - Complete deployment guide
|
||||||
|
- [setup_https.sh](setup_https.sh) - Semi-automated setup script
|
||||||
|
- [deploy.sh](deploy.sh) - Fully automated deployment script
|
||||||
|
- [HTTPS_CONFIGURATION.md](old_code_documentation/HTTPS_CONFIGURATION.md) - HTTPS details
|
||||||
|
|
||||||
192
old_code_documentation/HTTPS_CONFIGURATION.md
Normal file
192
old_code_documentation/HTTPS_CONFIGURATION.md
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
# HTTPS Configuration Management System
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The DigiServer v2 now includes a built-in HTTPS configuration management system accessible through the Admin Panel. This allows administrators to enable and manage HTTPS/SSL settings directly from the web interface without needing to manually edit configuration files.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Enable/Disable HTTPS**: Toggle HTTPS on and off from the admin panel
|
||||||
|
- **Domain Management**: Set the full domain name (e.g., `digiserver.sibiusb.harting.intra`)
|
||||||
|
- **Hostname Configuration**: Configure server hostname (e.g., `digiserver`)
|
||||||
|
- **IP Address Management**: Set the IP address for direct access (e.g., `10.76.152.164`)
|
||||||
|
- **Port Configuration**: Customize HTTPS port (default: 443)
|
||||||
|
- **Status Tracking**: View current HTTPS status and configuration details
|
||||||
|
- **Real-time Preview**: See access points as you configure settings
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### Step 1: Initial Setup (HTTP Only)
|
||||||
|
1. Start the application normally: `docker-compose up -d`
|
||||||
|
2. The app runs on HTTP port 80
|
||||||
|
3. Access via: `http://<server-ip>`
|
||||||
|
|
||||||
|
### Step 2: Enable HTTPS via Admin Panel
|
||||||
|
1. Log in to the admin panel as an administrator
|
||||||
|
2. Navigate to: **Admin Panel → 🔒 HTTPS Configuration**
|
||||||
|
3. Toggle the "Enable HTTPS" switch
|
||||||
|
4. Fill in the required fields:
|
||||||
|
- **Hostname**: Short name for your server (e.g., `digiserver`)
|
||||||
|
- **Full Domain Name**: Complete domain (e.g., `digiserver.sibiusb.harting.intra`)
|
||||||
|
- **IP Address**: Server IP address (e.g., `10.76.152.164`)
|
||||||
|
- **HTTPS Port**: Port number (default: 443)
|
||||||
|
|
||||||
|
### Step 3: Verify Configuration
|
||||||
|
1. The status section shows your HTTPS configuration
|
||||||
|
2. Access points are displayed:
|
||||||
|
- HTTPS: `https://digiserver.sibiusb.harting.intra`
|
||||||
|
- HTTP fallback: `http://10.76.152.164`
|
||||||
|
|
||||||
|
## Configuration Details
|
||||||
|
|
||||||
|
### Database Model (HTTPSConfig)
|
||||||
|
|
||||||
|
The configuration is stored in the `https_config` table with the following fields:
|
||||||
|
|
||||||
|
```python
|
||||||
|
- id: Primary key
|
||||||
|
- https_enabled: Boolean flag for HTTPS status
|
||||||
|
- hostname: Server hostname
|
||||||
|
- domain: Full domain name
|
||||||
|
- ip_address: IPv4 or IPv6 address
|
||||||
|
- port: HTTPS port (default: 443)
|
||||||
|
- created_at: Creation timestamp
|
||||||
|
- updated_at: Last modification timestamp
|
||||||
|
- updated_by: Username of admin who made the change
|
||||||
|
```
|
||||||
|
|
||||||
|
### Admin Routes
|
||||||
|
|
||||||
|
- **GET /admin/https-config**: View HTTPS configuration page
|
||||||
|
- **POST /admin/https-config/update**: Update HTTPS settings
|
||||||
|
- **GET /admin/https-config/status**: Get current status as JSON
|
||||||
|
|
||||||
|
## Integration with Docker & Caddy
|
||||||
|
|
||||||
|
The HTTPS configuration works in conjunction with:
|
||||||
|
|
||||||
|
1. **Caddy Reverse Proxy**: Automatically handles SSL/TLS
|
||||||
|
2. **Let's Encrypt**: Provides free SSL certificates
|
||||||
|
3. **docker-compose.yml**: Uses the configured domain for Caddy
|
||||||
|
|
||||||
|
### Current Setup
|
||||||
|
|
||||||
|
**docker-compose.yml** uses `digiserver.sibiusb.harting.intra` as the primary domain.
|
||||||
|
|
||||||
|
**Caddyfile** configurations:
|
||||||
|
- HTTPS: `digiserver.sibiusb.harting.intra` (auto-managed SSL)
|
||||||
|
- HTTP Fallback: `10.76.152.164` (direct IP access)
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before enabling HTTPS, ensure:
|
||||||
|
|
||||||
|
1. **DNS Resolution**: Domain must resolve to the server's IP
|
||||||
|
```bash
|
||||||
|
# Test DNS resolution
|
||||||
|
nslookup digiserver.sibiusb.harting.intra
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Ports Accessible**:
|
||||||
|
- Port 80 (HTTP): For Let's Encrypt challenges
|
||||||
|
- Port 443 (HTTPS): For secure traffic
|
||||||
|
- Port 443/UDP: For HTTP/3 support
|
||||||
|
|
||||||
|
3. **Firewall Rules**: Ensure inbound traffic is allowed on ports 80 and 443
|
||||||
|
|
||||||
|
4. **Hosts File** (if DNS not available):
|
||||||
|
```
|
||||||
|
10.76.152.164 digiserver.sibiusb.harting.intra
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Migration
|
||||||
|
|
||||||
|
To set up the HTTPS configuration table, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From inside the Docker container
|
||||||
|
python /app/migrations/add_https_config_table.py
|
||||||
|
|
||||||
|
# Or from the host machine
|
||||||
|
docker-compose exec digiserver-app python /app/migrations/add_https_config_table.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Access Points After Configuration
|
||||||
|
|
||||||
|
### HTTPS (Recommended)
|
||||||
|
- URL: `https://digiserver.sibiusb.harting.intra`
|
||||||
|
- Protocol: HTTPS with SSL/TLS
|
||||||
|
- Automatic redirects from HTTP
|
||||||
|
- Let's Encrypt certificate (auto-renewed)
|
||||||
|
|
||||||
|
### HTTP Fallback
|
||||||
|
- URL: `http://10.76.152.164`
|
||||||
|
- Protocol: Plain HTTP (no encryption)
|
||||||
|
- Used when domain is not accessible
|
||||||
|
- Automatically redirects to HTTPS
|
||||||
|
|
||||||
|
## Security Features
|
||||||
|
|
||||||
|
✅ Automatic SSL certificate management (Let's Encrypt)
|
||||||
|
✅ Automatic certificate renewal
|
||||||
|
✅ Security headers (HSTS, X-Frame-Options, etc.)
|
||||||
|
✅ HTTP/2 and HTTP/3 support
|
||||||
|
✅ Admin-only access to configuration
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
All HTTPS configuration changes are logged in the server logs:
|
||||||
|
|
||||||
|
```
|
||||||
|
✓ HTTPS enabled by admin: domain=digiserver.sibiusb.harting.intra, hostname=digiserver, ip=10.76.152.164
|
||||||
|
✓ HTTPS disabled by admin
|
||||||
|
```
|
||||||
|
|
||||||
|
Check admin panel → Logs for detailed audit trail.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### HTTPS Not Working
|
||||||
|
1. Verify DNS resolution: `nslookup digiserver.sibiusb.harting.intra`
|
||||||
|
2. Check Caddy logs: `docker-compose logs caddy`
|
||||||
|
3. Ensure ports 80 and 443 are open
|
||||||
|
4. Check firewall rules
|
||||||
|
|
||||||
|
### Certificate Issues
|
||||||
|
1. Check Caddy container logs
|
||||||
|
2. Verify domain is accessible from internet
|
||||||
|
3. Ensure Let's Encrypt can validate domain
|
||||||
|
4. Check email configuration for certificate notifications
|
||||||
|
|
||||||
|
### Configuration Not Applied
|
||||||
|
1. Verify database migration ran: `python migrations/add_https_config_table.py`
|
||||||
|
2. Restart containers: `docker-compose restart`
|
||||||
|
3. Check admin panel for error messages
|
||||||
|
4. Review server logs
|
||||||
|
|
||||||
|
## Example Configuration
|
||||||
|
|
||||||
|
For a typical setup:
|
||||||
|
|
||||||
|
```
|
||||||
|
Hostname: digiserver
|
||||||
|
Domain: digiserver.sibiusb.harting.intra
|
||||||
|
IP Address: 10.76.152.164
|
||||||
|
Port: 443
|
||||||
|
HTTPS Status: Enabled ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
Access via:
|
||||||
|
- `https://digiserver.sibiusb.harting.intra` ← Primary
|
||||||
|
- `http://10.76.152.164` ← Fallback
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
Potential improvements for future versions:
|
||||||
|
|
||||||
|
- Certificate upload/management interface
|
||||||
|
- Domain validation checker
|
||||||
|
- Automatic DNS verification
|
||||||
|
- Custom SSL certificate support
|
||||||
|
- Certificate expiration notifications
|
||||||
|
- A/B testing for domain migration
|
||||||
202
old_code_documentation/HTTPS_EMAIL_UPDATE.md
Normal file
202
old_code_documentation/HTTPS_EMAIL_UPDATE.md
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
# HTTPS Email Configuration - Update Guide
|
||||||
|
|
||||||
|
## What's New
|
||||||
|
|
||||||
|
The HTTPS configuration system now includes an **Email Address** field that is essential for:
|
||||||
|
- SSL certificate management (Let's Encrypt)
|
||||||
|
- Certificate expiration notifications
|
||||||
|
- Certificate renewal reminders
|
||||||
|
|
||||||
|
## Changes Made
|
||||||
|
|
||||||
|
### 1. **Database Model** (`app/models/https_config.py`)
|
||||||
|
- Added `email` field to HTTPSConfig model
|
||||||
|
- Updated `create_or_update()` method to accept email parameter
|
||||||
|
- Updated `to_dict()` method to include email in output
|
||||||
|
|
||||||
|
### 2. **Admin Routes** (`app/blueprints/admin.py`)
|
||||||
|
- Added email form field handling
|
||||||
|
- Added email validation (checks for '@' symbol)
|
||||||
|
- Updated configuration save to store email
|
||||||
|
- Updated logging to include email in configuration changes
|
||||||
|
|
||||||
|
### 3. **Admin Template** (`app/templates/admin/https_config.html`)
|
||||||
|
- Added email input field in configuration form
|
||||||
|
- Added email display in status section
|
||||||
|
- Added help text explaining email purpose
|
||||||
|
- Email marked as required when HTTPS is enabled
|
||||||
|
|
||||||
|
### 4. **CLI Utility** (`https_manager.py`)
|
||||||
|
- Updated enable command to accept email parameter
|
||||||
|
- Updated help text to show email requirement
|
||||||
|
- Example: `python https_manager.py enable digiserver domain.local admin@example.com 10.76.152.164`
|
||||||
|
|
||||||
|
### 5. **Database Migration** (`migrations/add_email_to_https_config.py`)
|
||||||
|
- New migration script to add email column to existing database
|
||||||
|
|
||||||
|
## Update Instructions
|
||||||
|
|
||||||
|
### Step 1: Run Database Migration
|
||||||
|
```bash
|
||||||
|
# Add email column to existing https_config table
|
||||||
|
python /app/migrations/add_email_to_https_config.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Restart Application
|
||||||
|
```bash
|
||||||
|
docker-compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Configure Email via Admin Panel
|
||||||
|
1. Navigate to: **Admin Panel → 🔒 HTTPS Configuration**
|
||||||
|
2. Fill in the new **Email Address** field
|
||||||
|
3. Example: `admin@example.com`
|
||||||
|
4. Click **Save HTTPS Configuration**
|
||||||
|
|
||||||
|
## Configuration Form - New Field
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- Email Field -->
|
||||||
|
<label for="email">Email Address *</label>
|
||||||
|
<input type="email" id="email" name="email"
|
||||||
|
value="admin@example.com"
|
||||||
|
placeholder="e.g., admin@example.com"
|
||||||
|
required>
|
||||||
|
<p>Email address for SSL certificate notifications and Let's Encrypt communications</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
## CLI Usage - New Syntax
|
||||||
|
|
||||||
|
**Old (still works for HTTP):**
|
||||||
|
```bash
|
||||||
|
python https_manager.py enable digiserver domain.local 10.76.152.164 443
|
||||||
|
```
|
||||||
|
|
||||||
|
**New (with email - recommended):**
|
||||||
|
```bash
|
||||||
|
python https_manager.py enable digiserver domain.local admin@example.com 10.76.152.164 443
|
||||||
|
```
|
||||||
|
|
||||||
|
## Status Display - Updated
|
||||||
|
|
||||||
|
The status card now shows:
|
||||||
|
```
|
||||||
|
✅ HTTPS ENABLED
|
||||||
|
Domain: digiserver.sibiusb.harting.intra
|
||||||
|
Hostname: digiserver
|
||||||
|
Email: admin@example.com ← NEW
|
||||||
|
IP Address: 10.76.152.164
|
||||||
|
Port: 443
|
||||||
|
Access URL: https://digiserver.sibiusb.harting.intra
|
||||||
|
Last Updated: 2026-01-14 15:30:45 by admin
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
The system now validates:
|
||||||
|
- ✅ Email format (must contain '@')
|
||||||
|
- ✅ Email is required when HTTPS is enabled
|
||||||
|
- ✅ Email is stored in database
|
||||||
|
- ✅ Email is logged when configuration changes
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
📧 **Proper SSL Certificate Management**
|
||||||
|
- Let's Encrypt sends notifications to configured email
|
||||||
|
- Certificate expiration warnings before renewal
|
||||||
|
|
||||||
|
📋 **Better Configuration**
|
||||||
|
- Email is persisted in database
|
||||||
|
- No need to set environment variables
|
||||||
|
- Fully managed through admin panel
|
||||||
|
|
||||||
|
🔐 **Professional Setup**
|
||||||
|
- Real email address for certificate notifications
|
||||||
|
- Easier to manage multiple servers
|
||||||
|
- Complete audit trail with email address
|
||||||
|
|
||||||
|
## Backwards Compatibility
|
||||||
|
|
||||||
|
If you have an existing HTTPS configuration without an email:
|
||||||
|
1. The email field will be NULL
|
||||||
|
2. You'll see an error when trying to use HTTPS without email
|
||||||
|
3. Simply add the email through the admin panel and save
|
||||||
|
4. Configuration will be complete
|
||||||
|
|
||||||
|
## Database Schema Update
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE https_config ADD COLUMN email VARCHAR(255);
|
||||||
|
```
|
||||||
|
|
||||||
|
New schema:
|
||||||
|
```
|
||||||
|
https_config table:
|
||||||
|
├── id (PK)
|
||||||
|
├── https_enabled (BOOLEAN)
|
||||||
|
├── hostname (VARCHAR)
|
||||||
|
├── domain (VARCHAR)
|
||||||
|
├── ip_address (VARCHAR)
|
||||||
|
├── email (VARCHAR) ← NEW
|
||||||
|
├── port (INTEGER)
|
||||||
|
├── created_at (DATETIME)
|
||||||
|
├── updated_at (DATETIME)
|
||||||
|
└── updated_by (VARCHAR)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example Configuration
|
||||||
|
|
||||||
|
**Complete HTTPS Setup:**
|
||||||
|
```
|
||||||
|
Hostname: digiserver
|
||||||
|
Domain: digiserver.sibiusb.harting.intra
|
||||||
|
Email: admin@example.com
|
||||||
|
IP: 10.76.152.164
|
||||||
|
Port: 443
|
||||||
|
Status: ✅ ENABLED
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Email Field Not Showing?
|
||||||
|
1. Clear browser cache (Ctrl+Shift+Del)
|
||||||
|
2. Reload the page
|
||||||
|
3. Check that containers restarted: `docker-compose restart`
|
||||||
|
|
||||||
|
### Migration Error?
|
||||||
|
If migration fails:
|
||||||
|
```bash
|
||||||
|
# Option 1: Add column manually
|
||||||
|
docker-compose exec digiserver-app python -c "
|
||||||
|
from app.app import create_app
|
||||||
|
from app.extensions import db
|
||||||
|
from sqlalchemy import text
|
||||||
|
app = create_app()
|
||||||
|
with app.app_context():
|
||||||
|
db.engine.execute(text('ALTER TABLE https_config ADD COLUMN email VARCHAR(255)'))
|
||||||
|
"
|
||||||
|
|
||||||
|
# Option 2: Reset database (if testing)
|
||||||
|
rm instance/digiserver.db
|
||||||
|
python /app/migrations/add_https_config_table.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Email Required" Error When HTTPS Enabled?
|
||||||
|
- Admin panel: Fill in the Email Address field before saving
|
||||||
|
- CLI: Include email in command: `python https_manager.py enable ... email@example.com ...`
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. Run the database migration
|
||||||
|
2. Restart the application
|
||||||
|
3. Navigate to HTTPS Configuration
|
||||||
|
4. Enter a valid email address (e.g., `admin@example.com`)
|
||||||
|
5. Enable HTTPS
|
||||||
|
6. System will use this email for Let's Encrypt notifications
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues or questions:
|
||||||
|
- Check `HTTPS_CONFIGURATION.md` for detailed documentation
|
||||||
|
- See `HTTPS_QUICK_REFERENCE.md` for quick examples
|
||||||
|
- Review server logs in admin panel for configuration changes
|
||||||
316
old_code_documentation/HTTPS_IMPLEMENTATION_SUMMARY.md
Normal file
316
old_code_documentation/HTTPS_IMPLEMENTATION_SUMMARY.md
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
# HTTPS Management System - Implementation Summary
|
||||||
|
|
||||||
|
## ✅ What Has Been Implemented
|
||||||
|
|
||||||
|
A complete HTTPS configuration management system has been added to DigiServer v2, allowing administrators to manage HTTPS settings through the web interface.
|
||||||
|
|
||||||
|
### Files Created
|
||||||
|
|
||||||
|
#### 1. **Database Model** (`app/models/https_config.py`)
|
||||||
|
- New `HTTPSConfig` model for storing HTTPS configuration
|
||||||
|
- Fields: hostname, domain, ip_address, port, enabled status, audit trail
|
||||||
|
- Methods: `get_config()`, `create_or_update()`, `to_dict()`
|
||||||
|
|
||||||
|
#### 2. **Admin Routes** (updated `app/blueprints/admin.py`)
|
||||||
|
- `GET /admin/https-config` - Display configuration page
|
||||||
|
- `POST /admin/https-config/update` - Update settings
|
||||||
|
- `GET /admin/https-config/status` - Get status as JSON
|
||||||
|
- Full validation and error handling
|
||||||
|
- Admin-only access with permission checks
|
||||||
|
|
||||||
|
#### 3. **Admin Template** (`app/templates/admin/https_config.html`)
|
||||||
|
- Beautiful, user-friendly configuration interface
|
||||||
|
- Status display showing current HTTPS settings
|
||||||
|
- Form with toggle switch for enable/disable
|
||||||
|
- Input fields for: hostname, domain, IP address, port
|
||||||
|
- Real-time preview of access points
|
||||||
|
- Comprehensive help text and information sections
|
||||||
|
- Responsive design for mobile compatibility
|
||||||
|
|
||||||
|
#### 4. **Database Migration** (`migrations/add_https_config_table.py`)
|
||||||
|
- Creates `https_config` table with all necessary fields
|
||||||
|
- Indexes on important columns
|
||||||
|
- Timestamps for audit trail
|
||||||
|
|
||||||
|
#### 5. **Admin Dashboard Link** (updated `app/templates/admin/admin.html`)
|
||||||
|
- Added new card in admin dashboard linking to HTTPS configuration
|
||||||
|
- Purple gradient card with lock icon (🔒)
|
||||||
|
- Easy access from main admin panel
|
||||||
|
|
||||||
|
#### 6. **CLI Utility** (`https_manager.py`)
|
||||||
|
- Command-line interface for managing HTTPS configuration
|
||||||
|
- Commands: `status`, `enable`, `disable`, `show`
|
||||||
|
- Useful for automation and scripting
|
||||||
|
|
||||||
|
#### 7. **Setup Script** (`setup_https.sh`)
|
||||||
|
- Automated setup script for database migration
|
||||||
|
- Step-by-step instructions for configuration
|
||||||
|
|
||||||
|
#### 8. **Documentation** (`HTTPS_CONFIGURATION.md`)
|
||||||
|
- Comprehensive guide covering:
|
||||||
|
- Feature overview
|
||||||
|
- Step-by-step workflow
|
||||||
|
- Configuration details
|
||||||
|
- Prerequisites
|
||||||
|
- Integration details
|
||||||
|
- Troubleshooting
|
||||||
|
- Examples
|
||||||
|
|
||||||
|
### Files Updated
|
||||||
|
|
||||||
|
#### 1. **Models Package** (`app/models/__init__.py`)
|
||||||
|
- Added import for `HTTPSConfig`
|
||||||
|
- Exported in `__all__` list
|
||||||
|
|
||||||
|
#### 2. **Admin Blueprint** (`app/blueprints/admin.py`)
|
||||||
|
- Imported `HTTPSConfig` model
|
||||||
|
- Added HTTPS management routes
|
||||||
|
|
||||||
|
#### 3. **Admin Dashboard** (`app/templates/admin/admin.html`)
|
||||||
|
- Added link to HTTPS configuration
|
||||||
|
|
||||||
|
#### 4. **Caddyfile**
|
||||||
|
- Already preconfigured with domain: `digiserver.sibiusb.harting.intra`
|
||||||
|
- IP fallback: `10.76.152.164`
|
||||||
|
- Ready to use with the new configuration system
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Quick Start Guide
|
||||||
|
|
||||||
|
### Step 1: Database Setup
|
||||||
|
```bash
|
||||||
|
# Run the migration to create the https_config table
|
||||||
|
python /app/migrations/add_https_config_table.py
|
||||||
|
|
||||||
|
# Or automatically with the setup script
|
||||||
|
bash setup_https.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Start the Application (HTTP Only)
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Configure HTTPS via Admin Panel
|
||||||
|
1. Log in as admin
|
||||||
|
2. Go to: **Admin Panel → 🔒 HTTPS Configuration**
|
||||||
|
3. Toggle "Enable HTTPS"
|
||||||
|
4. Fill in:
|
||||||
|
- Hostname: `digiserver`
|
||||||
|
- Domain: `digiserver.sibiusb.harting.intra`
|
||||||
|
- IP Address: `10.76.152.164`
|
||||||
|
- Port: `443` (default)
|
||||||
|
5. Click "Save HTTPS Configuration"
|
||||||
|
|
||||||
|
### Step 4: Verify Access
|
||||||
|
- HTTPS: `https://digiserver.sibiusb.harting.intra`
|
||||||
|
- HTTP Fallback: `http://10.76.152.164`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Workflow Explanation
|
||||||
|
|
||||||
|
### Initial State (HTTP Only)
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ App Running on │
|
||||||
|
│ Port 80 (HTTP) │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
└─ Accessible at: http://10.76.152.164
|
||||||
|
```
|
||||||
|
|
||||||
|
### After Configuration (HTTP + HTTPS)
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────┐
|
||||||
|
│ Admin Configures HTTPS Settings: │
|
||||||
|
│ • Hostname: digiserver │
|
||||||
|
│ • Domain: digiserver...intra │
|
||||||
|
│ • IP: 10.76.152.164 │
|
||||||
|
│ • Port: 443 │
|
||||||
|
└──────────────┬───────────────────────┘
|
||||||
|
│
|
||||||
|
┌───────┴────────┐
|
||||||
|
│ │
|
||||||
|
┌────▼────┐ ┌─────▼──────┐
|
||||||
|
│ HTTPS │ │ HTTP │
|
||||||
|
│ Port443 │ │ Port 80 │
|
||||||
|
└────┬────┘ └─────┬──────┘
|
||||||
|
│ │
|
||||||
|
└──────────────┘
|
||||||
|
Both available
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Security Features
|
||||||
|
|
||||||
|
✅ **Admin-Only Access**
|
||||||
|
- Only administrators can access HTTPS configuration
|
||||||
|
- All changes logged with admin username and timestamp
|
||||||
|
|
||||||
|
✅ **Input Validation**
|
||||||
|
- Domain format validation
|
||||||
|
- IP address format validation (IPv4/IPv6)
|
||||||
|
- Port range validation (1-65535)
|
||||||
|
|
||||||
|
✅ **SSL/TLS Management**
|
||||||
|
- Automatic Let's Encrypt integration (via Caddy)
|
||||||
|
- Automatic certificate renewal
|
||||||
|
- Security headers (HSTS, X-Frame-Options, etc.)
|
||||||
|
|
||||||
|
✅ **Audit Trail**
|
||||||
|
- All configuration changes logged
|
||||||
|
- Admin dashboard logs show who changed what and when
|
||||||
|
- Server logs track HTTPS enable/disable events
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ CLI Management
|
||||||
|
|
||||||
|
Configure HTTPS from command line:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Show current status
|
||||||
|
python https_manager.py status
|
||||||
|
|
||||||
|
# Enable HTTPS
|
||||||
|
python https_manager.py enable digiserver digiserver.sibiusb.harting.intra 10.76.152.164 443
|
||||||
|
|
||||||
|
# Disable HTTPS
|
||||||
|
python https_manager.py disable
|
||||||
|
|
||||||
|
# Show detailed configuration
|
||||||
|
python https_manager.py show
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Database Schema
|
||||||
|
|
||||||
|
**https_config table:**
|
||||||
|
```
|
||||||
|
┌──────────────────┬────────────────────┬──────────────┐
|
||||||
|
│ Column │ Type │ Description │
|
||||||
|
├──────────────────┼────────────────────┼──────────────┤
|
||||||
|
│ id │ Integer (PK) │ Primary key │
|
||||||
|
│ https_enabled │ Boolean │ Enable flag │
|
||||||
|
│ hostname │ String(255) │ Server name │
|
||||||
|
│ domain │ String(255) │ Domain name │
|
||||||
|
│ ip_address │ String(45) │ IP address │
|
||||||
|
│ port │ Integer │ HTTPS port │
|
||||||
|
│ created_at │ DateTime │ Created time │
|
||||||
|
│ updated_at │ DateTime │ Updated time │
|
||||||
|
│ updated_by │ String(255) │ Admin user │
|
||||||
|
└──────────────────┴────────────────────┴──────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
### Test HTTPS Configuration UI
|
||||||
|
1. Log in as admin
|
||||||
|
2. Go to Admin Panel → HTTPS Configuration
|
||||||
|
3. Test Enable/Disable toggle
|
||||||
|
4. Test form validation with invalid inputs
|
||||||
|
5. Verify real-time preview updates
|
||||||
|
|
||||||
|
### Test Access Points
|
||||||
|
```bash
|
||||||
|
# Test HTTPS
|
||||||
|
curl -k https://digiserver.sibiusb.harting.intra
|
||||||
|
|
||||||
|
# Test HTTP Fallback
|
||||||
|
curl http://10.76.152.164
|
||||||
|
|
||||||
|
# Test status endpoint
|
||||||
|
curl http://<admin>/admin/https-config/status
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Configuration Examples
|
||||||
|
|
||||||
|
### Default Configuration
|
||||||
|
```python
|
||||||
|
hostname = "digiserver"
|
||||||
|
domain = "digiserver.sibiusb.harting.intra"
|
||||||
|
ip_address = "10.76.152.164"
|
||||||
|
port = 443
|
||||||
|
https_enabled = True
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration for Different Network
|
||||||
|
```python
|
||||||
|
hostname = "myserver"
|
||||||
|
domain = "myserver.company.local"
|
||||||
|
ip_address = "192.168.1.100"
|
||||||
|
port = 8443
|
||||||
|
https_enabled = True
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Integration with Existing System
|
||||||
|
|
||||||
|
The HTTPS configuration system integrates seamlessly with:
|
||||||
|
|
||||||
|
1. **Caddy Reverse Proxy** - Uses configured domain for SSL termination
|
||||||
|
2. **Let's Encrypt** - Automatic certificate provisioning and renewal
|
||||||
|
3. **Flask Application** - No code changes needed, works with existing auth
|
||||||
|
4. **Database** - Stores configuration persistently
|
||||||
|
5. **Logging System** - All changes logged and auditable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Key Benefits
|
||||||
|
|
||||||
|
✨ **No Manual Configuration** - All settings through web UI
|
||||||
|
✨ **Easy to Use** - Intuitive interface with real-time preview
|
||||||
|
✨ **Audit Trail** - Track all HTTPS configuration changes
|
||||||
|
✨ **Flexible** - Support for multiple access points (HTTPS + HTTP)
|
||||||
|
✨ **Secure** - Admin-only access with validation
|
||||||
|
✨ **Automated** - Automatic SSL certificate management
|
||||||
|
✨ **CLI Support** - Programmatic configuration via command line
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Next Steps
|
||||||
|
|
||||||
|
1. ✅ **Run Database Migration**
|
||||||
|
```bash
|
||||||
|
python /app/migrations/add_https_config_table.py
|
||||||
|
```
|
||||||
|
|
||||||
|
2. ✅ **Start Application**
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
3. ✅ **Configure via Admin Panel**
|
||||||
|
- Navigate to Admin → HTTPS Configuration
|
||||||
|
- Enable HTTPS with your settings
|
||||||
|
|
||||||
|
4. ✅ **Verify Configuration**
|
||||||
|
- Check status displays correctly
|
||||||
|
- Test access points work
|
||||||
|
- Review logs for changes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Support & Troubleshooting
|
||||||
|
|
||||||
|
See `HTTPS_CONFIGURATION.md` for:
|
||||||
|
- Detailed troubleshooting guide
|
||||||
|
- DNS configuration instructions
|
||||||
|
- Firewall requirements
|
||||||
|
- Let's Encrypt certificate issues
|
||||||
|
- Error messages and solutions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 Implementation Complete!
|
||||||
|
|
||||||
|
The HTTPS configuration management system is ready to use. All components are in place and documented. Simply run the database migration and start using the feature through the admin panel!
|
||||||
259
old_code_documentation/HTTPS_QUICK_REFERENCE.md
Normal file
259
old_code_documentation/HTTPS_QUICK_REFERENCE.md
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
# HTTPS Configuration - Quick Reference Guide
|
||||||
|
|
||||||
|
## 🎯 Quick Access
|
||||||
|
|
||||||
|
**Admin Panel Location:** Main Dashboard → 🔒 **HTTPS Configuration** (Purple card)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚡ Quick Setup (5 Minutes)
|
||||||
|
|
||||||
|
### 1. Initial State
|
||||||
|
Your app is running on HTTP. Access: `http://10.76.152.164`
|
||||||
|
|
||||||
|
### 2. Navigate to HTTPS Config
|
||||||
|
- Admin Panel → 🔒 HTTPS Configuration
|
||||||
|
|
||||||
|
### 3. Configure (Fill In)
|
||||||
|
| Field | Value | Example |
|
||||||
|
|-------|-------|---------|
|
||||||
|
| Hostname | Server short name | `digiserver` |
|
||||||
|
| Domain | Full domain name | `digiserver.sibiusb.harting.intra` |
|
||||||
|
| IP Address | Server IP | `10.76.152.164` |
|
||||||
|
| Port | HTTPS port (default 443) | `443` |
|
||||||
|
|
||||||
|
### 4. Enable HTTPS
|
||||||
|
- Toggle: **Enable HTTPS** ✅
|
||||||
|
- Click: **💾 Save HTTPS Configuration**
|
||||||
|
|
||||||
|
### 5. Verify
|
||||||
|
- ✅ Configuration shows as "ENABLED"
|
||||||
|
- ✅ Access via: `https://digiserver.sibiusb.harting.intra`
|
||||||
|
- ✅ Check status card for current settings
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Status Display
|
||||||
|
|
||||||
|
### Enabled State ✅
|
||||||
|
```
|
||||||
|
✅ HTTPS ENABLED
|
||||||
|
Domain: digiserver.sibiusb.harting.intra
|
||||||
|
Hostname: digiserver
|
||||||
|
IP Address: 10.76.152.164
|
||||||
|
Port: 443
|
||||||
|
Access URL: https://digiserver.sibiusb.harting.intra
|
||||||
|
Last Updated: 2024-01-14 15:30:45 by admin
|
||||||
|
```
|
||||||
|
|
||||||
|
### Disabled State ⚠️
|
||||||
|
```
|
||||||
|
⚠️ HTTPS DISABLED
|
||||||
|
The application is currently running on HTTP only (port 80)
|
||||||
|
Enable HTTPS below to secure your application.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Access Points
|
||||||
|
|
||||||
|
### After HTTPS is Enabled
|
||||||
|
|
||||||
|
| Access Type | URL | Use Case |
|
||||||
|
|------------|-----|----------|
|
||||||
|
| **Primary (HTTPS)** | `https://digiserver.sibiusb.harting.intra` | Daily use, secure |
|
||||||
|
| **Fallback (HTTP)** | `http://10.76.152.164` | Troubleshooting, direct IP access |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Prerequisites Checklist
|
||||||
|
|
||||||
|
Before enabling HTTPS:
|
||||||
|
|
||||||
|
- [ ] DNS resolves domain to IP: `nslookup digiserver.sibiusb.harting.intra`
|
||||||
|
- [ ] Firewall allows port 80 (HTTP)
|
||||||
|
- [ ] Firewall allows port 443 (HTTPS)
|
||||||
|
- [ ] Server IP is `10.76.152.164`
|
||||||
|
- [ ] Domain is `digiserver.sibiusb.harting.intra`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### HTTPS Not Working?
|
||||||
|
|
||||||
|
1. **Check Status**
|
||||||
|
- Admin → HTTPS Configuration
|
||||||
|
- Verify "HTTPS ENABLED" is shown
|
||||||
|
|
||||||
|
2. **Test DNS**
|
||||||
|
```bash
|
||||||
|
nslookup digiserver.sibiusb.harting.intra
|
||||||
|
```
|
||||||
|
Should resolve to: `10.76.152.164`
|
||||||
|
|
||||||
|
3. **Test Ports**
|
||||||
|
```bash
|
||||||
|
# Should be reachable
|
||||||
|
telnet 10.76.152.164 443
|
||||||
|
telnet 10.76.152.164 80
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Check Logs**
|
||||||
|
- Admin Panel → Server Logs
|
||||||
|
- Look for HTTPS enable/disable messages
|
||||||
|
|
||||||
|
5. **View Caddy Logs**
|
||||||
|
```bash
|
||||||
|
docker-compose logs caddy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Domain Not Resolving?
|
||||||
|
|
||||||
|
**Add to hosts file** (temporary):
|
||||||
|
- Windows: `C:\Windows\System32\drivers\etc\hosts`
|
||||||
|
- Mac/Linux: `/etc/hosts`
|
||||||
|
|
||||||
|
Add line:
|
||||||
|
```
|
||||||
|
10.76.152.164 digiserver.sibiusb.harting.intra
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Common Tasks
|
||||||
|
|
||||||
|
### Enable HTTPS
|
||||||
|
1. Go to Admin → HTTPS Configuration
|
||||||
|
2. Toggle "Enable HTTPS"
|
||||||
|
3. Fill in hostname, domain, IP
|
||||||
|
4. Click "Save HTTPS Configuration"
|
||||||
|
|
||||||
|
### Disable HTTPS
|
||||||
|
1. Go to Admin → HTTPS Configuration
|
||||||
|
2. Toggle off "Enable HTTPS"
|
||||||
|
3. Click "Save HTTPS Configuration"
|
||||||
|
4. App returns to HTTP only
|
||||||
|
|
||||||
|
### Change Domain
|
||||||
|
1. Go to Admin → HTTPS Configuration
|
||||||
|
2. Update "Full Domain Name"
|
||||||
|
3. Click "Save HTTPS Configuration"
|
||||||
|
|
||||||
|
### Check Current Settings
|
||||||
|
1. Go to Admin → HTTPS Configuration
|
||||||
|
2. View status card at top
|
||||||
|
3. Shows all current settings
|
||||||
|
|
||||||
|
### View Configuration History
|
||||||
|
1. Admin Panel → Server Logs
|
||||||
|
2. Search for "HTTPS"
|
||||||
|
3. See all changes and who made them
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Configuration Examples
|
||||||
|
|
||||||
|
### Default Setup (Already Provided)
|
||||||
|
```
|
||||||
|
Hostname: digiserver
|
||||||
|
Domain: digiserver.sibiusb.harting.intra
|
||||||
|
IP: 10.76.152.164
|
||||||
|
Port: 443
|
||||||
|
```
|
||||||
|
|
||||||
|
### Different IP
|
||||||
|
```
|
||||||
|
Hostname: digiserver
|
||||||
|
Domain: digiserver.sibiusb.harting.intra
|
||||||
|
IP: 10.76.152.165 ← Change this
|
||||||
|
Port: 443
|
||||||
|
```
|
||||||
|
|
||||||
|
### Different Domain
|
||||||
|
```
|
||||||
|
Hostname: myserver
|
||||||
|
Domain: myserver.company.local ← Change this
|
||||||
|
IP: 10.76.152.164
|
||||||
|
Port: 443
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Security Notes
|
||||||
|
|
||||||
|
✅ **Admin-Only Feature**
|
||||||
|
- Only administrators can access this page
|
||||||
|
- All changes logged with admin username
|
||||||
|
|
||||||
|
✅ **Automatic SSL Certificates**
|
||||||
|
- Let's Encrypt manages certificates
|
||||||
|
- Auto-renewed before expiration
|
||||||
|
- No manual certificate management needed
|
||||||
|
|
||||||
|
✅ **Access Control**
|
||||||
|
- HTTP redirects to HTTPS automatically
|
||||||
|
- Security headers automatically added
|
||||||
|
- Safe for internal and external access
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Need Help?
|
||||||
|
|
||||||
|
1. **Check Documentation**
|
||||||
|
- See: `HTTPS_CONFIGURATION.md` for detailed guide
|
||||||
|
- See: `HTTPS_IMPLEMENTATION_SUMMARY.md` for architecture
|
||||||
|
|
||||||
|
2. **View Logs**
|
||||||
|
- Admin Panel → Server Logs
|
||||||
|
- Filter for HTTPS-related entries
|
||||||
|
|
||||||
|
3. **Test Configuration**
|
||||||
|
```bash
|
||||||
|
# Via CLI
|
||||||
|
python https_manager.py status
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Restart Application**
|
||||||
|
```bash
|
||||||
|
docker-compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Quick Status Check
|
||||||
|
|
||||||
|
**CLI Command:**
|
||||||
|
```bash
|
||||||
|
python https_manager.py status
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output:**
|
||||||
|
```
|
||||||
|
==================================================
|
||||||
|
HTTPS Configuration Status
|
||||||
|
==================================================
|
||||||
|
Status: ✅ ENABLED
|
||||||
|
Hostname: digiserver
|
||||||
|
Domain: digiserver.sibiusb.harting.intra
|
||||||
|
IP Address: 10.76.152.164
|
||||||
|
Port: 443
|
||||||
|
Updated: 2024-01-14 15:30:45 by admin
|
||||||
|
|
||||||
|
Access URL: https://digiserver.sibiusb.harting.intra
|
||||||
|
Fallback: http://10.76.152.164
|
||||||
|
==================================================
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 You're All Set!
|
||||||
|
|
||||||
|
Your HTTPS configuration is ready to use. The system will:
|
||||||
|
- ✅ Manage SSL certificates automatically
|
||||||
|
- ✅ Keep them renewed
|
||||||
|
- ✅ Provide secure access
|
||||||
|
- ✅ Log all configuration changes
|
||||||
|
- ✅ Offer fallback HTTP access
|
||||||
|
|
||||||
|
**That's it! Your app is now secure!** 🔒
|
||||||
75
old_code_documentation/HTTPS_SETUP.md
Normal file
75
old_code_documentation/HTTPS_SETUP.md
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# DigiServer v2 - HTTPS Setup with Caddy
|
||||||
|
|
||||||
|
This setup uses **Caddy** as a reverse proxy with automatic HTTPS via Let's Encrypt.
|
||||||
|
|
||||||
|
## Quick Setup
|
||||||
|
|
||||||
|
### 1. Configure Domain
|
||||||
|
Create a `.env` file or edit the existing one:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit `.env` and set:
|
||||||
|
```
|
||||||
|
DOMAIN=your-domain.com
|
||||||
|
EMAIL=admin@your-domain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Point Your Domain
|
||||||
|
Make sure your domain's DNS A record points to your server's IP address.
|
||||||
|
|
||||||
|
### 3. Start Services
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
That's it! Caddy will **automatically**:
|
||||||
|
- Obtain SSL certificates from Let's Encrypt
|
||||||
|
- Renew certificates before expiration
|
||||||
|
- Redirect HTTP to HTTPS
|
||||||
|
- Enable HTTP/2 and HTTP/3
|
||||||
|
|
||||||
|
## Access Your Site
|
||||||
|
|
||||||
|
- **HTTP**: http://your-domain.com (redirects to HTTPS)
|
||||||
|
- **HTTPS**: https://your-domain.com
|
||||||
|
|
||||||
|
## Testing Locally (Without Domain)
|
||||||
|
|
||||||
|
If you don't have a domain yet, leave DOMAIN as `localhost`:
|
||||||
|
```
|
||||||
|
DOMAIN=localhost
|
||||||
|
```
|
||||||
|
|
||||||
|
Then access: http://localhost (no HTTPS, but app works)
|
||||||
|
|
||||||
|
## Certificate Storage
|
||||||
|
|
||||||
|
SSL certificates are stored in Docker volumes:
|
||||||
|
- `caddy-data` - Certificate data
|
||||||
|
- `caddy-config` - Caddy configuration
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Check Caddy logs:
|
||||||
|
```bash
|
||||||
|
docker logs digiserver-caddy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify certificates:
|
||||||
|
```bash
|
||||||
|
docker exec digiserver-caddy caddy list-certificates
|
||||||
|
```
|
||||||
|
|
||||||
|
### Force certificate renewal:
|
||||||
|
```bash
|
||||||
|
docker exec digiserver-caddy caddy reload --config /etc/caddy/Caddyfile
|
||||||
|
```
|
||||||
|
|
||||||
|
## Port Forwarding
|
||||||
|
|
||||||
|
Make sure your firewall/router allows:
|
||||||
|
- Port 80 (HTTP - for Let's Encrypt challenge)
|
||||||
|
- Port 443 (HTTPS)
|
||||||
84
old_code_documentation/NGINX_SETUP_QUICK.md
Normal file
84
old_code_documentation/NGINX_SETUP_QUICK.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# Quick Start: Nginx Setup for DigiServer v2
|
||||||
|
|
||||||
|
## Pre-requisites
|
||||||
|
- SSL certificates in `./data/nginx-ssl/cert.pem` and `./data/nginx-ssl/key.pem`
|
||||||
|
- Docker and Docker Compose installed
|
||||||
|
- Port 80 and 443 available
|
||||||
|
|
||||||
|
## Quick Setup (3 steps)
|
||||||
|
|
||||||
|
### 1. Generate Self-Signed Certificates
|
||||||
|
```bash
|
||||||
|
./generate_nginx_certs.sh localhost 365
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Update Nginx Configuration
|
||||||
|
- Edit `nginx.conf` to set your domain:
|
||||||
|
```nginx
|
||||||
|
server_name localhost; # Change to your domain
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Start Docker Compose
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
### Check if Nginx is running
|
||||||
|
```bash
|
||||||
|
docker ps | grep nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test HTTP → HTTPS redirect
|
||||||
|
```bash
|
||||||
|
curl -L http://localhost
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test HTTPS (with self-signed cert)
|
||||||
|
```bash
|
||||||
|
curl -k https://localhost
|
||||||
|
```
|
||||||
|
|
||||||
|
### View logs
|
||||||
|
```bash
|
||||||
|
docker logs digiserver-nginx
|
||||||
|
docker exec digiserver-nginx tail -f /var/log/nginx/access.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using Production Certificates
|
||||||
|
|
||||||
|
### Option A: Let's Encrypt (Free)
|
||||||
|
1. Install certbot: `apt-get install certbot`
|
||||||
|
2. Generate cert: `certbot certonly --standalone -d your-domain.com`
|
||||||
|
3. Copy cert: `cp /etc/letsencrypt/live/your-domain.com/fullchain.pem ./data/nginx-ssl/cert.pem`
|
||||||
|
4. Copy key: `cp /etc/letsencrypt/live/your-domain.com/privkey.pem ./data/nginx-ssl/key.pem`
|
||||||
|
5. Fix permissions: `sudo chown 101:101 ./data/nginx-ssl/*`
|
||||||
|
6. Reload: `docker exec digiserver-nginx nginx -s reload`
|
||||||
|
|
||||||
|
### Option B: Commercial Certificate
|
||||||
|
1. Place your certificate files in `./data/nginx-ssl/cert.pem` and `./data/nginx-ssl/key.pem`
|
||||||
|
2. Fix permissions: `sudo chown 101:101 ./data/nginx-ssl/*`
|
||||||
|
3. Reload: `docker exec digiserver-nginx nginx -s reload`
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Issue | Solution |
|
||||||
|
|-------|----------|
|
||||||
|
| Port 80/443 in use | `sudo netstat -tlnp \| grep :80` or `:443` |
|
||||||
|
| Certificate permission denied | `sudo chown 101:101 ./data/nginx-ssl/*` |
|
||||||
|
| Nginx won't start | `docker logs digiserver-nginx` |
|
||||||
|
| Connection refused | Check firewall: `sudo ufw allow 80/tcp && sudo ufw allow 443/tcp` |
|
||||||
|
|
||||||
|
## File Locations
|
||||||
|
- Main config: `./nginx.conf`
|
||||||
|
- SSL certs: `./data/nginx-ssl/`
|
||||||
|
- Logs: `./data/nginx-logs/`
|
||||||
|
- Custom domains: `./nginx-custom-domains.conf` (auto-generated)
|
||||||
|
|
||||||
|
## Next: Production Setup
|
||||||
|
1. Update `.env` with your DOMAIN and EMAIL
|
||||||
|
2. Configure HTTPS settings in admin panel
|
||||||
|
3. Run: `python nginx_manager.py generate`
|
||||||
|
4. Test: `docker exec digiserver-nginx nginx -t`
|
||||||
|
5. Reload: `docker exec digiserver-nginx nginx -s reload`
|
||||||
181
old_code_documentation/PLAYER_EDIT_MEDIA_API.md
Normal file
181
old_code_documentation/PLAYER_EDIT_MEDIA_API.md
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
# Player Edit Media API
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This API allows players to upload edited media files back to the server, maintaining version history and automatically updating playlists.
|
||||||
|
|
||||||
|
## Endpoint
|
||||||
|
|
||||||
|
### POST `/api/player-edit-media`
|
||||||
|
|
||||||
|
Upload an edited media file from a player device.
|
||||||
|
|
||||||
|
**Authentication Required:** Yes (Bearer token)
|
||||||
|
|
||||||
|
**Rate Limit:** 60 requests per 60 seconds
|
||||||
|
|
||||||
|
**Content-Type:** `multipart/form-data`
|
||||||
|
|
||||||
|
## Request
|
||||||
|
|
||||||
|
### Form Data
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `image_file` | File | Yes | The edited image file |
|
||||||
|
| `metadata` | JSON String | Yes | Metadata about the edit (see below) |
|
||||||
|
|
||||||
|
### Metadata JSON Structure
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"time_of_modification": "2025-12-05T20:30:00Z",
|
||||||
|
"original_name": "image.jpg",
|
||||||
|
"new_name": "image_v1.jpg",
|
||||||
|
"version": 1,
|
||||||
|
"user": "player_user_name"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|-------|------|----------|-------------|
|
||||||
|
| `time_of_modification` | ISO 8601 DateTime | Yes | When the edit was made |
|
||||||
|
| `original_name` | String | Yes | Original filename (must exist in content) |
|
||||||
|
| `new_name` | String | Yes | New filename with version suffix |
|
||||||
|
| `version` | Integer | Yes | Version number (1, 2, 3, etc.) |
|
||||||
|
| `user` | String | No | User who made the edit |
|
||||||
|
|
||||||
|
## Response
|
||||||
|
|
||||||
|
### Success (200 OK)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "Edited media received and processed",
|
||||||
|
"edit_id": 123,
|
||||||
|
"version": 1,
|
||||||
|
"new_playlist_version": 5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Responses
|
||||||
|
|
||||||
|
#### 400 Bad Request
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "No image file provided"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 404 Not Found
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Original content not found: image.jpg"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 500 Internal Server Error
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Internal server error"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. **Player edits media** - User edits an image/PDF/PPTX on the player device
|
||||||
|
2. **Player uploads** - Player sends edited file + metadata to this endpoint
|
||||||
|
3. **Server processes**:
|
||||||
|
- Saves edited file to `/static/uploads/edited_media/<content_id>/<new_name>`
|
||||||
|
- Saves metadata JSON to `/static/uploads/edited_media/<content_id>/<new_name>_metadata.json`
|
||||||
|
- Replaces original file in `/static/uploads/` with edited version
|
||||||
|
- Creates database record in `player_edit` table
|
||||||
|
- Increments playlist version to trigger player refresh
|
||||||
|
- Clears playlist cache
|
||||||
|
4. **Player refreshes** - Next playlist check shows updated media
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
Each edit is saved with a version number:
|
||||||
|
- `image.jpg` → `image_v1.jpg` (first edit)
|
||||||
|
- `image.jpg` → `image_v2.jpg` (second edit)
|
||||||
|
- etc.
|
||||||
|
|
||||||
|
All versions are preserved in the `edited_media/<content_id>/` folder.
|
||||||
|
|
||||||
|
## Example cURL Request
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# First, authenticate to get token
|
||||||
|
TOKEN=$(curl -X POST http://server/api/auth/authenticate \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"hostname": "player-1", "password": "password123"}' \
|
||||||
|
| jq -r '.token')
|
||||||
|
|
||||||
|
# Upload edited media
|
||||||
|
curl -X POST http://server/api/player-edit-media \
|
||||||
|
-H "Authorization: Bearer $TOKEN" \
|
||||||
|
-F "image_file=@edited_image_v1.jpg" \
|
||||||
|
-F 'metadata={"time_of_modification":"2025-12-05T20:30:00Z","original_name":"image.jpg","new_name":"image_v1.jpg","version":1,"user":"john"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Python Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Authenticate
|
||||||
|
auth_response = requests.post(
|
||||||
|
'http://server/api/auth/authenticate',
|
||||||
|
json={'hostname': 'player-1', 'password': 'password123'}
|
||||||
|
)
|
||||||
|
token = auth_response.json()['token']
|
||||||
|
|
||||||
|
# Prepare metadata
|
||||||
|
metadata = {
|
||||||
|
'time_of_modification': '2025-12-05T20:30:00Z',
|
||||||
|
'original_name': 'image.jpg',
|
||||||
|
'new_name': 'image_v1.jpg',
|
||||||
|
'version': 1,
|
||||||
|
'user': 'john'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Upload edited file
|
||||||
|
with open('edited_image_v1.jpg', 'rb') as f:
|
||||||
|
response = requests.post(
|
||||||
|
'http://server/api/player-edit-media',
|
||||||
|
headers={'Authorization': f'Bearer {token}'},
|
||||||
|
files={'image_file': f},
|
||||||
|
data={'metadata': json.dumps(metadata)}
|
||||||
|
)
|
||||||
|
|
||||||
|
print(response.json())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
### player_edit Table
|
||||||
|
|
||||||
|
| Column | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| id | INTEGER | Primary key |
|
||||||
|
| player_id | INTEGER | Foreign key to player |
|
||||||
|
| content_id | INTEGER | Foreign key to content |
|
||||||
|
| original_name | VARCHAR(255) | Original filename |
|
||||||
|
| new_name | VARCHAR(255) | New filename with version |
|
||||||
|
| version | INTEGER | Version number |
|
||||||
|
| user | VARCHAR(255) | User who made the edit |
|
||||||
|
| time_of_modification | DATETIME | When edit was made |
|
||||||
|
| metadata_path | VARCHAR(512) | Path to metadata JSON |
|
||||||
|
| edited_file_path | VARCHAR(512) | Path to edited file |
|
||||||
|
| created_at | DATETIME | Record creation time |
|
||||||
|
|
||||||
|
## UI Display
|
||||||
|
|
||||||
|
Edited media history is displayed on the player management page under the "Edited Media on the Player" card, showing:
|
||||||
|
- Original filename
|
||||||
|
- Version number
|
||||||
|
- Editor name
|
||||||
|
- Modification time
|
||||||
|
- Link to view edited file
|
||||||
56
old_code_documentation/PROXY_FIX_SETUP.md
Normal file
56
old_code_documentation/PROXY_FIX_SETUP.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# ProxyFix Middleware Setup - DigiServer v2
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
ProxyFix middleware is now properly configured in the Flask app to handle reverse proxy headers from Nginx (or Caddy). This ensures correct handling of:
|
||||||
|
- **X-Real-IP**: Client's real IP address
|
||||||
|
- **X-Forwarded-For**: List of IPs in the proxy chain
|
||||||
|
- **X-Forwarded-Proto**: Original protocol (http/https)
|
||||||
|
- **X-Forwarded-Host**: Original hostname
|
||||||
|
|
||||||
|
## Configuration Details
|
||||||
|
|
||||||
|
### Flask App (app/app.py)
|
||||||
|
```python
|
||||||
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
|
|
||||||
|
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `x_for=1`: Trust one proxy for X-Forwarded-For header
|
||||||
|
- `x_proto=1`: Trust proxy for X-Forwarded-Proto header
|
||||||
|
- `x_host=1`: Trust proxy for X-Forwarded-Host header
|
||||||
|
- `x_port=1`: Trust proxy for X-Forwarded-Port header
|
||||||
|
|
||||||
|
### Config Settings (app/config.py)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Reverse proxy trust (for Nginx/Caddy with ProxyFix middleware)
|
||||||
|
TRUSTED_PROXIES = os.getenv('TRUSTED_PROXIES', '127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16')
|
||||||
|
PREFERRED_URL_SCHEME = os.getenv('PREFERRED_URL_SCHEME', 'https')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing ProxyFix
|
||||||
|
|
||||||
|
### 1. Test Real Client IP
|
||||||
|
```bash
|
||||||
|
docker exec digiserver-app flask shell
|
||||||
|
>>> from flask import request
|
||||||
|
>>> request.remote_addr # Should show client IP
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Test URL Scheme
|
||||||
|
```bash
|
||||||
|
docker exec digiserver-app flask shell
|
||||||
|
>>> from flask import url_for
|
||||||
|
>>> url_for('auth.login', _external=True) # Should use https://
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification Checklist
|
||||||
|
|
||||||
|
- [x] ProxyFix imported in app.py
|
||||||
|
- [x] app.wsgi_app wrapped with ProxyFix
|
||||||
|
- [x] TRUSTED_PROXIES configured
|
||||||
|
- [x] PREFERRED_URL_SCHEME set to 'https'
|
||||||
|
- [x] SESSION_COOKIE_SECURE=True in ProductionConfig
|
||||||
|
- [x] Nginx headers configured correctly
|
||||||
120
old_code_documentation/QUICK_START.md
Normal file
120
old_code_documentation/QUICK_START.md
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
# 🚀 DigiServer Deployment - Quick Reference Card
|
||||||
|
|
||||||
|
## Instant Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/digiserver-v2
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
That's it! ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 Documentation Files
|
||||||
|
|
||||||
|
| File | Purpose | Size |
|
||||||
|
|------|---------|------|
|
||||||
|
| [DEPLOYMENT_INDEX.md](DEPLOYMENT_INDEX.md) | Navigation guide | 8.1 KB |
|
||||||
|
| [DEPLOYMENT_README.md](DEPLOYMENT_README.md) | Complete guide | 9.4 KB |
|
||||||
|
| [DOCKER_EXEC_COMMANDS.md](DOCKER_EXEC_COMMANDS.md) | Command reference | 7.6 KB |
|
||||||
|
| [DEPLOYMENT_COMMANDS.md](DEPLOYMENT_COMMANDS.md) | Detailed guide | 6.8 KB |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Executable Scripts
|
||||||
|
|
||||||
|
| Script | Purpose | Time |
|
||||||
|
|--------|---------|------|
|
||||||
|
| [deploy.sh](deploy.sh) | Fully automated | 2-3 min |
|
||||||
|
| [setup_https.sh](setup_https.sh) | Semi-automated | 3-5 min |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Common Commands
|
||||||
|
|
||||||
|
### Check Status
|
||||||
|
```bash
|
||||||
|
docker-compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
### View Logs
|
||||||
|
```bash
|
||||||
|
docker-compose logs -f digiserver-app
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify HTTPS Configuration
|
||||||
|
```bash
|
||||||
|
docker-compose exec -T digiserver-app python /app/https_manager.py status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Access the Application
|
||||||
|
```
|
||||||
|
https://digiserver.sibiusb.harting.intra
|
||||||
|
https://10.76.152.164
|
||||||
|
https://digiserver
|
||||||
|
```
|
||||||
|
|
||||||
|
### Default Login
|
||||||
|
```
|
||||||
|
Username: admin
|
||||||
|
Password: admin123
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆘 Troubleshooting
|
||||||
|
|
||||||
|
| Issue | Solution |
|
||||||
|
|-------|----------|
|
||||||
|
| Containers won't start | `docker-compose logs` |
|
||||||
|
| Migration fails | Check DB connection, see docs |
|
||||||
|
| HTTPS errors | Clear Caddy cache: `docker volume rm digiserver-v2_caddy-*` |
|
||||||
|
| Port conflict | `lsof -i :443` or change in docker-compose.yml |
|
||||||
|
|
||||||
|
See [DEPLOYMENT_README.md#-troubleshooting](DEPLOYMENT_README.md#-troubleshooting) for full guide.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌍 Deploy on Different PC
|
||||||
|
|
||||||
|
1. Copy project files
|
||||||
|
2. Install Docker & Docker Compose
|
||||||
|
3. Run `./deploy.sh`
|
||||||
|
|
||||||
|
Done! 🎉
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔑 Customize Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
HOSTNAME=myserver \
|
||||||
|
DOMAIN=myserver.internal \
|
||||||
|
IP_ADDRESS=192.168.1.100 \
|
||||||
|
EMAIL=admin@example.com \
|
||||||
|
./deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Need More Help?
|
||||||
|
|
||||||
|
- **First time?** → [DEPLOYMENT_README.md](DEPLOYMENT_README.md)
|
||||||
|
- **Need a command?** → [DOCKER_EXEC_COMMANDS.md](DOCKER_EXEC_COMMANDS.md)
|
||||||
|
- **Lost?** → [DEPLOYMENT_INDEX.md](DEPLOYMENT_INDEX.md)
|
||||||
|
- **Troubleshooting?** → [DEPLOYMENT_README.md#-troubleshooting](DEPLOYMENT_README.md#-troubleshooting)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ What You Get
|
||||||
|
|
||||||
|
✅ Web application with admin dashboard
|
||||||
|
✅ HTTPS with self-signed certificates
|
||||||
|
✅ User management system
|
||||||
|
✅ Player & content management
|
||||||
|
✅ Fully configured & ready to use
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Ready to deploy?** `./deploy.sh` 🚀
|
||||||
@@ -4,7 +4,12 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
# Get the root directory of the application
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
APP_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
|
||||||
echo "🧹 Cleaning DigiServer v2 for deployment..."
|
echo "🧹 Cleaning DigiServer v2 for deployment..."
|
||||||
|
echo "📍 App root: $APP_ROOT"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Confirm action
|
# Confirm action
|
||||||
@@ -18,6 +23,9 @@ fi
|
|||||||
echo ""
|
echo ""
|
||||||
echo "📦 Cleaning development data..."
|
echo "📦 Cleaning development data..."
|
||||||
|
|
||||||
|
# Change to app root directory
|
||||||
|
cd "$APP_ROOT"
|
||||||
|
|
||||||
# Remove database files
|
# Remove database files
|
||||||
if [ -d "instance" ]; then
|
if [ -d "instance" ]; then
|
||||||
echo " 🗄️ Removing database files..."
|
echo " 🗄️ Removing database files..."
|
||||||
47
old_code_documentation/migrate_add_edit_enabled.py
Normal file
47
old_code_documentation/migrate_add_edit_enabled.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
"""Migration: Add edit_on_player_enabled column to playlist_content table."""
|
||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
|
||||||
|
DB_PATH = 'instance/dashboard.db'
|
||||||
|
|
||||||
|
def migrate():
|
||||||
|
"""Add edit_on_player_enabled column to playlist_content."""
|
||||||
|
if not os.path.exists(DB_PATH):
|
||||||
|
print(f"Database not found at {DB_PATH}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check if column already exists
|
||||||
|
cursor.execute("PRAGMA table_info(playlist_content)")
|
||||||
|
columns = [col[1] for col in cursor.fetchall()]
|
||||||
|
|
||||||
|
if 'edit_on_player_enabled' in columns:
|
||||||
|
print("Column 'edit_on_player_enabled' already exists!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Add the new column with default value False
|
||||||
|
print("Adding 'edit_on_player_enabled' column to playlist_content table...")
|
||||||
|
cursor.execute("""
|
||||||
|
ALTER TABLE playlist_content
|
||||||
|
ADD COLUMN edit_on_player_enabled BOOLEAN DEFAULT 0
|
||||||
|
""")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
print("✅ Migration completed successfully!")
|
||||||
|
print("Column 'edit_on_player_enabled' added with default value False (0)")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
conn.rollback()
|
||||||
|
print(f"❌ Migration failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
migrate()
|
||||||
@@ -1,254 +0,0 @@
|
|||||||
"""
|
|
||||||
Player Authentication Module for Kiwy-Signage
|
|
||||||
Handles authentication with DigiServer v2 and secure config storage
|
|
||||||
"""
|
|
||||||
import configparser
|
|
||||||
import os
|
|
||||||
import requests
|
|
||||||
from typing import Optional, Dict, Tuple
|
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
class PlayerAuth:
|
|
||||||
"""Handle player authentication and configuration management."""
|
|
||||||
|
|
||||||
def __init__(self, config_path: str = 'player_config.ini'):
|
|
||||||
"""Initialize player authentication.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
config_path: Path to configuration file
|
|
||||||
"""
|
|
||||||
self.config_path = config_path
|
|
||||||
self.config = configparser.ConfigParser()
|
|
||||||
self.load_config()
|
|
||||||
|
|
||||||
def load_config(self) -> None:
|
|
||||||
"""Load configuration from file."""
|
|
||||||
if os.path.exists(self.config_path):
|
|
||||||
self.config.read(self.config_path)
|
|
||||||
else:
|
|
||||||
# Create default config
|
|
||||||
self._create_default_config()
|
|
||||||
|
|
||||||
def _create_default_config(self) -> None:
|
|
||||||
"""Create default configuration file."""
|
|
||||||
self.config['server'] = {
|
|
||||||
'server_url': 'http://localhost:5000'
|
|
||||||
}
|
|
||||||
self.config['player'] = {
|
|
||||||
'hostname': '',
|
|
||||||
'auth_code': '',
|
|
||||||
'player_id': '',
|
|
||||||
'group_id': ''
|
|
||||||
}
|
|
||||||
self.config['display'] = {
|
|
||||||
'orientation': 'Landscape',
|
|
||||||
'resolution': '1920x1080'
|
|
||||||
}
|
|
||||||
self.config['security'] = {
|
|
||||||
'verify_ssl': 'true',
|
|
||||||
'timeout': '30'
|
|
||||||
}
|
|
||||||
self.config['cache'] = {
|
|
||||||
'cache_dir': './cache',
|
|
||||||
'max_cache_size': '1024'
|
|
||||||
}
|
|
||||||
self.config['logging'] = {
|
|
||||||
'enabled': 'true',
|
|
||||||
'log_level': 'INFO',
|
|
||||||
'log_file': './player.log'
|
|
||||||
}
|
|
||||||
self.save_config()
|
|
||||||
|
|
||||||
def save_config(self) -> None:
|
|
||||||
"""Save configuration to file."""
|
|
||||||
with open(self.config_path, 'w') as f:
|
|
||||||
self.config.write(f)
|
|
||||||
|
|
||||||
def get_server_url(self) -> str:
|
|
||||||
"""Get server URL from config."""
|
|
||||||
return self.config.get('server', 'server_url', fallback='http://localhost:5000')
|
|
||||||
|
|
||||||
def get_hostname(self) -> str:
|
|
||||||
"""Get player hostname from config."""
|
|
||||||
return self.config.get('player', 'hostname', fallback='')
|
|
||||||
|
|
||||||
def get_auth_code(self) -> str:
|
|
||||||
"""Get saved auth code from config."""
|
|
||||||
return self.config.get('player', 'auth_code', fallback='')
|
|
||||||
|
|
||||||
def is_authenticated(self) -> bool:
|
|
||||||
"""Check if player has valid authentication."""
|
|
||||||
return bool(self.get_hostname() and self.get_auth_code())
|
|
||||||
|
|
||||||
def authenticate(self, hostname: str, password: str = None,
|
|
||||||
quickconnect_code: str = None) -> Tuple[bool, Optional[str]]:
|
|
||||||
"""Authenticate with server and save credentials.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
hostname: Player hostname/identifier
|
|
||||||
password: Player password (optional if using quickconnect)
|
|
||||||
quickconnect_code: Quick connect code (optional if using password)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple of (success: bool, error_message: Optional[str])
|
|
||||||
"""
|
|
||||||
if not password and not quickconnect_code:
|
|
||||||
return False, "Password or quick connect code required"
|
|
||||||
|
|
||||||
server_url = self.get_server_url()
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Make authentication request
|
|
||||||
response = requests.post(
|
|
||||||
f"{server_url}/api/auth/player",
|
|
||||||
json={
|
|
||||||
'hostname': hostname,
|
|
||||||
'password': password,
|
|
||||||
'quickconnect_code': quickconnect_code
|
|
||||||
},
|
|
||||||
timeout=int(self.config.get('security', 'timeout', fallback='30')),
|
|
||||||
verify=self.config.getboolean('security', 'verify_ssl', fallback=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
# Save authentication data
|
|
||||||
self.config['player']['hostname'] = hostname
|
|
||||||
self.config['player']['auth_code'] = data.get('auth_code', '')
|
|
||||||
self.config['player']['player_id'] = str(data.get('player_id', ''))
|
|
||||||
self.config['player']['group_id'] = str(data.get('group_id', ''))
|
|
||||||
self.config['display']['orientation'] = data.get('orientation', 'Landscape')
|
|
||||||
|
|
||||||
self.save_config()
|
|
||||||
|
|
||||||
return True, None
|
|
||||||
|
|
||||||
else:
|
|
||||||
error_data = response.json()
|
|
||||||
return False, error_data.get('error', 'Authentication failed')
|
|
||||||
|
|
||||||
except requests.exceptions.ConnectionError:
|
|
||||||
return False, "Cannot connect to server"
|
|
||||||
except requests.exceptions.Timeout:
|
|
||||||
return False, "Connection timeout"
|
|
||||||
except Exception as e:
|
|
||||||
return False, f"Error: {str(e)}"
|
|
||||||
|
|
||||||
def verify_auth(self) -> Tuple[bool, Optional[Dict]]:
|
|
||||||
"""Verify current auth code with server.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple of (valid: bool, player_info: Optional[Dict])
|
|
||||||
"""
|
|
||||||
auth_code = self.get_auth_code()
|
|
||||||
|
|
||||||
if not auth_code:
|
|
||||||
return False, None
|
|
||||||
|
|
||||||
server_url = self.get_server_url()
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.post(
|
|
||||||
f"{server_url}/api/auth/verify",
|
|
||||||
json={'auth_code': auth_code},
|
|
||||||
timeout=int(self.config.get('security', 'timeout', fallback='30')),
|
|
||||||
verify=self.config.getboolean('security', 'verify_ssl', fallback=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
data = response.json()
|
|
||||||
return data.get('valid', False), data
|
|
||||||
|
|
||||||
return False, None
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
return False, None
|
|
||||||
|
|
||||||
def get_playlist(self) -> Optional[Dict]:
|
|
||||||
"""Get playlist for this player from server.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Playlist data or None if failed
|
|
||||||
"""
|
|
||||||
auth_code = self.get_auth_code()
|
|
||||||
player_id = self.config.get('player', 'player_id', fallback='')
|
|
||||||
|
|
||||||
if not auth_code or not player_id:
|
|
||||||
return None
|
|
||||||
|
|
||||||
server_url = self.get_server_url()
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.get(
|
|
||||||
f"{server_url}/api/playlists/{player_id}",
|
|
||||||
headers={'Authorization': f'Bearer {auth_code}'},
|
|
||||||
timeout=int(self.config.get('security', 'timeout', fallback='30')),
|
|
||||||
verify=self.config.getboolean('security', 'verify_ssl', fallback=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def send_heartbeat(self, status: str = 'online') -> bool:
|
|
||||||
"""Send heartbeat to server.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
status: Player status
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if successful, False otherwise
|
|
||||||
"""
|
|
||||||
auth_code = self.get_auth_code()
|
|
||||||
player_id = self.config.get('player', 'player_id', fallback='')
|
|
||||||
|
|
||||||
if not auth_code or not player_id:
|
|
||||||
return False
|
|
||||||
|
|
||||||
server_url = self.get_server_url()
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.post(
|
|
||||||
f"{server_url}/api/players/{player_id}/heartbeat",
|
|
||||||
headers={'Authorization': f'Bearer {auth_code}'},
|
|
||||||
json={'status': status},
|
|
||||||
timeout=int(self.config.get('security', 'timeout', fallback='30')),
|
|
||||||
verify=self.config.getboolean('security', 'verify_ssl', fallback=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
return response.status_code == 200
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def clear_auth(self) -> None:
|
|
||||||
"""Clear saved authentication data."""
|
|
||||||
self.config['player']['auth_code'] = ''
|
|
||||||
self.config['player']['player_id'] = ''
|
|
||||||
self.config['player']['group_id'] = ''
|
|
||||||
self.save_config()
|
|
||||||
|
|
||||||
|
|
||||||
# Example usage
|
|
||||||
if __name__ == '__main__':
|
|
||||||
auth = PlayerAuth()
|
|
||||||
|
|
||||||
# Check if already authenticated
|
|
||||||
if auth.is_authenticated():
|
|
||||||
print(f"Already authenticated as: {auth.get_hostname()}")
|
|
||||||
|
|
||||||
# Verify authentication
|
|
||||||
valid, info = auth.verify_auth()
|
|
||||||
if valid:
|
|
||||||
print(f"Authentication valid: {info}")
|
|
||||||
else:
|
|
||||||
print("Authentication expired or invalid")
|
|
||||||
else:
|
|
||||||
print("Not authenticated. Please run authentication:")
|
|
||||||
print("auth.authenticate(hostname='player-001', password='your_password')")
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
# Player Configuration File
|
|
||||||
# This file is automatically generated and updated by the signage player
|
|
||||||
# DO NOT EDIT MANUALLY unless you know what you're doing
|
|
||||||
|
|
||||||
[server]
|
|
||||||
# DigiServer URL (without trailing slash)
|
|
||||||
server_url = http://localhost:5000
|
|
||||||
|
|
||||||
[player]
|
|
||||||
# Player hostname/identifier (must be unique)
|
|
||||||
hostname =
|
|
||||||
|
|
||||||
# Player authentication code (obtained after first authentication)
|
|
||||||
auth_code =
|
|
||||||
|
|
||||||
# Player ID (assigned by server)
|
|
||||||
player_id =
|
|
||||||
|
|
||||||
# Group ID (assigned by server)
|
|
||||||
group_id =
|
|
||||||
|
|
||||||
[display]
|
|
||||||
# Display orientation: Landscape or Portrait
|
|
||||||
orientation = Landscape
|
|
||||||
|
|
||||||
# Screen resolution (width x height)
|
|
||||||
resolution = 1920x1080
|
|
||||||
|
|
||||||
[security]
|
|
||||||
# Enable SSL certificate verification
|
|
||||||
verify_ssl = true
|
|
||||||
|
|
||||||
# Connection timeout in seconds
|
|
||||||
timeout = 30
|
|
||||||
|
|
||||||
[cache]
|
|
||||||
# Local cache directory for downloaded content
|
|
||||||
cache_dir = ./cache
|
|
||||||
|
|
||||||
# Maximum cache size in MB
|
|
||||||
max_cache_size = 1024
|
|
||||||
|
|
||||||
[logging]
|
|
||||||
# Enable logging
|
|
||||||
enabled = true
|
|
||||||
|
|
||||||
# Log level: DEBUG, INFO, WARNING, ERROR
|
|
||||||
log_level = INFO
|
|
||||||
|
|
||||||
# Log file path
|
|
||||||
log_file = ./player.log
|
|
||||||
@@ -16,9 +16,6 @@ Flask-Caching==2.1.0
|
|||||||
SQLAlchemy==2.0.37
|
SQLAlchemy==2.0.37
|
||||||
alembic==1.14.1
|
alembic==1.14.1
|
||||||
|
|
||||||
# Redis (for caching in production)
|
|
||||||
redis==5.0.1
|
|
||||||
|
|
||||||
# Date parsing
|
# Date parsing
|
||||||
python-dateutil==2.9.0
|
python-dateutil==2.9.0
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user