diff --git a/backend/routes/main.js b/backend/routes/main.js index 7bc4323d..94682cfb 100644 --- a/backend/routes/main.js +++ b/backend/routes/main.js @@ -14,6 +14,7 @@ import schemaRoutes from "./schema.js"; import settingsRoutes from "./settings.js"; import tokensRoutes from "./tokens.js"; import usersRoutes from "./users.js"; +import versionRoutes from "./version.js"; const router = express.Router({ caseSensitive: true, @@ -46,6 +47,7 @@ router.use("/users", usersRoutes); router.use("/audit-log", auditLogRoutes); router.use("/reports", reportsRoutes); router.use("/settings", settingsRoutes); +router.use("/version", versionRoutes); router.use("/nginx/proxy-hosts", proxyHostsRoutes); router.use("/nginx/redirection-hosts", redirectionHostsRoutes); router.use("/nginx/dead-hosts", deadHostsRoutes); diff --git a/backend/routes/version.js b/backend/routes/version.js new file mode 100644 index 00000000..ac1ac4d7 --- /dev/null +++ b/backend/routes/version.js @@ -0,0 +1,101 @@ +import express from "express"; +import { debug, express as logger } from "../logger.js"; +import pjson from "../package.json" with { type: "json" }; +import https from "node:https"; +import { ProxyAgent } from "proxy-agent"; + +const router = express.Router({ + caseSensitive: true, + strict: true, + mergeParams: true, +}); + +/** + * /api/version/check + */ +router + .route("/check") + .options((_, res) => { + res.sendStatus(204); + }) + + /** + * GET /api/version/check + * + * Check for available updates + */ + .get(async (req, res, next) => { + try { + const agent = new ProxyAgent(); + const url = "https://api.github.com/repos/NginxProxyManager/nginx-proxy-manager/releases/latest"; + + const data = await new Promise((resolve, reject) => { + https + .get(url, { agent }, (response) => { + if (response.statusCode !== 200) { + reject(new Error(`GitHub API returned ${response.statusCode}`)); + return; + } + + response.setEncoding("utf8"); + let raw_data = ""; + + response.on("data", (chunk) => { + raw_data += chunk; + }); + + response.on("end", () => { + try { + resolve(JSON.parse(raw_data)); + } catch (err) { + reject(err); + } + }); + }) + .on("error", (err) => { + reject(err); + }); + }); + + const latestVersion = data.tag_name; + + const version = pjson.version.split("-").shift().split("."); + const currentVersion = `v${version[0]}.${version[1]}.${version[2]}`; + + res.status(200).send({ + current: currentVersion, + latest: latestVersion, + updateAvailable: compareVersions(currentVersion, latestVersion), + }); + } catch (error) { + debug(logger, `${req.method.toUpperCase()} ${req.path}: ${error}`); + res.status(200).send({ + current: null, + latest: null, + updateAvailable: false, + }); + } + }); + +/** + * Compare two version strings + * + */ +function compareVersions(current, latest) { + const cleanCurrent = current.replace(/^v/, ""); + const cleanLatest = latest.replace(/^v/, ""); + + const currentParts = cleanCurrent.split(".").map(Number); + const latestParts = cleanLatest.split(".").map(Number); + + for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) { + const curr = currentParts[i] || 0; + const lat = latestParts[i] || 0; + + if (lat > curr) return true; + if (lat < curr) return false; + } + return false; +} + +export default router; diff --git a/frontend/src/components/SiteFooter.tsx b/frontend/src/components/SiteFooter.tsx index a1778395..2135d2c6 100644 --- a/frontend/src/components/SiteFooter.tsx +++ b/frontend/src/components/SiteFooter.tsx @@ -1,8 +1,11 @@ +import { useEffect, useState } from "react"; import { useHealth } from "src/hooks"; import { T } from "src/locale"; export function SiteFooter() { const health = useHealth(); + const [latestVersion, setLatestVersion] = useState(null); + const [isNewVersionAvailable, setIsNewVersionAvailable] = useState(false); const getVersion = () => { if (!health.data) { @@ -12,6 +15,25 @@ export function SiteFooter() { return `v${v.major}.${v.minor}.${v.revision}`; }; + useEffect(() => { + const checkForUpdates = async () => { + try { + const response = await fetch("/api/version/check"); + if (response.ok) { + const data = await response.json(); + setLatestVersion(data.latest); + setIsNewVersionAvailable(data.updateAvailable); + } + } catch (error) { + console.debug("Could not check for updates:", error); + } + }; + + if (health.data) { + checkForUpdates(); + } + }, [health.data]); + return (