功能: - StarLight Yggdrasil 第三方登录(后端中转,accessToken 不落地) - 管理员可视化权限管理(用户列表 + 一键授予/移出) - 设置页热配置(11 项可编辑,立即生效) - 循环池(每流独立手动歌单,队列空时随机不重复抽取) - 登录页统一主站浅色风格 - 全站改名「Starlight专属电台」 - 删除关于页 安全: - 全局 CSRF 防护(双提交 Cookie) - 登录限流防 XFF 伪造、错误信息脱敏 - 权限中间件 requireAdminForWrites(读放行/写 admin) - 密码哈希存储(scrypt/sha256) - SSRF 防护(封面下载域名白名单) - 空 catch 加日志、SSE 连接上限 修复: - 多角色账号 selectedProfile 缺失 - 普通用户越权删歌(V4) - streams 写操作越权(V9) - 设置页布局溢出 测试:38 项全绿(api16 / e2e17 / write5)
542 lines
26 KiB
Markdown
542 lines
26 KiB
Markdown
# 交接文档 · Starlight专属电台
|
||
|
||
> 跨会话交接。**新会话先读此文件再动手**。最后更新:2026-09-25(v2.6)。
|
||
|
||
---
|
||
|
||
## 0. TL;DR
|
||
|
||
- **项目**:`F:\music_online\demo` — Node.js + FFmpeg 的 HLS 个人音乐电台
|
||
- **状态**:v2.6,功能完整。回归测试 **api 16 / e2e 17 / write 5** 全绿
|
||
- **服务**:`svc003` 运行中,`http://127.0.0.1:3000`
|
||
- **密码**:管理员 `test123` / 用户 `user123`(start.cmd 里设的环境变量,测试用)
|
||
- **Git**:`a6a01b7`(v2.5) → `2fc2548`(v2.6 最新)。工作区干净。
|
||
|
||
---
|
||
|
||
## 1. 运行 / 环境
|
||
|
||
- Windows 11,Node v24.5.0,FFmpeg 9.0.1(`F:\ffmpge\ffmpeg-9.0.1-essentials_build\bin`,含 ffprobe),Python 3.11
|
||
- **启动**(推荐托管):
|
||
```cmd
|
||
cmd /c "set ADMIN_PASSWORD=test123&& set USER_PASSWORD=user123&& node index.js"
|
||
```
|
||
或双击 `start.cmd`(已配好双密码)
|
||
- 登录:浏览器开 `http://127.0.0.1:3000`
|
||
- 依赖:`better-sqlite3`、`multer`、`chokidar`、`express`(已装)
|
||
|
||
---
|
||
|
||
## 2. 目录结构
|
||
|
||
```
|
||
demo/
|
||
├── index.js # 入口,仅 require("./src/server").start()
|
||
├── start.cmd # 启动脚本(含 ADMIN_PASSWORD/USER_PASSWORD)
|
||
├── data.db # SQLite(WAL,自动生成)
|
||
├── HANDOFF.md / README.md
|
||
├── src/
|
||
│ ├── server.js # 装配:中间件/路由/启动/优雅退出
|
||
│ ├── config.js # 全部配置(环境变量解析)
|
||
│ ├── logger.js # 分级日志 + 落盘 + 内存环形缓冲(tail)
|
||
│ ├── db.js # SQLite init + schema(5表) + WAL + 运行时迁移
|
||
│ ├── store.js # 数据访问层,所有 SQL 收敛于此
|
||
│ ├── util.js # fileHash/文件名解析/原子写/目录大小
|
||
│ ├── media.js # ffprobe + FFmpeg 切片 + 并发队列 + 进程追踪
|
||
│ ├── metadata.js # 网易云 API + 封面下载 + 元数据补全
|
||
│ ├── livestream.js # LiveStream 类 + 多路流 + 缓存状态/清理
|
||
│ ├── scanner.js # audio/ 监听 + 同步 DB
|
||
│ ├── auth.js # 双密码鉴权 + 角色 + 会话 + 登录锁定
|
||
│ ├── sse.js # SSE 广播
|
||
│ └── routes/ # auth / music / resources / streams / system
|
||
├── public/
|
||
│ ├── index.html # 主面板(6 tab)
|
||
│ ├── login.html # 登录页(独立暗色主题)
|
||
│ ├── css/style.css # token 体系(v2.4 重构)
|
||
│ ├── js/app.js # 前端逻辑
|
||
│ ├── vendor/ # 本地化:bootstrap/icons/sortable/hls
|
||
│ └── covers/ # 本地封面
|
||
├── audio/ hls/ hls_cache/ # 音频 / 直播输出 / 切片缓存(均 gitignore)
|
||
├── scripts/fetch_vendor.py
|
||
└── test_api.py / test_e2e.py / test_write.py / test_enrich.py
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 数据模型(SQLite, src/db.js)
|
||
|
||
```sql
|
||
songs(id, file UNIQUE, name, artist, cover, cover_remote, duration,
|
||
file_size, heat, cache_status, created_at)
|
||
-- file 存「文件名」非绝对路径;cover 本地路径;cover_remote 远程 URL(优先展示)
|
||
streams(id PK, name, enabled, current_pos, media_seq, created_at)
|
||
-- 默认流 id='main',不可删
|
||
stream_playlist(id PK, stream_id, song_id, position) -- 外键 CASCADE
|
||
settings(key PK, value)
|
||
sessions(token PK, role, created_at, expires_at) -- role: admin|user
|
||
```
|
||
|
||
- 切片缓存 `hls_cache/{md5(文件名)}/` 与流无关,多路共享
|
||
- **运行时迁移**:init() 用 `PRAGMA table_info` 检测缺列后 ALTER(已支持 sessions.role、songs.cover_remote)
|
||
|
||
---
|
||
|
||
## 4. API 清单
|
||
|
||
除 `/api/auth/*`、`/hls`、`/hls_cache`、静态资源、`/login` 外**全部需登录**。
|
||
🔒 = 需**管理员**。
|
||
|
||
| 方法 | 路径 | 说明 |
|
||
|------|------|------|
|
||
| POST | `/api/auth/login` | `{password}` → `{ok,role}`;失败过多返回 429 |
|
||
| POST | `/api/auth/logout` · GET `/api/auth/me` | 登出 / 返回 `{authed,role}` |
|
||
| GET | `/api/resources` | 资源列表(含 albumImage/coverLocal/cacheSize) |
|
||
| GET | `/api/music/search?keyword=` | 网易云搜索(后端**缓存+限流**) |
|
||
| POST | `/api/music/detail` · `/download` | 详情 / 下载入库 |
|
||
| POST | `/api/music/import` 🔒 | **multipart**:audio+title+artist |
|
||
| POST | `/api/music/delete` · `/delete-batch` | 级联删 |
|
||
| POST | `/api/music/enrich` | `{file,skipMatch?}` 重匹配元数据 |
|
||
| POST | `/api/music/refresh-covers` 🔒 | **一键刷新所有缺失封面** |
|
||
| GET/POST/DELETE | `/api/streams[/:id]` · `/start\|stop` | 流管理 |
|
||
| GET/POST | `/api/streams/:id/playlist[/add\|remove\|reorder\|clear\|next\|prev\|jump]` | 播放列表 |
|
||
| GET | `/api/system/cache` 🔒 · POST `/cache/cleanup` 🔒 | 缓存 |
|
||
| GET | `/api/system/logs?tail=` 🔒 · `/config` 🔒 · `/settings` 🔒 | 系统 |
|
||
| GET | `/api/events` | SSE(cookie 鉴权) |
|
||
|
||
统一返回 `{ok:true,...}` / `{ok:false,error:"..."}`。
|
||
|
||
---
|
||
|
||
## 5. 直播流地址
|
||
|
||
- 默认 `/hls/main/index.m3u8`;其它 `/hls/{streamId}/index.m3u8`
|
||
- 切片 `/hls_cache/{hash}/indexN.ts`
|
||
|
||
---
|
||
|
||
## 6. 关键设计决策(勿轻易推翻)
|
||
|
||
1. **SQLite better-sqlite3 + WAL**:单进程同步 API 最省事,WAL 解读写互斥
|
||
2. **不迁 v1 JSON 数据**(旧 heat/metadata/playlist.json 已删)
|
||
3. **鉴权整站 Cookie 会话**;HLS 流**不锁**(播放器难带 cookie)
|
||
4. **封面/资源全本地化**:`public/covers/`、`public/vendor/`;系统字体
|
||
5. **songs.file 存文件名**非绝对路径,可移植
|
||
6. **导入先落 tmp 再 rename**,避免 chokidar 扫到半截
|
||
7. **双密码角色**:用户 = 管理员 − 上传 − 设置页;后端 `requireAdmin` 硬挡,非只藏前端
|
||
|
||
---
|
||
|
||
## 7. 测试方法
|
||
|
||
服务起来后:
|
||
```cmd
|
||
cd /d F:\music_online\demo
|
||
set PYTHONIOENCODING=utf-8
|
||
python -X utf8 test_api.py # 16 项
|
||
python -X utf8 test_e2e.py # 17 项
|
||
python -X utf8 test_write.py # 5 项(需 tmp/test_import.mp3 素材)
|
||
```
|
||
素材生成:
|
||
```cmd
|
||
"F:\ffmpge\ffmpeg-9.0.1-essentials_build\bin\ffmpeg.exe" -y -f lavfi -i anullsrc=r=44100:cl=stereo -t 3 -q:a 9 tmp\test_import.mp3
|
||
```
|
||
> ⚠️ 测试只覆盖 HTTP 层,**浏览器 JS 交互未自动化**。
|
||
|
||
---
|
||
|
||
## 8. 已知问题 / 待实测
|
||
|
||
- **浏览器端未实测**:SSE 实时刷新、拖拽 reorder、mini player、上传进度、频道弹窗交互
|
||
- `metadata.js enrichSong` 启动重匹配慢歌 → 可接受
|
||
- 多路流并发预生成排队 → 观察中
|
||
- `app.js esc()` 用 `String.fromCharCode(38)` 绕过工具转义,**勿优化**
|
||
- 登录失败锁定 `failMap` 是内存态,重启清零(可接受)
|
||
|
||
---
|
||
|
||
## 9. 工具层踩坑记录
|
||
|
||
1. **XML 实体转义吃 `&` `"`**:写 JS/CSS 用 `String.fromCharCode(38)` 或字符码构造
|
||
2. **⚠️ powershell 行号偏移**:`Select-String`/`Get-Content` 行号比 replace_lines 实际大 ~2,照它改必错位!**用 node 查行号**:
|
||
```js
|
||
fs.readFileSync(f,"utf8").split("\n").forEach((l,i)=>{if(l.includes(p))console.log((i+1)+": "+l)})
|
||
```
|
||
3. **replace_lines 多段连锁崩**:同一文件多段改,某段行号错会留残留。**逐条改**,改前 node 查号、改后 `node --check`
|
||
4. **文件被改乱**:别手修,`git checkout <commit> -- <file>` 恢复
|
||
5. **后台服务用 action background="true"**(勿 `start /b`)
|
||
6. **service_stop 连子进程一起杀**(含 node 的 ffmpeg 子进程)
|
||
7. **改完源码要重启 node** 才生效
|
||
8. **Windows 无 head/tail**,用 findstr 或 node
|
||
9. **cmd 下 curl**:`%{http_code}` 要写 `%%{http_code}`;中文输出 GBK 乱码(功能正常)
|
||
10. **git 提交中文**:powershell 写**无 BOM** UTF-8 文件 + `git -c i18n.commitEncoding=utf-8 commit -F 文件`
|
||
11. **python 输出**:`chcp 65001` + `set PYTHONIOENCODING=utf-8`;脚本末尾 `sys.stdout.flush()` 再 `os._exit(0)`
|
||
|
||
---
|
||
|
||
## 10. 版本时间线
|
||
|
||
| 版本 | 日期 | 要点 |
|
||
|------|------|------|
|
||
| v2 | 09-23 | SQLite+多路流+鉴权+导入/删除+资源本地化+模块化重构 |
|
||
| v2.1 | 09-23 | 频道管理模态框、死代码清理、forgetSong 内存泄漏修复 |
|
||
| v2.2 | 09-24 | 站内 HLS 收听页、设置页、上传进度、正在播放实时化 |
|
||
| v2.2.1 | 09-24 | 资源排序(6 种) |
|
||
| v2.3 | 09-24 | UI 去 AI 味:清全部渐变/glow,单强调色(依 ibelick/ui-skills) |
|
||
| v2.4 | 09-24 | UI token 化:字号 42→9、圆角 20→8、硬编码色清零 |
|
||
| v2.5 | 09-25 | **双密码+角色权限**、登录锁定、搜索缓存限流、**封面远程优先** |
|
||
| v2.6 | 09-25 | 设置页**一键刷新缺失封面** |
|
||
|
||
各版本详细变更见 git log(`git log --oneline`)。
|
||
|
||
---
|
||
|
||
## 11. 下一步可选
|
||
|
||
1. **浏览器手动过一遍**所有交互,修 Console 报错 ← 最优先
|
||
2. 播放延迟调节(HLS_TIME 热更新,老板曾暂缓)
|
||
3. 密码设置 UI(现走环境变量)
|
||
4. 缓存生成进度条、搜索分页/加载更多
|
||
5. mini player 音量控制、播放历史/最热排序(heat 字段已有)
|
||
6. 正式化:固定强密码、服务化启动
|
||
|
||
---
|
||
|
||
## 12. 当前运行态
|
||
|
||
- 后台服务:`svc003`(`http://127.0.0.1:3000`)
|
||
- DB:`data.db`(6 首歌曲,封面已本地化)
|
||
- 缓存:`hls_cache/` 约 34MB
|
||
- Git:`2fc2548` 干净
|
||
---
|
||
|
||
## 13. StarLight 第三方登录接入(v2.7)
|
||
|
||
**背景**:管理员要求用 StarLight(`skin.starlight.cool/yggdrasil`)做第三方登录,替换原密码登录(保留应急后门)。
|
||
|
||
**方案**:后端中转(浏览器→本服务→Yggdrasil),accessToken 不落地、不下发。
|
||
|
||
**改动文件**:
|
||
- `src/starlight.js`(新)— Yggdrasil 客户端,仅 `authenticate()`;CLIENT_TOKEN 启动随机
|
||
- `src/config.js` — 新增 `STARLIGHT_API`、`STARLIGHT_ADMINS`(逗号分隔,小写匹配)
|
||
- `src/db.js` — sessions 表迁移加 `starlight_name` 列
|
||
- `src/store.js` — `createSession(token, role, expiresAt, starlightName)`
|
||
- `src/auth.js` — `loginViaStarLight()`、`getStarlightName()`;exports 更新
|
||
- `src/routes/auth.js` — `/login` 双模式:有 username→StarLight;无→应急密码
|
||
- `public/login.html` — 用户名+密码表单 + "管理员应急登录"折叠区
|
||
- `public/index.html` — 顶栏 `#userName` 显示"你好,xxx"
|
||
- `public/js/app.js` — `applyRoleUI(role, starlightName)`
|
||
- `start.cmd` — 加 STARLIGHT_API / STARLIGHT_ADMINS 配置项
|
||
|
||
**登录逻辑**:
|
||
- 前端 `{username, password}` → 后端调 authenticate → 成功建本地会话
|
||
- 角色:`STARLIGHT_ADMINS` 命中=admin,否则 user
|
||
- **应急后门**:username 留空 + `{password}` → 走 ADMIN_PASSWORD(防皮肤站挂掉锁死)
|
||
|
||
**测试**:api 16 / e2e 17 / write 5 全绿。真实打到 StarLight(错误凭证→401)。
|
||
|
||
**注意**:`test_write.py` 第 30 行写死旧路径 `F:\music_online\demo\tmp\`,复制项目时未改(跑测试需先把素材放到该路径)。
|
||
---
|
||
|
||
## 14. 安全加固(v2.7.1,2026-10-05)
|
||
|
||
对上一条 StarLight 登录做了自审+修复,详见 `SECURITY_REVIEW.md`。
|
||
|
||
**修复清单**:
|
||
- 🔴 XFF 伪造绕过限流 → `trust proxy=false` + `getClientIp()` 取 socket IP
|
||
- 🔴 白名单按用户名判定可抢注提权 → 改绑 **UUID**
|
||
- 🟠 错误信息泄露 StarLight 内部细节 → 脱敏
|
||
- 🟠 用户名字符集未校验 → 正则过滤 `[A-Za-z0-9_]`
|
||
- 🟠 日志记 PII(邮箱)→ 移除
|
||
- 🟠 无 body 大小限制 → `express.json({limit:"64kb"})`
|
||
- 🟡 无滑动续期 → `getValidSession` 加续期 + `store.touchSession()`
|
||
|
||
**验证**:38 项回归全绿 + XFF 伪造测试(6 个不同 XFF 仍触发 429)+ 真实账号登录正常。
|
||
|
||
**待办**:
|
||
- `STARLIGHT_ADMINS` 填 **UUID**(不是用户名!)
|
||
- 生产环境用强密码 + HTTPS(Cookie 加 Secure)
|
||
- 登录锁定无解锁途径(重启服务可清 failMap)
|
||
---
|
||
|
||
## 15. 全局 CSRF 防护(v2.7.2,2026-10-05)
|
||
|
||
**背景**:管理员要求加全局 CSRF(选项 C)。
|
||
|
||
**方案**:双提交 Cookie(Double Submit Cookie)
|
||
- 登录时发 `mr_csrf` cookie(**非 HttpOnly**,JS 可读)+ session cookie
|
||
- 前端所有写请求带 `X-CSRF-Token` 头(值 = cookie)
|
||
- 后端 `csrfProtection` 中间件校验(GET/HEAD/OPTIONS 豁免)
|
||
|
||
**改动文件**:
|
||
- `src/auth.js` — CSRF_COOKIE / genCsrfToken / setCsrfCookie / clearCsrfCookie / csrfProtection;setSessionCookie 改用 `res.append`(与 csrf cookie 共存)
|
||
- `src/routes/auth.js` — 登录成功发 csrf cookie;登出清 csrf
|
||
- `src/server.js` — 受保护 API 挂 `auth.csrfProtection`
|
||
- `public/js/app.js` — `api()` 统一给写请求加 `X-CSRF-Token`
|
||
- `test_api.py` / `test_e2e.py` / `test_write.py` — call() 加 CSRF 头(从 cookie jar 读);test_write 的 multipart import 也加
|
||
|
||
**验证**:38 项全绿(api16/e2e17/write5)+ CSRF 专项(无头403 / 带头200 / GET豁免)。
|
||
|
||
**⚠️ 代价**:所有 API 客户端(脚本、第三方集成)写操作必须带 `X-CSRF-Token` 头,否则 403。
|
||
|
||
**测试脚本适配**:三个 test_*.py 已改,写请求自动读 cookie jar 的 mr_csrf。
|
||
---
|
||
|
||
## 16. 渗透测试修复(v2.7.3,2026-10-05)
|
||
|
||
依据 `SECURITY_PENTEST.md`(黑盒渗透报告)修复 3 个漏洞:
|
||
|
||
- **V1 硬编码密码** → start.cmd 改存 **sha256 哈希**;auth.js 加 `passwordMatches()` 支持明文/hash 两种。⚠️ git 历史里的明文未清(老板决定不动云端),**test123 需轮换**。
|
||
- **V2 堆栈泄露** → server.js 加全局错误处理中间件,客户端只收通用文案。
|
||
- **V3 限流 DoS** → ①类型校验前置不计失败 ②新增 `TRUSTED_IPS` 白名单豁免 ③`isTrustedIp` 归一化 IPv4-mapped IPv6。
|
||
|
||
**新配置**:`TRUSTED_IPS`(默认 127.0.0.1,::1)。
|
||
**测试**:38 项全绿 + V1/V2/V3 专项验证。
|
||
**遗留**:轮换 test123、生产 TRUSTED_IPS 收紧、HTTPS。
|
||
---
|
||
|
||
## 17. 第三/四轮渗透修复(v2.7.4,2026-10-05)
|
||
|
||
依据 `SECURITY_PENTEST_R2.md`(复测)+ `SECURITY_PENTEST_R3_WHITEBOX.md`(白盒):
|
||
|
||
- **V4【高危·水平越权】**:普通用户可删歌/批量删/改元数据 → `routes/music.js` 给 delete/delete-batch/enrich 补 `requireAdmin`。**已动态验证 403。**
|
||
- **V5【低】**:settings 无 schema → 字段白名单。
|
||
- **V6【低】**:/hls_cache 未鉴权 → **评估后不改**(切片路径被 m3u8 引用,加锁断播放;低危接受)。
|
||
- **V8【低】**:updateSong 动态列名 → 字段白名单。
|
||
- **V1**:passwordMatches 新增 **scrypt** 支持(`scrypt:<salt>:<hash>`),兼容 sha256/明文。start.cmd 仍是 sha256,可升级。
|
||
|
||
**测试脚本**:改从 `TEST_PASSWORD` 环境变量读密码(默认 `123test`)。
|
||
**回归**:38 项全绿。
|
||
**遗留**:V1 密码强度(弱密码救不了)、git 历史明文。
|
||
---
|
||
|
||
## 18. 第五轮渗透修复(v2.7.5,2026-10-05)
|
||
|
||
依据更新的 `SECURITY_PENTEST.md`:
|
||
|
||
- **V9【高危】/api/streams 写操作缺管理员** → **根治**:新增 `auth.requireAdminForWrites`(GET/HEAD/OPTIONS 放行,写要 admin),挂到 server.js 的 /api/music + /api/streams。
|
||
- **V10/V11** → 被同一中间件覆盖。
|
||
- **V12** SSE 无上限 → sse.js 加总 500 + 每 IP 10 上限,超限 503。
|
||
- **V13** 搜索限流被缓存绕过 → 限流检查前置。
|
||
- **V14** 日志 CRLF 注入 → logger.js 过滤控制字符。
|
||
- **V1** → start.cmd 升级为 **scrypt** 加盐慢哈希。
|
||
- **V5** 上轮已修;**V6** 评估后不改(切片被 m3u8 引用)。
|
||
|
||
**测试**:38 项全绿。
|
||
**遗留**:弱密码 `123test`(建议换强随机)。
|
||
---
|
||
|
||
## 19. 权限中间件细化(v2.7.6,2026-10-05)
|
||
|
||
**背景**:`requireAdminForWrites` 的"非 GET 即需 admin"太粗暴,误伤 `/api/music/detail`(POST 但是普通用户查详情用的)。另:要求**搜索仅管理员**,普通用户只能用 id/链接解析。
|
||
|
||
**改动**:
|
||
1. `auth.js` `requireAdminForWrites` 加 **POST 读白名单** `POST_READ_WHITELIST = {"/music/detail"}`——这些 POST 语义是读,放行给普通用户。
|
||
2. `routes/music.js` `/search` 单独加 `requireAdmin`(GET 也要 admin)。
|
||
3. `public/js/app.js` `searchMusic()`:普通用户输入非 id → 提示"请用 ID 或链接",不调 search。
|
||
|
||
**验证**:
|
||
- 普通用户:detail 200 ✅ / search 403 ✅
|
||
- 管理员:search 200 ✅
|
||
- 38 项回归全绿。
|
||
|
||
**权限模型现状**:
|
||
- 读(GET):普通用户可用(除非单独加 admin,如 search)
|
||
- 写(POST/DELETE/...):默认 admin,白名单里的"读语义 POST"(detail)例外
|
||
---
|
||
|
||
## 20. 前端误退档恢复(v2.7.6,2026-10-06)
|
||
|
||
**事件**:前端 `public/` 被误退档回 v2.6 旧版(丢了 StarLight 登录、CSRF、userName 显示、搜索适配)。
|
||
|
||
**恢复源**:`F:\music_online\demo_starlight_gitbackup_20261005_2206`(本喵清 git 历史前的完整快照,10-05 21:33~21:55)。
|
||
|
||
**恢复动作**:
|
||
1. 从备份拷回 `public/login.html`、`index.html`、`js/app.js`、`css/style.css`。
|
||
2. 备份缺的"搜索仅管理员"适配**重做**:`app.js` `searchMusic()` 加 `state.role === "admin"` 判断,普通用户提示用 id/链接。
|
||
3. `test_*.py` 从 `demo_starlight_backup_20261005_2130` 恢复后,**重做 CSRF 头 + TEST_PASSWORD 环境变量**适配。
|
||
|
||
**验证**:38 项全绿(api16/e2e17/write5)。
|
||
|
||
**教训**:**改动后及时 commit**(哪怕本地),别让工作区长期处于"未提交"状态——一旦误操作(退档/覆盖),未提交的改动最难找回。本次全靠"清 git 历史前的意外备份"救回。
|
||
---
|
||
|
||
## 21. 多角色账号登录修复(v2.7.7,2026-10-06)
|
||
|
||
**问题**:账号加了第 2 个角色(`Xiao_Tps`)后,StarLight **不再返回 `selectedProfile`**(改为返回 `availableProfiles` 数组),导致 `starlight.js` 误判"该账号尚未创建游戏角色",登录 401。
|
||
|
||
**修复**:`starlight.js` 角色选择逻辑改为——
|
||
1. 有 `selectedProfile` → 用它
|
||
2. 无但有 `availableProfiles` → **取第一个**(老板选方案 A,自动选第一个)
|
||
3. 都无 → 才报"无角色"
|
||
|
||
**验证**:多角色账号(1184070342@qq.com)登录 → 200,自动选 `Xiao_love`(role=admin)。
|
||
|
||
**踩坑**:修复后首次测试仍 401——**旧服务进程占着 3000 端口没清**(EADDRINUSE),新服务没起来,跑的还是旧代码。`taskkill /F /T /PID 106328` 后正常。
|
||
---
|
||
|
||
## 22. 设置页扩充 + 删关于页(v2.8.0,2026-10-06)
|
||
|
||
### 删关于页
|
||
- `index.html`:删导航 tab + `pageAbout` 页面(139 行);`app.js` `pageOrder` 去掉 pageAbout。
|
||
|
||
### 设置页热配置(真·管理员控制)
|
||
**新增 11 项可热改配置**(存 settings 表,**立即生效,无需重启**):
|
||
| 配置项 | key | 说明 |
|
||
|--------|-----|------|
|
||
| 切片时长 | hlsTime | 影响新流 |
|
||
| 滑动窗口 | slideWindow | 影响新流 |
|
||
| 缓存上限 MB | maxCacheMB | |
|
||
| 默认频道 | defaultStream | 存值(待接入默认流逻辑) |
|
||
| 日志级别 | logLevel | 热改 levelNum |
|
||
| 会话有效期 | sessionTtlHours | |
|
||
| 登录失败上限 | loginMaxFails | |
|
||
| 登录锁定时长 | loginLockMin | |
|
||
| 搜索缓存时长 | searchCacheTtlSec | |
|
||
| 搜索限流上限 | searchRateMax | |
|
||
| 搜索限流窗口 | searchRateWindowSec | |
|
||
|
||
**实现**:
|
||
- `config.js` 新增 `applyOverrides(settings)`——从 settings 覆盖 config。
|
||
- `logger.js` 新增 `setLevel(name)`(`levelNum` 从 const 改 let)。
|
||
- `routes/system.js` `POST /settings` 扩白名单 + 保存后调 `applyOverrides` + `setLevel`;`GET /config` 返回全部可配字段。
|
||
- `server.js` `bootstrap` 启动时加载 settings 覆盖。
|
||
- `public/index.html` 设置页"运行配置"只读 grid → **可编辑表单**(11 项)。
|
||
- `public/js/app.js` `loadSettings` 填充表单 + `btnSaveSettings` 保存。
|
||
- `public/css/style.css` 加 `.settings-form`/`.setting-row` 样式。
|
||
|
||
**验证**:POST 改 hlsTime/logLevel/searchRateMax → GET /config 立即反映新值。38 项回归全绿。
|
||
|
||
**注意**:HTTP 端口、FFmpeg 并发等**需重启的项不做**(老板要求只做热生效)。
|
||
---
|
||
|
||
## 23. 登录页风格统一(v2.8.1,2026-10-06)
|
||
|
||
**背景**:登录页原本是旧暗色主题(`#0d1117`),与主页面浅色主题(`#f4f5f9`)不一致。
|
||
|
||
**改动**:`public/login.html` 完全重写样式,套用主页面设计 token:
|
||
- 浅色背景 `--bg #f4f5f9` + 白卡片 `--surface` + 阴影 `--shadow-lg`
|
||
- 强调色统一 `--accent #4f6ef7`
|
||
- 字体 `Plus Jakarta Sans`
|
||
- 大圆角 `--radius-xl (28px)` + 卡片升起动画
|
||
- 输入框聚焦光晕 `--accent-glow`
|
||
- 主按钮 `.btn-primary`(蓝底白字)+ 应急按钮 `.btn-secondary`(描边)
|
||
- 引入 `bootstrap-icons.css`(登录/图标)
|
||
|
||
**功能不变**:StarLight 用户名+密码登录、管理员应急登录、`/api/auth/login` 调用。
|
||
|
||
**验证**:/login.html 200、38 项回归全绿。
|
||
---
|
||
|
||
## 24. 可视化权限管理(v2.8.2,2026-10-06)
|
||
|
||
**背景**:原管理员靠 `start.cmd` 的 `STARLIGHT_ADMINS` 环境变量(改要重启),老板问"总不能还去改文件"。
|
||
|
||
**方案**:设置页加"权限管理"——**用户列表 + 一键授予/取消管理员**,热生效。
|
||
|
||
**改动**:
|
||
- **`db.js`**:新增 `admins` 表(uuid PK, name, added_at);sessions 加 `starlight_uuid` 列(迁移)。
|
||
- **`store.js`**:`listAdmins/getAdmin/isAdminUuid/addAdmin/removeAdmin/listUsers`。
|
||
- **`auth.js`**:`loginViaStarLight` 判角色**先查 admins 表再查环境变量**;`createSession` 存 uuid;新增 `getStarlightUuid`。
|
||
- **`routes/auth.js`**:登录传 uuid 给 session;`/me` 返回 `myUuid`。
|
||
- **`routes/system.js`**:`GET /users`(登录过的用户+isAdmin)、`GET/POST/DELETE /admins`。
|
||
- **`public/index.html`**:设置页加"权限管理"卡片(用户列表)。
|
||
- **`public/js/app.js`**:`loadUsers()` 渲染用户列表 + 设为/取消管理员按钮。
|
||
- **`public/css/style.css`**:`.user-list/.user-item/.admin-badge/.btn-mini` 样式。
|
||
|
||
**判定优先级**:`admins` 表 ∪ 环境变量 `STARLIGHT_ADMINS`(UUID)。
|
||
|
||
**验证**:真实账号登录→`/me` 返回 myUuid;用户列表显示 isAdmin;加/删管理员正常。38 项回归全绿。
|
||
|
||
**注意**:旧 sessions 记录无 uuid(迁移前登录的),需**重新登录一次**补上。
|
||
---
|
||
|
||
## 25. 设置页布局优化(v2.8.3,2026-10-06)
|
||
|
||
**问题**:设置页"很挤,每个卡片都变成独立滚动"。
|
||
|
||
**根因**:`.section-body` 有 `flex:1; min-height:0; overflow-y:auto` → **每个卡片内容区各自滚动**,一堆小滚动条。
|
||
|
||
**修复**:
|
||
- `.section-body` 去掉 `flex:1 / min-height:0 / overflow-y:auto`(内容自然撑开)。
|
||
- `.page.active` 加 `overflow-y:auto`(**整页统一滚动**,一个滚动条)。
|
||
- 设置页布局优化:`#pageSettings` gap 加大、`.section-body` padding 加大、`.setting-row` 行距加大、日志区限高自身滚动、用户列表间距加大。
|
||
|
||
**验证**:CSS 括号平衡、38 项回归全绿。各页无独立滚动规则,统一滚动安全。
|
||
---
|
||
|
||
## 26. 设置页布局优化 v2(v2.8.4,2026-10-06)
|
||
|
||
**前情**:v2.8.3 第一次改崩(去掉 `.section-body` 的 flex+overflow 导致内容溢出),已回滚。
|
||
|
||
**本次正确解法**(理解布局链后):
|
||
- `.page.active`:+`overflow-y: auto` → **整页统一滚动**
|
||
- `.section-card`:+`flex-shrink: 0` → **卡片不被压缩**(关键!首次失败就漏了这个)
|
||
- `.section-body`:去掉 `flex:1 / min-height:0 / overflow-y:auto` → 内容自然撑开
|
||
|
||
**布局链**(勿乱动):
|
||
```
|
||
.page-container (flex:1; min-height:0)
|
||
└── .page.active (flex:1; min-height:0; overflow-y:auto) ← 整页滚
|
||
└── .section-card (flex-shrink:0) ← 不压缩
|
||
├── .section-header (flex-shrink:0)
|
||
└── .section-body (自然高度)
|
||
```
|
||
|
||
**验证**:CSS 括号平衡 395/395、38 项回归全绿。
|
||
|
||
**教训**:CSS 布局改动前先理解 flex/height/overflow 依赖链;用 python 精确替换 + `assert count==1` 防误伤;改前备份。
|
||
---
|
||
|
||
## 27. 全站改名:Starlight专属电台(v2.8.5,2026-10-06)
|
||
|
||
**改动**:"Xiao の 电台" / "音乐在线电台" → **"Starlight专属电台"**,全项目统一。
|
||
|
||
| 文件 | 位置 |
|
||
|------|------|
|
||
| `index.html` | title + 顶栏品牌 |
|
||
| `login.html` | title + h1 |
|
||
| `app.js` | 文件注释 |
|
||
| `package.json` | description |
|
||
| `README.md` | 标题 |
|
||
| `HANDOFF.md` | 标题 |
|
||
|
||
**验证**:全项目零旧名残留。
|
||
---
|
||
|
||
## 28. 循环池(手动歌单 + 随机不重复播放)(v2.9.0,2026-10-06)
|
||
|
||
**需求**:资源列表"重新匹配作者"按钮 → 换成"加入/移出循环池";队列播完空时从循环池随机抽(抽过标记,全播完重置);池空降级从全部资源随机。
|
||
|
||
**改动**:
|
||
- **`db.js`**:新增 `stream_pool` 表(stream_id, song_id, played, added_at, PK(stream_id,song_id))。
|
||
- **`store.js`**:`getPoolRows/listPoolSongIds/isInPool/addToPool/removeFromPool/drawFromPool/markPoolPlayed`。`drawFromPool` 抽未播的,全播过则重置。
|
||
- **`livestream.js`**:`ensureNotEmpty` 队列空→`drawFromPool`(抽到标记 played)→池空降级 `getAllSongs` 随机。
|
||
- **`routes/streams.js`**:`GET /:id/pool`、`POST /:id/pool/add|remove`。
|
||
- **`public/js/app.js`**:资源列表按钮 `js-enrich`→`js-pool`(切换加/移,active 高亮);`loadResources` 查池标记 `inPool`;`togglePool()`。
|
||
- **`public/css/style.css`**:`.js-pool.active` 样式。
|
||
|
||
**每流独立**池。**enrich 接口保留**(仅删资源列表入口)。
|
||
|
||
**验证**:池增删查正常;抽歌不重复+重置;38 项回归全绿。
|
||
---
|
||
|
||
## 29. 白盒安全审查 + 修复(v2.9.1,2026-10-06)
|
||
|
||
**审查范围**:全部 `src/`(2963 行)+ 前端。发现并修复:
|
||
|
||
### 🔴 P1 SSRF(metadata.js)✅
|
||
- `request()` / `downloadCover()` URL 来自网易云响应(HTTP 明文可劫持),**无域名校验** → 可请求内网/`file://`。
|
||
- **修**:加 `isAllowedHost()` 白名单(`*.music.163.com` / `*.music.126.net`),`request` 和 `downloadCover` 都校验。实测 `169.254.169.254` 被拒。
|
||
|
||
### 🟡 P3 SSE 计数泄漏(sse.js)✅
|
||
- `broadcast` 删失败 client 时未减 `ipCount` → 计数泄漏。
|
||
- **修**:client 打 `__sseIp` 标记,删除时同步减计数。
|
||
|
||
### 🟡 P4 空 catch 加日志 ✅
|
||
- `logger.js`(日志流)、`metadata.js`(时长/文件大小探测)、`media.js`(计数)、`routes/music.js`(删临时文件)——加 warn 日志。
|
||
- `util.js`(getDirSize 读不到文件)——保持静默 + 注释说明。
|
||
|
||
### ✅ 已确认无问题
|
||
权限挂载、SQL 参数化、输入校验(isValidFileName)、无命令注入(spawn 数组)、advance 同步无重入、TaskQueue 正确、FFmpeg 超时 kill、SSE 连接上限。
|
||
|
||
**验证**:38 项回归全绿 + SSRF 白名单实测。 |