import os import time import uuid import secrets import base64 import requests as py_requests from flask import Blueprint, request, jsonify, send_file from backend.core.decorators import get_current_user, require_api_key_auth from backend.database.db_manager import get_user_db_conn from backend.services.translation import get_engine developer_bp = Blueprint("developer", __name__) ADMIN_GEMINI_KEY = os.environ.get("ADMIN_GEMINI_KEY", "") def serialize_row(row): if not row: return {} d = dict(row) for k, v in d.items(): if hasattr(v, "isoformat"): d[k] = v.isoformat() return d def deduct_developer_balance(api_key, amount, model, tokens=0, status_code=200): conn = get_user_db_conn() try: conn.execute( "INSERT INTO api_usage (api_key, model, tokens, cost, status_code) VALUES (?, ?, ?, ?, ?)", (api_key, model, tokens, amount, status_code) ) conn.execute( "UPDATE users SET api_balance = api_balance - ? WHERE id = (SELECT user_id FROM api_keys WHERE api_key = ?)", (amount, api_key) ) conn.commit() except Exception as e: print(f"[API GATEWAY ERROR] Failed to deduct balance: {e}") finally: conn.close() @developer_bp.route("/api/developer/keys", methods=["GET"]) def api_dev_keys_list(): user = get_current_user() print(f"[DEBUG api_dev_keys_list] Current user from auth: {user}") if not user: return jsonify({"error": "Vui lòng đăng nhập."}), 401 conn = get_user_db_conn() keys = conn.execute( "SELECT api_key, name, status, created_at, last_used_at FROM api_keys WHERE user_id = ? AND status = 'active' ORDER BY created_at DESC", (user["id"],) ).fetchall() user_row = conn.execute("SELECT api_balance FROM users WHERE id = ?", (user["id"],)).fetchone() conn.close() balance = user_row["api_balance"] if user_row and user_row["api_balance"] is not None else 0.0 print(f"[DEBUG api_dev_keys_list] Queried user_id: {user['id']}, found balance: {balance}, keys count: {len(keys)}") return jsonify({"balance": balance, "keys": [serialize_row(k) for k in keys]}) @developer_bp.route("/api/developer/keys/create", methods=["POST"]) def api_dev_keys_create(): user = get_current_user() if not user: return jsonify({"error": "Vui lòng đăng nhập."}), 401 data = request.json or {} name = data.get("name", "Default Key").strip()[:50] new_key = f"sk-tc-{secrets.token_hex(16)}" conn = get_user_db_conn() conn.execute( "INSERT INTO api_keys (user_id, api_key, name, status) VALUES (?, ?, ?, 'active')", (user["id"], new_key, name) ) conn.commit() conn.close() return jsonify({"success": True, "api_key": new_key, "name": name}) @developer_bp.route("/api/developer/keys/delete", methods=["POST"]) def api_dev_keys_delete(): user = get_current_user() if not user: return jsonify({"error": "Vui lòng đăng nhập."}), 401 data = request.json or {} api_key = data.get("api_key", "").strip() if not api_key: return jsonify({"error": "Thiếu API Key cần xóa."}), 400 conn = get_user_db_conn() conn.execute( "UPDATE api_keys SET status = 'revoked' WHERE user_id = ? AND api_key = ?", (user["id"], api_key) ) conn.commit() conn.close() return jsonify({"success": True}) @developer_bp.route("/api/developer/usage", methods=["GET"]) def api_dev_usage(): user = get_current_user() if not user: return jsonify({"error": "Vui lòng đăng nhập."}), 401 conn = get_user_db_conn() keys_rows = conn.execute("SELECT api_key FROM api_keys WHERE user_id = ?", (user["id"],)).fetchall() keys = [k["api_key"] for k in keys_rows] if not keys: conn.close() return jsonify({"usage": []}) placeholders = ",".join(["?"] * len(keys)) usage = conn.execute( f"SELECT * FROM api_usage WHERE api_key IN ({placeholders}) ORDER BY timestamp DESC LIMIT 100", keys ).fetchall() conn.close() return jsonify({"usage": [serialize_row(u) for u in usage]}) @developer_bp.route("/v1/chat/completions", methods=["POST"]) @require_api_key_auth def openai_chat_completions(): api_key = request.api_key data = request.json or {} messages = data.get("messages", []) model = data.get("model", "gemini-1.5-flash") if not messages: return jsonify({"error": "Missing messages array"}), 400 prompt_length = sum(len(m.get("content", "")) for m in messages) cost = max(20.0, prompt_length * 0.02) if request.api_balance < cost: return jsonify({"error": f"Số dư không đủ. Chi phí ước tính: {cost:.2f}đ, Số dư: {request.api_balance:.2f}đ"}), 402 if not ADMIN_GEMINI_KEY: return jsonify({"error": "Hệ thống chưa được Admin cấu hình khóa Gemini."}), 501 try: contents = [] system_instruction = "" for msg in messages: role = msg.get("role") content = msg.get("content", "") if role == "system": system_instruction = content else: contents.append({ "role": "user" if role == "user" else "model", "parts": [{"text": content}] }) url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={ADMIN_GEMINI_KEY}" payload = {"contents": contents} if system_instruction: payload["systemInstruction"] = {"parts": [{"text": system_instruction}]} res = py_requests.post(url, json=payload, timeout=30) if res.status_code != 200: deduct_developer_balance(api_key, 0.0, model, prompt_length, res.status_code) return jsonify({"error": f"Google Gemini API error: {res.text}"}), res.status_code gemini_data = res.json() response_text = gemini_data.get("candidates", [{}])[0].get("content", {}).get("parts", [{}])[0].get("text", "") deduct_developer_balance(api_key, cost, model, prompt_length + len(response_text), 200) res_id = f"chatcmpl-{uuid.uuid4().hex}" return jsonify({ "id": res_id, "object": "chat.completion", "created": int(time.time()), "model": model, "choices": [{ "index": 0, "message": {"role": "assistant", "content": response_text}, "finish_reason": "stop" }], "usage": { "prompt_tokens": int(prompt_length / 4), "completion_tokens": int(len(response_text) / 4), "total_tokens": int((prompt_length + len(response_text)) / 4) } }) except Exception as e: deduct_developer_balance(api_key, 0.0, model, prompt_length, 500) return jsonify({"error": f"API Gateway Exception: {str(e)}"}), 500 @developer_bp.route("/api/v1/translate", methods=["POST"]) @require_api_key_auth def api_v1_translate(): api_key = request.api_key data = request.json or {} texts = data.get("texts", []) mode = data.get("mode", "fast") if not texts: return jsonify({"error": "Missing texts array"}), 400 total_chars = sum(len(t) for t in texts) cost = total_chars * 0.01 if request.api_balance < cost: return jsonify({"error": f"Số dư không đủ. Chi phí ước tính: {cost:.2f}đ, Số dư: {request.api_balance:.2f}đ"}), 402 try: eng = get_engine() translations = [] for text in texts: if not text.strip(): translations.append(text) else: translations.append(eng.translate(text, multi_option=False, mode=mode)) deduct_developer_balance(api_key, cost, f"vietphrase-{mode}", total_chars, 200) return jsonify({"translations": translations, "characters": total_chars, "cost": cost}) except Exception as e: deduct_developer_balance(api_key, 0.0, f"vietphrase-{mode}", total_chars, 500) return jsonify({"error": f"Translation Error: {str(e)}"}), 500 @developer_bp.route("/v1/audio/speech", methods=["POST"]) @require_api_key_auth def openai_audio_speech(): api_key = request.api_key data = request.json or {} input_text = data.get("input", "").strip() if not input_text: return jsonify({"error": "Missing 'input' text"}), 400 cost = len(input_text) * 0.1 if request.api_balance < cost: return jsonify({"error": f"Số dư không đủ. Chi phí: {cost:.2f}đ"}), 402 # 1. Check if Local TTS Server is configured (host machine) local_tts_url = os.environ.get("LOCAL_TTS_URL", "") if local_tts_url: try: payload = { "input": input_text, "speed": float(data.get("speed", 1.0)), "voice": data.get("voice", "the_gioi_hoan_my"), "ref_audio": data.get("ref_audio"), "ref_text": data.get("ref_text") } if "bypass_cache" in data: payload["bypass_cache"] = data["bypass_cache"] if "temperature" in data: payload["temperature"] = data["temperature"] if "steps" in data: payload["steps"] = data["steps"] print(f"[Local TTS Proxy] Forwarding request to {local_tts_url} with voice={payload['voice']}, speed={payload['speed']}, bypass_cache={payload.get('bypass_cache')}") r = py_requests.post(local_tts_url, json=payload, timeout=(2.0, 60.0)) if r.status_code == 200: import io deduct_developer_balance(api_key, cost, f"local-tts-{payload['voice']}", len(input_text), 200) return send_file( io.BytesIO(r.content), mimetype="audio/wav", as_attachment=False ) print(f"[Local TTS Proxy Error]: {r.status_code} - {r.text}") except Exception as e: print(f"[Local TTS Proxy Exception]: {e}") # 2. RunPod Fallback runpod_api_key = os.environ.get("RUNPOD_API_TOKEN", os.environ.get("RUNPOD_API_KEY", "")) runpod_endpoint_id = os.environ.get("RUNPOD_TTS_ENDPOINT_ID", "") if runpod_api_key and runpod_endpoint_id: try: url = f"https://api.runpod.ai/v1/{runpod_endpoint_id}/runsync" headers = {"Authorization": f"Bearer {runpod_api_key}", "Content-Type": "application/json"} payload = { "input": { "text": input_text, "speed": 1.0, "voice": data.get("voice", "the_gioi_hoan_my") } } r = py_requests.post(url, json=payload, headers=headers, timeout=45) res = r.json() if r.status_code == 200 and res.get("status") == "COMPLETED": audio_b64 = res.get("output", {}).get("audio_base64", "") if audio_b64: import io audio_data = base64.b64decode(audio_b64) deduct_developer_balance(api_key, cost, "runpod-matcha-tts", len(input_text), 200) return send_file( io.BytesIO(audio_data), mimetype="audio/wav" if res.get("output", {}).get("format") == "wav" else "audio/mpeg", as_attachment=False ) print(f"[RunPod TTS Error]: {res}") except Exception as e: print(f"[RunPod TTS exception]: {e}") # Fallback: Generate a tiny 1-second silent WAV for testing/sandbox purposes try: import struct import io num_samples = 8000 # 8-bit PCM silence is represented by 128 data_bytes = bytearray([128] * num_samples) header = struct.pack( '<4sI4s4sIHHIIHH4sI', b'RIFF', 36 + num_samples, b'WAVE', b'fmt ', 16, 1, # PCM 1, # Mono 8000, # Sample rate 8000, # Byte rate 1, # Block align 8, # Bits per sample b'data', num_samples ) audio_data = bytes(header + data_bytes) deduct_developer_balance(api_key, cost, "local-silent-tts-fallback", len(input_text), 200) return send_file( io.BytesIO(audio_data), mimetype="audio/wav", as_attachment=False ) except Exception as e: return jsonify({"error": f"Failed to generate fallback TTS: {str(e)}"}), 500 @developer_bp.route("/api/developer/all-sessions", methods=["GET"]) def api_developer_all_sessions(): user = get_current_user() if not user or user.get("username") not in ["admin", "havucong25", "congkx123789"]: return jsonify({"error": "Quyền truy cập bị từ chối."}), 403 conn = get_user_db_conn() try: rows = conn.execute( """SELECT lh.id, lh.user_id, u.username, lh.ip_address, lh.os, lh.browser, lh.device_type, lh.login_time, lh.last_active, lh.status FROM login_history lh JOIN users u ON lh.user_id = u.id ORDER BY lh.login_time DESC LIMIT 100""" ).fetchall() sessions = [] for r in rows: s = dict(r) if hasattr(s["login_time"], "isoformat"): s["login_time"] = s["login_time"].isoformat() if hasattr(s["last_active"], "isoformat"): s["last_active"] = s["last_active"].isoformat() sessions.append(s) return jsonify({ "success": True, "sessions": sessions }) except Exception as e: return jsonify({"error": str(e), "success": False}), 500 finally: conn.close() @developer_bp.route("/api/developer/sessions/revoke", methods=["POST"]) def api_developer_revoke_session(): user = get_current_user() if not user or user.get("username") not in ["admin", "havucong25", "congkx123789"]: return jsonify({"error": "Quyền truy cập bị từ chối."}), 403 data = request.json or {} session_id = data.get("session_id") if not session_id: return jsonify({"error": "Missing session_id", "success": False}), 400 conn = get_user_db_conn() try: row = conn.execute( "SELECT token FROM login_history WHERE id = ?", (session_id,) ).fetchone() if not row: return jsonify({"error": "Session not found", "success": False}), 404 token = row["token"] conn.execute("UPDATE login_history SET status = 'logged_out' WHERE id = ?", (session_id,)) if token: conn.execute("UPDATE refresh_tokens SET revoked = 1 WHERE token = ?", (token,)) conn.commit() return jsonify({"message": "Đã đăng xuất thiết bị thành công", "success": True}) except Exception as e: return jsonify({"error": str(e), "success": False}), 500 finally: conn.close() @developer_bp.route("/api/releases", methods=["GET"]) def api_get_releases(): conn = get_user_db_conn() try: rows = conn.execute( "SELECT platform, version, download_url, patch_url, file_size, release_notes, updated_at FROM app_releases ORDER BY updated_at DESC" ).fetchall() releases = {} for r in rows: plat = r["platform"] if plat not in releases: releases[plat] = { "version": r["version"], "download_url": r["download_url"], "patch_url": r["patch_url"], "file_size": r["file_size"], "release_notes": r["release_notes"], "updated_at": r["updated_at"].isoformat() if hasattr(r["updated_at"], "isoformat") else r["updated_at"], "history": [] } releases[plat]["history"].append({ "version": r["version"], "download_url": r["download_url"], "patch_url": r["patch_url"], "file_size": r["file_size"], "release_notes": r["release_notes"], "updated_at": r["updated_at"].isoformat() if hasattr(r["updated_at"], "isoformat") else r["updated_at"] }) return jsonify({"success": True, "releases": releases}) except Exception as e: return jsonify({"error": str(e), "success": False}), 500 finally: conn.close() @developer_bp.route("/api/releases/update", methods=["POST"]) def api_update_release(): user = get_current_user() if not user or user.get("username") not in ["admin", "havucong25", "congkx123789"]: return jsonify({"error": "Quyền truy cập bị từ chối. Chỉ dành cho admin.", "success": False}), 403 data = request.json or {} platform = data.get("platform") version = data.get("version") download_url = data.get("download_url") file_size = data.get("file_size") release_notes = data.get("release_notes") if not platform or platform not in ['extension', 'desktop_linux', 'desktop_windows']: return jsonify({"error": "Platform không hợp lệ.", "success": False}), 400 conn = get_user_db_conn() try: existing = conn.execute("SELECT 1 FROM app_releases WHERE platform = ? AND version = ?", (platform, version)).fetchone() if existing: conn.execute( """UPDATE app_releases SET download_url = ?, file_size = ?, release_notes = ?, updated_at = CURRENT_TIMESTAMP WHERE platform = ? AND version = ?""", (download_url, file_size, release_notes, platform, version) ) else: conn.execute( """INSERT INTO app_releases (platform, version, download_url, file_size, release_notes) VALUES (?, ?, ?, ?, ?)""", (platform, version, download_url, file_size, release_notes) ) conn.commit() return jsonify({"message": f"Cập nhật phiên bản {platform} v{version} thành công.", "success": True}) except Exception as e: return jsonify({"error": str(e), "success": False}), 500 finally: conn.close()