Files
iot-dboard/README.md
T
2026-08-11 22:07:55 +08:00

312 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Dashboard - 多人副屏系统
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
---
## 项目简介
多用户副屏系统,支持日历、天气、闹钟、番茄钟、倒计时等功能。每个用户拥有完全隔离的数据与独立的显示端/控制端:
- **显示端** (`/`) — 大屏/触屏设备展示信息
- **控制端** (`/control`) — 完整配置面板
- **Root 管理员** (`id=0`) — 首个自动创建的账户,拥有全部功能与用户管理权限
数据库使用 **PostgreSQL**(Docker 内置),生产以 **Gunicorn** 多 worker 运行。
---
## 功能
- **多用户**:Root 管理员可注册/编辑/删除用户;所有用户数据完全隔离,普通用户无管理权限
- **账户创建**:仅管理员可创建 —— 管理员代注册(正式账户,可代配 TOTP/Passkey)或创建临时账户(用户自助填写登录方式)
- **临时账户**:服务器持有临时 TOTP 密钥,管理员仅可见实时验证码;用户登录后被强制引导配置自己的登录方式,配置完成即正式生效
- **日历**:支持订阅 iCalendar (.ics) 链接及本地文件上传,自动同步日程
- **天气**:基于和风天气 (QWeather) API,含逐小时/分钟级预报
- **闹钟**:多组闹钟设定,自动弹出提醒,支持自定义铃声
- **番茄钟**:定时提醒,自动继续,断网重连自动恢复
- **倒计时**:可自定义时长,到时提醒
- **自定义展示页**:支持多组自定义 HTML 模板,一键切换
- **数据库管理(仅 Root)**:在 `/control → 设置 → 数据库管理` 导入旧版 SQLite 数据库文件或本系统导出的备份(自动识别格式)、导出现有数据库备份
---
## 🔐 安全特性
支持**双因素认证 (MFA)**,适合公网部署:
| 方式 | 说明 |
|------|------|
| **Passkey (WebAuthn)** | 指纹、面容、安全密钥等设备内置生物/硬件认证 |
| **TOTP** | Google Authenticator / Microsoft Authenticator 等标准应用 |
| **一次性恢复码** | 8 个恢复码,每个仅可使用一次 |
| **临时密码** | 首次启动为 Root 自动生成 6 位随机密码,显示在控制台 |
**登录流程**:
```
首次启动(空数据库)→ 自动创建 Root 管理员 (id=0),控制台打印临时密码
↓
访问 /control → 输入 6 位数字(自动识别 TOTP / 临时账户 TOTP / Root 临时密码)
↓
登录成功 → 签发 access token(前端持有)+ refresh token(httpOnly cookie)
↓
Root 临时密码 → 强制跳转 /mfa-setup 配置 MFA
正式用户 TOTP/Passkey → 直接进入控制面板
临时账户 TOTP → 强制跳转 /mfa-setup 配置登录方式,配置完成后临时 TOTP 立即失效
```
**会话管理**:
- 控制端「设置 → 踢掉当前账号的所有设备」:所有登录该账号的设备(控制端 + 显示端,含当前设备)立即下线,**所有登录用户可用**
- Root「用户管理 → 踢掉所有用户」:所有用户的控制端与显示端全部下线(含 Root 自身),需二次验证
**用户管理(仅 Root)**:
- 管理员代注册:填写信息 → 正式账户,可随后代配置 TOTP(生成密钥+二维码)或 Passkey(注册仪式在当前浏览器完成)
- 创建临时账户:生成临时 TOTP(密钥仅存服务器),管理员仅看到实时验证码
- 注册/编辑备注/删除用户均需**二次验证管理员身份**(TOTP 优先,无 TOTP 时用 Passkey)
- 删除用户会级联删除其全部数据并使其会话立即失效
- 用户 ID 对用户透明(仅在备注默认为 ID 时可见一次),普通用户只能看到自己的数据
**安全防护**:
- 登录频率限制(渐进式封禁:5次→5分钟、10次→15分钟、15次→24小时)
- CSRF 防护(Origin/Referer 校验)
- 敏感操作二次验证(用户管理、MFA 配置、数据库管理需重新确认身份)
- **双 Token 认证**:短期 access token(`Authorization: Bearer`,15 分钟)+ 长期 refresh token(httpOnly cookie,30 天滑动续期、每次刷新轮换、并发重放安全)
- Access token 快照会话版本:删除用户 / 踢设备 / 踢所有用户后**立即**使全部 access token 失效
- 显示端 SSE 通过一次性短时 ticket 鉴权(EventSource 无法带自定义 header)
- 内容安全策略(CSP)限制脚本/样式来源
- 铃声上传仅允许音频格式+MIME 校验
- 第三方日历数据 HTML 转义防止 XSS
---
## 快速开始
### 环境要求
- Python 3.11+
- PostgreSQL 14+(Docker 部署时无需单独安装,镜像内置)
### 开发环境
```bash
# 1. 准备 PostgreSQL(本机或 Docker)
# docker run -d --name pg -e POSTGRES_USER=iot -e POSTGRES_PASSWORD=iot \
# -e POSTGRES_DB=iot_dboard -p 5432:5432 postgres:16-alpine
# 2. 安装依赖
pip install -r requirements.txt
# 3. 运行(开发模式)
DATABASE_URL="postgresql://iot:iot@localhost:5432/iot_dboard" python run.py
# 4. 访问
# 显示端: http://localhost:5001
# 控制端: http://localhost:5001/control
```
### 生产部署 (推荐 Nginx + Gunicorn)
```
客户端 (HTTPS)
│
▼ 443
Nginx (SSL termination + Certbot)
│ proxy_pass http://127.0.0.1:5000
│
▼ 5000 (HTTP)
Gunicorn (2 workers × 2 threads, start:app)
│
▼
PostgreSQL (Docker 内置, 端口 5432)
```
#### 1. Nginx 配置
```nginx
server {
listen 443 ssl;
server_name your-domain.com;
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 300s; # SSE 长连接
}
client_max_body_size 200M; # 数据库导入需要较大体积
}
```
#### 2. 启动 Gunicorn
```bash
DATABASE_URL="postgresql://iot:iot@127.0.0.1:5432/iot_dboard" \
FLASK_HTTPS=yes gunicorn --workers 2 --threads 2 --bind 127.0.0.1:5000 start:app
```
后台调度器(闹钟/提醒/天气/番茄钟)通过 **PostgreSQL advisory lock** 保证只在其中一个 worker 内运行;若持有者异常退出,其他 worker 的 watchdog 会自动接管。
#### 3. Certbot 申请证书
```bash
sudo certbot --nginx -d your-domain.com
```
---
## Docker 部署
### 快速启动
```bash
# 构建并启动(自动创建 PostgreSQL 与 Web 服务)
docker compose up -d
# 查看启动日志(获取临时密码)
docker compose logs app
# 停止
docker compose down
```
访问 `http://你的IP:5000`,日志中会打印临时密码。
### 更新代码
```bash
# 方法一:进入容器内 git pull(推荐)
docker compose exec app git pull
docker compose restart app
# 方法二:重建镜像(较慢)
docker compose down
docker compose build --no-cache
docker compose up -d
```
### 环境变量配置
编辑 `docker-compose.yml` 的 `environment` 段:
```yaml
environment:
- DATABASE_URL=postgresql://iot:${POSTGRES_PASSWORD:-iot}@db:5432/iot_dboard
- FLASK_SECRET_KEY=your-secret-here # 可选:固定密钥
- FLASK_HTTPS=yes # 如果前面有 HTTPS 反代
- GUNICORN_WORKERS=2 # 默认 2(对应 1 物理核心 = 2 逻辑核心)
- GUNICORN_THREADS=2
```
### 资源限制
`docker-compose.yml` 中已按 **1 物理核心(2 逻辑核心)+ 2G 内存** 配置:
```yaml
deploy:
resources:
limits:
cpus: "2"
memory: 2G
```
### 持久化数据
- **PostgreSQL 数据**:命名卷 `pgdata`(`docker compose down` 不丢失)
- **旧数据库导入**:将旧 SQLite `.db` 文件放入 `./data/` 目录后,在 `/control → 设置 → 数据库管理` 上传导入(也支持直接选择文件上传)
- **项目代码**:命名卷 `app_code` 挂载到 `/app`,首次自动初始化为镜像内代码,之后 `git pull` 的修改永久保留
### 搭配 Nginx 反代(生产)
使用宿主机上的 Nginx(或额外 Nginx 容器)做 SSL 终结。配置与前文 Nginx 部分相同,只需将 `proxy_pass` 指向 Docker 宿主机 IP:5000。
> ⚠️ 注意:容器内 Gunicorn 绑定 `0.0.0.0:5000`,宿主机可直接访问 `localhost:5000`。如需限制仅 Nginx 访问,在 `docker-compose.yml` 中将 `ports` 改为 `127.0.0.1:5000:5000`。
---
## 环境变量
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `DATABASE_URL` | `postgresql://iot:iot@db:5432/iot_dboard` | PostgreSQL 连接串(Docker 内指向 db 服务) |
| `FLASK_HTTPS` | 空 | 设为 `yes`/`true` 启用 refresh token cookie 的 Secure 属性(Nginx 反代时启用) |
| `FLASK_SECRET_KEY` | 自动随机生成 | 保留兼容;认证已由 access/refresh 双 token 承载,不再依赖 Flask session |
| `FLASK_DISABLE_SCHEDULER` | 空 | 设为 `true` 禁用后台调度器(调试用) |
| `GUNICORN_WORKERS` | `2` | Gunicorn worker 数(对应逻辑核心数) |
| `GUNICORN_THREADS` | `2` | 每个 worker 的线程数 |
---
## 数据库管理(仅 Root)
> 位置:`/control → 设置 → 数据库管理`(仅 Root 用户可见)
- **导出现有数据库**:下载自包含的 JSON 备份(含 schema 版本与全部数据,不含会话令牌;**含登录凭据,请妥善保管**)
- **导入旧版 SQLite 数据库**:上传旧版单用户 `.db` 文件,自动迁移(旧数据归属 Root、缺失字段用 DEFAULT 填充、旧版弃用表自动丢弃、旧版 PIN 不会迁移)
- **导入备份文件**:上传本系统导出的 JSON 备份(与 SQLite 文件自动识别)
导入为**单事务原子操作**:任何一行失败都会整体回滚,当前数据不受影响;导入成功会清空会话(所有设备需重新登录)。
---
## 数据库迁移(版本化,升级无需重写数据)
数据库内置 `meta` 表记录 **schema 版本**(程序内 `DB_VERSION`,当前为 **2**)。每次启动时比对:
| 情况 | 行为 |
|------|------|
| 数据库版本 == 程序版本 | 直接使用,跳过升级 |
| 无版本字段或 数据库版本 < 程序版本 | 创建缺失的表/列,缺失字段一律用 SCHEMA DEFAULT 填充,并更新版本号 |
| 数据库版本 > 程序版本 | **报错并终止启动**(数据库比程序新,需先升级程序) |
从旧版 SQLite 数据库迁移到 PostgreSQL **不依赖自动迁移**:由 Root 在 `/control → 设置 → 数据库管理` 手动导入旧 `.db` 文件完成。
运行测试:`python3 tests/test_multi_user.py`(需本地 PostgreSQL,可用环境变量 `TEST_PG_HOST/TEST_PG_USER/TEST_PG_PASS/TEST_PG_DB` 覆盖)。
---
## 文件结构
```
├── start.py # 生产入口(Gunicorn WSGI: start:app,含调度器单例)
├── run.py # 开发入口(Flask dev server,端口 5001)
├── Dockerfile # 镜像构建(Gunicorn)
├── docker-compose.yml # PostgreSQL + Web 服务编排
├── requirements.txt
├── backend/
│ ├── app.py # Flask 应用工厂 + 安全配置
│ ├── auth.py # 双 Token 认证(TOTP/Passkey/恢复码/临时密码)
│ ├── pg_compat.py # sqlite3 兼容的 PostgreSQL 访问层(方言翻译)
│ ├── models.py # 数据模型、schema 版本迁移、token 管理
│ ├── db_admin.py # 数据库导入/导出(SQLite/JSON、原子恢复)
│ ├── routes.py # 全部 API 路由 + CSRF 防护 + SSE ticket
│ ├── scheduler.py # 后台调度器(advisory lock 单例 + watchdog)
│ └── *_mgr.py # 各功能模块管理器
└── frontend/
├── templates/ # HTML 模板
└── static/
├── css/ # 样式
├── js/ # JavaScript(纯原生,无框架)
│ ├── token.js # 双 Token 客户端(access 存储/刷新/401 重试/SSE ticket)
│ ├── control.js # 控制端逻辑(含数据库管理)
│ └── display.js # 显示端逻辑(账户时区渲染、本地到期兜底)
└── ringtones/ # 自定义铃声
```
---
## 注意
- **Passkey (WebAuthn) 需要 HTTPS**(localhost 除外)
- 旧版 4 位 PIN 已弃用,SQLite 导入时旧 PIN 不会被迁移
- 多 Gunicorn worker 下,显示端闹钟/番茄钟/倒计时提醒除 SSE 即时推送外,还有**本地到期检测兜底**,保证到点必定提醒
### License
MIT