fix(guest): deny favorites + auth-status reads to guests; UI polish

Consolidated fix wave from the final whole-branch review of guest mode.

- FIX 1 (critical): gate /api/favorites mount with requireNotGuest — the
  router keys off req.user.id (shared __guest__ principal), so guests could
  read/write a shared favorites bucket. Added focused guest-deny tests.
- FIX 2: gate GET /api/auth/status and /api/auth/qrcode/status with
  requireNotGuest so config reads no longer leak to guests.
- FIX 3: requireAuthInline in createSessionRouter now rejects guest sessions
  with 401 once guest mode is disabled (mirrors createRequireAuth), so /me
  stops returning guest data after an admin disables the feature.
- FIX 4: Login guest button now sits BELOW the card (auth-page flex-direction
  column + guest-btn width 360px) instead of beside it.
- FIX 5: mobile mini-player transport buttons in App.vue are now per-button
  gated for guests (prev/play/next/mode/volume), mirroring Player.vue.
- FIX 6: refreshed stale "gated on player.control" seek comments in Player.vue
  and relabeled the now-stale quality-GET test.

npm test: 354/354 pass. npm run build: tsc + vue-tsc + vite all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
saopig1andClaude Opus 4.8 committed 2026-06-25 12:44:07 +08:00
1 parent e47fc76529
commit f142c514cd
8 files changed
+113 -12

No files matched your search

+54 -1
View File
@@ -6,6 +6,8 @@ import { createPlayerRouter } from "./player.js";
import { createBotRouter } from "./bot.js";
import { createAuthRouter } from "./auth.js";
import { createMusicRouter } from "./music.js";
import { createFavoritesRouter } from "./favorites.js";
import { requireNotGuest } from "../middleware/requireNotGuest.js";
const logger = pino({ level: "silent" });
@@ -204,7 +206,7 @@ describe("permission enforcement on action routes", () => {
expect(res.status).not.toBe(403);
});
it("GET /api/music/quality not gated", async () => {
it("GET /api/music/quality readable by members, denied to guests", async () => {
const app = makeApp(member([], "all"));
const res = await request(app).get("/api/music/quality");
expect(res.status).not.toBe(403);
@@ -215,6 +217,12 @@ describe("permission enforcement on action routes", () => {
const res = await request(app).get("/api/bot");
expect(res.status).not.toBe(403);
});
it("GET /api/auth/status and /api/auth/qrcode/status are 403 for guests", async () => {
const app = makeApp(guest());
expect((await request(app).get("/api/auth/status")).status).toBe(403);
expect((await request(app).get("/api/auth/qrcode/status?key=k")).status).toBe(403);
});
});
describe("admin bypasses every gate", () => {
@@ -368,3 +376,48 @@ describe("guest enforcement on player routes", () => {
expect((await request(m).post(`/api/player/${ALLOWED_BOT}/add-song`).send({ song: SONG })).status).not.toBe(403);
});
});
// --------------------------------------------------------------------------
// Favorites are member-only: the router keys everything off req.user.id and
// all guests share the __guest__ principal, so a guest must never reach it.
// server.ts gates the mount with requireNotGuest; we mirror that mount here
// and assert a guest gets 403 (the requireNotGuest guard runs before any
// handler, so the fake database is never touched).
// --------------------------------------------------------------------------
function makeFavoritesApp(user: any) {
const app = express();
app.use(express.json());
app.use((req, _res, next) => { (req as any).user = user; next(); });
const fakeDb = {
getFavorites: () => [],
addFavorite: () => {},
removeFavorite: () => {},
isFavorited: () => false,
} as any;
app.use("/api/favorites", requireNotGuest, createFavoritesRouter(fakeDb, logger));
return app;
}
describe("favorites are denied to guests", () => {
it("403 for a guest on GET /api/favorites", async () => {
const app = makeFavoritesApp(guest());
expect((await request(app).get("/api/favorites")).status).toBe(403);
});
it("403 for a guest on GET /api/favorites/check", async () => {
const app = makeFavoritesApp(guest());
expect((await request(app).get("/api/favorites/check?platform=netease&playlistId=x")).status).toBe(403);
});
it("403 for a guest on POST /api/favorites", async () => {
const app = makeFavoritesApp(guest());
const res = await request(app).post("/api/favorites").send({ platform: "netease", playlistId: "x", name: "n" });
expect(res.status).toBe(403);
});
it("NOT 403 for a member on GET /api/favorites", async () => {
const app = makeFavoritesApp(member([], "all"));
expect((await request(app).get("/api/favorites")).status).not.toBe(403);
});
});