Spaces:
Running
Running
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 \ | |
git | |
RUN pip install flask pymongo[srv] --break-system-packages | |
COPY config.yaml /app/librechat.yaml | |
#secrets | |
RUN --mount=type=secret,example=SUDO_SECRET,required=true cat /run/secrets/SUDO_SECRET > /app/sudo.sec | |
RUN export SUDO_SECRET=$(cat /app/sudo.sec) | |
RUN --mount=type=secret,example=MONGO_URI,required=true cat /run/secrets/MONGO_URI > /app/mongo.sec | |
RUN export MONGO_URI=$(cat /app/mongo.sec) | |
RUN --mount=type=secret,example=FLASK_SECRET,required=true cat /run/secrets/FLASK_SECRET > /app/flask.sec | |
RUN export FLASK_SECRET=$(cat /app/flask.sec) | |
# 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, render_template | |
from pymongo.mongo_client import MongoClient | |
from pymongo.server_api import ServerApi | |
from werkzeug.security import generate_password_hash | |
import os | |
import hmac | |
from functools import wraps | |
app = Flask(__name__, template_folder='/app/sudo/templates') | |
app.secret_key = os.getenv("FLASK_SECRET") | |
# MongoDB connection | |
uri = os.getenv("MONGO_URI") | |
client = MongoClient(uri, server_api=ServerApi('1')) | |
db = client['librechat'] | |
ADMIN_SECRET = os.getenv("ADMIN_SECRET") | |
# Authentication decorator | |
def require_auth(f): | |
@wraps(f) | |
def wrapper(*args, **kwargs): | |
auth_token = request.headers.get('X-Auth-Token') | |
if not auth_token or not hmac.compare_digest(auth_token, ADMIN_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"}) | |
@app.route('/sudo/debug') | |
def debug(): | |
return jsonify({ | |
"expected_password": os.getenv("ADMIN_SECRET", "NOT_SET!"), | |
"flask_secret_set": bool(os.getenv("FLASK_SECRET")), | |
"mongo_connected": bool(client) | |
}) | |
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"] |