From f07fbbc21f5dbbbbe9e70d1e8b6c333139a8c2ac Mon Sep 17 00:00:00 2001 From: Xiao-no-love <140927721+Xiao-no-love@users.noreply.github.com> Date: Fri, 25 Sep 2026 22:24:48 +0800 Subject: [PATCH] =?UTF-8?q?v2.5=20=E5=AE=89=E5=85=A8=E5=8A=A0=E5=9B=BA=20+?= =?UTF-8?q?=20=E6=9D=83=E9=99=90=E4=BD=93=E7=B3=BB=20+=20=E5=B0=81?= =?UTF-8?q?=E9=9D=A2=E7=BD=91=E7=BB=9C=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 双密码(管理员/用户) + requireAdmin 角色隔离 - 登录失败按IP锁定 + 搜索缓存限流 - 封面远程URL优先+本地回退 - sessions.role / songs.cover_remote 运行时迁移 --- HANDOFF.md | 42 ++++++++++++++++++ public/js/app.js | 24 +++++++++- src/auth.js | 97 ++++++++++++++++++++++++++++++++--------- src/config.js | 8 +++- src/db.js | 16 +++++++ src/metadata.js | 2 + src/routes/auth.js | 23 +++++++--- src/routes/music.js | 39 ++++++++++++++++- src/routes/resources.js | 3 +- src/routes/streams.js | 3 +- src/server.js | 2 +- src/store.js | 14 +++--- start.cmd | 3 +- 13 files changed, 236 insertions(+), 40 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index b8b34a3..e926d1b 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -415,3 +415,45 @@ python test_enrich.py # 给 5 首补封面(已跑过,5/5 成功) - `login.html` 独立暗色主题(自己的 `:root`)。 - `body` 字体栈、`--log-bg` 终端风格:保留。 + +--- + +## 18. v2.5 安全加固 + 权限体系 + 封面网络化(已完成 2026-09-25) + +> 老板提出 6 点问题,本轮解决其中 5 点(延迟调节因复杂度暂缓)。 + +### 18.1 双密码 + 角色权限(核心) +- **config.js**:`ADMIN_PASSWORD`(兼容旧 `PANEL_PASSWORD`)/ `USER_PASSWORD`;新增 `LOGIN_MAX_FAILS`/`LOGIN_LOCK_MS`/`SEARCH_CACHE_TTL_MS`/`SEARCH_RATE_MAX`/`SEARCH_RATE_WINDOW_MS`。 +- **db.js**:`sessions` 表加 `role` 列;`songs` 表加 `cover_remote` 列。均带**运行时迁移**(PRAGMA table_info 检测后 ALTER)。 +- **auth.js**:重写。`verifyLogin` 返回 `"admin"|"user"|null`;`createSession(role)`;新增 `requireAdmin` 中间件、`getRole/isAdmin`;**按 IP 登录失败锁定**(默认 5 次锁 10 分钟,内存 Map)。 +- **routes/auth.js**:登录返回 `{ok, role}`;锁定返回 429;`/me` 返回 role。 +- **权限矩阵**:用户 = 管理员 − 上传 − 设置页。`/api/music/import` 加 `requireAdmin`;`/api/system/*` 整块 `requireAdmin`。其余接口(资源/流/播放列表/搜索/下载)两者一致。 +- **前端**:`init()` 先查 `/api/auth/me` 拿 role → `applyRoleUI()` 隐藏设置页 tab + 本地导入卡片;`loadSettings()` 非管理员直接 return。 + +### 18.2 搜索缓存 + 限流(防服务器 IP 被封) +- **实测确认**:网易云搜索 API **无 CORS 头**(返回 text/plain),**无法下放到浏览器**,必须后端代理。 +- **对策**:`routes/music.js` 的 `/search` 加 ①内存缓存(同关键词 TTL 5 分钟内直接返回,上限 200 条 LRU);②按 IP 限流(默认 60 秒 20 次,超限 429)。命中缓存不消耗限流额度。 + +### 18.3 封面网络化(远程优先 + 本地回退) +- **实测确认**:网易云图片 CDN 返回 `Access-Control-Allow-Origin: *` 且不校验 Referer,浏览器可直接加载。 +- **改动**:`enrichSong` 下载封面时**同时**存远程 URL(`cover_remote`)和本地(`cover`);API 返回 `albumImage`(远程优先)+ `coverLocal`(本地兜底)。 +- **前端**:`coverSrcAttr(item)` 生成 ``;mini player / 收听页封面同样 onerror 回退。 + +### 18.4 刷新优化 +- init 时统一获取角色;SSE 事件保持增量刷新(`loadPlaylist`+`refreshLive` 节流)。 + +### 18.5 验证 +- 回归:**api 16/16、e2e 17/17、write 5/5** 全绿。 +- 新增功能手测(curl): + - admin 登录 → `/api/system/cache` 200、`/logs` 200;user 登录 → 同接口 **403**,`/api/music/import` **403**,`/api/resources` 200、`/api/streams` 200。 + - `/api/auth/me` 正确返回 role。 + - 搜索同关键词第二次返回 `cached:true`。 + +### 18.6 启动方式 +- `start.cmd` 更新为:`set "ADMIN_PASSWORD=test123"` + `set "USER_PASSWORD=user123"` + `node index.js`。 +- 托管启动:`cmd /c "set ADMIN_PASSWORD=test123&& set USER_PASSWORD=user123&& node index.js"`(注意用 `&&` 且不加尾空格,否则密码带空格)。 + +### 18.7 未做 +- **播放延迟调节**(HLS_TIME 运行时热更新):老板明确暂缓。 +- 用户密码/管理员密码的 UI 设置界面(现走环境变量)。 +- 会话表已有 role 列,但登录失败锁定的 failMap 是内存态,重启清零(可接受)。 \ No newline at end of file diff --git a/public/js/app.js b/public/js/app.js index 8eba2f6..df276a0 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -35,6 +35,15 @@ function formatBytes(b) { if (b >= 1024) return (b / 1024).toFixed(1) + " KB"; return b + " B"; } +// 封面:远程 URL 优先,失败自动回退本地 +function coverSrcAttr(item) { + const remote = item.albumImage || ""; + const local = item.coverLocal || ""; + if (remote && local && remote !== local) { + return `src="${esc(remote)}" data-local="${esc(local)}" onerror="this.onerror=null;this.src=this.dataset.local;"`; + } + return `src="${esc(remote || local)}"`; +} // ==================== API(统一带 cookie,401 跳登录) ==================== async function api(url, options = {}) { @@ -49,6 +58,14 @@ async function api(url, options = {}) { if (!res.ok) throw new Error((data && data.error) || text || `HTTP ${res.status}`); return data; } +// 角色 UI:非管理员隐藏 设置页 + 本地导入 +function applyRoleUI(role) { + const isAdmin = role === "admin"; + const tab = document.querySelector('.nav-tab[data-page="pageSettings"]'); + if (tab) tab.style.display = isAdmin ? "" : "none"; + const imp = document.getElementById("importCard"); + if (imp) imp.style.display = isAdmin ? "" : "none"; +} function post(url, body) { return api(url, { method: "POST", @@ -238,7 +255,7 @@ function reconcileList(container, items, keyOf, create, update) { // ==================== Resources ==================== function renderResItem(item) { const cover = item.albumImage - ? `` + ? `` : `
`; const bg = item.albumImage ? `
` @@ -456,7 +473,7 @@ const mpExpand = document.getElementById("mpExpand"); function showMiniPlayer(info) { mpName.textContent = info.name || "未知"; mpArtist.textContent = info.artist || "--"; - if (info.albumImage) { mpCover.src = info.albumImage; mpCover.style.display = ""; } + if (info.albumImage) { mpCover.onerror = info.coverLocal ? () => { mpCover.onerror = null; mpCover.src = info.coverLocal; } : null; mpCover.src = info.albumImage; mpCover.style.display = ""; } else mpCover.style.display = "none"; miniPlayer.classList.add("show"); document.body.classList.add("mini-player-visible"); @@ -755,6 +772,7 @@ async function loadLiveCover() { const r = await api(`/api/streams/${currentStreamId}/playlist`); const cur = (r.list || []).find((x) => x.isCurrent) || (r.list || [])[0]; if (cur && cur.albumImage) { + liveCover.onerror = cur.coverLocal ? () => { liveCover.onerror = null; liveCover.src = cur.coverLocal; } : null; liveCover.src = cur.albumImage; liveCover.style.display = ""; liveCoverPh.style.display = "none"; @@ -824,6 +842,7 @@ livePlayBtn.addEventListener("click", toggleLive); function fmtMB(b) { return (b / 1048576).toFixed(1) + " MB"; } async function loadSettings() { + if (state.role !== "admin") return; // 缓存 try { const c = await api("/api/system/cache"); @@ -882,6 +901,7 @@ document.getElementById("logTail").addEventListener("change", loadLogs); // ==================== Init ==================== (async function init() { + try { const me = await api("/api/auth/me"); state.role = me.role || null; applyRoleUI(state.role); } catch (_) {} await loadStreams(); loadLiveCover(); await loadResources(); diff --git a/src/auth.js b/src/auth.js index bae9ddf..16489ed 100644 --- a/src/auth.js +++ b/src/auth.js @@ -6,15 +6,43 @@ const store = require("./store"); const COOKIE_NAME = "mr_session"; -// 密码来源:环境变量 PANEL_PASSWORD;未设置则首次启动随机生成并打印 -let panelPassword = config.PANEL_PASSWORD; -let generated = false; -if (!panelPassword) { - panelPassword = crypto.randomBytes(6).toString("hex"); - generated = true; +/* ==================== 密码来源 ==================== */ +// 管理员密码:ADMIN_PASSWORD(兼容旧 PANEL_PASSWORD);未设置则首次随机生成并打印 +// 用户密码:USER_PASSWORD;未设置则「无用户密码」——只有管理员能登录 +let adminPassword = config.ADMIN_PASSWORD; +let userPassword = config.USER_PASSWORD; +let adminGenerated = false; +if (!adminPassword) { + adminPassword = crypto.randomBytes(6).toString("hex"); + adminGenerated = true; } +/* ==================== 登录失败锁定(按 IP) ==================== */ +const failMap = new Map(); // ip -> { count, lockedUntil } +function isLocked(ip) { + const rec = failMap.get(ip); + if (!rec) return 0; + if (rec.lockedUntil && rec.lockedUntil > Date.now()) { + return Math.ceil((rec.lockedUntil - Date.now()) / 1000); + } + return 0; +} +function recordFail(ip) { + const rec = failMap.get(ip) || { count: 0, lockedUntil: 0 }; + rec.count += 1; + if (rec.count >= config.LOGIN_MAX_FAILS) { + rec.lockedUntil = Date.now() + config.LOGIN_LOCK_MS; + rec.count = 0; + logger.warn(`[AUTH] IP ${ip} 登录失败次数过多,锁定 ${config.LOGIN_LOCK_MS / 60000} 分钟`); + } + failMap.set(ip, rec); +} +function clearFail(ip) { + failMap.delete(ip); +} + +/* ==================== Cookie ==================== */ function parseCookies(header) { const out = {}; if (!header) return out; @@ -25,22 +53,29 @@ function parseCookies(header) { return out; } -function createSession() { +/* ==================== 会话 ==================== */ +function createSession(role) { const token = crypto.randomBytes(24).toString("hex"); const expiresAt = Date.now() + config.SESSION_TTL_MS; - store.createSession(token, expiresAt); - return { token, expiresAt }; + store.createSession(token, role, expiresAt); + return { token, expiresAt, role }; } +function timingEqual(a, b) { + const ba = Buffer.from(String(a)); + const bb = Buffer.from(String(b)); + if (ba.length !== bb.length) return false; + return crypto.timingSafeEqual(ba, bb); +} + +// 返回角色 "admin" | "user" | null function verifyLogin(password) { - // 定长比较,防时序攻击 - const a = Buffer.from(String(password)); - const b = Buffer.from(panelPassword); - if (a.length !== b.length) return false; - return crypto.timingSafeEqual(a, b); + if (!password) return null; + if (timingEqual(password, adminPassword)) return "admin"; + if (userPassword && timingEqual(password, userPassword)) return "user"; + return null; } -// 从请求里取有效会话 function getValidSession(req) { const cookies = parseCookies(req.headers.cookie); const token = cookies[COOKIE_NAME]; @@ -51,6 +86,13 @@ function getValidSession(req) { function isAuthed(req) { return !!getValidSession(req); } +function getRole(req) { + const s = getValidSession(req); + return s ? s.role || "user" : null; +} +function isAdmin(req) { + return getRole(req) === "admin"; +} function setSessionCookie(res, token, expiresAt) { const maxAge = Math.floor((expiresAt - Date.now()) / 1000); @@ -63,7 +105,7 @@ function clearSessionCookie(res) { res.setHeader("Set-Cookie", `${COOKIE_NAME}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`); } -// 中间件:保护 /api(登录相关除外)与面板 +/* ==================== 中间件 ==================== */ function requireAuth(req, res, next) { if (isAuthed(req)) return next(); const isApi = (req.originalUrl || req.url || "").startsWith("/api/"); @@ -71,6 +113,13 @@ function requireAuth(req, res, next) { return res.redirect("/login"); } +// 需要管理员角色 +function requireAdmin(req, res, next) { + if (!isAuthed(req)) return res.status(401).json({ ok: false, error: "未登录" }); + if (!isAdmin(req)) return res.status(403).json({ ok: false, error: "需要管理员权限" }); + next(); +} + function logout(req) { const cookies = parseCookies(req.headers.cookie); const token = cookies[COOKIE_NAME]; @@ -78,14 +127,16 @@ function logout(req) { } function initPasswordLog() { - if (generated) { + if (adminGenerated) { logger.warn("========================================"); - logger.warn(`[AUTH] 未设置 PANEL_PASSWORD,已生成随机登录密码: ${panelPassword}`); - logger.warn("[AUTH] 请用此密码登录面板,或在环境变量中设置 PANEL_PASSWORD 固定密码"); + logger.warn(`[AUTH] 未设置 ADMIN_PASSWORD,已生成随机管理员密码: ${adminPassword}`); + logger.warn("[AUTH] 请用此密码登录面板,或在环境变量中设置 ADMIN_PASSWORD 固定密码"); logger.warn("========================================"); } else { - logger.info("[AUTH] 已启用面板鉴权(PANEL_PASSWORD 已设置)"); + logger.info("[AUTH] 已启用管理员鉴权(ADMIN_PASSWORD 已设置)"); } + if (userPassword) logger.info("[AUTH] 已启用用户密码(USER_PASSWORD 已设置)"); + else logger.info("[AUTH] 未设置 USER_PASSWORD,当前仅管理员可登录"); } module.exports = { @@ -94,9 +145,15 @@ module.exports = { verifyLogin, getValidSession, isAuthed, + getRole, + isAdmin, setSessionCookie, clearSessionCookie, requireAuth, + requireAdmin, logout, initPasswordLog, + isLocked, + recordFail, + clearFail, }; \ No newline at end of file diff --git a/src/config.js b/src/config.js index 29d495c..ec8e5ef 100644 --- a/src/config.js +++ b/src/config.js @@ -40,8 +40,14 @@ const config = { LOG_LEVEL: envStr("LOG_LEVEL", "info"), LOG_FILE: path.join(ROOT, "server.log"), - PANEL_PASSWORD: envStr("PANEL_PASSWORD", ""), + ADMIN_PASSWORD: envStr("ADMIN_PASSWORD", envStr("PANEL_PASSWORD", "")), + USER_PASSWORD: envStr("USER_PASSWORD", ""), SESSION_TTL_MS: envInt("SESSION_TTL_HOURS", 72) * 3600 * 1000, + LOGIN_MAX_FAILS: envInt("LOGIN_MAX_FAILS", 5), + LOGIN_LOCK_MS: envInt("LOGIN_LOCK_MIN", 10) * 60 * 1000, + SEARCH_CACHE_TTL_MS: envInt("SEARCH_CACHE_TTL_SEC", 300) * 1000, + SEARCH_RATE_MAX: envInt("SEARCH_RATE_MAX", 20), + SEARCH_RATE_WINDOW_MS: envInt("SEARCH_RATE_WINDOW_SEC", 60) * 1000, }; config.LIVE_TICK_INTERVAL = config.HLS_TIME * 1000; diff --git a/src/db.js b/src/db.js index 94328bd..21e7dc4 100644 --- a/src/db.js +++ b/src/db.js @@ -14,6 +14,7 @@ CREATE TABLE IF NOT EXISTS songs ( name TEXT NOT NULL DEFAULT '', artist TEXT NOT NULL DEFAULT '', cover TEXT NOT NULL DEFAULT '', + cover_remote TEXT NOT NULL DEFAULT '', duration REAL, file_size INTEGER NOT NULL DEFAULT 0, heat INTEGER NOT NULL DEFAULT 0, @@ -47,6 +48,7 @@ CREATE TABLE IF NOT EXISTS settings ( CREATE TABLE IF NOT EXISTS sessions ( token TEXT PRIMARY KEY, + role TEXT NOT NULL DEFAULT 'user', created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL ); @@ -61,6 +63,20 @@ function init() { db.pragma("foreign_keys = ON"); db.pragma("busy_timeout = 5000"); db.exec(SCHEMA); + + // 迁移:旧库 sessions 表补 role 列 + const sessCols = db.prepare("PRAGMA table_info(sessions)").all(); + if (!sessCols.some((c) => c.name === "role")) { + db.exec("ALTER TABLE sessions ADD COLUMN role TEXT NOT NULL DEFAULT 'user'"); + logger.info("[DB] 迁移 sessions 表:新增 role 列"); + } + + // 迁移:songs 表补 cover_remote 列(远程封面 URL,优先展示,省带宽) + const songCols = db.prepare("PRAGMA table_info(songs)").all(); + if (!songCols.some((c) => c.name === "cover_remote")) { + db.exec("ALTER TABLE songs ADD COLUMN cover_remote TEXT NOT NULL DEFAULT ''"); + logger.info("[DB] 迁移 songs 表:新增 cover_remote 列"); + } logger.info(`[DB] 已打开 ${config.DB_FILE} (WAL)`); ensureDefaultStream(); diff --git a/src/metadata.js b/src/metadata.js index 05f4d2b..88f80a5 100644 --- a/src/metadata.js +++ b/src/metadata.js @@ -184,6 +184,8 @@ async function enrichSong(file, opts = {}) { if (hit) { if (needArtist && hit.artist) fields.artist = hit.artist; if (needCover && hit.albumImage) { + // 远程 URL 优先(省带宽),本地下载作兜底 + fields.cover_remote = hit.albumImage; const cover = await downloadCover(hit.albumImage, fileHash(file)); if (cover) fields.cover = cover; } diff --git a/src/routes/auth.js b/src/routes/auth.js index c529cde..04cb2ab 100644 --- a/src/routes/auth.js +++ b/src/routes/auth.js @@ -6,15 +6,27 @@ const logger = require("../logger"); const router = express.Router(); router.post("/login", (req, res) => { + const ip = req.ip || req.connection?.remoteAddress || "unknown"; + const locked = auth.isLocked(ip); + if (locked > 0) { + return res.status(429).json({ ok: false, error: `登录失败次数过多,请 ${locked} 秒后再试` }); + } + const { password } = req.body || {}; if (!password) return res.status(400).json({ ok: false, error: "请输入密码" }); - if (!auth.verifyLogin(password)) { - logger.warn(`[AUTH] 登录失败 from ${req.ip}`); + + const role = auth.verifyLogin(password); + if (!role) { + auth.recordFail(ip); + logger.warn(`[AUTH] 登录失败 from ${ip}`); return res.status(401).json({ ok: false, error: "密码错误" }); } - const { token, expiresAt } = auth.createSession(); + + auth.clearFail(ip); + const { token, expiresAt } = auth.createSession(role); auth.setSessionCookie(res, token, expiresAt); - res.json({ ok: true }); + logger.info(`[AUTH] 登录成功 (${role}) from ${ip}`); + res.json({ ok: true, role }); }); router.post("/logout", (req, res) => { @@ -24,7 +36,8 @@ router.post("/logout", (req, res) => { }); router.get("/me", (req, res) => { - res.json({ ok: true, authed: auth.isAuthed(req) }); + const role = auth.getRole(req); + res.json({ ok: true, authed: !!role, role: role || null }); }); module.exports = router; \ No newline at end of file diff --git a/src/routes/music.js b/src/routes/music.js index 9240602..41bc777 100644 --- a/src/routes/music.js +++ b/src/routes/music.js @@ -13,13 +13,48 @@ const { sanitizeFileName, isValidFileName, isAudioFile, fileHash, getDirSize } = const router = express.Router(); +/* ==================== 搜索缓存 + 限流 ==================== */ +const searchCache = new Map(); // keyword -> { at, data } +const searchRate = new Map(); // ip -> { count, resetAt } + +function searchRateLimited(ip) { + const now = Date.now(); + const rec = searchRate.get(ip); + if (!rec || rec.resetAt < now) { + searchRate.set(ip, { count: 1, resetAt: now + config.SEARCH_RATE_WINDOW_MS }); + return false; + } + rec.count += 1; + return rec.count > config.SEARCH_RATE_MAX; +} + /* ==================== 网易云搜索/详情 ==================== */ router.get("/search", async (req, res) => { const { keyword } = req.query; if (!keyword || !String(keyword).trim()) return res.status(400).json({ ok: false, error: "缺少搜索关键词" }); + const kw = String(keyword).trim(); + + // 命中缓存直接返回(不消耗限流额度) + const cached = searchCache.get(kw); + if (cached && Date.now() - cached.at < config.SEARCH_CACHE_TTL_MS) { + return res.json({ ok: true, ...cached.data, cached: true }); + } + + // 未命中 → 限流检查 + const ip = req.ip || req.connection?.remoteAddress || "unknown"; + if (searchRateLimited(ip)) { + return res.status(429).json({ ok: false, error: "搜索过于频繁,请稍后再试" }); + } + try { - const r = await meta.searchNetEase(String(keyword).trim()); + const r = await meta.searchNetEase(kw); + searchCache.set(kw, { at: Date.now(), data: r }); + // 缓存上限,防内存膨胀 + if (searchCache.size > 200) { + const oldest = [...searchCache.entries()].sort((a, b) => a[1].at - b[1].at)[0]; + if (oldest) searchCache.delete(oldest[0]); + } res.json({ ok: true, ...r }); } catch (e) { res.status(400).json({ ok: false, error: e.message }); @@ -106,7 +141,7 @@ const upload = multer({ }, }); -router.post("/import", upload.single("audio"), async (req, res) => { +router.post("/import", require("../auth").requireAdmin, upload.single("audio"), async (req, res) => { if (!req.file) return res.status(400).json({ ok: false, error: "未收到文件" }); const tmpPath = req.file.path; try { diff --git a/src/routes/resources.js b/src/routes/resources.js index 3adc72e..0f51a69 100644 --- a/src/routes/resources.js +++ b/src/routes/resources.js @@ -18,7 +18,8 @@ router.get("/", (req, res) => { file: s.file, title: s.name || s.file, artist: s.artist || "", - albumImage: s.cover || "", + albumImage: s.cover_remote || s.cover || "", + coverLocal: s.cover || "", duration: s.duration || null, fileSize: s.file_size || 0, heat: s.heat || 0, diff --git a/src/routes/streams.js b/src/routes/streams.js index f7f5eb7..9025ae0 100644 --- a/src/routes/streams.js +++ b/src/routes/streams.js @@ -71,7 +71,8 @@ function snapshot(streamId) { file: r.file, title: r.name || r.file, artist: r.artist || "", - albumImage: r.cover || "", + albumImage: r.cover_remote || r.cover || "", + coverLocal: r.cover || "", duration: r.duration || null, heat: r.heat || 0, cacheStatus: live.getStatus(r.file), diff --git a/src/server.js b/src/server.js index 7589ea4..948c1de 100644 --- a/src/server.js +++ b/src/server.js @@ -123,7 +123,7 @@ function createApp() { app.use("/api/music", auth.requireAuth, musicRoutes); app.use("/api/resources", auth.requireAuth, resourceRoutes); app.use("/api/streams", auth.requireAuth, streamRoutes); - app.use("/api/system", auth.requireAuth, systemRoutes); + app.use("/api/system", auth.requireAdmin, systemRoutes); // 其余 /api 需要登录 app.use("/api", auth.requireAuth); diff --git a/src/store.js b/src/store.js index 4d73f02..f6ba8a8 100644 --- a/src/store.js +++ b/src/store.js @@ -18,11 +18,12 @@ function upsertSong(s) { const ex = getSongByFile(s.file); if (ex) { d.prepare( - `UPDATE songs SET name=?, artist=?, cover=?, duration=?, file_size=? WHERE file=?` + `UPDATE songs SET name=?, artist=?, cover=?, cover_remote=?, duration=?, file_size=? WHERE file=?` ).run( s.name != null ? s.name : ex.name, s.artist != null ? s.artist : ex.artist, s.cover != null ? s.cover : ex.cover, + s.cover_remote != null ? s.cover_remote : (ex.cover_remote || ""), s.duration != null ? s.duration : ex.duration, s.file_size != null ? s.file_size : ex.file_size, s.file @@ -30,13 +31,14 @@ function upsertSong(s) { return getSongByFile(s.file); } d.prepare( - `INSERT INTO songs (file, name, artist, cover, duration, file_size, heat, cache_status, created_at) - VALUES (?, ?, ?, ?, ?, ?, 0, 'pending', ?)` + `INSERT INTO songs (file, name, artist, cover, cover_remote, duration, file_size, heat, cache_status, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, 0, 'pending', ?)` ).run( s.file, s.name || "", s.artist || "", s.cover || "", + s.cover_remote || "", s.duration != null ? s.duration : null, s.file_size || 0, Date.now() @@ -135,10 +137,10 @@ function clearPlaylist(streamId) { } /* ==================== sessions ==================== */ -function createSession(token, expiresAt) { +function createSession(token, role, expiresAt) { getDb() - .prepare("INSERT INTO sessions (token, created_at, expires_at) VALUES (?, ?, ?)") - .run(token, Date.now(), expiresAt); + .prepare("INSERT INTO sessions (token, role, created_at, expires_at) VALUES (?, ?, ?, ?)") + .run(token, role || "user", Date.now(), expiresAt); } function getSession(token) { const s = getDb().prepare("SELECT * FROM sessions WHERE token=?").get(token); diff --git a/start.cmd b/start.cmd index c5f2815..4f2d5fa 100644 --- a/start.cmd +++ b/start.cmd @@ -1,3 +1,4 @@ @echo off -set "PANEL_PASSWORD=test123" +set "ADMIN_PASSWORD=test123" +set "USER_PASSWORD=user123" node index.js \ No newline at end of file