Spaces:
Paused
Paused
| FROM ghcr.io/danny-avila/librechat-dev:latest | |
| # Install dependencies | |
| USER root | |
| RUN apk update && apk add --no-cache \ | |
| caddy \ | |
| python3 \ | |
| py3-pip \ | |
| py3-dotenv \ | |
| && pip install flask pymongo[srv] --break-system-packages | |
| copy config.yaml /app/librechat.yaml | |
| # Create admin structure | |
| RUN mkdir -p /app/sudo/{templates,static} \ | |
| && chown -R 1000:1000 /app | |
| # Environment variables | |
| ENV HOST=0.0.0.0 \ | |
| PORT=3080 \ | |
| SESSION_EXPIRY=900000 \ | |
| REFRESH_TOKEN_EXPIRY=604800000 \ | |
| SEARCH=true \ | |
| MEILI_NO_ANALYTICS=true \ | |
| MEILI_HOST=https://martynka-meilisearch.hf.space \ | |
| CONFIG_PATH=/app/librechat.yaml \ | |
| CUSTOM_FOOTER=EasierIT \ | |
| MONGO_URI="$MONGO_URI" \ | |
| SUDO_SECRET="$SUDO_SECRET" \ | |
| ADMIN_SECRET="$SUDO_SECRET" \ | |
| FLASK_SECRET="$FLASK_SECRET" \ | |
| NODE_ENV=production | |
| #========================== | |
| # ===== HTML Admin Panel ===== | |
| COPY <<"EOF" /app/sudo/templates/index.html | |
| <html> | |
| <head> | |
| <title>LibreChat Admin</title> | |
| <style> | |
| body { font-family: Arial, sans-serif; margin: 0; padding: 20px; } | |
| .container { max-width: 1000px; margin: 0 auto; } | |
| table { width: 100%; border-collapse: collapse; margin-top: 20px; } | |
| th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; } | |
| button { padding: 6px 12px; cursor: pointer; } | |
| .login-form { max-width: 400px; margin: 50px auto; } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="login" class="login-form" style="display: block;"> | |
| <h2>Admin Login</h2> | |
| <input type="password" id="password" placeholder="Admin Password"> | |
| <button onclick="login()">Login</button> | |
| </div> | |
| <div id="admin-panel" class="container" style="display: none;"> | |
| <h1>User Management</h1> | |
| <div> | |
| <input type="text" id="new-username" placeholder="Username"> | |
| <input type="password" id="new-password" placeholder="Password"> | |
| <button onclick="addUser()">Add User</button> | |
| </div> | |
| <table id="users-table"> | |
| <thead> | |
| <tr> | |
| <th>Username</th> | |
| <th>Actions</th> | |
| </tr> | |
| </thead> | |
| <tbody></tbody> | |
| </table> | |
| </div> | |
| <script> | |
| let authToken = ''; | |
| async function login() { | |
| const password = document.getElementById('password').value; | |
| const response = await fetch('/sudo/login', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ password }) | |
| }); | |
| if (response.ok) { | |
| const data = await response.json(); | |
| authToken = data.token; | |
| document.getElementById('login').style.display = 'none'; | |
| document.getElementById('admin-panel').style.display = 'block'; | |
| loadUsers(); | |
| } else { | |
| alert('Login failed!'); | |
| } | |
| } | |
| async function loadUsers() { | |
| const response = await fetch('/sudo/users', { | |
| headers: { 'X-Auth-Token': authToken } | |
| }); | |
| const users = await response.json(); | |
| const tbody = document.querySelector('#users-table tbody'); | |
| tbody.innerHTML = users.map(user => ` | |
| <tr> | |
| <td>${user.username}</td> | |
| <td> | |
| <button onclick="deleteUser('${user.username}')">Delete</button> | |
| </td> | |
| </tr> | |
| `).join(''); | |
| } | |
| async function addUser() { | |
| const username = document.getElementById('new-username').value; | |
| const password = document.getElementById('new-password').value; | |
| const response = await fetch('/sudo/users', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'X-Auth-Token': authToken | |
| }, | |
| body: JSON.stringify({ username, password }) | |
| }); | |
| if (response.ok) { | |
| loadUsers(); | |
| document.getElementById('new-username').value = ''; | |
| document.getElementById('new-password').value = ''; | |
| } | |
| } | |
| async function deleteUser(username) { | |
| if (confirm(`Delete ${username}?`)) { | |
| await fetch(`/sudo/users/${username}`, { | |
| method: 'DELETE', | |
| headers: { 'X-Auth-Token': authToken } | |
| }); | |
| loadUsers(); | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> | |
| EOF | |
| # ===== Admin Backend ===== | |
| COPY <<"EOF" /app/sudo/app.py | |
| from flask import Flask, request, jsonify | |
| from pymongo.mongo_client import MongoClient | |
| from pymongo.server_api import ServerApi | |
| from werkzeug.security import generate_password_hash | |
| import os | |
| import hmac | |
| from datetime import datetime | |
| from functools import wraps | |
| app = Flask(__name__, template_folder='/app/sudo/templates') | |
| app.secret_key = os.getenv("FLASK_SECRET") | |
| # MongoDB connection | |
| url = (os.getenv("MONGO_URI")) | |
| client = MongoClient(url, server_api=ServerApi('1')) | |
| # Authentication decorator | |
| def sudo_required(f): | |
| @wraps(f) | |
| def wrapper(*args, **kwargs): | |
| auth_token = request.headers.get('X-Sudo-Token') | |
| if auth_token != os.getenv("SUDO_SECRET"): | |
| return jsonify({"error": "Unauthorized"}), 403 | |
| return f(*args, **kwargs) | |
| return wrapper | |
| # Routes | |
| @app.route('/sudo') | |
| def admin_panel(): | |
| return render_template('index.html') | |
| @app.route('/sudo/login', methods=['POST']) | |
| def login(): | |
| if not hmac.compare_digest(request.json.get('password') or '', ADMIN_SECRET): | |
| return jsonify({"error": "Invalid credentials"}), 401 | |
| return jsonify({"token": ADMIN_SECRET}) | |
| @app.route('/sudo/users', methods=['GET']) | |
| @require_auth | |
| def list_users(): | |
| users = list(db.users.find({}, {"_id": 0, "username": 1})) | |
| return jsonify(users) | |
| @app.route('/sudo/users', methods=['POST']) | |
| @require_auth | |
| def add_user(): | |
| user_data = { | |
| "username": request.json["username"], | |
| "password": generate_password_hash(request.json["password"]), | |
| "role": "user" | |
| } | |
| db.users.insert_one(user_data) | |
| return jsonify({"status": "User added"}) | |
| @app.route('/sudo/users/<username>', methods=['DELETE']) | |
| @require_auth | |
| def delete_user(username): | |
| result = db.users.delete_one({"username": username}) | |
| if result.deleted_count == 0: | |
| return jsonify({"error": "User not found"}), 404 | |
| return jsonify({"status": "User deleted"}) | |
| if __name__ == "__main__": | |
| app.run(host='0.0.0.0', port=5000) | |
| EOF | |
| # ===== Caddy Configuration ===== | |
| RUN mkdir -p /app/caddy/ | |
| copy Caddyfile /app/caddy/Caddyfile | |
| ################################## | |
| # Startup script | |
| COPY <<"EOF" /app/start.sh | |
| #!/bin/sh | |
| # Start the combined server | |
| cd /app && npm run backend & | |
| python3 /app/sudo/app.py & | |
| caddy run --config /app/caddy/Caddyfile & | |
| wait | |
| EOF | |
| RUN chmod +x /app/start.sh | |
| EXPOSE 7860 | |
| CMD ["/app/start.sh"] |