diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..51ee4c2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,56 @@ +# Changelog + +All notable changes to the Update Server project will be documented in this file. + +## [1.2.0] - 2026-03-13 + +### Added +- **Admin GUI** - New web-based administration interface for managing bot versions + - Available on port 5566 (separate from API) + - Beautiful, responsive dashboard with add/edit/delete functionality + - Real-time version list with edit and delete buttons + - Success/error message feedback + - Robot emoji (🤖) favicon + +- **Dual-server architecture** - API and Admin interfaces now run independently + - API Server on port 5555 + - Admin Server on port 5566 + - Both can be secured separately using reverse proxies + +- **CORS support** - Cross-origin requests enabled for admin GUI + - Added Flask-CORS dependency + - Allows admin GUI (port 5566) to communicate with API (port 5555) + +### Changed +- **Default ports updated** + - API Server: 5000 → 5555 + - Admin Server: N/A → 5566 (new) + +- **Docker configuration** + - Updated Dockerfile to expose both ports + - Changed CMD from gunicorn to Python for dual-threading support + - Updated docker-compose.yml with new port mappings + +### Technical Details +- Refactored Flask app into two separate instances +- Implemented threading for concurrent server execution +- Added `save_versions()` function for admin API +- Embedded HTML admin interface with inline CSS and JavaScript +- Admin API endpoints: `/admin/api/add` and `/admin/api/delete` + +### Documentation +- Updated README.md with new features and configuration +- Added Nginx reverse proxy examples for Authentik integration +- Documented admin GUI usage and API endpoints + +### Security Notes +- Admin GUI has no built-in authentication (as designed) +- Recommended to protect admin port (5566) with reverse proxy authentication (e.g., Authentik) +- API port (5555) can remain public for bot communication + +## [1.1.0] - Previous Release + +- Initial release with API-only functionality +- Bot version tracking and serving +- Health check endpoint +- Logging of version check requests diff --git a/Dockerfile b/Dockerfile index 2fca1f8..34ad79f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,8 +10,8 @@ RUN pip install --no-cache-dir -r requirements.txt COPY app.py . COPY versions.json . -# Expose port -EXPOSE 5000 +# Expose ports (API on 5000, Admin on 5001) +EXPOSE 5000 5001 -# Run the application -CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "app:app"] +# Run the application with both API and Admin servers +CMD ["python", "app.py"] diff --git a/README.md b/README.md index ed467dd..f107af6 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,13 @@ Flask-based update server that bots query to check for new versions and send modmail notifications when updates are available. +## Features + +- **API Server** (Port 5555) - Bots query this for version information +- **Admin GUI** (Port 5566) - Web interface to easily manage bot versions +- Simple JSON-based version storage +- Health check endpoint + ## Setup ### Local Development @@ -14,32 +21,32 @@ Flask-based update server that bots query to check for new versions and send mod ```bash python app.py ``` - The server will run on `http://localhost:5000` + - API Server: http://localhost:5555 + - Admin GUI: http://localhost:5566 ### Production Deployment -#### Using Gunicorn -```bash -pip install -r requirements.txt -gunicorn -w 4 -b 0.0.0.0:5000 app:app -``` - #### Using Docker ```bash docker build -t update-server . -docker run -p 5000:5000 update-server +docker run -p 5555:5555 -p 5566:5566 update-server ``` #### Using Docker Compose From the parent directory: ```bash -docker-compose up update-server +docker-compose up botupdateserver ``` +This exposes: +- Port 5555 for API calls (public-facing) +- Port 5566 for Admin GUI (should be protected, e.g., behind Authentik) + ### Production with Nginx (HTTPS) -Configure nginx as a reverse proxy to handle HTTPS on port 443: +Configure nginx as a reverse proxy: ```nginx +# Public API endpoint server { listen 443 ssl http2; server_name updts.slfhstd.uk; @@ -48,7 +55,27 @@ server { ssl_certificate_key /path/to/key.pem; location / { - proxy_pass http://localhost:5000; + proxy_pass http://localhost:5555; + 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; + } +} + +# Admin GUI with authentication (Authentik, etc.) +server { + listen 443 ssl http2; + server_name updts-admin.slfhstd.uk; + + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; + + # Protect with Authentik (or your auth system) + auth_request /auth; + + location / { + proxy_pass http://localhost:5566; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; @@ -59,7 +86,18 @@ server { ## Managing Versions -Edit `versions.json` to add, update, or remove bots: +### Using the Admin GUI +1. Navigate to `http://localhost:5566` (or your admin domain for production) +2. Fill in the bot details: + - **Bot Name**: The name of the bot (e.g., `TestPostsBot`) + - **Version**: The current version (e.g., `0.2`) + - **Changelog URL**: (Optional) Link to the changelog +3. Click "Save Version" +4. View all current versions in the right panel +5. Edit by clicking "Edit" on any bot, or delete with "Delete" + +### Manual Edit (versions.json) +If preferred, directly edit `versions.json`: ```json { @@ -74,10 +112,9 @@ Edit `versions.json` to add, update, or remove bots: } ``` -When you update the version here, all connected bots will detect the change and send modmail to their respective subreddits automatically. - -## API Endpoints +## API Endpoints (Port 5555) +### Public API - `GET /` - Server info and available endpoints - `GET /health` - Health check - `GET /api/versions` - Get all bot versions @@ -96,10 +133,39 @@ Response: } ``` +### Admin API (Port 5566) + +**Note:** These endpoints should be protected. Secure with Authentik or another authentication method. + +#### Add/Update Version +```bash +curl -X POST http://localhost:5566/admin/api/add \ + -H "Content-Type: application/json" \ + -d '{ + "bot_name": "TestPostsBot", + "version": "0.3", + "changelog_url": "https://github.com/yourrepo/releases/tag/v0.3" + }' +``` + +#### Delete Version +```bash +curl -X POST http://localhost:5566/admin/api/delete \ + -H "Content-Type: application/json" \ + -d '{"bot_name": "TestPostsBot"}' +``` + ## Logs Update check requests are logged to `update_checks.log` with timestamps and bot names. +## Architecture + +- **API Server (Port 5555)**: Public endpoint for bots to check versions +- **Admin GUI (Port 5566)**: Web interface for managing versions +- **Single versioning source**: Both use the same `versions.json` file +- **No authentication built-in**: Use a reverse proxy (Authentik, etc.) for security + ## Hosting Options - **Heroku** - Easy deployment with free tier diff --git a/app.py b/app.py index 3d00b95..5fd6f45 100644 --- a/app.py +++ b/app.py @@ -3,17 +3,24 @@ Update Server for Bots Serves version information to bots checking for updates. """ -from flask import Flask, jsonify, request +from flask import Flask, jsonify, request, render_template_string +from flask_cors import CORS import json import os from datetime import datetime - -app = Flask(__name__) +import threading # Load version data from JSON file VERSIONS_FILE = 'versions.json' LOG_FILE = 'update_checks.log' +# Create API app +api_app = Flask(__name__, static_folder=None) +CORS(api_app) # Enable CORS for cross-origin requests from admin GUI + +# Create Admin app +admin_app = Flask(__name__) + def load_versions(): """Load version info from file.""" @@ -27,6 +34,17 @@ def load_versions(): return {} +def save_versions(versions): + """Save version info to file.""" + try: + with open(VERSIONS_FILE, 'w') as f: + json.dump(versions, f, indent=2) + return True + except Exception as e: + print(f"Error saving versions.json: {e}") + return False + + def log_check(bot_name): """Log update check requests.""" try: @@ -36,7 +54,9 @@ def log_check(bot_name): print(f"Error logging check: {e}") -@app.route('/api/version/', methods=['GET']) +# ====== API ROUTES (Port 5555) ====== + +@api_app.route('/api/version/', methods=['GET']) def get_version(bot_name): """Get latest version for a specific bot.""" log_check(bot_name) @@ -48,19 +68,19 @@ def get_version(bot_name): return jsonify({"error": "Bot not found"}), 404 -@app.route('/api/versions', methods=['GET']) +@api_app.route('/api/versions', methods=['GET']) def get_all_versions(): """Get all bot versions.""" return jsonify(load_versions()) -@app.route('/health', methods=['GET']) +@api_app.route('/health', methods=['GET']) def health(): """Health check endpoint.""" return jsonify({"status": "ok"}) -@app.route('/', methods=['GET']) +@api_app.route('/', methods=['GET']) def index(): """Index page with server info.""" versions = load_versions() @@ -77,19 +97,405 @@ def index(): }) -@app.errorhandler(404) -def not_found(error): +@api_app.errorhandler(404) +def api_not_found(error): """Handle 404 errors.""" return jsonify({"error": "Endpoint not found"}), 404 -@app.errorhandler(500) -def internal_error(error): +@api_app.errorhandler(500) +def api_internal_error(error): """Handle 500 errors.""" return jsonify({"error": "Internal server error"}), 500 +# ====== ADMIN ROUTES (Port 5566) ====== + +ADMIN_HTML = """ + + + + Update Server Admin + + + + +
+
+

