Compare commits

...

22 Commits

Author SHA1 Message Date
Jakub Blažej b481b3be0c
Merge 812b4ad0ff into dbbc79a05a 2024-05-02 11:28:29 +02:00
Frank Elsinga dbbc79a05a
Fixed a typo introduced in #3836 (#4729)
Co-authored-by: Nelson Chan <3271800+chakflying@users.noreply.github.com>
2024-05-02 12:11:49 +08:00
Frank Elsinga 812b4ad0ff
Merge branch 'master' into subdirectory 2024-02-19 21:44:24 +01:00
Louis Lam 600a34b43c Fix 2023-09-05 13:10:32 +08:00
Louis Lam cb2caaa074 Merge branch 'master' into subdirectory
# Conflicts:
#	index.html
#	server/server.js
#	src/pages/Entry.vue
#	src/pages/NotFound.vue
2023-09-05 12:39:28 +08:00
Jakub Blažej bbf5c19a6e
Update src/mixins/socket.js
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2022-09-23 23:10:02 +02:00
Jakub Blažej fcea9af799 Merge branch 'master' of https://github.com/louislam/uptime-kuma into subdirectory 2022-09-23 20:10:45 +02:00
Jakub Blažej b01a534ac9 changed @click to @click.prevent in <a> tags,
replaced <a> with <router-link>,
replaced 'location.href =' navigation by router.push()
2022-07-03 23:40:33 +02:00
Jakub Blažej 86c2ad75ea fix statusPage list href 2022-07-03 21:49:37 +02:00
Jakub Blažej bd112b497a fix linter errors 2022-07-03 21:40:11 +02:00
Jakub Blažej d1caecde98 Merge remote-tracking branch 'upstream/master' into subdirectory 2022-07-03 21:18:29 +02:00
Jakub Blažej 6101eaae27 remove href="#" to prevent unintentional redirections 2022-07-03 20:26:03 +02:00
Jakub Blažej b46aa4014b
Update src/mixins/socket.js: add trailing comma
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2021-12-25 20:10:57 +01:00
Jakub Blažej 20291427e3 remove duplicate slash in push url
remove unnecessary regex escape
2021-12-25 14:13:17 +01:00
Jakub Blažej 92a064d09d fix lint warnings 2021-12-24 13:27:47 +01:00
Jakub Blažej 4f8826cf5c fix manifest.json 401 error when behind basic auth proxy 2021-12-24 12:32:22 +01:00
Jakub Blažej c165f6cad9 fix socket.io path 2021-12-24 12:31:09 +01:00
Jakub Blažej c8d729883c change api call paths to relative
change icon paths to relative
2021-12-24 12:30:29 +01:00
Jakub Blažej 2544363fc0 fix router-links in and to status page 2021-12-24 12:29:18 +01:00
Jakub Blažej 9a405b5773 fix autoGetPrimaryBaseURL 2021-12-24 12:26:49 +01:00
Jakub Blažej 35c428b280 add basic basepath sanitization 2021-12-24 12:26:17 +01:00
Jakub Blažej 8cf827e6e1 add support for deployment in subfolder
configurable using BASE_PATH env variable
2021-12-24 00:48:26 +01:00
25 changed files with 96 additions and 56 deletions

View File

