diff --git a/package.json b/package.json
index da430581..95ee8f71 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
"dockerode": "^3.3.3",
"js-yaml": "^4.1.0",
"json-rpc-2.0": "^1.3.0",
+ "lodash": "^4.17.21",
"memory-cache": "^0.2.0",
"next": "12.2.5",
"node-os-utils": "^1.3.7",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7b4915e9..8a2bfa26 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -9,6 +9,7 @@ specifiers:
eslint-config-next: 12.2.5
js-yaml: ^4.1.0
json-rpc-2.0: ^1.3.0
+ lodash: ^4.17.21
memory-cache: ^0.2.0
next: 12.2.5
node-os-utils: ^1.3.7
@@ -28,6 +29,7 @@ dependencies:
dockerode: 3.3.3
js-yaml: 4.1.0
json-rpc-2.0: 1.3.0
+ lodash: 4.17.21
memory-cache: 0.2.0
next: 12.2.5_biqbaboplfbrettd7655fr4n2y
node-os-utils: 1.3.7
@@ -43,7 +45,7 @@ devDependencies:
eslint: 8.22.0
eslint-config-next: 12.2.5_4rv7y5c6xz3vfxwhbrcxxi73bq
postcss: 8.4.16
- tailwindcss: 3.1.8
+ tailwindcss: 3.1.8_postcss@8.4.16
typescript: 4.7.4
packages:
@@ -271,7 +273,7 @@ packages:
tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1'
dependencies:
mini-svg-data-uri: 1.4.4
- tailwindcss: 3.1.8
+ tailwindcss: 3.1.8_postcss@8.4.16
dev: false
/@types/json5/0.0.29:
@@ -1558,6 +1560,10 @@ packages:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
dev: true
+ /lodash/4.17.21:
+ resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
+ dev: false
+
/loose-envify/1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
@@ -2245,10 +2251,12 @@ packages:
react: 18.2.0
dev: false
- /tailwindcss/3.1.8:
+ /tailwindcss/3.1.8_postcss@8.4.16:
resolution: {integrity: sha512-YSneUCZSFDYMwk+TGq8qYFdCA3yfBRdBlS7txSq0LUmzyeqRe3a8fBQzbz9M3WS/iFT4BNf/nmw9mEzrnSaC0g==}
engines: {node: '>=12.13.0'}
hasBin: true
+ peerDependencies:
+ postcss: ^8.0.9
dependencies:
arg: 5.0.2
chokidar: 3.5.3
diff --git a/src/components/services/widgets/service/docker.jsx b/src/components/services/widgets/service/docker.jsx
index 4a41565d..7a6729cc 100644
--- a/src/components/services/widgets/service/docker.jsx
+++ b/src/components/services/widgets/service/docker.jsx
@@ -1,24 +1,24 @@
import useSWR from "swr";
-import { calculateCPUPercent, formatBytes } from "utils/stats-helpers";
-
import Widget from "../widget";
import Block from "../block";
+import { calculateCPUPercent, formatBytes } from "utils/stats-helpers";
+
export default function Docker({ service }) {
const config = service.widget;
const { data: statusData, error: statusError } = useSWR(
`/api/docker/status/${config.container}/${config.server || ""}`,
{
- refreshInterval: 1500,
+ refreshInterval: 5000,
}
);
const { data: statsData, error: statsError } = useSWR(
`/api/docker/stats/${config.container}/${config.server || ""}`,
{
- refreshInterval: 1500,
+ refreshInterval: 5000,
}
);
diff --git a/src/components/services/widgets/service/emby.jsx b/src/components/services/widgets/service/emby.jsx
index af22338c..ac964566 100644
--- a/src/components/services/widgets/service/emby.jsx
+++ b/src/components/services/widgets/service/emby.jsx
@@ -3,17 +3,12 @@ import useSWR from "swr";
import Widget from "../widget";
import Block from "../block";
+import { formatApiUrl } from "utils/api-helpers";
+
export default function Emby({ service, title = "Emby" }) {
const config = service.widget;
- function buildApiUrl(endpoint) {
- const { url, key } = config;
- return `${url}/emby/${endpoint}?api_key=${key}`;
- }
-
- const { data: sessionsData, error: sessionsError } = useSWR(buildApiUrl(`Sessions`), {
- refreshInterval: 1000,
- });
+ const { data: sessionsData, error: sessionsError } = useSWR(formatApiUrl(config, "Sessions"));
if (sessionsError) {
return ;
diff --git a/src/components/services/widgets/service/jellyseerr.jsx b/src/components/services/widgets/service/jellyseerr.jsx
index 9d1ebad8..3820c2a1 100644
--- a/src/components/services/widgets/service/jellyseerr.jsx
+++ b/src/components/services/widgets/service/jellyseerr.jsx
@@ -3,49 +3,32 @@ import useSWR from "swr";
import Widget from "../widget";
import Block from "../block";
+import { formatApiUrl } from "utils/api-helpers";
+
export default function Jellyseerr({ service }) {
- const config = service.widget;
+ const config = service.widget;
- function buildApiUrl(endpoint) {
- const { url } = config;
- const reqUrl = new URL(`/api/v1/${endpoint}`, url);
- return `/api/proxy?url=${encodeURIComponent(reqUrl)}`;
- }
+ const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `request/count`));
- const fetcher = async (url) => {
- const res = await fetch(url, {
- method: "GET",
- withCredentials: true,
- credentials: "include",
- headers: {
- "X-Api-Key": `${config.key}`,
- "Content-Type": "application/json"
- }
- });
- return await res.json();
- };
-
- const { data: statsData, error: statsError } = useSWR(buildApiUrl(`request/count`), fetcher);
-
- if (statsError) {
- return ;
- }
-
- if (!statsData) {
- return (
-
-
-
-
-
- );
- }
+ if (statsError) {
+ return ;
+ }
+ if (!statsData) {
return (
-
-
-
-
-
+
+
+
+
+
);
+ }
+
+ return (
+
+
+
+
+
+ );
}
diff --git a/src/components/services/widgets/service/npm.jsx b/src/components/services/widgets/service/npm.jsx
index 083ee27a..916136e1 100644
--- a/src/components/services/widgets/service/npm.jsx
+++ b/src/components/services/widgets/service/npm.jsx
@@ -3,39 +3,12 @@ import useSWR from "swr";
import Widget from "../widget";
import Block from "../block";
+import { formatApiUrl } from "utils/api-helpers";
+
export default function Npm({ service }) {
const config = service.widget;
- const { url } = config;
- const fetcher = async (reqUrl) => {
- const { url, username, password } = config;
- const loginUrl = `${url}/api/tokens`;
- const body = { identity: username, secret: password };
-
- const res = await fetch(loginUrl, {
- method: "POST",
- body: JSON.stringify(body),
- headers: {
- "Content-Type": "application/json",
- },
- })
- .then((response) => response.json())
- .then(
- async (data) =>
- await fetch(reqUrl, {
- method: "GET",
- headers: {
- "Content-Type": "application/json",
- Authorization: "Bearer " + data.token,
- },
- })
- );
- return res.json();
- };
-
- const { data: infoData, error: infoError } = useSWR(`${url}/api/nginx/proxy-hosts`, fetcher);
-
- console.log(infoData);
+ const { data: infoData, error: infoError } = useSWR(formatApiUrl(config, "nginx/proxy-hosts"));
if (infoError) {
return ;
diff --git a/src/components/services/widgets/service/nzbget.jsx b/src/components/services/widgets/service/nzbget.jsx
index 87d77b91..56baa631 100644
--- a/src/components/services/widgets/service/nzbget.jsx
+++ b/src/components/services/widgets/service/nzbget.jsx
@@ -1,44 +1,15 @@
import useSWR from "swr";
-import { JSONRPCClient } from "json-rpc-2.0";
-
-import { formatBytes } from "utils/stats-helpers";
import Widget from "../widget";
import Block from "../block";
+import { formatApiUrl } from "utils/api-helpers";
+import { formatBytes } from "utils/stats-helpers";
+
export default function Nzbget({ service }) {
const config = service.widget;
- const constructedUrl = new URL(config.url);
- constructedUrl.pathname = "jsonrpc";
-
- const client = new JSONRPCClient((jsonRPCRequest) =>
- fetch(constructedUrl.toString(), {
- method: "POST",
- headers: {
- "content-type": "application/json",
- authorization: `Basic ${btoa(`${config.username}:${config.password}`)}`,
- },
- body: JSON.stringify(jsonRPCRequest),
- }).then(async (response) => {
- if (response.status === 200) {
- const jsonRPCResponse = await response.json();
- return client.receive(jsonRPCResponse);
- } else if (jsonRPCRequest.id !== undefined) {
- return Promise.reject(new Error(response.statusText));
- }
- })
- );
-
- const { data: statusData, error: statusError } = useSWR(
- "status",
- (resource) => {
- return client.request(resource).then((response) => response);
- },
- {
- refreshInterval: 1000,
- }
- );
+ const { data: statusData, error: statusError } = useSWR(formatApiUrl(config, "status"));
if (statusError) {
return ;
diff --git a/src/components/services/widgets/service/ombi.jsx b/src/components/services/widgets/service/ombi.jsx
index 54c44f7a..4bbf96fd 100644
--- a/src/components/services/widgets/service/ombi.jsx
+++ b/src/components/services/widgets/service/ombi.jsx
@@ -3,27 +3,12 @@ import useSWR from "swr";
import Widget from "../widget";
import Block from "../block";
+import { formatApiUrl } from "utils/api-helpers";
+
export default function Ombi({ service }) {
const config = service.widget;
- function buildApiUrl(endpoint) {
- const { url } = config;
- return `${url}/api/v1/${endpoint}`;
- }
-
- const fetcher = (url) => {
- return fetch(url, {
- method: "GET",
- withCredentials: true,
- credentials: "include",
- headers: {
- ApiKey: `${config.key}`,
- "Content-Type": "application/json",
- },
- }).then((res) => res.json());
- };
-
- const { data: statsData, error: statsError } = useSWR(buildApiUrl(`Request/count`), fetcher);
+ const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, `Request/count`));
if (statsError) {
return ;
diff --git a/src/components/services/widgets/service/pihole.jsx b/src/components/services/widgets/service/pihole.jsx
index c29b5d6c..7c777ce7 100644
--- a/src/components/services/widgets/service/pihole.jsx
+++ b/src/components/services/widgets/service/pihole.jsx
@@ -3,21 +3,12 @@ import useSWR from "swr";
import Widget from "../widget";
import Block from "../block";
+import { formatApiUrl } from "utils/api-helpers";
+
export default function Pihole({ service }) {
const config = service.widget;
- function buildApiUrl(endpoint) {
- const { url, proxy } = config;
-
- if (proxy) {
- const fullUrl = `${url}/admin/${endpoint}`;
- return "/api/proxy?url=" + encodeURIComponent(fullUrl);
- }
-
- return `${url}/admin/${endpoint}`;
- }
-
- const { data: piholeData, error: piholeError } = useSWR(buildApiUrl("api.php"));
+ const { data: piholeData, error: piholeError } = useSWR(formatApiUrl(config, "api.php"));
if (piholeError) {
return ;
diff --git a/src/components/services/widgets/service/portainer.jsx b/src/components/services/widgets/service/portainer.jsx
index ed3d184a..1e97052d 100644
--- a/src/components/services/widgets/service/portainer.jsx
+++ b/src/components/services/widgets/service/portainer.jsx
@@ -3,29 +3,12 @@ import useSWR from "swr";
import Widget from "../widget";
import Block from "../block";
+import { formatApiUrl } from "utils/api-helpers";
+
export default function Portainer({ service }) {
const config = service.widget;
- function buildApiUrl(endpoint) {
- const { url, env } = config;
- const reqUrl = new URL(`/api/endpoints/${env}/${endpoint}`, url);
- return `/api/proxy?url=${encodeURIComponent(reqUrl)}`;
- }
-
- const fetcher = async (url) => {
- const res = await fetch(url, {
- method: "GET",
- withCredentials: true,
- credentials: "include",
- headers: {
- "X-API-Key": `${config.key}`,
- "Content-Type": "application/json",
- },
- });
- return await res.json();
- };
-
- const { data: containersData, error: containersError } = useSWR(buildApiUrl(`docker/containers/json?all=1`), fetcher);
+ const { data: containersData, error: containersError } = useSWR(formatApiUrl(config, `docker/containers/json?all=1`));
if (containersError) {
return ;
diff --git a/src/components/services/widgets/service/radarr.jsx b/src/components/services/widgets/service/radarr.jsx
index 4e4bb3c6..fec155c8 100644
--- a/src/components/services/widgets/service/radarr.jsx
+++ b/src/components/services/widgets/service/radarr.jsx
@@ -3,16 +3,13 @@ import useSWR from "swr";
import Widget from "../widget";
import Block from "../block";
+import { formatApiUrl } from "utils/api-helpers";
+
export default function Radarr({ service }) {
const config = service.widget;
- function buildApiUrl(endpoint) {
- const { url, key } = config;
- return `${url}/api/v3/${endpoint}?apikey=${key}`;
- }
-
- const { data: moviesData, error: moviesError } = useSWR(buildApiUrl("movie"));
- const { data: queuedData, error: queuedError } = useSWR(buildApiUrl("queue/status"));
+ const { data: moviesData, error: moviesError } = useSWR(formatApiUrl(config, "movie"));
+ const { data: queuedData, error: queuedError } = useSWR(formatApiUrl(config, "queue/status"));
if (moviesError || queuedError) {
return ;
diff --git a/src/components/services/widgets/service/rutorrent.jsx b/src/components/services/widgets/service/rutorrent.jsx
index e5cb12b6..5dd11333 100644
--- a/src/components/services/widgets/service/rutorrent.jsx
+++ b/src/components/services/widgets/service/rutorrent.jsx
@@ -1,32 +1,15 @@
import useSWR from "swr";
-import RuTorrent from "rutorrent-promise";
-
-import { formatBytes } from "utils/stats-helpers";
import Widget from "../widget";
import Block from "../block";
+import { formatApiUrl } from "utils/api-helpers";
+import { formatBytes } from "utils/stats-helpers";
+
export default function Rutorrent({ service }) {
const config = service.widget;
- function buildApiUrl() {
- const { url, username, password } = config;
-
- const options = {
- url: `${url}/plugins/httprpc/action.php`,
- };
-
- if (username && password) {
- options.username = username;
- options.password = password;
- }
-
- const params = new URLSearchParams(options);
-
- return `/api/widgets/rutorrent?${params.toString()}`;
- }
-
- const { data: statusData, error: statusError } = useSWR(buildApiUrl());
+ const { data: statusData, error: statusError } = useSWR(formatApiUrl(config));
if (statusError) {
return ;
diff --git a/src/components/services/widgets/service/sonarr.jsx b/src/components/services/widgets/service/sonarr.jsx
index 36afee20..fe042a19 100644
--- a/src/components/services/widgets/service/sonarr.jsx
+++ b/src/components/services/widgets/service/sonarr.jsx
@@ -3,17 +3,14 @@ import useSWR from "swr";
import Widget from "../widget";
import Block from "../block";
+import { formatApiUrl } from "utils/api-helpers";
+
export default function Sonarr({ service }) {
const config = service.widget;
- function buildApiUrl(endpoint) {
- const { url, key } = config;
- return `${url}/api/v3/${endpoint}?apikey=${key}`;
- }
-
- const { data: wantedData, error: wantedError } = useSWR(buildApiUrl("wanted/missing"));
- const { data: queuedData, error: queuedError } = useSWR(buildApiUrl("queue"));
- const { data: seriesData, error: seriesError } = useSWR(buildApiUrl("series"));
+ const { data: wantedData, error: wantedError } = useSWR(formatApiUrl(config, "wanted/missing"));
+ const { data: queuedData, error: queuedError } = useSWR(formatApiUrl(config, "queue"));
+ const { data: seriesData, error: seriesError } = useSWR(formatApiUrl(config, "series"));
if (wantedError || queuedError || seriesError) {
return ;
diff --git a/src/components/services/widgets/service/speedtest.jsx b/src/components/services/widgets/service/speedtest.jsx
index b2e28007..7bcf884d 100644
--- a/src/components/services/widgets/service/speedtest.jsx
+++ b/src/components/services/widgets/service/speedtest.jsx
@@ -4,16 +4,12 @@ import Widget from "../widget";
import Block from "../block";
import { formatBits } from "utils/stats-helpers";
+import { formatApiUrl } from "utils/api-helpers";
export default function Speedtest({ service }) {
const config = service.widget;
- function buildApiUrl(endpoint) {
- const { url } = config;
- return `${url}/api/${endpoint}`;
- }
-
- const { data: speedtestData, error: speedtestError } = useSWR(buildApiUrl("speedtest/latest"));
+ const { data: speedtestData, error: speedtestError } = useSWR(formatApiUrl(config, "speedtest/latest"));
if (speedtestError) {
return ;
@@ -31,8 +27,8 @@ export default function Speedtest({ service }) {
return (
-
-
+
+
);
diff --git a/src/components/services/widgets/service/tautulli.jsx b/src/components/services/widgets/service/tautulli.jsx
index bed8afa2..32fe91b3 100644
--- a/src/components/services/widgets/service/tautulli.jsx
+++ b/src/components/services/widgets/service/tautulli.jsx
@@ -3,18 +3,12 @@ import useSWR from "swr";
import Widget from "../widget";
import Block from "../block";
+import { formatApiUrl } from "utils/api-helpers";
+
export default function Tautulli({ service }) {
const config = service.widget;
- function buildApiUrl(endpoint) {
- const { url, key } = config;
- const fullUrl = `${url}/api/v2?apikey=${key}&cmd=${endpoint}`;
- return "/api/proxy?url=" + encodeURIComponent(fullUrl);
- }
-
- const { data: statsData, error: statsError } = useSWR(buildApiUrl("get_activity"), {
- refreshInterval: 1000,
- });
+ const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, "get_activity"));
if (statsError) {
return ;
diff --git a/src/components/services/widgets/service/traefik.jsx b/src/components/services/widgets/service/traefik.jsx
index 05811ebb..ce3fd4fe 100644
--- a/src/components/services/widgets/service/traefik.jsx
+++ b/src/components/services/widgets/service/traefik.jsx
@@ -3,16 +3,12 @@ import useSWR from "swr";
import Widget from "../widget";
import Block from "../block";
+import { formatApiUrl } from "utils/api-helpers";
+
export default function Traefik({ service }) {
const config = service.widget;
- function buildApiUrl(endpoint) {
- const { url } = config;
- const fullUrl = `${url}/api/${endpoint}`;
- return `/api/proxy?url=${encodeURIComponent(fullUrl)}`;
- }
-
- const { data: traefikData, error: traefikError } = useSWR(buildApiUrl("overview"));
+ const { data: traefikData, error: traefikError } = useSWR(formatApiUrl(config, "overview"));
if (traefikError) {
return ;
diff --git a/src/pages/api/services/index.js b/src/pages/api/services/index.js
index 53d50869..572ab404 100644
--- a/src/pages/api/services/index.js
+++ b/src/pages/api/services/index.js
@@ -15,10 +15,23 @@ export default async function handler(req, res) {
return {
name: Object.keys(group)[0],
services: group[Object.keys(group)[0]].map((entries) => {
- return {
+ const { widget, ...service } = entries[Object.keys(entries)[0]];
+ let res = {
name: Object.keys(entries)[0],
- ...entries[Object.keys(entries)[0]],
+ ...service,
};
+
+ if (widget) {
+ const { type } = widget;
+
+ res.widget = {
+ type: type,
+ service_group: Object.keys(group)[0],
+ service_name: Object.keys(entries)[0],
+ };
+ }
+
+ return res;
}),
};
});
diff --git a/src/pages/api/services/proxy.js b/src/pages/api/services/proxy.js
new file mode 100644
index 00000000..029b8187
--- /dev/null
+++ b/src/pages/api/services/proxy.js
@@ -0,0 +1,36 @@
+import genericProxyHandler from "utils/proxies/generic";
+import credentialedProxyHandler from "utils/proxies/credentialed";
+import rutorrentProxyHandler from "utils/proxies/rutorrent";
+import nzbgetProxyHandler from "utils/proxies/nzbget";
+import npmProxyHandler from "utils/proxies/npm";
+
+const serviceProxyHandlers = {
+ // uses query param auth
+ emby: genericProxyHandler,
+ pihole: genericProxyHandler,
+ radarr: genericProxyHandler,
+ sonarr: genericProxyHandler,
+ speedtest: genericProxyHandler,
+ tautulli: genericProxyHandler,
+ traefik: genericProxyHandler,
+ // uses X-API-Key header auth
+ portainer: credentialedProxyHandler,
+ jellyseerr: credentialedProxyHandler,
+ ombi: credentialedProxyHandler,
+ // super specific handlers
+ rutorrent: rutorrentProxyHandler,
+ nzbget: nzbgetProxyHandler,
+ npm: npmProxyHandler,
+};
+
+export default async function handler(req, res) {
+ const { type } = req.query;
+
+ const serviceProxyHandler = serviceProxyHandlers[type];
+
+ if (serviceProxyHandler) {
+ return serviceProxyHandler(req, res);
+ }
+
+ res.status(403).json({ error: "Unkown proxy service type" });
+}
diff --git a/src/pages/api/widgets/rutorrent.js b/src/pages/api/widgets/rutorrent.js
deleted file mode 100644
index 1e60edd9..00000000
--- a/src/pages/api/widgets/rutorrent.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import RuTorrent from "rutorrent-promise";
-
-// TODO: Remove the 3rd party dependency once I figure out how to
-// call this myself with fetch. Just need to destruct the package.
-
-export default async function handler(req, res) {
- const { url, username, password } = req.query;
-
- const constructedUrl = new URL(url);
-
- const rutorrent = new RuTorrent({
- host: constructedUrl.hostname, // default: localhost
- port: constructedUrl.port, // default: 80
- path: constructedUrl.pathname, // default: /rutorrent
- ssl: constructedUrl.protocol === "https:", // default: false
- username: username, // default: none
- password: password, // default: none
- });
-
- const data = await rutorrent.get(["d.get_down_rate", "d.get_up_rate", "d.get_state"]);
-
- res.status(200).send(data);
-}
diff --git a/src/utils/api-helpers.js b/src/utils/api-helpers.js
new file mode 100644
index 00000000..fa7e489b
--- /dev/null
+++ b/src/utils/api-helpers.js
@@ -0,0 +1,34 @@
+const formats = {
+ emby: `{url}/emby/{endpoint}?api_key={key}`,
+ pihole: `{url}/admin/{endpoint}`,
+ radarr: `{url}/api/v3/{endpoint}?apikey={key}`,
+ sonarr: `{url}/api/v3/{endpoint}?apikey={key}`,
+ speedtest: `{url}/api/{endpoint}`,
+ tautulli: `{url}/api/v2?apikey={key}&cmd={endpoint}`,
+ traefik: `{url}/api/{endpoint}`,
+ portainer: `{url}/api/endpoints/{env}/{endpoint}`,
+ rutorrent: `{url}/plugins/httprpc/action.php`,
+ jellyseerr: `{url}/api/v1/{endpoint}`,
+ ombi: `{url}/api/v1/{endpoint}`,
+ npm: `{url}/api/{endpoint}`,
+};
+
+export function formatApiCall(api, args) {
+ const match = /\{.*?\}/g;
+ const replace = (match) => {
+ const key = match.replace(/\{|\}/g, "");
+ return args[key];
+ };
+
+ return formats[api].replace(match, replace);
+}
+
+export function formatApiUrl(widget, endpoint) {
+ const params = new URLSearchParams({
+ type: widget.type,
+ group: widget.service_group,
+ service: widget.service_name,
+ endpoint,
+ });
+ return `/api/services/proxy?${params.toString()}`;
+}
diff --git a/src/utils/http.js b/src/utils/http.js
index 8d37a2b3..91c8c5fb 100644
--- a/src/utils/http.js
+++ b/src/utils/http.js
@@ -44,3 +44,20 @@ export function httpRequest(url, params) {
request.end();
});
}
+
+export function httpProxy(url, params = {}) {
+ const constructedUrl = new URL(url);
+
+ if (constructedUrl.protocol === "https:") {
+ const httpsAgent = new https.Agent({
+ rejectUnauthorized: false,
+ });
+
+ return httpsRequest(constructedUrl, {
+ agent: httpsAgent,
+ ...params,
+ });
+ } else {
+ return httpRequest(constructedUrl, params);
+ }
+}
diff --git a/src/utils/proxies/credentialed.js b/src/utils/proxies/credentialed.js
new file mode 100644
index 00000000..f1348746
--- /dev/null
+++ b/src/utils/proxies/credentialed.js
@@ -0,0 +1,28 @@
+import { getServiceWidget } from "utils/service-helpers";
+import { formatApiCall } from "utils/api-helpers";
+import { httpProxy } from "utils/http";
+
+export default async function credentialedProxyHandler(req, res) {
+ const { group, service, endpoint } = req.query;
+
+ if (group && service) {
+ const widget = await getServiceWidget(group, service);
+
+ if (widget) {
+ const url = new URL(formatApiCall(widget.type, { endpoint, ...widget }));
+ const [status, contentType, data] = await httpProxy(url, {
+ withCredentials: true,
+ credentials: "include",
+ headers: {
+ "X-API-Key": `${widget.key}`,
+ "Content-Type": "application/json",
+ },
+ });
+
+ res.setHeader("Content-Type", contentType);
+ return res.status(status).send(data);
+ }
+ }
+
+ return res.status(400).json({ error: "Invalid proxy service type" });
+}
diff --git a/src/utils/proxies/generic.js b/src/utils/proxies/generic.js
new file mode 100644
index 00000000..6899ebf9
--- /dev/null
+++ b/src/utils/proxies/generic.js
@@ -0,0 +1,21 @@
+import { getServiceWidget } from "utils/service-helpers";
+import { formatApiCall } from "utils/api-helpers";
+import { httpProxy } from "utils/http";
+
+export default async function genericProxyHandler(req, res) {
+ const { group, service, endpoint } = req.query;
+
+ if (group && service) {
+ const widget = await getServiceWidget(group, service);
+
+ if (widget) {
+ const url = new URL(formatApiCall(widget.type, { endpoint, ...widget }));
+ const [status, contentType, data] = await httpProxy(url);
+
+ res.setHeader("Content-Type", contentType);
+ return res.status(status).send(data);
+ }
+ }
+
+ return res.status(400).json({ error: "Invalid proxy service type" });
+}
diff --git a/src/utils/proxies/npm.js b/src/utils/proxies/npm.js
new file mode 100644
index 00000000..d6c68cd8
--- /dev/null
+++ b/src/utils/proxies/npm.js
@@ -0,0 +1,37 @@
+import { getServiceWidget } from "utils/service-helpers";
+import { formatApiCall } from "utils/api-helpers";
+
+export default async function npmProxyHandler(req, res) {
+ const { group, service, endpoint } = req.query;
+
+ if (group && service) {
+ const widget = await getServiceWidget(group, service);
+
+ if (widget) {
+ const url = new URL(formatApiCall(widget.type, { endpoint, ...widget }));
+
+ const loginUrl = `${widget.url}/api/tokens`;
+ const body = { identity: widget.username, secret: widget.password };
+
+ const authResponse = await fetch(loginUrl, {
+ method: "POST",
+ body: JSON.stringify(body),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }).then((response) => response.json());
+
+ const apiResponse = await fetch(url, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: "Bearer " + authResponse.token,
+ },
+ }).then((response) => response.json());
+
+ return res.send(apiResponse);
+ }
+ }
+
+ return res.status(400).json({ error: "Invalid proxy service type" });
+}
diff --git a/src/utils/proxies/nzbget.js b/src/utils/proxies/nzbget.js
new file mode 100644
index 00000000..7bf078b4
--- /dev/null
+++ b/src/utils/proxies/nzbget.js
@@ -0,0 +1,39 @@
+import { JSONRPCClient } from "json-rpc-2.0";
+import { getServiceWidget } from "utils/service-helpers";
+
+export default async function nzbgetProxyHandler(req, res) {
+ const { group, service, endpoint } = req.query;
+
+ if (group && service) {
+ const widget = await getServiceWidget(group, service);
+
+ if (widget) {
+ const constructedUrl = new URL(widget.url);
+ constructedUrl.pathname = "jsonrpc";
+
+ const authorization = Buffer.from(`${widget.username}:${widget.password}`).toString("base64");
+
+ const client = new JSONRPCClient((jsonRPCRequest) =>
+ fetch(constructedUrl.toString(), {
+ method: "POST",
+ headers: {
+ "content-type": "application/json",
+ authorization: `Basic ${authorization}`,
+ },
+ body: JSON.stringify(jsonRPCRequest),
+ }).then(async (response) => {
+ if (response.status === 200) {
+ const jsonRPCResponse = await response.json();
+ return client.receive(jsonRPCResponse);
+ } else if (jsonRPCRequest.id !== undefined) {
+ return Promise.reject(new Error(response.statusText));
+ }
+ })
+ );
+
+ return res.send(await client.request(endpoint));
+ }
+ }
+
+ return res.status(400).json({ error: "Invalid proxy service type" });
+}
diff --git a/src/utils/proxies/rutorrent.js b/src/utils/proxies/rutorrent.js
new file mode 100644
index 00000000..3f9dda3c
--- /dev/null
+++ b/src/utils/proxies/rutorrent.js
@@ -0,0 +1,30 @@
+import RuTorrent from "rutorrent-promise";
+
+import { getServiceWidget } from "utils/service-helpers";
+
+export default async function rutorrentProxyHandler(req, res) {
+ const { group, service } = req.query;
+
+ if (group && service) {
+ const widget = await getServiceWidget(group, service);
+
+ if (widget) {
+ const constructedUrl = new URL(widget.url);
+
+ const rutorrent = new RuTorrent({
+ host: constructedUrl.hostname,
+ port: constructedUrl.port,
+ path: constructedUrl.pathname,
+ ssl: constructedUrl.protocol === "https:",
+ username: widget.username,
+ password: widget.password,
+ });
+
+ const data = await rutorrent.get(["d.get_down_rate", "d.get_up_rate", "d.get_state"]);
+
+ return res.status(200).send(data);
+ }
+ }
+
+ return res.status(400).json({ error: "Invalid proxy service type" });
+}
diff --git a/src/utils/service-helpers.js b/src/utils/service-helpers.js
new file mode 100644
index 00000000..986fc227
--- /dev/null
+++ b/src/utils/service-helpers.js
@@ -0,0 +1,33 @@
+import { promises as fs } from "fs";
+import path from "path";
+import yaml from "js-yaml";
+
+export async function getServiceWidget(group, service) {
+ const servicesYaml = path.join(process.cwd(), "config", "services.yaml");
+ const fileContents = await fs.readFile(servicesYaml, "utf8");
+ const services = yaml.load(fileContents);
+
+ // map easy to write YAML objects into easy to consume JS arrays
+ const servicesArray = services.map((group) => {
+ return {
+ name: Object.keys(group)[0],
+ services: group[Object.keys(group)[0]].map((entries) => {
+ return {
+ name: Object.keys(entries)[0],
+ ...entries[Object.keys(entries)[0]],
+ };
+ }),
+ };
+ });
+
+ const serviceGroup = servicesArray.find((g) => g.name === group);
+ if (serviceGroup) {
+ const serviceEntry = serviceGroup.services.find((s) => s.name === service);
+ if (serviceEntry) {
+ const { widget } = serviceEntry;
+ return widget;
+ }
+ }
+
+ return false;
+}