🤖 Update Server Admin

+

Manage bot versions easily

+
+ +
+ +
+

Add/Edit Version

+
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+

Current Versions

+
    +
  • Loading...
  • +
+
+
+
+ + + + +""" + + +@admin_app.route('/', methods=['GET']) +def admin_index(): + """Admin dashboard.""" + return render_template_string(ADMIN_HTML) + + +@admin_app.route('/admin/api/add', methods=['POST']) +def admin_add_version(): + """Add or update a bot version.""" + try: + data = request.get_json() + bot_name = data.get('bot_name', '').strip() + version = data.get('version', '').strip() + changelog_url = data.get('changelog_url', '').strip() + + if not bot_name or not version: + return jsonify({"success": False, "message": "Bot name and version are required"}), 400 + + versions = load_versions() + versions[bot_name] = { + "version": version, + "changelog_url": changelog_url if changelog_url else versions.get(bot_name, {}).get('changelog_url', '') + } + + if save_versions(versions): + return jsonify({"success": True, "message": f"Version saved for {bot_name}"}) + else: + return jsonify({"success": False, "message": "Failed to save version"}), 500 + except Exception as e: + return jsonify({"success": False, "message": f"Error: {str(e)}"}), 500 + + +@admin_app.route('/admin/api/delete', methods=['POST']) +def admin_delete_version(): + """Delete a bot version.""" + try: + data = request.get_json() + bot_name = data.get('bot_name', '').strip() + + if not bot_name: + return jsonify({"success": False, "message": "Bot name is required"}), 400 + + versions = load_versions() + if bot_name in versions: + del versions[bot_name] + if save_versions(versions): + return jsonify({"success": True, "message": f"{bot_name} deleted"}) + + return jsonify({"success": False, "message": "Bot not found"}), 404 + except Exception as e: + return jsonify({"success": False, "message": f"Error: {str(e)}"}), 500 + + +@admin_app.errorhandler(404) +def admin_not_found(error): + """Handle 404 errors.""" + return jsonify({"error": "Endpoint not found"}), 404 + + +@admin_app.errorhandler(500) +def admin_internal_error(error): + """Handle 500 errors.""" + return jsonify({"error": "Internal server error"}), 500 + + +# ====== RUN BOTH APPS ====== + +def run_api(): + """Run API on port 5555.""" + print("[API] Starting on port 5555...") + api_app.run(host='0.0.0.0', port=5555, debug=False) + + +def run_admin(): + """Run Admin on port 5566.""" + print("[ADMIN] Starting on port 5566...") + admin_app.run(host='0.0.0.0', port=5566, debug=False) + + if __name__ == '__main__': - print("Starting Update Server...") - print(f"Loaded {len(load_versions())} bots from versions.json") - app.run(host='0.0.0.0', port=5000, debug=False) + versions_count = len(load_versions()) + print(f"[STARTUP] Loaded {versions_count} bots from versions.json") + + # Run both apps in separate threads + api_thread = threading.Thread(target=run_api, daemon=True) + admin_thread = threading.Thread(target=run_admin, daemon=True) + + api_thread.start() + admin_thread.start() + + # Keep main thread alive + try: + api_thread.join() + except KeyboardInterrupt: + print("\n[SHUTDOWN] Stopping servers...") + diff --git a/docker-compose.yml b/docker-compose.yml index db18202..01c02d3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,10 @@ services: botupdateserver: - image: slfhstd.uk/slfhstd/botupdateserver:latest + image: slfhstd.uk/slfhstd/botupdateserver:dev container_name: botupdateserver ports: - - "5000:5000" + - "5555:5555" + - "5566:5566" volumes: - ./versions/versions.json:/app/versions.json restart: unless-stopped diff --git a/requirements.txt b/requirements.txt index d8e047d..a7a0469 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ Flask==2.3.0 +Flask-CORS==4.0.0 Werkzeug==2.3.0 gunicorn==21.2.0 diff --git a/versions/versions.json b/versions/versions.json new file mode 100644 index 0000000..6901aa2 --- /dev/null +++ b/versions/versions.json @@ -0,0 +1,6 @@ +{ + "TestPostsBot": { + "version": "0.2", + "changelog_url": "https://slfhstd.uk/slfhstd/TestPostsBot/releases" + } +}