tienhiep-api / backend /__init__.py
Cong123779
deploy: update backend production to new Space
d9bfc2d
Raw
History Blame Contribute Delete
10.4 kB
import os
import logging
from flask import Flask, jsonify
from flask_cors import CORS
from werkzeug.middleware.proxy_fix import ProxyFix
from backend.config import Config
from backend.database.db_manager import init_user_db, health_check
from backend.api import (
auth_bp, books_bp, payment_bp, translate_bp, epub_bp, developer_bp, user_features_bp, sects_bp
)
from backend.api.monitoring import monitoring_bp
logger = logging.getLogger("backend")
def create_app():
"""Flask Application Factory."""
root_dir = Config.ROOT_DIR
app = Flask(
__name__,
static_folder=os.path.join(root_dir, "frontend-web", "dist"),
static_url_path="",
)
# Wrap with ProxyFix to handle reverse proxy headers
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1)
# ── Configuration ────────────────────────────────────────────────
app.secret_key = Config.SECRET_KEY
app.config["MAX_CONTENT_LENGTH"] = 256 * 1024 * 1024 # 256MB
# ── CORS ─────────────────────────────────────────────────────────
CORS(app, resources={
r"/*": {
"origins": ["*"],
"allow_headers": ["Content-Type", "Authorization", "X-VIP-Code", "X-VIP-Key"],
"methods": ["GET", "POST", "OPTIONS", "DELETE", "PUT"],
"max_age": 86400
}
})
# ── Register Blueprints ───────────────────────────────────────────
app.register_blueprint(auth_bp)
app.register_blueprint(books_bp)
app.register_blueprint(payment_bp)
app.register_blueprint(translate_bp)
app.register_blueprint(epub_bp)
app.register_blueprint(developer_bp)
app.register_blueprint(user_features_bp)
app.register_blueprint(sects_bp)
app.register_blueprint(monitoring_bp)
# ── Profiler & Latency Monitoring ─────────────────────────────────
from backend.core.profiler import setup_profiler
setup_profiler(app)
# ── Frontend Route ───────────────────────────────────────────────
@app.route("/")
def index():
try:
return app.send_static_file("index.html")
except Exception:
return jsonify({"status": "healthy", "message": "Tienhiep API Backend is running."})
@app.errorhandler(404)
def page_not_found(e):
from flask import request
path = request.path.lstrip("/")
if path.startswith("api/") or path.startswith("assets/") or path.startswith("v1/") or path.startswith("health") or path.startswith("translate"):
return jsonify({"error": "Not Found"}), 404
try:
return app.send_static_file("index.html")
except Exception:
return jsonify({"error": "Not Found"}), 404
# ── Health Check ────────────────────────────────────────────────
@app.route("/health", methods=["GET"])
def health_endpoint():
status = health_check()
http_code = 200 if status["status"] in ("healthy", "degraded") else 503
return jsonify(status), http_code
# ── CORS & Security after-request headers ────────────────────────
@app.after_request
def add_cors_headers(response):
from flask import request
origin = request.headers.get("Origin")
if origin:
response.headers["Access-Control-Allow-Origin"] = origin
response.headers["Access-Control-Allow-Credentials"] = "true"
response.headers["Access-Control-Max-Age"] = "86400"
# Cho phΓ©p Cloudflare Cache luΓ΄n lệnh thΔƒm dΓ² OPTIONS (24h)
if request.method == "OPTIONS":
response.headers["Cache-Control"] = "public, max-age=86400"
response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization, X-VIP-Code, X-VIP-Key"
response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS, DELETE, PUT"
else:
# NgΔƒn Cloudflare vΓ  TrΓ¬nh duyệt lΖ°u Cache cΓ‘c nα»™i dung API thαΊ­t
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
response.headers["X-Content-Type-Options"] = "nosniff"
if request.path.startswith("/embed") or request.path.startswith("/iframe_proxy"):
response.headers["Content-Security-Policy"] = "frame-ancestors *"
response.headers.pop("X-Frame-Options", None)
else:
response.headers["X-Frame-Options"] = "SAMEORIGIN"
# Sanitize raw database error leaks from reaching public clients
if response.content_type == "application/json":
try:
data = response.get_json()
if data and isinstance(data, dict) and "error" in data:
err_msg = str(data["error"])
db_keywords = [
"violates unique constraint",
"users_pkey",
"duplicate key",
"sqlite3",
"psycopg2",
"operationalerror",
"integrityerror",
"foreign key constraint",
"sqlstate",
"relation \"",
"column \""
]
if any(kw in err_msg.lower() for kw in db_keywords):
logger.error(f"[Security Shield] Intercepted raw DB error leak: {err_msg}")
from flask import json
sanitized_data = {"error": "Đã xαΊ£y ra lα»—i hệ thα»‘ng. Vui lΓ²ng thα»­ lαΊ‘i sau."}
response.set_data(json.dumps(sanitized_data))
except Exception as e:
logger.warning(f"Error in sanitize_database_errors: {e}")
return response
@app.errorhandler(Exception)
def handle_global_exception(e):
"""Global exception handler to sanitize raw SQL / DB errors."""
from werkzeug.exceptions import HTTPException
if isinstance(e, HTTPException):
return jsonify({"error": e.description}), e.code
logger.exception(f"Unhandled system error: {e}")
return jsonify({"error": "Đã xαΊ£y ra lα»—i hệ thα»‘ng. Vui lΓ²ng thα»­ lαΊ‘i sau."}), 500
# ── Initialize DB ────────────────────────────────────────────────
try:
init_user_db()
logger.info("βœ” Database initialized on app startup.")
except Exception as e:
logger.error(f"⚠️ Auto database initialization failed: {e}")
# ── Email Worker ─────────────────────────────────────────────────
# Email Worker chαΊ‘y Δ‘α»™c lαΊ­p qua start_hf.sh (python3 backend/workers/email_worker.py &)
# KhΓ΄ng khởi Δ‘α»™ng trong Flask init để trΓ‘nh thread chαΊΏt sau Gunicorn --preload fork.
logger.info("πŸ“§ [Email Worker] Managed externally by start_hf.sh")
# ── Pre-load translation engine ──────────────────────────────────
try:
logger.info("⏳ Pre-loading Vietphrase Engine...")
from backend.services.translation import get_engine
engine = get_engine()
logger.info("⏳ Warming up translation modes...")
for mode in ["fast", "advanced", "vietphrase", "hanviet", "advanced_hanviet"]:
engine.translate("叢凑", mode=mode)
logger.info("βœ… Vietphrase Engine loaded & warmed up successfully.")
except Exception as e:
logger.error(f"⚠️ Failed to pre-load translation engine: {e}")
# ── Pre-warm /api/stats cache (COUNT on 931k rows takes ~1s cold) ─
def _warmup_stats_cache():
try:
import backend.database.db_manager as db_mod
if db_mod.cached_stats is None:
from backend.database.db_manager import get_db
conn = get_db()
total = conn.execute("SELECT COUNT(*) FROM books").fetchone()[0]
cats = conn.execute(
"SELECT categories FROM books WHERE categories IS NOT NULL AND categories != ''"
).fetchall()
cat_counts = {}
for row in cats:
for c in row[0].split(","):
c = c.strip()
if c:
cat_counts[c] = cat_counts.get(c, 0) + 1
top_categories = sorted(cat_counts.items(), key=lambda x: x[1], reverse=True)[:20]
db_mod.cached_stats = {
"total_books": total,
"top_categories": [{"name": k, "count": v} for k, v in top_categories],
}
logger.info(f"βœ… Stats cache warmed up: {total:,} books indexed.")
except Exception as e:
logger.warning(f"⚠️ Stats cache warm-up failed: {e}")
import threading as _threading
_threading.Thread(target=_warmup_stats_cache, daemon=True, name="StatsCacheWarmup").start()
# ── Watchdog Keepalive (HF Space anti-sleep) ─────────────────────
if not app.testing and os.environ.get("SPACE_HOST"):
try:
from backend.core.watchdog import start_watchdog
space_url = f"https://{os.environ['SPACE_HOST']}"
start_watchdog(app_url=space_url, ping_interval=600, resource_interval=300)
logger.info(f"βœ… Watchdog keepalive started β†’ {space_url}")
except Exception as e:
logger.warning(f"⚠️ Watchdog could not start: {e}")
return app