diff --git a/src/web/api/music.ts b/src/web/api/music.ts index 3a0f682..d4446e7 100644 --- a/src/web/api/music.ts +++ b/src/web/api/music.ts @@ -128,7 +128,7 @@ export function createMusicRouter( } }); - router.get("/recommend/songs", async (req, res) => { + router.get("/recommend/songs", requireNotGuest, async (req, res) => { try { const provider = getProvider(req.query.platform as string); if (!provider.getDailyRecommendSongs) { @@ -143,7 +143,7 @@ export function createMusicRouter( } }); - router.get("/personal/fm", async (req, res) => { + router.get("/personal/fm", requireNotGuest, async (req, res) => { try { const provider = getProvider(req.query.platform as string); if (!provider.getPersonalFm) { @@ -158,7 +158,7 @@ export function createMusicRouter( } }); - router.get("/user/playlists", async (req, res) => { + router.get("/user/playlists", requireNotGuest, async (req, res) => { try { const provider = getProvider(req.query.platform as string); if (!provider.getUserPlaylists) { diff --git a/src/web/api/permissions-enforcement.test.ts b/src/web/api/permissions-enforcement.test.ts index c669cdb..f74a7bf 100644 --- a/src/web/api/permissions-enforcement.test.ts +++ b/src/web/api/permissions-enforcement.test.ts @@ -199,6 +199,29 @@ describe("permission enforcement on action routes", () => { }); }); + // The operator's personal-account reads (their recommendations, FM, and + // playlists) must never leak to login-less guests. These routes are gated + // with requireNotGuest; generic search/browse stays open. + describe("operator personal-data reads are denied to guests", () => { + const personalRoutes = [ + "/api/music/recommend/songs", + "/api/music/personal/fm", + "/api/music/user/playlists", + ]; + + for (const route of personalRoutes) { + it(`GET ${route} is 403 for a guest`, async () => { + const app = makeApp(guest()); + expect((await request(app).get(route)).status).toBe(403); + }); + + it(`GET ${route} is NOT 403 for a member`, async () => { + const app = makeApp(member([], "all")); + expect((await request(app).get(route)).status).not.toBe(403); + }); + } + }); + describe("read-only routes stay open", () => { it("GET /api/auth/status not gated", async () => { const app = makeApp(member([], "all"));