diff --git a/.env.example b/.env.example index 3cce986..6ce58f6 100644 --- a/.env.example +++ b/.env.example @@ -14,6 +14,12 @@ HOST=0.0.0.0 PORT=3018 LOG_LEVEL=INFO +# ===== Auth ===== +# 首次启动时预置访问密码(可选)。公网服务器部署时填入,免去 SSH 端口转发设密码。 +# 仅在尚未设置密码时生效(一次性初始化);设过后改密码请用页面 UI, 此处不再读取。 +# 建议至少 6 位。.env 文件权限保持 600 且不要提交到 Git。 +AUTH_PASSWORD= + # Optional Docker build extras. Set to legacy-cpu on older VPS hosts # without AVX2/FMA support. BACKEND_EXTRAS= diff --git a/README.md b/README.md index 51dbc3c..f6b329d 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,8 @@ git pull - 多条件 AND/OR + 冷却期去重 + 严重级别(info/warn/critical) - 多入口配置:监控中心新建 / 个股详情页「加监控」/ 策略卡片一键开启 - 命中后右下角弹窗(可配声效)+ 持久化到 `alerts.jsonl`,菜单未读徽标 +- **触发记录详情**:每条记录展示命中的具体条件(如 `RSI>80`)与当前价位,一眼看清为何触发 +- **飞书 Webhook 推送**:全局一处配置飞书群机器人地址,启用推送的规则命中即推送到飞书群(支持签名校验);可在设置页设「默认推送渠道」,新建规则自动预填 ### 📈 个股分析(Beta) @@ -256,6 +258,18 @@ LOG_LEVEL=INFO # DEBUG | INFO | WARNING | ERROR DATA_DIR=./data # Parquet / DuckDB 数据存储目录 ``` +### 访问密码 + +面板首次设置访问密码时,出于安全考虑**仅允许本机或内网访问**(防公网陌生人抢先设置锁死面板)。公网服务器部署有两种方式设首个密码: + +1. **环境变量预置(推荐)** — 在 `.env` 填入 `AUTH_PASSWORD`,首次启动自动初始化(哈希后写入 `auth.json`,之后不再读取): + ```ini + AUTH_PASSWORD=你的密码 # 至少 6 位;仅首次生效,已设过则不覆盖 + ``` +2. **SSH 端口转发** — 本机执行 `ssh -L 3018:127.0.0.1:3018 用户@服务器IP`,浏览器开 `http://127.0.0.1:3018` 设密码 + +> 详细步骤与重置密码见 [docs/deploy-password.md](./docs/deploy-password.md)。设完密码后改密码走页面 UI(`设置 → 修改密码`)。 + --- ## 🏗️ 技术栈 diff --git a/backend/app/config.py b/backend/app/config.py index c39ac2c..91d987b 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -94,6 +94,10 @@ class Settings(BaseSettings): log_level: str = "INFO" backtest_range_guard: bool = False + # Auth — 首次启动时预置访问密码(明文, 仅用于初始化, 详见 services/auth.bootstrap_from_env) + # 公网服务器部署时免去 SSH 端口转发设密码的麻烦。写入 auth.json(哈希)后即不再读取。 + auth_password: str = "" + # Data — frozen: exe 同级 data/ 子目录; 非 frozen: 项目根 data/ # (均可被环境变量 DATA_DIR 覆盖, pydantic-settings 自动注入) data_dir: Path = _user_data_root() diff --git a/backend/app/main.py b/backend/app/main.py index dcc7366..871d252 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -34,6 +34,14 @@ async def lifespan(app: FastAPI): __version__, tf_client.current_mode(), ) + # 首次启动: 若配置了 AUTH_PASSWORD 环境变量且未设过密码, 用它初始化。 + # 公网部署免 SSH 端口转发; 已设过密码则不覆盖 (改密码走 UI)。 + try: + from app.services import auth as auth_service + auth_service.bootstrap_from_env() + except Exception as e: # noqa: BLE001 + logger.warning("auth bootstrap failed: %s", e) + # 数据层 store = DataStore() repo = KlineRepository(store) diff --git a/backend/app/services/auth.py b/backend/app/services/auth.py index e5aadd7..77c7b35 100644 --- a/backend/app/services/auth.py +++ b/backend/app/services/auth.py @@ -108,6 +108,34 @@ def set_password(password: str) -> None: logger.info("access password set") +def bootstrap_from_env() -> bool: + """首次初始化: 若环境变量 AUTH_PASSWORD 已配置且尚未设过密码, 则用它设密码。 + + 公网服务器部署场景: 避免每次都要 SSH 端口转发才能设首个密码。 + 明文密码只在内存/配置中, 经 set_password() 哈希后写入 auth.json (chmod 0600)。 + 一旦设置成功, 后续重启不再覆盖 (用户改密码走 UI, 不受环境变量影响)。 + + Returns: + True 表示本次用环境变量初始化了密码; False 表示无需初始化。 + """ + from app.config import settings + + pwd = (settings.auth_password or "").strip() + if not pwd: + return False + if is_configured(): + # 已设过密码, 不覆盖 (避免环境变量反复重置用户在 UI 改的密码) + return False + try: + set_password(pwd) + logger.info("access password bootstrapped from AUTH_PASSWORD env (one-time)") + return True + except ValueError as e: + # 密码不合规 (< 6 位), 记日志但不阻断启动 + logger.warning("AUTH_PASSWORD bootstrap skipped: %s", e) + return False + + def verify_and_create_session(password: str) -> str | None: """验证密码, 成功则创建会话并返回 token, 失败返回 None。""" d = _load() diff --git a/docs/deploy-password.md b/docs/deploy-password.md new file mode 100644 index 0000000..c828b31 --- /dev/null +++ b/docs/deploy-password.md @@ -0,0 +1,99 @@ +# 公网部署:如何设置访问密码 + +面板部署在公网服务器时,首次设置访问密码有限制 —— **必须从本机或内网访问**,以防公网上陌生人抢先设置密码锁死你的面板。 + +如果你在公网浏览器直接打开页面,会看到提示: + +> 首次设置密码仅允许本机或内网访问,请通过 SSH/本地浏览器操作 + +有两种方式解决,任选其一。 + +--- + +## 方式一:环境变量预置密码(最简单,推荐) + +在 `.env` 文件(或 Docker / 系统环境变量)里设置 `AUTH_PASSWORD`: + +```bash +# 编辑服务器上的 .env (通常在项目根目录或 backend/ 下) +AUTH_PASSWORD=你的密码 +``` + +然后重启服务。启动时会自动: +1. 读取 `AUTH_PASSWORD` +2. 用 PBKDF2 哈希后写入 `auth.json`(`chmod 600`,只存哈希不存明文) +3. **之后这个环境变量就不再被读取** —— 是一次性的初始化 + +设完后即可用公网地址 + 这个密码正常登录。后续改密码请用页面 UI(`设置 → 修改密码`),不受环境变量影响。 + +### 注意事项 + +- **密码至少 6 位**,否则会被跳过并记一条 warning 日志 +- **仅在未设过密码时生效**。已设过密码后,改这里不会覆盖(避免重启时重置你在 UI 改的密码) +- `.env` 文件权限保持 `600`,**不要提交到 Git** +- 明文密码只存在于 `.env` / 环境变量中,落盘的是哈希,安全性等同 `auth.json` + +### 重置密码(忘密码时) + +如果忘了密码想重置:删除或清空 `data/user_data/auth.json`,重启服务,会回到"未设密码"状态,此时 `AUTH_PASSWORD` 会重新生效。 + +```bash +# 停服后执行, 清空后重启 +rm data/user_data/auth.json +``` + +--- + +## 方式二:SSH 端口转发 + +不用改配置,在你**自己电脑**的终端执行(不是服务器上): + +```bash +ssh -L 3018:127.0.0.1:3018 用户名@服务器IP +``` + +例如服务器是 `123.45.67.89`、用户名 `root`、面板端口 `3018`: + +```bash +ssh -L 3018:127.0.0.1:3018 root@123.45.67.89 +``` + +保持这个 SSH 连接**不要关**,然后在**自己电脑的浏览器**打开: + +``` +http://127.0.0.1:3018 +``` + +此时后端看到的客户端 IP 是 `127.0.0.1`(本机),能通过校验,正常显示设置密码界面。 + +**设完密码后**,SSH 连接可以断开 —— 密码已存进服务器,之后直接用公网地址 + 刚设的密码访问即可。 + +### 端口说明 + +上面的 `3018` 是默认端口。如果你用 `PORT` 环境变量改过端口(比如 `PORT=8080`),两处都要替换: + +```bash +ssh -L 8080:127.0.0.1:8080 root@123.45.67.89 +``` + +--- + +## 原理说明 + +- **为什么限制本机/内网?** 面板部署到公网后,任何人都能访问 URL。如果不限制,攻击者可以在你之前打开页面、设置一个密码,把你的面板锁死。 +- **本机/内网如何判断?** 后端检查客户端 IP 是否属于 `127.0.0.1 / ::1 / 10.x / 192.168.x / 172.16-31.x`。 +- **SSH 转发为什么有效?** `-L` 把本机端口通过 SSH 隧道转发到服务器的 `127.0.0.1`,等同于在服务器本地访问,客户端 IP 变成 `127.0.0.1`,通过校验。 +- **反向代理注意:** 若面板在 Nginx 等反代之后,需正确配置 `X-Forwarded-For` 头,后端据此取真实客户端 IP。 + +--- + +## 两种方式怎么选 + +| | 环境变量 | SSH 转发 | +|---|---|---| +| 操作 | 改一行配置 + 重启 | 一条 ssh 命令 | +| 需要改配置 | 是 | 否 | +| 适合 | Docker / 自动化部署 / 不熟 SSH | 临时设密码 / 能 SSH 到服务器 | +| 后续改密码 | UI(`设置 → 修改密码`) | 同左 | + +推荐**方式一(环境变量)**,一次配置即可,Docker 部署尤其方便。