From a7bab17f9726a66bcbba556b2912aa0c37708e4a Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sun, 12 Apr 2026 11:00:54 -0700 Subject: [PATCH] Enhancement: support pyload API key, fix error message (#6558) --- docs/widgets/services/pyload.md | 1 + src/widgets/pyload/proxy.js | 25 +++++++++++++------- src/widgets/pyload/proxy.test.js | 40 ++++++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/docs/widgets/services/pyload.md b/docs/widgets/services/pyload.md index 3a37d083a..1e03c3299 100644 --- a/docs/widgets/services/pyload.md +++ b/docs/widgets/services/pyload.md @@ -13,4 +13,5 @@ widget: url: http://pyload.host.or.ip:port username: username password: password # only needed if set + key: pyloadapikey # only needed if set, takes precedence over username/password ``` diff --git a/src/widgets/pyload/proxy.js b/src/widgets/pyload/proxy.js index 57bd68fd7..9dc071ad2 100644 --- a/src/widgets/pyload/proxy.js +++ b/src/widgets/pyload/proxy.js @@ -45,17 +45,20 @@ async function fetchFromPyloadAPI(url, sessionId, params, service) { return [status, returnData, responseHeaders]; } -async function fetchFromPyloadAPIBasic(url, params, username, password) { +async function fetchFromPyloadAPIWithCredentials(url, params, username, password, key) { const parsedUrl = new URL(url); const isGetRequest = !params || Object.keys(params).length === 0; const options = { method: isGetRequest ? "GET" : "POST", - headers: { - Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`, - }, }; + if (key) { + options.headers = { "X-API-Key": key }; + } else { + options.headers = { Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}` }; + } + if (isGetRequest) { if (params) { Object.keys(params).forEach((key) => parsedUrl.searchParams.append(key, params[key])); @@ -106,10 +109,16 @@ export default async function pyloadProxyHandler(req, res, map = {}) { const url = new URL(formatApiCall(apiTemplate, { endpoint, ...widget })); const ngUrl = ngEndpoint ? new URL(formatApiCall(apiTemplate, { endpoint: ngEndpoint, ...widget })) : url; const loginUrl = `${widget.url}/api/login`; - const hasCredentials = widget.username && widget.password; + const hasCredentials = widget.key || (widget.username && widget.password); if (hasCredentials) { - const [status, data] = await fetchFromPyloadAPIBasic(ngUrl, null, widget.username, widget.password); + const [status, data] = await fetchFromPyloadAPIWithCredentials( + ngUrl, + null, + widget.username, + widget.password, + widget.key, + ); if (status === 200 && !data?.error) { cache.put(`${isNgCacheKey}.${service}`, true); @@ -117,9 +126,7 @@ export default async function pyloadProxyHandler(req, res, map = {}) { } if (status === 401) { - return res - .status(status) - .send({ error: { message: "Invalid credentials communicating with Pyload API", data } }); + return res.status(status).send({ error: "Invalid credentials communicating with Pyload API", data }); } } diff --git a/src/widgets/pyload/proxy.test.js b/src/widgets/pyload/proxy.test.js index d9d558fb7..0a4ad1599 100644 --- a/src/widgets/pyload/proxy.test.js +++ b/src/widgets/pyload/proxy.test.js @@ -75,6 +75,46 @@ describe("widgets/pyload/proxy", () => { expect(res.body).toEqual({ ok: true }); }); + it("uses api key auth and returns data", async () => { + getServiceWidget.mockResolvedValue({ + type: "pyload", + url: "http://pyload", + key: "apikey", + }); + + httpProxy.mockResolvedValueOnce([200, "application/json", Buffer.from(JSON.stringify({ ok: true })), {}]); + + const req = { query: { group: "g", service: "svc", endpoint: "status", index: "0" } }; + const res = createMockRes(); + + await pyloadProxyHandler(req, res); + + expect(httpProxy).toHaveBeenCalledTimes(1); + expect(httpProxy.mock.calls[0][1].headers["X-API-Key"]).toBe("apikey"); + expect(cache.put).toHaveBeenCalledWith("pyloadProxyHandler__isNg.svc", true); + expect(res.body).toEqual({ ok: true }); + }); + + it("returns error if login fails", async () => { + getServiceWidget.mockResolvedValue({ + type: "pyload", + url: "http://pyload", + username: "u", + password: "p", + }); + + httpProxy.mockResolvedValueOnce([401, "application/json", Buffer.from(JSON.stringify({ error: "bad" })), {}]); + + const req = { query: { group: "g", service: "svc", endpoint: "status", index: "0" } }; + const res = createMockRes(); + + await pyloadProxyHandler(req, res); + + expect(httpProxy).toHaveBeenCalledTimes(1); + expect(res.statusCode).toBe(401); + expect(res.body).toMatchObject({ error: "Invalid credentials communicating with Pyload API" }); + }); + it("retries after 403 by clearing session and logging in again", async () => { getServiceWidget.mockResolvedValue({ type: "pyload",