| """ |
| Main DB Server - Central Registry for all ServerClass instances. |
| Runs at: https://dooratre-db.hf.space |
| |
| Responsibilities: |
| - Store userβserver mapping |
| - Device fingerprint (token) β user lookup |
| - Server load balancing (assign new users to least loaded server) |
| - GitHub backup of registry data |
| """ |
|
|
| from flask import Flask, request, jsonify |
| from datetime import datetime |
| from functools import wraps |
|
|
| from config import ( |
| SECRET_KEY, PORT, ADMIN_SECRET, API_SECRET, |
| SERVER_URL_PATTERN, TOTAL_SERVERS, MAX_USERS_PER_SERVER, |
| ALLOWED_ORIGINS, WEBSITE_URL, LOGIN_WEBSITE_URL |
| ) |
| from memory import get_db |
|
|
| app = Flask(__name__) |
| app.secret_key = SECRET_KEY |
|
|
|
|
| |
| |
| |
|
|
| @app.after_request |
| def add_cors_headers(response): |
| origin = request.headers.get('Origin', '') |
| if origin in ALLOWED_ORIGINS or not origin: |
| response.headers['Access-Control-Allow-Origin'] = origin if origin else '*' |
| else: |
| |
| for i in range(1, TOTAL_SERVERS + 1): |
| if f"serverclass{i}" in origin: |
| response.headers['Access-Control-Allow-Origin'] = origin |
| break |
|
|
| response.headers['Access-Control-Allow-Headers'] = 'Content-Type, X-API-Key, X-Admin-Secret' |
| response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS' |
| response.headers['Access-Control-Allow-Credentials'] = 'true' |
| return response |
|
|
|
|
| @app.before_request |
| def handle_preflight(): |
| if request.method == 'OPTIONS': |
| response = jsonify({"ok": True}) |
| return response |
|
|
|
|
| |
| |
| |
|
|
| def require_api_key(f): |
| """Require X-API-Key header matching API_SECRET.""" |
| @wraps(f) |
| def decorated(*args, **kwargs): |
| api_key = request.headers.get('X-API-Key', '') |
| if api_key != API_SECRET: |
| return jsonify({"ok": False, "error": "Unauthorized"}), 401 |
| return f(*args, **kwargs) |
| return decorated |
|
|
|
|
| def require_admin(f): |
| """Require X-Admin-Secret header.""" |
| @wraps(f) |
| def decorated(*args, **kwargs): |
| secret = request.headers.get('X-Admin-Secret', '') or request.args.get('secret', '') |
| if secret != ADMIN_SECRET: |
| return jsonify({"ok": False, "error": "Unauthorized"}), 401 |
| return f(*args, **kwargs) |
| return decorated |
|
|
|
|
| def _server_url(server_num): |
| """Convert server number to full URL.""" |
| return SERVER_URL_PATTERN.replace("{N}", str(server_num)) |
|
|
|
|
| |
| |
| |
| |
|
|
| @app.route('/api/check-token', methods=['POST']) |
| @require_api_key |
| def check_token(): |
| """ |
| Check if a device fingerprint token is linked to a user. |
| |
| Request: {"token": "device_fingerprint_string"} |
| |
| Response if found: |
| {"ok": true, "found": true, "username": "...", "server_num": N, "server_url": "..."} |
| |
| Response if not found: |
| {"ok": true, "found": false} |
| """ |
| data = request.get_json() |
| if not data: |
| return jsonify({"ok": False, "error": "No JSON data"}), 400 |
|
|
| token = (data.get('token', '') or '').strip() |
| if not token: |
| return jsonify({"ok": False, "error": "No token provided"}), 400 |
|
|
| db = get_db() |
| user = db.get_user_by_token(token) |
|
|
| if user: |
| server_num = user.get('server_num') |
| return jsonify({ |
| "ok": True, |
| "found": True, |
| "username": user.get('username'), |
| "server_num": server_num, |
| "server_url": _server_url(server_num), |
| }) |
| else: |
| return jsonify({ |
| "ok": True, |
| "found": False, |
| }) |
|
|
|
|
| |
| |
| |
| |
|
|
| @app.route('/api/lookup-user', methods=['POST']) |
| @require_api_key |
| def lookup_user(): |
| """ |
| Look up which server a username is on. |
| |
| Request: {"username": "..."} |
| |
| Response if found: |
| {"ok": true, "found": true, "server_num": N, "server_url": "..."} |
| |
| Response if not found: |
| {"ok": true, "found": false} |
| """ |
| data = request.get_json() |
| if not data: |
| return jsonify({"ok": False, "error": "No JSON data"}), 400 |
|
|
| username = (data.get('username', '') or '').strip() |
| if not username: |
| return jsonify({"ok": False, "error": "No username provided"}), 400 |
|
|
| db = get_db() |
| user = db.get_user(username) |
|
|
| if user: |
| server_num = user.get('server_num') |
| return jsonify({ |
| "ok": True, |
| "found": True, |
| "username": username, |
| "server_num": server_num, |
| "server_url": _server_url(server_num), |
| }) |
| else: |
| return jsonify({ |
| "ok": True, |
| "found": False, |
| }) |
|
|
|
|
| |
| |
| |
| |
|
|
| @app.route('/api/get-best-server', methods=['POST']) |
| @require_api_key |
| def get_best_server(): |
| """ |
| Find the server with the lowest user count for new signup. |
| |
| Request: {} (empty or with token) |
| |
| Response: |
| {"ok": true, "server_num": N, "server_url": "..."} |
| """ |
| db = get_db() |
| best = db.get_best_server() |
|
|
| if best is None: |
| return jsonify({ |
| "ok": False, |
| "error": "All servers are full", |
| }), 503 |
|
|
| return jsonify({ |
| "ok": True, |
| "server_num": best, |
| "server_url": _server_url(best), |
| }) |
|
|
|
|
| |
| |
| |
| |
|
|
| @app.route('/api/register-user', methods=['POST']) |
| @require_api_key |
| def register_user(): |
| """ |
| Register a new user in the central registry. |
| Called by the server after successful signup+verification. |
| |
| Request: { |
| "username": "...", |
| "telegram_id": "...", |
| "server_num": N, |
| "token": "device_fingerprint" (optional) |
| } |
| |
| Response: |
| {"ok": true, "message": "User registered"} |
| """ |
| data = request.get_json() |
| if not data: |
| return jsonify({"ok": False, "error": "No JSON data"}), 400 |
|
|
| username = (data.get('username', '') or '').strip() |
| telegram_id = (data.get('telegram_id', '') or '').strip() |
| server_num = data.get('server_num') |
| token = (data.get('token', '') or '').strip() or None |
|
|
| if not username: |
| return jsonify({"ok": False, "error": "No username"}), 400 |
| if not server_num: |
| return jsonify({"ok": False, "error": "No server_num"}), 400 |
|
|
| try: |
| server_num = int(server_num) |
| except (ValueError, TypeError): |
| return jsonify({"ok": False, "error": "Invalid server_num"}), 400 |
|
|
| db = get_db() |
| success, error = db.register_user(username, telegram_id, server_num, token) |
|
|
| if not success: |
| return jsonify({"ok": False, "error": error}), 409 |
|
|
| return jsonify({ |
| "ok": True, |
| "message": f"User '{username}' registered on server {server_num}", |
| }) |
|
|
|
|
| |
| |
| |
| |
|
|
| @app.route('/api/link-token', methods=['POST']) |
| @require_api_key |
| def link_token(): |
| """ |
| Link a device fingerprint token to a user. |
| Called after successful login. |
| |
| Request: {"username": "...", "token": "device_fingerprint"} |
| |
| Response: |
| {"ok": true} |
| """ |
| data = request.get_json() |
| if not data: |
| return jsonify({"ok": False, "error": "No JSON data"}), 400 |
|
|
| username = (data.get('username', '') or '').strip() |
| token = (data.get('token', '') or '').strip() |
|
|
| if not username: |
| return jsonify({"ok": False, "error": "No username"}), 400 |
| if not token: |
| return jsonify({"ok": False, "error": "No token"}), 400 |
|
|
| db = get_db() |
| success, error = db.link_token(username, token) |
|
|
| if not success: |
| return jsonify({"ok": False, "error": error}), 404 |
|
|
| return jsonify({"ok": True}) |
|
|
| @app.route('/api/unlink-token', methods=['POST']) |
| @require_api_key |
| def unlink_token(): |
| """Remove a specific token from the registry.""" |
| data = request.get_json() |
| if not data: |
| return jsonify({"ok": False, "error": "No JSON data"}), 400 |
|
|
| token = (data.get('token', '') or '').strip() |
| if not token: |
| return jsonify({"ok": False, "error": "No token"}), 400 |
|
|
| db = get_db() |
| db.unlink_token(token) |
|
|
| return jsonify({"ok": True}) |
|
|
|
|
| @app.route('/api/unlink-user-tokens', methods=['POST']) |
| @require_api_key |
| def unlink_user_tokens(): |
| """Remove ALL tokens for a user. Returns count of tokens removed.""" |
| data = request.get_json() |
| if not data: |
| return jsonify({"ok": False, "error": "No JSON data"}), 400 |
|
|
| username = (data.get('username', '') or '').strip() |
| if not username: |
| return jsonify({"ok": False, "error": "No username"}), 400 |
|
|
| db = get_db() |
| removed_count = 0 |
|
|
| with db._locks['users_registry']: |
| user = db._data['users_registry'].get(username) |
| if user: |
| old_tokens = user.get('tokens', []) |
| for t in old_tokens: |
| token_val = t.get('token', '') |
| if token_val: |
| with db._locks['tokens_index']: |
| if token_val in db._data['tokens_index']: |
| del db._data['tokens_index'][token_val] |
| removed_count += 1 |
| user['tokens'] = [] |
| else: |
| |
| |
| with db._locks['tokens_index']: |
| tokens_to_remove = [ |
| tok for tok, uname in db._data['tokens_index'].items() |
| if uname == username |
| ] |
| for tok in tokens_to_remove: |
| del db._data['tokens_index'][tok] |
| removed_count += 1 |
|
|
| db._save_to_shared() |
|
|
| print(f" β
Cleared {removed_count} tokens for '{username}'") |
| return jsonify({ |
| "ok": True, |
| "removed": removed_count, |
| "username": username, |
| }) |
| |
| |
| |
| |
|
|
| @app.route('/api/login-success', methods=['POST']) |
| @require_api_key |
| def login_success(): |
| """ |
| Server notifies Main DB that a user logged in successfully. |
| Updates last_login and optionally links a new token. |
| |
| Request: { |
| "username": "...", |
| "token": "device_fingerprint" (optional) |
| } |
| """ |
| data = request.get_json() |
| if not data: |
| return jsonify({"ok": False, "error": "No JSON data"}), 400 |
|
|
| username = (data.get('username', '') or '').strip() |
| token = (data.get('token', '') or '').strip() or None |
|
|
| if not username: |
| return jsonify({"ok": False, "error": "No username"}), 400 |
|
|
| db = get_db() |
| db.update_user_login(username) |
|
|
| if token: |
| db.link_token(username, token) |
|
|
| return jsonify({"ok": True}) |
|
|
|
|
| |
| |
| |
| |
|
|
| @app.route('/api/check-username', methods=['POST']) |
| @require_api_key |
| def check_username(): |
| """ |
| Check if a username already exists in any server. |
| |
| Request: {"username": "..."} |
| Response: {"ok": true, "exists": true/false, "server_num": N or null} |
| """ |
| data = request.get_json() |
| if not data: |
| return jsonify({"ok": False, "error": "No JSON data"}), 400 |
|
|
| username = (data.get('username', '') or '').strip() |
| if not username: |
| return jsonify({"ok": False, "error": "No username"}), 400 |
|
|
| db = get_db() |
| user = db.get_user(username) |
|
|
| if user: |
| return jsonify({ |
| "ok": True, |
| "exists": True, |
| "server_num": user.get('server_num'), |
| }) |
| else: |
| return jsonify({ |
| "ok": True, |
| "exists": False, |
| "server_num": None, |
| }) |
|
|
|
|
| |
| |
| |
|
|
| @app.route('/api/server-status', methods=['GET']) |
| @require_api_key |
| def server_status(): |
| """Get all server user counts.""" |
| db = get_db() |
| counts = db.get_server_counts() |
|
|
| servers = [] |
| for i in range(1, TOTAL_SERVERS + 1): |
| num_str = str(i) |
| count = counts.get(num_str, 0) |
| servers.append({ |
| "server_num": i, |
| "users": count, |
| "max": MAX_USERS_PER_SERVER, |
| "available": count < MAX_USERS_PER_SERVER, |
| "url": _server_url(i), |
| }) |
|
|
| return jsonify({ |
| "ok": True, |
| "servers": servers, |
| "total_users": db.get_total_users(), |
| "total_servers": TOTAL_SERVERS, |
| "max_per_server": MAX_USERS_PER_SERVER, |
| }) |
|
|
|
|
| |
| |
| |
|
|
| @app.route('/admin/backup', methods=['POST']) |
| @require_admin |
| def admin_backup(): |
| """Push all registry data to GitHub.""" |
| db = get_db() |
| success, errors = db.push_to_github() |
|
|
| if success: |
| return jsonify({ |
| "ok": True, |
| "message": "All data pushed to GitHub", |
| "stats": db.get_stats(), |
| }) |
| else: |
| return jsonify({ |
| "ok": False, |
| "errors": errors, |
| "stats": db.get_stats(), |
| }), 500 |
|
|
|
|
| |
| |
| |
|
|
| @app.route('/admin/stats', methods=['GET']) |
| @require_admin |
| def admin_stats(): |
| db = get_db() |
| return jsonify({ |
| "ok": True, |
| "stats": db.get_stats(), |
| }) |
|
|
|
|
| |
| |
| |
|
|
| @app.route('/admin/register', methods=['POST']) |
| @require_admin |
| def admin_register(): |
| """Manually register a user (for testing/migration).""" |
| data = request.get_json() |
| if not data: |
| return jsonify({"ok": False, "error": "No JSON data"}), 400 |
|
|
| username = (data.get('username', '') or '').strip() |
| telegram_id = (data.get('telegram_id', '') or '').strip() |
| server_num = data.get('server_num') |
|
|
| if not username or not server_num: |
| return jsonify({"ok": False, "error": "username and server_num required"}), 400 |
|
|
| db = get_db() |
| success, error = db.register_user(username, telegram_id, int(server_num)) |
|
|
| if not success: |
| return jsonify({"ok": False, "error": error}), 409 |
|
|
| return jsonify({"ok": True, "message": f"Registered {username} on server {server_num}"}) |
|
|
|
|
| |
| |
| |
|
|
| @app.route('/admin/users', methods=['GET']) |
| @require_admin |
| def admin_users(): |
| """List all registered users.""" |
| db = get_db() |
| with db._locks['users_registry']: |
| all_users = [] |
| for uname, udata in db._data['users_registry'].items(): |
| all_users.append({ |
| "username": uname, |
| "server_num": udata.get('server_num'), |
| "telegram_id": udata.get('telegram_id', ''), |
| "tokens_count": len(udata.get('tokens', [])), |
| "created_at": udata.get('created_at', ''), |
| "last_login": udata.get('last_login', ''), |
| }) |
|
|
| return jsonify({ |
| "ok": True, |
| "users": all_users, |
| "total": len(all_users), |
| }) |
|
|
|
|
| |
| |
| |
|
|
| @app.route('/admin/delete-user', methods=['POST']) |
| @require_admin |
| def admin_delete_user(): |
| data = request.get_json() |
| if not data: |
| return jsonify({"ok": False, "error": "No JSON data"}), 400 |
|
|
| username = (data.get('username', '') or '').strip() |
| if not username: |
| return jsonify({"ok": False, "error": "No username"}), 400 |
|
|
| db = get_db() |
|
|
| with db._locks['users_registry']: |
| user = db._data['users_registry'].pop(username, None) |
|
|
| if not user: |
| return jsonify({"ok": False, "error": "User not found"}), 404 |
|
|
| |
| for t in user.get('tokens', []): |
| token_val = t.get('token', '') |
| if token_val: |
| with db._locks['tokens_index']: |
| db._data['tokens_index'].pop(token_val, None) |
|
|
| |
| server_num = str(user.get('server_num', 0)) |
| with db._locks['server_counts']: |
| if server_num in db._data['server_counts']: |
| db._data['server_counts'][server_num] = max( |
| 0, db._data['server_counts'][server_num] - 1 |
| ) |
|
|
| return jsonify({"ok": True, "message": f"Deleted user '{username}'"}) |
|
|
|
|
| |
| |
| |
|
|
| @app.route('/health', methods=['GET']) |
| def health(): |
| db = get_db() |
| return jsonify({ |
| "status": "healthy", |
| "service": "main-db-registry", |
| "total_users": db.get_total_users(), |
| "total_servers": TOTAL_SERVERS, |
| "version": "1.0.0", |
| }) |
|
|
|
|
| @app.route('/', methods=['GET']) |
| def index(): |
| return jsonify({ |
| "service": "ServerClass Main DB Registry", |
| "status": "running", |
| "docs": { |
| "POST /api/check-token": "Check device fingerprint", |
| "POST /api/lookup-user": "Find user's server", |
| "POST /api/get-best-server": "Get server for new signup", |
| "POST /api/register-user": "Register new user", |
| "POST /api/link-token": "Link device to user", |
| "POST /api/login-success": "Notify login success", |
| "POST /api/check-username": "Check if username exists", |
| "GET /api/server-status": "Server load info", |
| "GET /health": "Health check", |
| } |
| }) |
|
|
|
|
| |
| |
| |
|
|
| if __name__ == '__main__': |
| print("\n" + "β" * 60) |
| print(" ποΈ ServerClass Main DB Registry") |
| print("β" * 60) |
| print(f" π http://localhost:{PORT}") |
| print("β" * 60 + "\n") |
| app.run(debug=True, host='0.0.0.0', port=PORT) |