[阶段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:
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { apiClient, errorMessage } from '../services/api';
|
||||
import './Login.css';
|
||||
@@ -10,6 +10,16 @@ function Login() {
|
||||
const [activeTab, setActiveTab] = useState<TabType>('login');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string>('');
|
||||
// Sign-up closes once an account exists, so the tab is hidden rather than
|
||||
// offering something the server will refuse.
|
||||
const [canRegister, setCanRegister] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
apiClient
|
||||
.getRegistrationStatus()
|
||||
.then(setCanRegister)
|
||||
.catch(() => setCanRegister(false));
|
||||
}, []);
|
||||
|
||||
// Login form
|
||||
const [loginEmail, setLoginEmail] = useState('');
|
||||
@@ -112,15 +122,17 @@ function Login() {
|
||||
>
|
||||
登录
|
||||
</button>
|
||||
<button
|
||||
className={`tab-button ${activeTab === 'register' ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
setActiveTab('register');
|
||||
setError('');
|
||||
}}
|
||||
>
|
||||
注册
|
||||
</button>
|
||||
{canRegister && (
|
||||
<button
|
||||
className={`tab-button ${activeTab === 'register' ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
setActiveTab('register');
|
||||
setError('');
|
||||
}}
|
||||
>
|
||||
注册
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
@@ -159,7 +171,7 @@ function Login() {
|
||||
</form>
|
||||
)}
|
||||
|
||||
{activeTab === 'register' && (
|
||||
{activeTab === 'register' && canRegister && (
|
||||
<form onSubmit={handleRegister} className="login-form">
|
||||
<div className="form-group">
|
||||
<label htmlFor="reg-email">邮箱</label>
|
||||
|
||||
@@ -138,6 +138,14 @@ class ApiClient {
|
||||
return data;
|
||||
}
|
||||
|
||||
/** Whether sign-up is currently permitted (closes after the first account). */
|
||||
async getRegistrationStatus() {
|
||||
const { data } = await this.client.get<{ open: boolean }>(
|
||||
'/auth/registration-status'
|
||||
);
|
||||
return data.open;
|
||||
}
|
||||
|
||||
async login(email: string, password: string) {
|
||||
const { data } = await this.client.post<AuthResponse>('/auth/login', {
|
||||
email,
|
||||
|
||||
Reference in New Issue
Block a user