Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ba9d15954 | |||
| 83c9d4d164 |
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
|
|||||||
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.7.16] - 2026-05-24
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Blocklist-Search Queue ID Type Mismatch** — Resolved a bug where the "Blocklist and search" action consistently returned `403 Download not found or permission denied` for all users. The server-side download lookup in `server/routes/dashboard.js` used strict equality (`===`) to compare `arrQueueId` values, but the value sent from the SPA client (read from a DOM `data-*` attribute) is always a `string`, while the value populated from the Radarr/Sonarr queue API is a `number`. Both sides are now cast to `String` before comparison, resolving the false-negative match failure. Resolves Gitea Issue [#48](https://git.i3omb.com/Gandalf/sofarr/issues/48).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [1.7.15] - 2026-05-24
|
## [1.7.15] - 2026-05-24
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "sofarr",
|
"name": "sofarr",
|
||||||
"version": "1.7.15",
|
"version": "1.7.16",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "sofarr",
|
"name": "sofarr",
|
||||||
"version": "1.7.15",
|
"version": "1.7.16",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sofarr",
|
"name": "sofarr",
|
||||||
"version": "1.7.15",
|
"version": "1.7.16",
|
||||||
"description": "A personal media download dashboard that shows your downloads 'so far' while you relax on the sofa waiting for your *arr services to finish",
|
"description": "A personal media download dashboard that shows your downloads 'so far' while you relax on the sofa waiting for your *arr services to finish",
|
||||||
"main": "server/index.js",
|
"main": "server/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+1
-1
@@ -132,7 +132,7 @@ function createApp({ skipRateLimits = false } = {}) {
|
|||||||
* version:
|
* version:
|
||||||
* type: string
|
* type: string
|
||||||
* description: sofarr version
|
* description: sofarr version
|
||||||
* example: "1.7.14"
|
* example: "1.7.16"
|
||||||
* x-code-samples:
|
* x-code-samples:
|
||||||
* - lang: curl
|
* - lang: curl
|
||||||
* label: cURL
|
* label: cURL
|
||||||
|
|||||||
+1
-1
@@ -249,7 +249,7 @@ app.use(express.json({ limit: '64kb' })); // prevent oversized JSON payloads
|
|||||||
* version:
|
* version:
|
||||||
* type: string
|
* type: string
|
||||||
* description: sofarr version
|
* description: sofarr version
|
||||||
* example: "1.7.14"
|
* example: "1.7.16"
|
||||||
*/
|
*/
|
||||||
app.get('/health', (req, res) => {
|
app.get('/health', (req, res) => {
|
||||||
res.json({ status: 'ok', uptime: process.uptime(), version });
|
res.json({ status: 'ok', uptime: process.uptime(), version });
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ info:
|
|||||||
|
|
||||||
## SSE Streaming
|
## SSE Streaming
|
||||||
Real-time updates are available via Server-Sent Events at GET /api/dashboard/stream.
|
Real-time updates are available via Server-Sent Events at GET /api/dashboard/stream.
|
||||||
version: 1.7.15
|
version: 1.7.16
|
||||||
contact:
|
contact:
|
||||||
name: sofarr
|
name: sofarr
|
||||||
license:
|
license:
|
||||||
|
|||||||
@@ -686,9 +686,12 @@ router.post('/blocklist-search', requireAuth, async (req, res) => {
|
|||||||
return res.status(400).json({ error: 'arrType must be sonarr or radarr' });
|
return res.status(400).json({ error: 'arrType must be sonarr or radarr' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look up the download to verify permission
|
// Look up the download to verify permission.
|
||||||
|
// Note: arrQueueId from req.body is always a string (DOM dataset), while
|
||||||
|
// d.arrQueueId from the Radarr/Sonarr API is a number. Cast both to String
|
||||||
|
// to avoid a type mismatch causing a false-negative lookup.
|
||||||
const allDownloads = await downloadClientRegistry.getAllDownloads();
|
const allDownloads = await downloadClientRegistry.getAllDownloads();
|
||||||
const download = allDownloads.find(d => d.arrQueueId === arrQueueId && d.arrType === arrType);
|
const download = allDownloads.find(d => d.arrQueueId != null && String(d.arrQueueId) === String(arrQueueId) && d.arrType === arrType);
|
||||||
|
|
||||||
if (!download) {
|
if (!download) {
|
||||||
console.error('[Blocklist] Download not found:', { arrQueueId, arrType });
|
console.error('[Blocklist] Download not found:', { arrQueueId, arrType });
|
||||||
|
|||||||
@@ -972,6 +972,38 @@ describe('POST /api/dashboard/blocklist-search', () => {
|
|||||||
expect(res.body.ok).toBe(true);
|
expect(res.body.ok).toBe(true);
|
||||||
mockGetAllDownloads.mockRestore();
|
mockGetAllDownloads.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('matches download correctly when arrQueueId is sent as a string but stored as a number (type mismatch regression)', async () => {
|
||||||
|
// Regression test for GitHub #48: arrQueueId from the SPA DOM dataset is always
|
||||||
|
// a string, but the value stored in allDownloads from the Radarr/Sonarr API is a number.
|
||||||
|
// Without String() casting the === comparison fails and returns 403.
|
||||||
|
const app = createApp({ skipRateLimits: true });
|
||||||
|
const { cookies, csrf, csrfCookie } = await getAuthHeaders(app);
|
||||||
|
|
||||||
|
const downloadClientRegistry = require('../../server/utils/downloadClients');
|
||||||
|
const mockGetAllDownloads = vi.spyOn(downloadClientRegistry, 'getAllDownloads').mockResolvedValue([
|
||||||
|
// arrQueueId stored as a number (as Radarr API returns it)
|
||||||
|
{ arrQueueId: 9050001, arrType: 'radarr', importIssues: [], qbittorrent: null }
|
||||||
|
]);
|
||||||
|
|
||||||
|
nock(RADARR_BASE)
|
||||||
|
.delete('/api/v3/queue/9050001')
|
||||||
|
.query({ removeFromClient: 'true', blocklist: 'true' })
|
||||||
|
.reply(200, {});
|
||||||
|
nock(RADARR_BASE)
|
||||||
|
.post('/api/v3/command')
|
||||||
|
.reply(200, {});
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/api/dashboard/blocklist-search')
|
||||||
|
.set('Cookie', [...cookies, csrfCookie].join('; '))
|
||||||
|
.set('X-CSRF-Token', csrf)
|
||||||
|
// arrQueueId sent as a STRING from the client (as the SPA DOM dataset does)
|
||||||
|
.send({ arrQueueId: '9050001', arrType: 'radarr', arrInstanceUrl: RADARR_BASE, arrInstanceKey: 'rk', arrContentId: 77, arrContentType: 'movie' });
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.ok).toBe(true);
|
||||||
|
mockGetAllDownloads.mockRestore();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user