Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
34 changes: 18 additions & 16 deletions backend/routes/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,31 @@ 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,
strict: true,
mergeParams: true,
caseSensitive: true,
strict: true,
mergeParams: true,
});

/**
* Health Check
* GET /api
*/
router.get("/", async (_, res /*, next*/) => {
const version = pjson.version.split("-").shift().split(".");
const setup = await isSetup();
const version = pjson.version.split("-").shift().split(".");
const setup = await isSetup();

res.status(200).send({
status: "OK",
setup,
version: {
major: Number.parseInt(version.shift(), 10),
minor: Number.parseInt(version.shift(), 10),
revision: Number.parseInt(version.shift(), 10),
},
});
res.status(200).send({
status: "OK",
setup,
version: {
major: Number.parseInt(version.shift(), 10),
minor: Number.parseInt(version.shift(), 10),
revision: Number.parseInt(version.shift(), 10),
},
});
});

router.use("/schema", schemaRoutes);
Expand All @@ -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 All @@ -59,8 +61,8 @@ router.use("/nginx/certificates", certificatesHostsRoutes);
* ALL /api/*
*/
router.all(/(.+)/, (req, _, next) => {
req.params.page = req.params["0"];
next(new errs.ItemNotFoundError(req.params.page));
req.params.page = req.params["0"];
next(new errs.ItemNotFoundError(req.params.page));
});

export default router;
77 changes: 77 additions & 0 deletions backend/routes/version.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import express from "express";
import { debug, express as logger } from "../logger.js";
import pjson from "../package.json" with { type: "json" };

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 response = await fetch(
"https://api.github.com/repos/NginxProxyManager/nginx-proxy-manager/releases/latest"
);

if (!response.ok) {
throw new Error(`GitHub API returned ${response.status}`);
}

const data = await response.json();
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;
151 changes: 93 additions & 58 deletions frontend/src/components/SiteFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,64 +1,99 @@
import { useEffect, useState } from "react";
import { useHealth } from "src/hooks";
import { T } from "src/locale";

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

const getVersion = () => {
if (!health.data) {
return "";
}
const v = health.data.version;
return `v${v.major}.${v.minor}.${v.revision}`;
};
const getVersion = () => {
if (!health.data) {
return "";
}
const v = health.data.version;
return `v${v.major}.${v.minor}.${v.revision}`;
};

return (
<footer className="footer d-print-none py-3">
<div className="container-xl">
<div className="row text-center align-items-center flex-row-reverse">
<div className="col-lg-auto ms-lg-auto">
<ul className="list-inline list-inline-dots mb-0">
<li className="list-inline-item">
<a
href="https://github.com/NginxProxyManager/nginx-proxy-manager"
target="_blank"
className="link-secondary"
rel="noopener"
>
<T id="footer.github-fork" />
</a>
</li>
</ul>
</div>
<div className="col-12 col-lg-auto mt-3 mt-lg-0">
<ul className="list-inline list-inline-dots mb-0">
<li className="list-inline-item">
© 2025{" "}
<a href="https://jc21.com" rel="noreferrer" target="_blank" className="link-secondary">
jc21.com
</a>
</li>
<li className="list-inline-item">
Theme by{" "}
<a href="https://tabler.io" rel="noreferrer" target="_blank" className="link-secondary">
Tabler
</a>
</li>
<li className="list-inline-item">
<a
href={`https://github.com/NginxProxyManager/nginx-proxy-manager/releases/tag/${getVersion()}`}
className="link-secondary"
target="_blank"
rel="noopener"
>
{" "}
{getVersion()}{" "}
</a>
</li>
</ul>
</div>
</div>
</div>
</footer>
);
}
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">
<div className="row text-center align-items-center flex-row-reverse">
<div className="col-lg-auto ms-lg-auto">
<ul className="list-inline list-inline-dots mb-0">
<li className="list-inline-item">
<a
href="https://github.com/NginxProxyManager/nginx-proxy-manager"
target="_blank"
className="link-secondary"
rel="noopener"
>
<T id="footer.github-fork" />
</a>
</li>
</ul>
</div>
<div className="col-12 col-lg-auto mt-3 mt-lg-0">
<ul className="list-inline list-inline-dots mb-0">
<li className="list-inline-item">
© 2025{" "}
<a href="https://jc21.com" rel="noreferrer" target="_blank" className="link-secondary">
jc21.com
</a>
</li>
<li className="list-inline-item">
Theme by{" "}
<a href="https://tabler.io" rel="noreferrer" target="_blank" className="link-secondary">
Tabler
</a>
</li>
<li className="list-inline-item">
<a
href={`https://github.com/NginxProxyManager/nginx-proxy-manager/releases/tag/${getVersion()}`}
className="link-secondary"
target="_blank"
rel="noopener"
>
{" "}
{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>
</div>
</footer>
);
}