部署 (NAS 192.168.50.64): - MariaDB 建库 garmin_health_lab,5 张表由 init_db 建好 - Python 3.8.15 venv;NAS 无 gcc,依赖全部走纯 Python 轮子 - gunicorn 2 worker × 4 线程,--timeout 300(AI 生成耗时可达数分钟) - start.sh / stop.sh,可重复执行;日志落 logs/ - 在 NAS 真机 + 真实 MariaDB 上跑通全部测试:205 passed app.py / config.py: - STATIC_DIR 存在时由同一个 Flask 进程托管 React 构建产物, 部署即单端口单进程,不需要额外反代 - 404 处理区分 /api 前缀:API 仍返回 JSON,其余回退到 index.html, 这样 /settings 这类前端路由刷新后不会 404 安全 - 注册锁 (ALLOW_REGISTRATION): - 服务要挂到公网,而原本 /register 完全开放,任何人都能注册进来 读取健康数据 - 默认策略 auto:仅在尚无任何账号时开放,注册完第一个即自动关闭 - 另支持 true / false 显式覆盖;按请求读取,改配置无需重启 - 新增 GET /auth/registration-status,前端据此隐藏注册标签页 frp 公网映射: - 复用 NAS 上已有的 frpc (/etc/frp/frpc.toml),追加 garmin 隧道 NAS:8123 -> 甲骨文:8123(改前已按既有惯例备份 .bak.<时间戳>) - 经 S99frpc.sh restart 生效,原有 4 条隧道均正常恢复 tests/test_registration_policy.py (13 通过): - auto 策略下第一个账号放行、第二个 403 且不落库 - true/false 显式覆盖,大小写不敏感 - 策略按请求读取而非 import 时冻结 - 关闭注册不影响登录;status 端点无需鉴权 公网实测: 页面、SPA 路由、鉴权 401、注册锁 403 均符合预期。 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
63 lines
2.8 KiB
Python
63 lines
2.8 KiB
Python
"""
|
|
Central configuration for the Garmin Health Lab Flask backend.
|
|
|
|
Reads settings from a `.env` file (backend/.env) and the process environment.
|
|
The same code runs against SQLite (local dev) or MariaDB (NAS production)
|
|
by switching DB_TYPE — business code never branches on the backend.
|
|
"""
|
|
import os
|
|
|
|
from dotenv import load_dotenv
|
|
|
|
# Load .env from the backend directory (falls back to cwd / parent search).
|
|
_BACKEND_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
_ENV_PATH = os.path.join(_BACKEND_DIR, ".env")
|
|
if os.path.exists(_ENV_PATH):
|
|
load_dotenv(_ENV_PATH)
|
|
else:
|
|
load_dotenv() # walk up from cwd
|
|
|
|
# --- Database selection -----------------------------------------------------
|
|
DB_TYPE = (os.environ.get("DB_TYPE") or "sqlite").lower()
|
|
|
|
# SQLite (default, zero-config local development)
|
|
SQLITE_PATH = os.environ.get("DATABASE_PATH") or os.path.join(
|
|
_BACKEND_DIR, "data", "health.db"
|
|
)
|
|
|
|
# MariaDB (production, runs on the NAS)
|
|
MARIADB_SOCKET = os.environ.get("MARIADB_SOCKET") or ""
|
|
MARIADB_HOST = os.environ.get("MARIADB_HOST") or "127.0.0.1"
|
|
MARIADB_PORT = int(os.environ.get("MARIADB_PORT") or 3306)
|
|
MARIADB_USER = os.environ.get("MARIADB_USER") or "root"
|
|
MARIADB_PASSWORD = os.environ.get("MARIADB_PASSWORD") or ""
|
|
MARIADB_DATABASE = os.environ.get("MARIADB_DATABASE") or "garmin_health_lab"
|
|
|
|
# --- Auth -------------------------------------------------------------------
|
|
JWT_SECRET = os.environ.get("JWT_SECRET") or "dev_secret_change_me"
|
|
JWT_EXPIRY_DAYS = int(os.environ.get("JWT_EXPIRY_DAYS") or 7)
|
|
|
|
# Who may create an account.
|
|
# "auto" - only while no user exists yet (first-run setup, then closed).
|
|
# "true" - always open.
|
|
# "false" - never; accounts must be created out of band.
|
|
# "auto" is the default because this deployment is reachable from the public
|
|
# internet, where an open registration endpoint would let anyone create an
|
|
# account and start pulling health data.
|
|
ALLOW_REGISTRATION = (os.environ.get("ALLOW_REGISTRATION") or "auto").lower()
|
|
|
|
# --- Static UI --------------------------------------------------------------
|
|
# Directory holding the built React app. When set and populated, the Flask
|
|
# process serves the UI too, so a deployment is one port and one service.
|
|
STATIC_DIR = os.environ.get("STATIC_DIR") or os.path.join(_BACKEND_DIR, "static")
|
|
|
|
# --- Server -----------------------------------------------------------------
|
|
# BACKEND_PORT wins over PORT: `PORT` is set by many dev tools and PaaS
|
|
# runtimes for the *frontend*, and letting it through made Flask seize the
|
|
# React dev server's port during `npm run dev`.
|
|
PORT = int(os.environ.get("BACKEND_PORT") or os.environ.get("PORT") or 5000)
|
|
|
|
# Comma-separated list of allowed front-end origins (CORS).
|
|
_CORS_RAW = os.environ.get("CORS_ORIGIN") or "http://localhost:3000,http://localhost:5173"
|
|
CORS_ORIGINS = [o.strip() for o in _CORS_RAW.split(",") if o.strip()]
|