Spaces:
Running
Running
# Use official LibreChat base image | |
FROM ghcr.io/danny-avila/librechat-dev:latest | |
# Install Python dependencies | |
USER root | |
RUN apk update && apk add --no-cache \ | |
python3 \ | |
py3-pip \ | |
sqlite \ | |
&& pip3 install flask werkzeug waitress requests --break-system-packages | |
# Setup directory structure | |
RUN mkdir -p /app/{admin/{templates,static},data,uploads,client/public/images,api/logs} \ | |
&& mkdir -p /app/uploads/temp \ | |
&& chown -R 1000:1000 /app \ | |
&& chmod -R 777 /app/uploads \ | |
&& chmod -R 777 /app/client/public/images \ | |
&& chmod -R 777 /app/api/logs | |
# ===== Admin Templates ===== | |
COPY <<"EOF" /app/admin/templates/login.html | |
<html> | |
<head> | |
<title>Admin Login</title> | |
<link rel="stylesheet" href="/static/admin.css"> | |
</head> | |
<body> | |
<div class="container"> | |
<h2>Admin Portal</h2> | |
<form id="loginForm"> | |
<input type="password" name="sudo_secret" placeholder="Admin Secret" required> | |
<button type="submit">Login</button> | |
</form> | |
</div> | |
<script src="/static/admin.js"></script> | |
</body> | |
</html> | |
EOF | |
COPY <<"EOF" /app/admin/templates/dashboard.html | |
<html> | |
<head> | |
<title>User Management</title> | |
<link rel="stylesheet" href="/static/admin.css"> | |
</head> | |
<body> | |
<div class="container"> | |
<h1>User Management</h1> | |
<div class="card"> | |
<h2>Add User</h2> | |
<form id="addUserForm"> | |
<input type="text" name="username" placeholder="Username" required> | |
<input type="password" name="password" placeholder="Password" required> | |
<button type="submit">Add User</button> | |
</form> | |
</div> | |
<div class="card"> | |
<h2>Current Users</h2> | |
<ul id="userList"></ul> | |
</div> | |
</div> | |
<script src="/static/admin.js"></script> | |
</body> | |
</html> | |
EOF | |
# ===== Admin Static Files ===== | |
COPY <<"EOF" /app/admin/static/admin.css | |
body { | |
font-family: Arial, sans-serif; | |
margin: 0; | |
padding: 20px; | |
background-color: #f5f5f5; | |
} | |
.container { | |
max-width: 800px; | |
margin: 0 auto; | |
} | |
.card { | |
background: white; | |
padding: 20px; | |
margin: 20px 0; | |
border-radius: 8px; | |
box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
} | |
input, button { | |
width: 100%; | |
padding: 10px; | |
margin: 8px 0; | |
box-sizing: border-box; | |
} | |
button { | |
background-color: #4CAF50; | |
color: white; | |
border: none; | |
cursor: pointer; | |
} | |
EOF | |
COPY <<"EOF" /app/admin/static/admin.js | |
async function loadUsers() { | |
const response = await fetch('/sudo/list_users', { | |
headers: {'X-Sudo-Secret': localStorage.getItem('sudo_token')} | |
}); | |
if (response.ok) { | |
const users = await response.json(); | |
document.getElementById('userList').innerHTML = users.map(user => | |
\`<li>\${user.username} <button onclick="deleteUser('\${user.username}')">Delete</button></li>\` | |
).join(''); | |
} | |
} | |
async function deleteUser(username) { | |
if (confirm(\`Delete \${username}?\`)) { | |
await fetch('/sudo/remove_user', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
'X-Sudo-Secret': localStorage.getItem('sudo_token') | |
}, | |
body: JSON.stringify({ username: username }) | |
}); | |
loadUsers(); | |
} | |
} | |
document.getElementById('addUserForm').addEventListener('submit', async (e) => { | |
e.preventDefault(); | |
const formData = new FormData(e.target); | |
const response = await fetch('/sudo/add_user', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
'X-Sudo-Secret': localStorage.getItem('sudo_token') | |
}, | |
body: JSON.stringify({ | |
username: formData.get('username'), | |
password: formData.get('password') | |
}) | |
}); | |
if (response.ok) { | |
e.target.reset(); | |
await loadUsers(); | |
} | |
}); | |
document.addEventListener('DOMContentLoaded', () => { | |
const token = localStorage.getItem('sudo_token'); | |
if (!token && !window.location.pathname.includes('login')) { | |
window.location.href = '/sudo'; | |
} else if (token) { | |
loadUsers(); | |
} | |
}); | |
EOF | |
# ===== Combined Server ===== | |
COPY <<"EOF" /app/combined_server.py | |
from flask import Flask, request, jsonify, render_template, make_response | |
from werkzeug.middleware.proxy_fix import ProxyFix | |
from werkzeug.security import generate_password_hash | |
import os | |
import sqlite3 | |
from pathlib import Path | |
import subprocess | |
import requests | |
from waitress import serve | |
app = Flask(__name__, | |
template_folder='/app/admin/templates', | |
static_folder='/app/admin/static') | |
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1) | |
app.secret_key = os.getenv("FLASK_SECRET") | |
# Database setup | |
DB_PATH = '/app/data/admin.db' | |
def get_db(): | |
Path(DB_PATH).parent.mkdir(exist_ok=True) | |
db = sqlite3.connect(DB_PATH) | |
db.execute('''CREATE TABLE IF NOT EXISTS users | |
(username TEXT PRIMARY KEY, password TEXT, role TEXT)''') | |
return db | |
# Admin routes | |
@app.route('/sudo') | |
def admin_home(): | |
return render_template('login.html') | |
@app.route('/sudo/login', methods=['POST']) | |
def login(): | |
if request.json.get('sudo_secret') == os.getenv("SUDO_SECRET"): | |
return jsonify({"status": "success"}) | |
return jsonify({"error": "Invalid secret"}), 403 | |
@app.route('/sudo/dashboard') | |
def dashboard(): | |
return render_template('dashboard.html') | |
@app.route('/sudo/add_user', methods=['POST']) | |
def add_user(): | |
if request.headers.get('X-Sudo-Secret') != os.getenv("SUDO_SECRET"): | |
return jsonify({"error": "Unauthorized"}), 403 | |
db = get_db() | |
try: | |
db.execute("INSERT INTO users VALUES (?,?,?)", [ | |
request.json["username"], | |
generate_password_hash(request.json["password"]), | |
"user" | |
]) | |
db.commit() | |
return jsonify({"status": "User added"}) | |
except sqlite3.IntegrityError: | |
return jsonify({"error": "User exists"}), 400 | |
@app.route('/sudo/list_users', methods=['GET']) | |
def list_users(): | |
if request.headers.get('X-Sudo-Secret') != os.getenv("SUDO_SECRET"): | |
return jsonify({"error": "Unauthorized"}), 403 | |
db = get_db() | |
return jsonify([ | |
{"username": row[0]} for row in db.execute("SELECT username FROM users") | |
]) | |
@app.route('/sudo/remove_user', methods=['POST']) | |
def remove_user(): | |
if request.headers.get('X-Sudo-Secret') != os.getenv("SUDO_SECRET"): | |
return jsonify({"error": "Unauthorized"}), 403 | |
db = get_db() | |
db.execute("DELETE FROM users WHERE username=?", [request.json["username"]]) | |
db.commit() | |
return jsonify({"status": "User removed"}) | |
# LibreChat proxy | |
@app.route('/', defaults={'path': ''}) | |
@app.route('/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE']) | |
def proxy(path): | |
if path.startswith('sudo/') or path == 'sudo': | |
return make_response("Not Found", 404) | |
resp = requests.request( | |
method=request.method, | |
url=f"http://localhost:3080/{path}", | |
headers={key: value for (key, value) in request.headers if key != 'Host'}, | |
data=request.get_data(), | |
cookies=request.cookies, | |
allow_redirects=False | |
) | |
response = make_response(resp.content, resp.status_code) | |
for key, value in resp.headers.items(): | |
if key.lower() not in ['content-encoding', 'content-length', 'transfer-encoding']: | |
response.headers[key] = value | |
return response | |
if __name__ == "__main__": | |
# Start LibreChat backend | |
subprocess.Popen(["npm", "run", "backend"], cwd="/app") | |
# Start combined server | |
serve(app, host='0.0.0.0', port=7860, threads=4) | |
EOF | |
# Startup script | |
COPY <<"EOF" /app/start.sh | |
#!/bin/sh | |
# Start the combined server | |
python3 /app/combined_server.py | |
EOF | |
RUN chmod +x /app/start.sh | |
# 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" \ | |
FLASK_SECRET="$FLASK_SECRET" \ | |
NODE_ENV=production | |
EXPOSE 7860 | |
CMD ["/app/start.sh"] |