Dashboard - 多人副屏系统
项目简介
多用户副屏系统,支持日历、天气、闹钟、番茄钟、倒计时等功能。每个用户拥有完全隔离的数据与独立的显示端/控制端:
- 显示端 (
/) — 大屏/触屏设备展示信息 - 控制端 (
/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 部署时无需单独安装,镜像内置)
开发环境
# 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 配置
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
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 申请证书
sudo certbot --nginx -d your-domain.com
Docker 部署
快速启动
# 构建并启动(自动创建 PostgreSQL 与 Web 服务)
docker compose up -d
# 查看启动日志(获取临时密码)
docker compose logs app
# 停止
docker compose down
访问 http://你的IP:5000,日志中会打印临时密码。
更新代码
# 方法一:进入容器内 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 段:
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 内存 配置:
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,当前为 3)。每次启动时比对:
| 情况 | 行为 |
|---|---|
| 数据库版本 == 程序版本 | 直接使用,跳过升级 |
| 无版本字段或 数据库版本 < 程序版本 | 创建缺失的表/列,缺失字段一律用 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