Skip to content

Commit 8838dab

Browse files
authored
Merge pull request #4906 from sopex/develop
Available upgrade notification
2 parents 6ce9567 + b4fd242 commit 8838dab

File tree

3 files changed

+138
-0
lines changed

3 files changed

+138
-0
lines changed

backend/routes/main.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import schemaRoutes from "./schema.js";
1414
import settingsRoutes from "./settings.js";
1515
import tokensRoutes from "./tokens.js";
1616
import usersRoutes from "./users.js";
17+
import versionRoutes from "./version.js";
1718

1819
const router = express.Router({
1920
caseSensitive: true,
@@ -46,6 +47,7 @@ router.use("/users", usersRoutes);
4647
router.use("/audit-log", auditLogRoutes);
4748
router.use("/reports", reportsRoutes);
4849
router.use("/settings", settingsRoutes);
50+
router.use("/version", versionRoutes);
4951
router.use("/nginx/proxy-hosts", proxyHostsRoutes);
5052
router.use("/nginx/redirection-hosts", redirectionHostsRoutes);
5153
router.use("/nginx/dead-hosts", deadHostsRoutes);

backend/routes/version.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import express from "express";
2+
import { debug, express as logger } from "../logger.js";
3+
import pjson from "../package.json" with { type: "json" };
4+
import https from "node:https";
5+
import { ProxyAgent } from "proxy-agent";
6+
7+
const router = express.Router({
8+
caseSensitive: true,
9+
strict: true,
10+
mergeParams: true,
11+
});
12+
13+
/**
14+
* /api/version/check
15+
*/
16+
router
17+
.route("/check")
18+
.options((_, res) => {
19+
res.sendStatus(204);
20+
})
21+
22+
/**
23+
* GET /api/version/check
24+
*
25+
* Check for available updates
26+
*/
27+
.get(async (req, res, next) => {
28+
try {
29+
const agent = new ProxyAgent();
30+
const url = "https://api.github.com/repos/NginxProxyManager/nginx-proxy-manager/releases/latest";
31+
32+
const data = await new Promise((resolve, reject) => {
33+
https
34+
.get(url, { agent }, (response) => {
35+
if (response.statusCode !== 200) {
36+
reject(new Error(`GitHub API returned ${response.statusCode}`));
37+
return;
38+
}
39+
40+
response.setEncoding("utf8");
41+
let raw_data = "";
42+
43+
response.on("data", (chunk) => {
44+
raw_data += chunk;
45+
});
46+
47+
response.on("end", () => {
48+
try {
49+
resolve(JSON.parse(raw_data));
50+
} catch (err) {
51+
reject(err);
52+
}
53+
});
54+
})
55+
.on("error", (err) => {
56+
reject(err);
57+
});
58+
});
59+
60+
const latestVersion = data.tag_name;
61+
62+
const version = pjson.version.split("-").shift().split(".");
63+
const currentVersion = `v${version[0]}.${version[1]}.${version[2]}`;
64+
65+
res.status(200).send({
66+
current: currentVersion,
67+
latest: latestVersion,
68+
updateAvailable: compareVersions(currentVersion, latestVersion),
69+
});
70+
} catch (error) {
71+
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${error}`);
72+
res.status(200).send({
73+
current: null,
74+
latest: null,
75+
updateAvailable: false,
76+
});
77+
}
78+
});
79+
80+
/**
81+
* Compare two version strings
82+
*
83+
*/
84+
function compareVersions(current, latest) {
85+
const cleanCurrent = current.replace(/^v/, "");
86+
const cleanLatest = latest.replace(/^v/, "");
87+
88+
const currentParts = cleanCurrent.split(".").map(Number);
89+
const latestParts = cleanLatest.split(".").map(Number);
90+
91+
for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
92+
const curr = currentParts[i] || 0;
93+
const lat = latestParts[i] || 0;
94+
95+
if (lat > curr) return true;
96+
if (lat < curr) return false;
97+
}
98+
return false;
99+
}
100+
101+
export default router;

frontend/src/components/SiteFooter.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import { useEffect, useState } from "react";
12
import { useHealth } from "src/hooks";
23
import { T } from "src/locale";
34

45
export function SiteFooter() {
56
const health = useHealth();
7+
const [latestVersion, setLatestVersion] = useState<string | null>(null);
8+
const [isNewVersionAvailable, setIsNewVersionAvailable] = useState(false);
69

710
const getVersion = () => {
811
if (!health.data) {
@@ -12,6 +15,25 @@ export function SiteFooter() {
1215
return `v${v.major}.${v.minor}.${v.revision}`;
1316
};
1417

18+
useEffect(() => {
19+
const checkForUpdates = async () => {
20+
try {
21+
const response = await fetch("/api/version/check");
22+
if (response.ok) {
23+
const data = await response.json();
24+
setLatestVersion(data.latest);
25+
setIsNewVersionAvailable(data.updateAvailable);
26+
}
27+
} catch (error) {
28+
console.debug("Could not check for updates:", error);
29+
}
30+
};
31+
32+
if (health.data) {
33+
checkForUpdates();
34+
}
35+
}, [health.data]);
36+
1537
return (
1638
<footer className="footer d-print-none py-3">
1739
<div className="container-xl">
@@ -55,6 +77,19 @@ export function SiteFooter() {
5577
{getVersion()}{" "}
5678
</a>
5779
</li>
80+
{isNewVersionAvailable && latestVersion && (
81+
<li className="list-inline-item">
82+
<a
83+
href={`https://github.com/NginxProxyManager/nginx-proxy-manager/releases/tag/${latestVersion}`}
84+
className="link-warning fw-bold"
85+
target="_blank"
86+
rel="noopener"
87+
title={`New version ${latestVersion} is available`}
88+
>
89+
Update Available: ({latestVersion})
90+
</a>
91+
</li>
92+
)}
5893
</ul>
5994
</div>
6095
</div>

0 commit comments

Comments
 (0)