[阶段5] 部署到 NAS + frp 公网映射,并加注册锁
部署 (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>
This commit is contained in:
@@ -48,6 +48,10 @@ _AI_ENV_VARS = (
|
||||
def _isolate_ai_env(monkeypatch):
|
||||
for var in _AI_ENV_VARS:
|
||||
monkeypatch.delenv(var, raising=False)
|
||||
# Most tests need to create users freely; the production default closes
|
||||
# registration once one account exists. test_registration_policy.py clears
|
||||
# this to exercise the real default.
|
||||
monkeypatch.setenv("ALLOW_REGISTRATION", "true")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
87
backend/tests/test_registration_policy.py
Normal file
87
backend/tests/test_registration_policy.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""
|
||||
Tests for who may create an account.
|
||||
|
||||
This matters because the deployment is reachable from the public internet: an
|
||||
unconditionally open /register would let a stranger sign up and start pulling
|
||||
health data.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
|
||||
def signup(client, email="new@example.com"):
|
||||
return client.post(
|
||||
"/api/auth/register",
|
||||
json={
|
||||
"email": email,
|
||||
"garminEmail": "g@example.com",
|
||||
"garminPassword": "pw123456",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _default_policy(monkeypatch):
|
||||
monkeypatch.delenv("ALLOW_REGISTRATION", raising=False)
|
||||
|
||||
|
||||
class TestAutoPolicy:
|
||||
"""Default: open until the first account exists, then closed."""
|
||||
|
||||
def test_first_account_is_allowed(self, client, db):
|
||||
assert signup(client).status_code == 201
|
||||
|
||||
def test_second_account_is_refused(self, client, user):
|
||||
r = signup(client, "stranger@example.com")
|
||||
assert r.status_code == 403
|
||||
assert "注册已关闭" in r.get_json()["error"]
|
||||
|
||||
def test_refusal_does_not_create_the_account(self, client, user, db):
|
||||
signup(client, "stranger@example.com")
|
||||
assert db.query_one(
|
||||
"SELECT id FROM users WHERE email = ?", ["stranger@example.com"]
|
||||
) is None
|
||||
|
||||
def test_status_reports_open_before_any_signup(self, client, db):
|
||||
assert client.get("/api/auth/registration-status").get_json()["open"] is True
|
||||
|
||||
def test_status_reports_closed_afterwards(self, client, user):
|
||||
assert client.get("/api/auth/registration-status").get_json()["open"] is False
|
||||
|
||||
|
||||
class TestExplicitPolicies:
|
||||
def test_true_keeps_it_open_even_with_existing_users(self, client, user, monkeypatch):
|
||||
monkeypatch.setenv("ALLOW_REGISTRATION", "true")
|
||||
assert signup(client, "second@example.com").status_code == 201
|
||||
|
||||
def test_false_closes_it_even_on_an_empty_instance(self, client, db, monkeypatch):
|
||||
monkeypatch.setenv("ALLOW_REGISTRATION", "false")
|
||||
assert signup(client).status_code == 403
|
||||
|
||||
def test_policy_is_read_per_request_not_at_import(self, client, db, monkeypatch):
|
||||
monkeypatch.setenv("ALLOW_REGISTRATION", "false")
|
||||
assert client.get("/api/auth/registration-status").get_json()["open"] is False
|
||||
monkeypatch.setenv("ALLOW_REGISTRATION", "true")
|
||||
assert client.get("/api/auth/registration-status").get_json()["open"] is True
|
||||
|
||||
def test_value_is_case_insensitive(self, client, user, monkeypatch):
|
||||
monkeypatch.setenv("ALLOW_REGISTRATION", "TRUE")
|
||||
assert signup(client, "second@example.com").status_code == 201
|
||||
|
||||
|
||||
class TestUnaffectedBehaviour:
|
||||
def test_status_endpoint_needs_no_auth(self, client, db):
|
||||
"""The login page must be able to ask before anyone is signed in."""
|
||||
assert client.get("/api/auth/registration-status").status_code == 200
|
||||
|
||||
def test_closing_registration_does_not_block_login(self, client, user):
|
||||
r = client.post(
|
||||
"/api/auth/login",
|
||||
json={"email": user["email"], "password": user["password"]},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
def test_duplicate_email_still_reports_409_when_open(
|
||||
self, client, user, monkeypatch
|
||||
):
|
||||
monkeypatch.setenv("ALLOW_REGISTRATION", "true")
|
||||
assert signup(client, user["email"]).status_code == 409
|
||||
Reference in New Issue
Block a user