First Push

This commit is contained in:
喆OvO
2026-08-06 11:11:11 +08:00
commit 0fef309972
551 changed files with 15793 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
"""Flask application factory and SSE event system."""
import json
import os
import queue
import secrets
import sqlite3
import threading
from datetime import datetime, timedelta
from backend.time_util import now as tz_now, strptime as tz_strptime
from flask import Flask
from flask_cors import CORS
from werkzeug.middleware.proxy_fix import ProxyFix
from backend.models import init_db
from backend.routes import register_routes
from backend.events import EventBus
from backend.weather_mgr import fetch_weather, fetch_hourly
def create_app(db_path: str = "data.db") -> Flask:
app = Flask(
__name__,
template_folder="../frontend/templates",
static_folder="../frontend/static",
)
# Generate a secure random key at startup.
# Set FLASK_SECRET_KEY env var for a fixed key (sessions survive restarts).
app.config["SECRET_KEY"] = os.environ.get("FLASK_SECRET_KEY") or secrets.token_hex(32)
# Defensive check: ensure key is never empty
if not app.config["SECRET_KEY"]:
app.config["SECRET_KEY"] = secrets.token_hex(32)
app.config["DB_PATH"] = db_path
# Trust Nginx X-Forwarded-* headers (x_for=1, x_proto=1, x_host=1)
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)
# Session hardening for production
https = os.environ.get("FLASK_HTTPS", "").lower() in ("1", "true", "yes")
app.config["SESSION_COOKIE_HTTPONLY"] = True
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
app.config["SESSION_COOKIE_SECURE"] = https
app.config["SESSION_PERMANENT"] = False
# Strict CORS: only allow same-origin requests (no wildcard for cookie auth)
CORS(app, supports_credentials=True, origins=[]) # empty = same-origin only
# Content size limit: 5MB for uploads
app.config["MAX_CONTENT_LENGTH"] = 5 * 1024 * 1024
# Session lifetime: 24 hours idle timeout
app.config["PERMANENT_SESSION_LIFETIME"] = 86400
# Global event bus
app.event_bus = EventBus()
init_db(db_path)
register_routes(app)
# Security headers on every response
@app.after_request
def _security_headers(resp):
resp.headers["X-Content-Type-Options"] = "nosniff"
resp.headers["X-Frame-Options"] = "DENY"
resp.headers["Referrer-Policy"] = "same-origin"
# Strict CSP limits inline styles/scripts — relax if custom HTML used
resp.headers["Content-Security-Policy"] = (
"default-src 'self'; "
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; "
"style-src 'self' 'unsafe-inline'; "
"img-src 'self' data:; "
"connect-src 'self'; "
"frame-ancestors 'none'"
)
return resp
# Startup weather fetch: only if cache is stale (>= 30 min) or empty
with app.app_context():
_startup_weather(app, db_path)
return app
def _startup_weather(app, db_path: str):
try:
conn = sqlite3.connect(db_path)
row = conn.execute(
"SELECT updated_at FROM weather_cache WHERE user_id=0 ORDER BY id DESC LIMIT 1"
).fetchone()
conn.close()
except Exception:
row = None
should_fetch = True
if row and row[0]:
try:
updated = tz_strptime(row[0], "%Y-%m-%d %H:%M:%S")
age = tz_now() - updated
if age < timedelta(minutes=30):
should_fetch = False
app.logger.info(
f"Startup: weather cache is {int(age.total_seconds()/60)} min old, skip fetch"
)
except Exception:
pass
if should_fetch:
try:
fetch_weather()
fetch_hourly()
app.logger.info("Startup: weather fetched (cache stale or empty)")
except Exception as e:
app.logger.warning(f"Startup weather fetch failed: {e}")