部署 (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>
89 lines
3.1 KiB
Python
89 lines
3.1 KiB
Python
"""Auth routes: register / login / logout / refresh."""
|
|
import uuid
|
|
|
|
from flask import Blueprint, request, g, jsonify
|
|
|
|
import os
|
|
|
|
from auth import hash_password, verify_password, sign_token, require_auth
|
|
import config
|
|
from db import execute, query_one
|
|
|
|
bp = Blueprint("auth", __name__)
|
|
|
|
|
|
def registration_open():
|
|
"""Whether an account may be created right now.
|
|
|
|
Under the default "auto" policy the endpoint closes as soon as the first
|
|
account exists, so a publicly reachable deployment cannot be signed up to
|
|
by strangers.
|
|
"""
|
|
# Read at call time, not import time, so the policy can be changed without
|
|
# a restart and so tests are not bound to whatever .env held at startup.
|
|
policy = (os.environ.get("ALLOW_REGISTRATION") or config.ALLOW_REGISTRATION).lower()
|
|
if policy == "true":
|
|
return True
|
|
if policy == "false":
|
|
return False
|
|
return query_one("SELECT id FROM users LIMIT 1") is None
|
|
|
|
|
|
@bp.route("/registration-status", methods=["GET"])
|
|
def registration_status():
|
|
"""Lets the UI hide the sign-up tab when registration is closed."""
|
|
return jsonify({"open": registration_open()})
|
|
|
|
|
|
@bp.route("/register", methods=["POST"])
|
|
def register():
|
|
if not registration_open():
|
|
return jsonify({"error": "注册已关闭:本实例已有账号"}), 403
|
|
|
|
data = request.get_json(silent=True) or {}
|
|
email = (data.get("email") or "").strip()
|
|
garmin_email = (data.get("garminEmail") or "").strip()
|
|
garmin_password = data.get("garminPassword") or ""
|
|
if not email or not garmin_email or not garmin_password:
|
|
return jsonify({"error": "email, garminEmail, garminPassword 均为必填"}), 400
|
|
|
|
if query_one("SELECT id FROM users WHERE email = ?", [email]):
|
|
return jsonify({"error": "该邮箱已注册"}), 409
|
|
|
|
uid = str(uuid.uuid4())
|
|
token = sign_token(uid)
|
|
execute(
|
|
"INSERT INTO users (id, email, garmin_email, garmin_password_hash, jwt_token) "
|
|
"VALUES (?, ?, ?, ?, ?)",
|
|
[uid, email, garmin_email, hash_password(garmin_password), token],
|
|
)
|
|
return jsonify({"id": uid, "email": email, "token": token}), 201
|
|
|
|
|
|
@bp.route("/login", methods=["POST"])
|
|
def login():
|
|
data = request.get_json(silent=True) or {}
|
|
email = (data.get("email") or "").strip()
|
|
password = data.get("password") or ""
|
|
user = query_one("SELECT * FROM users WHERE email = ?", [email])
|
|
if not user or not verify_password(password, user["garmin_password_hash"]):
|
|
return jsonify({"error": "邮箱或密码错误"}), 401
|
|
token = sign_token(user["id"])
|
|
execute("UPDATE users SET jwt_token = ? WHERE id = ?", [token, user["id"]])
|
|
return jsonify({"id": user["id"], "email": user["email"], "token": token})
|
|
|
|
|
|
@bp.route("/logout", methods=["POST"])
|
|
@require_auth
|
|
def logout():
|
|
execute("UPDATE users SET jwt_token = NULL WHERE id = ?", [g.user_id])
|
|
return jsonify({"message": "ok"})
|
|
|
|
|
|
@bp.route("/refresh", methods=["POST"])
|
|
@require_auth
|
|
def refresh():
|
|
token = sign_token(g.user_id)
|
|
execute("UPDATE users SET jwt_token = ? WHERE id = ?", [token, g.user_id])
|
|
return jsonify({"token": token})
|