Pārlūkot izejas kodu

Merge pull request #4906 from sopex/develop

Available upgrade notification
jc21 1 mēnesi atpakaļ
vecāks
revīzija
8838dabe8a

+ 2 - 0
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);

+ 101 - 0
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;

+ 35 - 0
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<string | null>(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 (
 		<footer className="footer d-print-none py-3">
 			<div className="container-xl">
@@ -55,6 +77,19 @@ export function SiteFooter() {
 									{getVersion()}{" "}
 								</a>
 							</li>
+							{isNewVersionAvailable && latestVersion && (
+								<li className="list-inline-item">
+									<a
+										href={`https://github.com/NginxProxyManager/nginx-proxy-manager/releases/tag/${latestVersion}`}
+										className="link-warning fw-bold"
+										target="_blank"
+										rel="noopener"
+										title={`New version ${latestVersion} is available`}
+									>
+										Update Available: ({latestVersion})
+									</a>
+								</li>
+							)}
 						</ul>
 					</div>
 				</div>