@ -49,7 +49,7 @@ Uptime Kuma is now running on <http://0.0.0.0:3001>.
> File Systems like **NFS** (Network File System) are **NOT** supported. Please map to a local directory or volume.
> [!NOTE]
> If you want to limit exppoure to localhost (without exposing port for other users or to use a [reverse proxyx](https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy)), you can expose the port like this:
> If you want to limit exposure to localhost (without exposing port for other users or to use a [reverse proxy](https://github.com/louislam/uptime-kuma/wiki/Reverse-Proxy)), you can expose the port like this:
>
> ```bash
> docker run -d --restart=always -p 127.0.0.1:3001:3001 -v uptime-kuma:/app/data --name uptime-kuma louislam/uptime-kuma:1

View File

@ -11,6 +11,7 @@ const viteCompressionFilter = /\.(js|mjs|json|css|html|svg)$/i;
// https://vitejs.dev/config/
export default defineConfig({
base: "./",
server: {
port: 3000,
},

View File

@ -1,11 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<base href="/" >
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<link rel="manifest" href="/manifest.json" />
<link rel="apple-touch-icon" sizes="180x180" href="./apple-touch-icon.png">
<link rel="icon" type="image/svg+xml" href="./icon.svg" />
<link rel="manifest" href="./manifest.json" crossorigin="use-credentials" />
<meta name="theme-color" id="theme-color" content="" />
<meta name="description" content="Uptime Kuma monitoring tool" />
<title>Uptime Kuma</title>
@ -28,6 +29,6 @@
</div>
</noscript>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<script type="module" src="./src/main.js"></script>
</body>
</html>

View File

@ -23,6 +23,7 @@
"start": "npm run start-server",
"start-server": "node server/server.js",
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
"start-server-dev:test-subdir": "cross-env NODE_ENV=development BASE_PATH=/my-kuma node server/server.js",
"start-server-dev:watch": "cross-env NODE_ENV=development node --watch server/server.js",
"build": "vite build --config ./config/vite.config.js",
"test": "npm run test-backend && npm run test-e2e",

View File

@ -1,7 +1,7 @@
{
"name": "Uptime Kuma",
"short_name": "Uptime Kuma",
"start_url": "/",
"start_url": "./",
"background_color": "#fff",
"display": "standalone",
"icons": [

View File

@ -288,9 +288,9 @@ class StatusPage extends BeanModel {
*/
getIcon() {
if (!this.icon) {
return "/icon.svg";
return "./icon.svg";
} else {
return this.icon;
return "." + this.icon;
}
}

View File

@ -514,7 +514,7 @@ function ApiCache() {
* Return cache performance statistics (hit rate). Suitable for
* putting into a route:
* <code>
* app.get('/api/cache/performance', (req, res) => {
* app.get('./api/cache/performance', (req, res) => {
* res.json(apicache.getPerformance())
* })
* </code>

View File

@ -189,6 +189,8 @@ let needSetup = false;
// Database should be ready now
await server.initAfterDatabaseReady();
server.entryPage = await Settings.get("entryPage");
const mainRouter = express.Router();
await StatusPage.loadDomainMappingList();
log.debug("server", "Adding route");
@ -198,7 +200,7 @@ let needSetup = false;
// ***************************
// Entry Page
app.get("/", async (request, response) => {
mainRouter.get("/", async (request, response) => {
let hostname = request.hostname;
if (await setting("trustProxy")) {
const proxy = request.headers["x-forwarded-host"];
@ -207,7 +209,7 @@ let needSetup = false;
}
}
log.debug("entry", `Request Domain: ${hostname}`);
log.debug("entry", `Request Domain: ${request.hostname}`);
const uptimeKumaEntryPage = server.entryPage;
if (hostname in StatusPage.domainMappingList) {
@ -217,14 +219,14 @@ let needSetup = false;
await StatusPage.handleStatusPageResponse(response, server.indexHTML, slug);
} else if (uptimeKumaEntryPage && uptimeKumaEntryPage.startsWith("statusPage-")) {
response.redirect("/status/" + uptimeKumaEntryPage.replace("statusPage-", ""));
response.redirect(server.basePath + "status/" + uptimeKumaEntryPage.replace("statusPage-", ""));
} else {
response.redirect("/dashboard");
response.redirect(server.basePath + "dashboard");
}
});
app.get("/setup-database-info", (request, response) => {
mainRouter.get("/setup-database-info", (request, response) => {
allowDevAllOrigin(response);
response.json({
runningSetup: false,
@ -233,8 +235,8 @@ let needSetup = false;
});
if (isDev) {
app.use(express.urlencoded({ extended: true }));
app.post("/test-webhook", async (request, response) => {
mainRouter.use(express.urlencoded({ extended: true }));
mainRouter.post("/test-webhook", async (request, response) => {
log.debug("test", request.headers);
log.debug("test", request.body);
response.send("OK");
@ -248,7 +250,7 @@ let needSetup = false;
}
// Robots.txt
app.get("/robots.txt", async (_request, response) => {
mainRouter.get("/robots.txt", async (_request, response) => {
let txt = "User-agent: *\nDisallow:";
if (!await setting("searchEngineIndex")) {
txt += " /";
@ -261,29 +263,29 @@ let needSetup = false;
// Prometheus API metrics /metrics
// With Basic Auth using the first user's username/password
app.get("/metrics", apiAuth, prometheusAPIMetrics());
mainRouter.get("/metrics", apiAuth, prometheusAPIMetrics());
app.use("/", expressStaticGzip("dist", {
mainRouter.use("/", expressStaticGzip("dist", {
enableBrotli: true,
}));
// ./data/upload
app.use("/upload", express.static(Database.uploadDir));
mainRouter.use("/upload", express.static(Database.uploadDir));
app.get("/.well-known/change-password", async (_, response) => {
mainRouter.get("/.well-known/change-password", async (_, response) => {
response.redirect("https://github.com/louislam/uptime-kuma/wiki/Reset-Password-via-CLI");
});
// API Router
const apiRouter = require("./routers/api-router");
app.use(apiRouter);
mainRouter.use(apiRouter);
// Status Page Router
const statusPageRouter = require("./routers/status-page-router");
app.use(statusPageRouter);
mainRouter.use(statusPageRouter);
// Universal Route Handler, must be at the end of all express routes.
app.get("*", async (_request, response) => {
mainRouter.get("*", async (_request, response) => {
if (_request.originalUrl.startsWith("/upload/")) {
response.status(404).send("File not found.");
} else {
@ -291,6 +293,14 @@ let needSetup = false;
}
});
app.use(server.basePath, mainRouter);
if (server.basePath !== "/") {
app.get("/", (request, response) => {
response.status(404).send("Your Uptime Kuma is running at " + server.basePath);
});
}
log.debug("server", "Adding socket handler");
io.on("connection", async (socket) => {

View File

@ -145,7 +145,7 @@ module.exports.statusPageSocketHandler = (socket) => {
// Convert to file
await ImageDataURI.outputFile(imgDataUrl, Database.uploadDir + filename);
config.logo = `/upload/${filename}?t=` + Date.now();
config.logo = `./upload/${filename}?t=` + Date.now();
} else {
config.icon = imgDataUrl;

View File

@ -84,6 +84,21 @@ class UptimeKumaServer {
// Set default axios timeout to 5 minutes instead of infinity
axios.defaults.timeout = 300 * 1000;
let basePathEnv = process.env.UPTIME_KUMA_BASE_PATH || process.env.BASE_PATH || "/";
if (!basePathEnv.startsWith("/")) {
basePathEnv = "/" + basePathEnv;
}
if (!basePathEnv.endsWith("/")) {
basePathEnv = basePathEnv + "/";
}
if (basePathEnv !== "/") {
log.info("server", "Base Path enabled: " + basePathEnv);
}
this.basePath = basePathEnv;
log.info("server", "Creating express and socket.io instance");
this.app = express();
if (isSSL) {
@ -100,6 +115,7 @@ class UptimeKumaServer {
try {
this.indexHTML = fs.readFileSync("./dist/index.html").toString();
this.indexHTML = this.indexHTML.replace(/<base href.*?>/, `<base href="${this.basePath}">`);
} catch (e) {
// "dist/index.html" is not necessary for development
if (process.env.NODE_ENV !== "development") {
@ -123,6 +139,7 @@ class UptimeKumaServer {
}
this.io = new Server(this.httpServer, {
path: this.basePath + "socket.io"
cors,
allowRequest: async (req, callback) => {
let transport;

View File

@ -6,7 +6,7 @@
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li v-for="(item, key) in chartPeriodOptions" :key="key">
<a class="dropdown-item" :class="{ active: chartPeriodHrs == key }" href="#" @click="chartPeriodHrs = key">{{ item }}</a>
<a class="dropdown-item" :class="{ active: chartPeriodHrs == key }" @click="chartPeriodHrs = key">{{ item }}</a>
</li>
</ul>
</div>
@ -293,6 +293,7 @@ export default {
.dropdown-item {
border-radius: 0.3rem;
padding: 2px 16px 4px;
cursor: pointer;
.dark & {
background: $dark-bg;

View File

@ -1,7 +1,7 @@
<template>
<div class="d-flex justify-content-center align-items-center">
<div class="logo d-flex flex-column justify-content-center align-items-center">
<object class="my-4" width="200" height="200" data="/icon.svg" />
<object class="my-4" width="200" height="200" data="./icon.svg" />
<div class="fs-4 fw-bold">Uptime Kuma</div>
<div>{{ $t("Version") }}: {{ $root.info.version }}</div>
<div class="frontend-version">{{ $t("Frontend Version") }}: {{ $root.frontendVersion }}</div>

View File

@ -266,7 +266,8 @@ export default {
* @returns {void}
*/
autoGetPrimaryBaseURL() {
this.settings.primaryBaseURL = location.protocol + "//" + location.host;
const basePath = document.querySelector("head base").getAttribute("href");
this.settings.primaryBaseURL = location.protocol + "//" + location.host + basePath;
},
/**
* Test the chrome executable

View File

@ -11,7 +11,7 @@
<ul class="list-group mb-3" style="border-radius: 1rem;">
<li v-for="(notification, index) in $root.notificationList" :key="index" class="list-group-item">
{{ notification.name }}<br>
<a href="#" @click="$refs.notificationDialog.show(notification.id)">{{ $t("Edit") }}</a>
<a href="#" @click.prevent="$refs.notificationDialog.show(notification.id)">{{ $t("Edit") }}</a>
</li>
</ul>

View File

@ -13,7 +13,7 @@
<li v-for="(proxy, index) in $root.proxyList" :key="index" class="list-group-item">
{{ proxy.host }}:{{ proxy.port }} ({{ proxy.protocol }})
<span v-if="proxy.default === true" class="badge bg-primary ms-2">{{ $t("Default") }}</span><br>
<a href="#" @click="$refs.proxyDialog.show(proxy.id)">{{ $t("Edit") }}</a>
<a href="#" @click.prevent="$refs.proxyDialog.show(proxy.id)">{{ $t("Edit") }}</a>
</li>
</ul>

View File

@ -12,7 +12,7 @@
<!-- Desktop header -->
<header v-if="! $root.isMobile" class="d-flex flex-wrap justify-content-center py-3 mb-3 border-bottom">
<router-link to="/dashboard" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
<object class="bi me-2 ms-4" width="40" height="40" data="/icon.svg" />
<object class="bi me-2 ms-4" width="40" height="40" data="./icon.svg" />
<span class="fs-4 title">{{ $t("Uptime Kuma") }}</span>
</router-link>
@ -84,7 +84,7 @@
<!-- Mobile header -->
<header v-else class="d-flex flex-wrap justify-content-center pt-2 pb-2 mb-3">
<router-link to="/dashboard" class="d-flex align-items-center text-dark text-decoration-none">
<object class="bi" width="40" height="40" data="/icon.svg" />
<object class="bi" width="40" height="40" data="./icon.svg" />
<span class="fs-4 title ms-2">Uptime Kuma</span>
</router-link>
</header>

View File

@ -112,7 +112,12 @@ export default {
url = undefined;
}
socket = io(url);
// always starts and ends with '/'
const basePath = document.querySelector("head base").getAttribute("href");
socket = io(url, {
path: basePath + "socket.io",
});
socket.on("info", (info) => {
this.info = info;

View File

@ -65,7 +65,7 @@ export default {
this.processing = false;
if (res.ok) {
location.href = "/status/" + this.slug + "?edit";
this.$router.push("/status/" + this.slug + "?edit");
} else {
if (res.msg.includes("UNIQUE constraint")) {

View File

@ -587,7 +587,7 @@
<label class="form-check-label" :for=" 'notification' + notification.id">
{{ notification.name }}
<a href="#" @click="$refs.notificationDialog.show(notification.id)">{{ $t("Edit") }}</a>
<a href="#" @click.prevent="$refs.notificationDialog.show(notification.id)">{{ $t("Edit") }}</a>
</label>
<span v-if="notification.isDefault == true" class="badge bg-primary ms-2">{{ $t("Default") }}</span>
@ -614,7 +614,7 @@
<label class="form-check-label" :for="`proxy-${proxy.id}`">
{{ proxy.host }}:{{ proxy.port }} ({{ proxy.protocol }})
<a href="#" @click="$refs.proxyDialog.show(proxy.id)">{{ $t("Edit") }}</a>
<a href="#" @click.prevent="$refs.proxyDialog.show(proxy.id)">{{ $t("Edit") }}</a>
</label>
<span v-if="proxy.default === true" class="badge bg-primary ms-2">{{ $t("default") }}</span>
@ -1049,7 +1049,7 @@ export default {
},
pushURL() {
return this.$root.baseURL + "/api/push/" + this.monitor.pushToken + "?status=up&msg=OK&ping=";
return this.$root.baseURL + "api/push/" + this.monitor.pushToken + "?status=up&msg=OK&ping=";
},
protoServicePlaceholder() {

View File

@ -25,7 +25,7 @@ export default {
// 3. Vue Frontend Dev (not setup database yet)
let res;
try {
res = (await axios.get("/api/entry-page")).data;
res = (await axios.get("./api/entry-page")).data;
if (res.type === "statusPageMatchedDomain") {
this.statusPageSlug = res.statusPageSlug;

View File

@ -16,7 +16,7 @@
</span>
<!-- use <a> instead of <router-link>, because the heartbeat won't load. -->
<a v-for="statusPage in $root.statusPageList" :key="statusPage.slug" :href="'/status/' + statusPage.slug" class="item">
<a v-for="statusPage in $root.statusPageList" :key="statusPage.slug" :href="'./status/' + statusPage.slug" class="item">
<img :src="icon(statusPage.icon)" alt class="logo me-2" />
<div class="info">
<div class="title">{{ statusPage.title }}</div>

View File

@ -32,7 +32,7 @@
<ul>
<li>{{ $t("Retype the address.") }}</li>
<li><a href="#" class="go-back" @click="goBack()">{{ $t("Go back to the previous page.") }}</a></li>
<li><a href="/" class="go-back">{{ $t("Go back to home page.") }}</a></li>
<li><a href="./" class="go-back">{{ $t("Go back to home page.") }}</a></li>
</ul>
</div>
</div>

View File

@ -3,7 +3,7 @@
<div class="form">
<form @submit.prevent="submit">
<div>
<object width="64" height="64" data="/icon.svg" />
<object width="64" height="64" data="./icon.svg" />
<div style="font-size: 28px; font-weight: bold; margin-top: 5px;">
Uptime Kuma
</div>

View File

@ -154,10 +154,10 @@
{{ $t("Edit Status Page") }}
</button>
<a href="/manage-status-page" class="btn btn-info">
<router-link to="/manage-status-page" class="btn btn-info">
<font-awesome-icon icon="tachometer-alt" />
{{ $t("Go to Dashboard") }}
</a>
</router-link>
</div>
<div v-else>
@ -210,12 +210,12 @@
{{ $t("Style") }}: {{ $t(incident.style) }}
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
<li><a class="dropdown-item" href="#" @click="incident.style = 'info'">{{ $t("info") }}</a></li>
<li><a class="dropdown-item" href="#" @click="incident.style = 'warning'">{{ $t("warning") }}</a></li>
<li><a class="dropdown-item" href="#" @click="incident.style = 'danger'">{{ $t("danger") }}</a></li>
<li><a class="dropdown-item" href="#" @click="incident.style = 'primary'">{{ $t("primary") }}</a></li>
<li><a class="dropdown-item" href="#" @click="incident.style = 'light'">{{ $t("light") }}</a></li>
<li><a class="dropdown-item" href="#" @click="incident.style = 'dark'">{{ $t("dark") }}</a></li>
<li><a class="dropdown-item" href="#" @click.prevent="incident.style = 'info'">{{ $t("info") }}</a></li>
<li><a class="dropdown-item" href="#" @click.prevent="incident.style = 'warning'">{{ $t("warning") }}</a></li>
<li><a class="dropdown-item" href="#" @click.prevent="incident.style = 'danger'">{{ $t("danger") }}</a></li>
<li><a class="dropdown-item" href="#" @click.prevent="incident.style = 'primary'">{{ $t("primary") }}</a></li>
<li><a class="dropdown-item" href="#" @click.prevent="incident.style = 'light'">{{ $t("light") }}</a></li>
<li><a class="dropdown-item" href="#" @click.prevent="incident.style = 'dark'">{{ $t("dark") }}</a></li>
</ul>
</div>
@ -432,7 +432,7 @@ export default {
incident: null,
previousIncident: null,
showImageCropUpload: false,
imgDataUrl: "/icon.svg",
imgDataUrl: "./icon.svg",
loadedTheme: false,
loadedData: false,
baseURL: "",
@ -710,7 +710,7 @@ export default {
this.loading = false;
}).catch( function (error) {
if (error.response.status === 404) {
location.href = "/page-not-found";
this.$router.push("/page-not-found");
}
console.log(error);
});
@ -742,7 +742,7 @@ export default {
data: window.preloadData
}));
} else {
return axios.get("/api/status-page/" + this.slug);
return axios.get("./api/status-page/" + this.slug);
}
},
@ -762,7 +762,7 @@ export default {
updateHeartbeatList() {
// If editMode, it will use the data from websocket.
if (! this.editMode) {
axios.get("/api/status-page/heartbeat/" + this.slug).then((res) => {
axios.get("./api/status-page/heartbeat/" + this.slug).then((res) => {
const { heartbeatList, uptimeList } = res.data;
this.$root.heartbeatList = heartbeatList;
@ -845,7 +845,7 @@ export default {
setTimeout(() => {
this.loading = false;
location.href = "/status/" + this.config.slug;
this.$router.push("/status/" + this.config.slug);
}, time);
} else {
@ -871,7 +871,7 @@ export default {
this.$root.getSocket().emit("deleteStatusPage", this.slug, (res) => {
if (res.ok) {
this.enableEditMode = false;
location.href = "/manage-status-page";
this.$router.push("/manage-status-page");
} else {
this.$root.toastError(res.msg);
}
@ -917,7 +917,7 @@ export default {
* @returns {void}
*/
discard() {
location.href = "/status/" + this.slug;
this.$router.back();
},
/**

3
vue.config.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
publicPath: "./",
};