Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/routes/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
101 changes: 101 additions & 0 deletions backend/routes/version.js
Original file line number Diff line number Diff line change
@@ -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 changes: 35 additions & 0 deletions frontend/src/components/SiteFooter.tsx
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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">
Expand Down Expand Up @@ -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>
Expand Down