diff --git a/public/locales/en/common.json b/public/locales/en/common.json index d8bfb039..9f5637bf 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -88,6 +88,13 @@ "bitrate": "Bitrate", "no_active": "No Active Streams" }, + "omada": { + "connectedAp": "Connected APs", + "activeUser": "Active devices", + "alerts": "Alerts", + "connectedGateway": "Connected gateways", + "connectedSwitches": "Connected switches" + }, "nzbget": { "rate": "Rate", "remaining": "Remaining", diff --git a/src/widgets/components.js b/src/widgets/components.js index eb7c686f..9f3011b3 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -26,6 +26,7 @@ const components = { nextdns: dynamic(() => import("./nextdns/component")), npm: dynamic(() => import("./npm/component")), nzbget: dynamic(() => import("./nzbget/component")), + omada: dynamic(() => import("./omada/component")), ombi: dynamic(() => import("./ombi/component")), overseerr: dynamic(() => import("./overseerr/component")), paperlessngx: dynamic(() => import("./paperlessngx/component")), diff --git a/src/widgets/omada/component.jsx b/src/widgets/omada/component.jsx new file mode 100644 index 00000000..dee60842 --- /dev/null +++ b/src/widgets/omada/component.jsx @@ -0,0 +1,39 @@ +import { useTranslation } from "next-i18next"; + +import useWidgetAPI from "../../utils/proxy/use-widget-api"; +import Container from "../../components/services/widget/container"; +import Block from "../../components/services/widget/block"; + +export default function Component({ service }) { + const { t } = useTranslation(); + + const { widget } = service; + + const { data: omadaData, error: omadaAPIError } = useWidgetAPI(widget, { + refreshInterval: 5000, + }); + + if (omadaAPIError) { + return ; + } + + if (!omadaData) { + return ( + + + + + + ); + } + + return ( + + + + + { omadaData.connectedGateways > 0 && } + { omadaData.connectedSwitches > 0 && } + + ); +} diff --git a/src/widgets/omada/proxy.js b/src/widgets/omada/proxy.js new file mode 100644 index 00000000..e89ad81d --- /dev/null +++ b/src/widgets/omada/proxy.js @@ -0,0 +1,252 @@ + +import { httpProxy } from "utils/proxy/http"; +import getServiceWidget from "utils/config/service-helpers"; +import createLogger from "utils/logger"; + +const proxyName = "omadaProxyHandler"; + +const logger = createLogger(proxyName); + +async function login(loginUrl, username, password, controllerVersionMajor) { + const params = { + username, + password + } + + if (controllerVersionMajor === 3) { + params.method = "login"; + params.params = { + name: username, + password + }; + } + + // eslint-disable-next-line no-unused-vars + const [status, contentType, data] = await httpProxy(loginUrl, { + method: "POST", + body: JSON.stringify(params), + headers: { + "Content-Type": "application/json", + }, + }); + + return [status, JSON.parse(data.toString())]; +} + + +export default async function omadaProxyHandler(req, res) { + const { group, service } = req.query; + + if (group && service) { + const widget = await getServiceWidget(group, service); + + if (widget) { + + const {url} = widget; + + const controllerInfoURL = `${widget.url}/api/info`; + + let [status, contentType, data] = await httpProxy(controllerInfoURL, { + headers: { + "Content-Type": "application/json", + }, + }); + + if (status !== 200) { + logger.error("Unable to retrieve Omada controller info"); + return res.status(status).json({error: {message: `HTTP Error ${status}`, url: controllerInfoURL, data}}); + } + + let cId; + let controllerVersion; + + try { + cId = JSON.parse(data).result.omadacId; + controllerVersion = JSON.parse(data).result.controllerVer; + } catch (e) { + controllerVersion = "3.2.x" + } + + const controllerVersionMajor = parseInt(controllerVersion.split('.')[0], 10) + + if (![3,4,5].includes(controllerVersionMajor)) { + return res.status(500).json({error: {message: "Error determining controller version", data}}); + } + + let loginUrl; + + switch (controllerVersionMajor) { + case 3: + loginUrl = `${widget.url}/api/user/login?ajax`; + break; + case 4: + loginUrl = `${widget.url}/api/v2/login`; + break; + case 5: + loginUrl = `${widget.url}/${cId}/api/v2/login`; + break; + default: + break; + } + + const [loginStatus, loginResponseData] = await login(loginUrl, widget.username, widget.password, controllerVersionMajor); + + if (loginStatus !== 200 || loginResponseData.errorCode > 0) { + return res.status(status).json({error: {message: "Error logging in to Oamda controller", url: loginUrl, data: loginResponseData}}); + } + + const { token } = loginResponseData.result; + + let sitesUrl; + let body = {}; + let params = { token }; + let headers = { "Csrf-Token": token }; + let method = "GET"; + + switch (controllerVersionMajor) { + case 3: + sitesUrl = `${widget.url}/web/v1/controller?ajax=&token=${token}`; + body = { + "method": "getUserSites", + "params": { + "userName": widget.username + } + }; + method = "POST"; + break; + case 4: + sitesUrl = `${widget.url}/api/v2/sites?token=${token}¤tPage=1¤tPageSize=1000`; + break; + case 5: + sitesUrl = `${widget.url}/${cId}/api/v2/sites?token=${token}¤tPage=1¤tPageSize=1000`; + break; + default: + break; + } + + [status, contentType, data] = await httpProxy(sitesUrl, { + method, + params, + body: JSON.stringify(body), + headers, + }); + + const sitesResponseData = JSON.parse(data); + + if (sitesResponseData.errorCode > 0) { + logger.debug(`HTTTP ${status} getting sites list: ${sitesResponseData.msg}`); + return res.status(status).json({error: {message: "Error getting sites list", url, data: sitesResponseData}}); + } + + const site = (controllerVersionMajor === 3) ? + sitesResponseData.result.siteList.find(s => s.name === widget.site): + sitesResponseData.result.data.find(s => s.name === widget.site); + + if (!site) { + return res.status(status).json({error: {message: `Site ${widget.site} is not found`, url, data}}); + } + + let siteResponseData; + + let connectedAp; + let activeUser; + let connectedSwitches; + let connectedGateways; + let alerts; + + if (controllerVersionMajor === 3) { + // Omada v3 controller requires switching site + const switchUrl = `${widget.url}/web/v1/controller?ajax=&token=${token}`; + method = "POST"; + body = { + method: "switchSite", + params: { + siteName: site.siteName, + userName: widget.username + } + }; + headers = { "Content-Type": "application/json" }; + params = { token }; + + [status, contentType, data] = await httpProxy(switchUrl, { + method, + params, + body: JSON.stringify(body), + headers, + }); + + const switchResponseData = JSON.parse(data); + if (status !== 200 || switchResponseData.errorCode > 0) { + logger.error(`HTTP ${status} getting sites list: ${data}`); + return res.status(status).json({error: {message: "Error switching site", url: switchUrl, data}}); + } + + const statsUrl = `${widget.url}/web/v1/controller?getGlobalStat=&token=${token}`; + [status, contentType, data] = await httpProxy(statsUrl, { + method, + params, + body: JSON.stringify({ + "method": "getGlobalStat", + }), + headers + }); + + siteResponseData = JSON.parse(data); + + if (status !== 200 || siteResponseData.errorCode > 0) { + return res.status(status).json({error: {message: "Error getting stats", url: statsUrl, data}}); + } + + connectedAp = siteResponseData.result.connectedAp; + activeUser = siteResponseData.result.activeUser; + alerts = siteResponseData.result.alerts; + } else if (controllerVersionMajor === 4 || controllerVersionMajor === 5) { + const siteName = (controllerVersionMajor === 5) ? site.id : site.key; + const siteStatsUrl = (controllerVersionMajor === 4) ? + `${url}/api/v2/sites/${siteName}/dashboard/overviewDiagram?token=${token}¤tPage=1¤tPageSize=1000` : + `${url}/${cId}/api/v2/sites/${siteName}/dashboard/overviewDiagram?token=${token}¤tPage=1¤tPageSize=1000`; + + [status, contentType, data] = await httpProxy(siteStatsUrl, { + headers: { + "Csrf-Token": token, + }, + }); + + siteResponseData = JSON.parse(data); + + if (status !== 200 || siteResponseData.errorCode > 0) { + logger.debug(`HTTP ${status} getting stats for site ${widget.site} with message ${siteResponseData.msg}`); + return res.status(500).send(data); + } + + const alertUrl = (controllerVersionMajor === 4) ? + `${url}/api/v2/sites/${siteName}/alerts/num?token=${token}¤tPage=1¤tPageSize=1000` : + `${url}/${cId}/api/v2/sites/${siteName}/alerts/num?token=${token}¤tPage=1¤tPageSize=1000`; + + // eslint-disable-next-line no-unused-vars + [status, contentType, data] = await httpProxy(alertUrl, { + headers: { + "Csrf-Token": token, + }, + }); + const alertResponseData = JSON.parse(data); + + activeUser = siteResponseData.result.totalClientNum; + connectedAp = siteResponseData.result.connectedApNum; + connectedGateways = siteResponseData.result.connectedGatewayNum; + connectedSwitches = siteResponseData.result.connectedSwitchNum; + alerts = alertResponseData.result.alertNum; + } + + return res.send(JSON.stringify({ + connectedAp, + activeUser, + alerts, + connectedGateways, + connectedSwitches, + })); + } + } + + return res.status(400).json({ error: "Invalid proxy service type" }); +} diff --git a/src/widgets/omada/widget.js b/src/widgets/omada/widget.js new file mode 100644 index 00000000..5e32edad --- /dev/null +++ b/src/widgets/omada/widget.js @@ -0,0 +1,7 @@ +import omadaProxyHandler from "./proxy"; + +const widget = { + proxyHandler: omadaProxyHandler, +}; + +export default widget; diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index 2b45e55a..c68dfe3e 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -21,6 +21,7 @@ import navidrome from "./navidrome/widget"; import nextdns from "./nextdns/widget"; import npm from "./npm/widget"; import nzbget from "./nzbget/widget"; +import omada from "./omada/widget"; import ombi from "./ombi/widget"; import overseerr from "./overseerr/widget"; import paperlessngx from "./paperlessngx/widget"; @@ -73,6 +74,7 @@ const widgets = { nextdns, npm, nzbget, + omada, ombi, overseerr, paperlessngx,