diff --git a/docs/widgets/services/qbittorrent.md b/docs/widgets/services/qbittorrent.md index fe9ec3b9d..769408878 100644 --- a/docs/widgets/services/qbittorrent.md +++ b/docs/widgets/services/qbittorrent.md @@ -5,7 +5,9 @@ description: qBittorrent Widget Configuration Learn more about [qBittorrent](https://github.com/qbittorrent/qBittorrent). -Uses the same username and password used to login from the web. +Authenticate using the WebUI username and password or the API Key `(qBittorrent ≥ v5.2.0)`. + +API Key is located in `Options > WebUI > Authentication > API Key`. Allowed fields: `["leech", "download", "seed", "upload"]`. @@ -15,6 +17,7 @@ widget: url: http://qbittorrent.host.or.ip username: username password: password + key: qbt_apikey # required if using API key instead of username/password enableLeechProgress: true # optional, defaults to false enableLeechSize: true # optional, defaults to false ``` diff --git a/src/widgets/qbittorrent/proxy.js b/src/widgets/qbittorrent/proxy.js index 9b789c885..c959605a7 100644 --- a/src/widgets/qbittorrent/proxy.js +++ b/src/widgets/qbittorrent/proxy.js @@ -9,12 +9,18 @@ async function login(widget) { logger.debug("qBittorrent is rejecting the request, logging in."); const loginUrl = new URL(`${widget.url}/api/v2/auth/login`).toString(); const loginBody = `username=${encodeURIComponent(widget.username)}&password=${encodeURIComponent(widget.password)}`; + const loginKey = `${widget.key}`; const loginParams = { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: loginBody, }; + if (widget.key) { + loginParams.headers.Authorization = `Bearer ${loginKey}`; + } else if (widget.username && widget.password) { + loginParams.body = loginBody; + } + const [status, contentType, data] = await httpProxy(loginUrl, loginParams); return [status, data]; } diff --git a/src/widgets/qbittorrent/proxy.test.js b/src/widgets/qbittorrent/proxy.test.js index caea6e6d0..f9ee82ffc 100644 --- a/src/widgets/qbittorrent/proxy.test.js +++ b/src/widgets/qbittorrent/proxy.test.js @@ -83,4 +83,24 @@ describe("widgets/qbittorrent/proxy", () => { expect(res.statusCode).toBe(401); expect(res.body).toEqual(Buffer.from("Denied")); }); + + it("supports API keys by including them in the Authorization header and not the body", async () => { + getServiceWidget.mockResolvedValue({ url: "http://qb", key: "abc123" }); + + httpProxy + .mockResolvedValueOnce([403, "application/json", Buffer.from("nope")]) + .mockResolvedValueOnce([200, "text/plain", Buffer.from("Ok.")]) + .mockResolvedValueOnce([200, "application/json", Buffer.from("data")]); + + const req = { query: { group: "g", service: "svc", endpoint: "torrents/info", index: "0" } }; + const res = createMockRes(); + + await qbittorrentProxyHandler(req, res); + + expect(httpProxy).toHaveBeenCalledTimes(3); + expect(httpProxy.mock.calls[1][0]).toBe("http://qb/api/v2/auth/login"); + expect(httpProxy.mock.calls[1][1].headers.Authorization).toBe("Bearer abc123"); + expect(res.statusCode).toBe(200); + expect(res.body).toEqual(Buffer.from("data")); + }); });