Files
iot-dboard/README.md
T
2026-08-11 19:57:21 +08:00

289 lines
10 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`) — 首个自动创建的账户,拥有全部功能与用户管理权限
---
## 功能
- **多用户**:Root 管理员可注册/编辑/删除用户;所有用户数据完全隔离,普通用户无管理权限
- **两种注册方式**:管理员代注册(正式账户,可代配 TOTP/Passkey);用户自助申请(管理员确认后生成临时账户)
- **临时账户**:服务器持有临时 TOTP 密钥,管理员仅可见实时验证码;用户登录后被强制引导配置自己的登录方式,配置完成即正式生效
- **日历**:支持订阅 iCalendar (.ics) 链接及本地文件上传,自动同步日程
- **天气**:基于和风天气 (QWeather) API,含逐小时/分钟级预报
- **闹钟**:多组闹钟设定,自动弹出提醒,支持自定义铃声
- **番茄钟**:定时提醒,自动继续,断网重连自动恢复
- **倒计时**:可自定义时长,到时提醒
- **自定义展示页**:支持多组自定义 HTML 模板,一键切换
---
## 🔐 安全特性
支持**双因素认证 (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(注册仪式在当前浏览器完成)
- 用户自助申请:用户在 `/register-request` 填写信息 → 管理员确认 → 生成临时账户(服务器持有临时 TOTP 密钥,管理员仅看到实时验证码)
- 注册/编辑备注/删除用户均需**二次验证管理员身份**(Passkey 优先,无 Passkey 时用 TOTP)
- 删除用户会级联删除其全部数据并使其会话立即失效
- 用户 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
---
## 快速开始
### 开发环境
```bash
# 1. 安装依赖
pip install -r requirements.txt
# 2. 直接运行(调试模式)
python start.py
# 3. 访问
# 显示端: http://localhost:5000
# 控制端: http://localhost:5000/control
```
### 生产部署 (推荐 Nginx + Certbot)
```
客户端 (HTTPS)
│
▼ 443
Nginx (SSL termination + Certbot)
│ proxy_pass http://127.0.0.1:5000
│
▼ 5000 (HTTP)
Flask (127.0.0.1:5000)
│
▼
data.db (SQLite)
```
#### 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";
}
# 可选:限制上传大小
client_max_body_size 5M;
}
# HTTP → HTTPS 强制跳转
server {
listen 80;
server_name your-domain.com;
return 301 https://$host$request_uri;
}
```
#### 2. 启动 Flask
```bash
# 生产模式启动
FLASK_HTTPS=yes python start.py
# 可选:固定 SECRET_KEY(保持跨重启会话有效)
export FLASK_SECRET_KEY="$(openssl rand -hex 32)"
```
#### 3. Certbot 申请证书
```bash
sudo certbot --nginx -d your-domain.com
```
---
---
## Docker 部署
### 快速启动
```bash
# 构建并启动(后台运行,首次会自动从 Gitee clone 代码)
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:
- FLASK_BIND=0.0.0.0
- FLASK_SECRET_KEY=your-secret-here # 可选:固定密钥
- FLASK_HTTPS=yes # 如果前面有 HTTPS 反代
```
### 持久化数据
- **数据库**:`./data/` 目录挂载到 `/app/data/`,数据库文件 `data.db` 自动在此创建
- **项目代码**:命名卷 `app_code` 挂载到 `/app`,首次自动初始化为镜像内克隆的代码,之后 `git pull` 的修改永久保留数据库文件 `data.db` 会自动在该目录下创建,重启/升级后数据保留。
```bash
# 数据文件位置
ls -la data/
# data.db ← 数据库
```
### 搭配 Nginx 反代(生产)
使用宿主机上的 Nginx(或额外 Nginx 容器)做 SSL 终结。配置与前文 Nginx 部分相同,只需将 `proxy_pass` 指向 Docker 宿主机 IP:5000:
```nginx
location / {
proxy_pass http://宿主机IP:5000;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
}
```
> ⚠️ 注意:容器内 Flask 绑定 `0.0.0.0:5000`,宿主机可直接访问 `localhost:5000`。如需限制仅 Nginx 访问,在 `docker-compose.yml` 中将 `ports` 改为 `127.0.0.1:5000:5000`。
---
## 环境变量
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `FLASK_HTTPS` | 空 | 设为 `yes`/`true` 启用 refresh token cookie 的 Secure 属性(Nginx 反代时启用) |
| `FLASK_SECRET_KEY` | 自动随机生成 | 保留兼容;认证已由 access/refresh 双 token 承载,不再依赖 Flask session |
| `FLASK_BIND` | `127.0.0.1` | 监听地址,Docker 部署需设为 `0.0.0.0` |
| `FLASK_DB_PATH` | `./data.db` | 数据库文件路径,Docker 挂载数据卷时需同步修改 |
| `FLASK_DEBUG` | 空 | 设为 `true` 开启 Flask 调试模式(**生产环境禁用**) |
---
## 文件结构
```
├── run.py # 入口(端口 5001,仅开发)
├── start.py # 入口(端口 5000,推荐生产使用)
├── requirements.txt
├── backend/
│ ├── app.py # Flask 应用工厂 + 安全配置
│ ├── auth.py # 双 Token 认证(TOTP/Passkey/恢复码/临时密码)
│ ├── webauthn_mgr.py # WebAuthn (Passkey) 注册与认证
│ ├── jwt_util.py # Ed25519 JWT 签名(和风天气)
│ ├── models.py # SQLite 模型、schema 版本迁移、token 管理
│ ├── routes.py # 全部 API 路由 + CSRF 防护 + SSE ticket
│ └── *_mgr.py # 各功能模块管理器
└── frontend/
├── templates/ # HTML 模板
└── static/
├── css/ # 样式
├── js/ # JavaScript(纯原生,无框架)
│ ├── token.js # 双 Token 客户端(access 存储/刷新/401 重试/SSE ticket)
│ ├── control.js # 控制端逻辑
│ └── display.js # 显示端逻辑(账户时区渲染)
└── ringtones/ # 自定义铃声
```
---
### 数据库迁移(版本化,升级无需重写数据)
数据库内置 `meta` 表记录 **schema 版本**(程序内 `DB_VERSION`,当前为 1)。每次启动时比对:
| 情况 | 行为 |
|------|------|
| 数据库版本 == 程序版本 | 直接使用,跳过重建 |
| 无版本字段(旧版数据库)或 数据库版本 < 程序版本 | 重建数据库并迁移(旧版单用户数据自动归属 Root (id=0),新增字段一律用 DEFAULT 填充,`camera_config` 等已删除的表自动丢弃) |
| 数据库版本 > 程序版本 | **报错并终止启动**(数据库比程序新,需先升级程序) |
旧版 4 位 PIN 不会被迁移。运行测试:`python3 tests/test_multi_user.py`。
## 注意
- **Passkey (WebAuthn) 需要 HTTPS**(localhost 除外)
- 旧版 4 位 PIN 已弃用,数据库迁移时旧 PIN 不会被迁移
### License
MIT