Merge branch 'master' into fix-sync

This commit is contained in:
Louis Lam 2025-06-23 15:24:59 +08:00 committed by GitHub
commit 6278267681
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 175 additions and 105 deletions

View file

@ -3,7 +3,7 @@ name: ❓ Ask for help
description: | description: |
Submit any question related to Uptime Kuma Submit any question related to Uptime Kuma
#title: "[Help]" #title: "[Help]"
labels: ["help", "P3-low"] labels: ["help"]
body: body:
- type: markdown - type: markdown
attributes: attributes:

View file

@ -3,7 +3,7 @@ name: 🐛 Bug Report
description: | description: |
Submit a bug report to help us improve Submit a bug report to help us improve
#title: "[Bug]" #title: "[Bug]"
labels: ["bug", "P2-medium"] labels: ["bug"]
body: body:
- type: markdown - type: markdown
attributes: attributes:

View file

@ -3,7 +3,7 @@ name: 🚀 Feature Request
description: | description: |
Submit a proposal for a new feature Submit a proposal for a new feature
# title: "[Feature]" # title: "[Feature]"
labels: ["feature-request", "P3-low"] labels: ["feature-request"]
body: body:
- type: markdown - type: markdown
attributes: attributes:

View file

@ -3,7 +3,7 @@ name: 🛡️ Security Issue
description: | description: |
Notify Louis Lam about a security concern. Please do NOT include any sensitive details in this issue. Notify Louis Lam about a security concern. Please do NOT include any sensitive details in this issue.
# title: "Security Issue" # title: "Security Issue"
labels: ["security", "P1-high"] labels: ["security"]
assignees: [louislam] assignees: [louislam]
body: body:
- type: markdown - type: markdown

View file

@ -1,10 +1,10 @@
**⚠️ Please Note: We do not accept all types of pull requests, and we want to ensure we dont waste your time. Before submitting, make sure you have read our pull request guidelines: [Pull Request Rules](https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md#can-i-create-a-pull-request-for-uptime-kuma)**
## ❗ Important Announcement ## ❗ Important Announcement
<details><summary>Click here for more details:</summary> <details><summary>Click here for more details:</summary>
</p> </p>
**⚠️ Please Note: We do not accept all types of pull requests, and we want to ensure we dont waste your time. Before submitting, make sure you have read our pull request guidelines: [Pull Request Rules](https://github.com/louislam/uptime-kuma/blob/master/CONTRIBUTING.md#can-i-create-a-pull-request-for-uptime-kuma)**
### 🚧 Temporary Delay in Feature Requests and Pull Request Reviews ### 🚧 Temporary Delay in Feature Requests and Pull Request Reviews
**At this time, we may be slower to respond to new feature requests and review pull requests. Existing requests and PRs will remain in the backlog but may not be prioritized immediately.** **At this time, we may be slower to respond to new feature requests and review pull requests. Existing requests and PRs will remain in the backlog but may not be prioritized immediately.**
@ -26,16 +26,22 @@ We appreciate your patience and understanding as we continue to improve Uptime K
## 📋 Overview ## 📋 Overview
Provide a clear summary of the purpose and scope of this pull request: <!-- Provide a clear summary of the purpose and scope of this pull request:-->
- **What problem does this pull request address?** - **What problem does this pull request address?**
- Please provide a detailed explanation here. - Please provide a detailed explanation here.
- **What features or functionality does this pull request introduce or enhance?** - **What features or functionality does this pull request introduce or enhance?**
- Please provide a detailed explanation here. - Please provide a detailed explanation here.
## 🔗 Related Issues
<!--
Please link any GitHub issues or tasks that this pull request addresses. Use the appropriate issue numbers or links.
-->
- Relates to #issue-number
- Resolves #issue-number
## 🔄 Changes ## 🔄 Changes
### 🛠️ Type of change ### 🛠️ Type of change
@ -52,19 +58,7 @@ Provide a clear summary of the purpose and scope of this pull request:
- [ ] 🔧 Other (please specify): - [ ] 🔧 Other (please specify):
- Provide additional details here. - Provide additional details here.
## 🔗 Related Issues ## 📄 Checklist
<!--
Please link any GitHub issues or tasks that this pull request addresses. Use the appropriate issue numbers or links.
**Note**: Include only issues directly related to this PR. Remove any irrelevant reference.
-->
- Relates to #issue-number
- Resolves #issue-number
- Fixes #issue-number
## 📄 Checklist *
<!-- Please select all options that apply --> <!-- Please select all options that apply -->
@ -97,26 +91,3 @@ If not, remove this section.
| `DOWN` | ![Before](image-link) | ![After](image-link) | | `DOWN` | ![Before](image-link) | ![After](image-link) |
| Certificate-expiry | ![Before](image-link) | ![After](image-link) | | Certificate-expiry | ![Before](image-link) | ![After](image-link) |
| Testing | ![Before](image-link) | ![After](image-link) | | Testing | ![Before](image-link) | ![After](image-link) |
## Additional Context
Provide any relevant details to assist reviewers in understanding the changes.
<details><summary>Click here for more details:</summary>
</p>
**Key Considerations**:
- **Design decisions** Key choices or trade-offs made during development.
- **Alternative solutions** Approaches considered but not implemented, along with reasons.
- **Relevant links** Specifications, discussions, or resources that provide context.
- **Dependencies** Related pull requests or issues that must be resolved before merging.
- **Additional context** Any other details that may help reviewers understand the changes.
Provide details here
## 💬 Requested Feedback
<!-- If a part of our docs is unclear, you are unsure how to do something/.. this is where we would appreciate your feedback -->
- `Mention documents needing feedback here`

View file

@ -79,6 +79,10 @@ USER node
RUN git config --global user.email "no-reply@no-reply.com" RUN git config --global user.email "no-reply@no-reply.com"
RUN git config --global user.name "PR Tester" RUN git config --global user.name "PR Tester"
RUN git clone https://github.com/louislam/uptime-kuma.git . RUN git clone https://github.com/louislam/uptime-kuma.git .
# Hide the warning when running in detached head state
RUN git config --global advice.detachedHead false
RUN npm ci RUN npm ci
EXPOSE 3000 3001 EXPOSE 3000 3001

View file

@ -1,33 +0,0 @@
const childProcess = require("child_process");
if (!process.env.UPTIME_KUMA_GH_REPO) {
console.error("Please set a repo to the environment variable 'UPTIME_KUMA_GH_REPO' (e.g. mhkarimi1383:goalert-notification)");
process.exit(1);
}
let inputArray = process.env.UPTIME_KUMA_GH_REPO.split(":");
if (inputArray.length !== 2) {
console.error("Invalid format. Please set a repo to the environment variable 'UPTIME_KUMA_GH_REPO' (e.g. mhkarimi1383:goalert-notification)");
}
let name = inputArray[0];
let branch = inputArray[1];
console.log("Checkout pr");
// Checkout the pr
let result = childProcess.spawnSync("git", [ "remote", "add", name, `https://github.com/${name}/uptime-kuma` ]);
console.log(result.stdout.toString());
console.error(result.stderr.toString());
result = childProcess.spawnSync("git", [ "fetch", name, branch ]);
console.log(result.stdout.toString());
console.error(result.stderr.toString());
result = childProcess.spawnSync("git", [ "checkout", `${name}/${branch}`, "--force" ]);
console.log(result.stdout.toString());
console.error(result.stderr.toString());

34
extra/checkout-pr.mjs Normal file
View file

@ -0,0 +1,34 @@
import childProcess from "child_process";
import { parsePrName } from "./kuma-pr/pr-lib.mjs";
let { name, branch } = parsePrName(process.env.UPTIME_KUMA_GH_REPO);
console.log(`Checking out PR from ${name}:${branch}`);
// Checkout the pr
let result = childProcess.spawnSync("git", [ "remote", "add", name, `https://github.com/${name}/uptime-kuma` ], {
stdio: "inherit"
});
if (result.status !== 0) {
console.error("Failed to add remote repository.");
process.exit(1);
}
result = childProcess.spawnSync("git", [ "fetch", name, branch ], {
stdio: "inherit"
});
if (result.status !== 0) {
console.error("Failed to fetch the branch.");
process.exit(1);
}
result = childProcess.spawnSync("git", [ "checkout", `${name}/${branch}`, "--force" ], {
stdio: "inherit"
});
if (result.status !== 0) {
console.error("Failed to checkout the branch.");
process.exit(1);
}

26
extra/kuma-pr/index.mjs Normal file
View file

@ -0,0 +1,26 @@
#!/usr/bin/env node
import { spawn } from "child_process";
import { parsePrName } from "./pr-lib.mjs";
const prName = process.argv[2];
// Pre-check the prName here, so testers don't need to wait until the Docker image is pulled to see the error.
try {
parsePrName(prName);
} catch (error) {
console.error(error.message);
process.exit(1);
}
spawn("docker", [
"run",
"--rm",
"-it",
"-p", "3000:3000",
"-p", "3001:3001",
"--pull", "always",
"-e", `UPTIME_KUMA_GH_REPO=${prName}`,
"louislam/uptime-kuma:pr-test2"
], {
stdio: "inherit",
});

View file

@ -0,0 +1,8 @@
{
"name": "kuma-pr",
"version": "1.0.0",
"type": "module",
"bin": {
"kuma-pr": "./index.mjs"
}
}

39
extra/kuma-pr/pr-lib.mjs Normal file
View file

@ -0,0 +1,39 @@
/**
* Parse <name>:<branch> to an object.
* @param {string} prName <name>:<branch>
* @returns {object} An object with name and branch properties.
*/
export function parsePrName(prName) {
let name = "louislam";
let branch;
const errorMessage = "Please set a repo to the environment variable 'UPTIME_KUMA_GH_REPO' (e.g. mhkarimi1383:goalert-notification)";
if (!prName) {
throw new Error(errorMessage);
}
prName = prName.trim();
if (prName === "") {
throw new Error(errorMessage);
}
let inputArray = prName.split(":");
// Just realized that owner's prs are not prefixed with "louislam:"
if (inputArray.length === 1) {
branch = inputArray[0];
} else if (inputArray.length === 2) {
name = inputArray[0];
branch = inputArray[1];
} else {
throw new Error("Invalid format. The format is like this: mhkarimi1383:goalert-notification");
}
return {
name,
branch
};
}

View file

@ -57,7 +57,7 @@
"release-nightly": "node ./extra/release/nightly.mjs", "release-nightly": "node ./extra/release/nightly.mjs",
"git-remove-tag": "git tag -d", "git-remove-tag": "git tag -d",
"build-dist-and-restart": "npm run build && npm run start-server-dev", "build-dist-and-restart": "npm run build && npm run start-server-dev",
"start-pr-test": "node extra/checkout-pr.js && npm install && npm run dev", "start-pr-test": "node extra/checkout-pr.mjs && npm install && npm run dev",
"build-healthcheck-armv7": "cross-env GOOS=linux GOARCH=arm GOARM=7 go build -x -o ./extra/healthcheck-armv7 ./extra/healthcheck.go", "build-healthcheck-armv7": "cross-env GOOS=linux GOARCH=arm GOARM=7 go build -x -o ./extra/healthcheck-armv7 ./extra/healthcheck.go",
"deploy-demo-server": "node extra/deploy-demo-server.js", "deploy-demo-server": "node extra/deploy-demo-server.js",
"sort-contributors": "node extra/sort-contributors.js", "sort-contributors": "node extra/sort-contributors.js",

View file

@ -26,7 +26,7 @@ exports.login = async function (username, password) {
// Upgrade the hash to bcrypt // Upgrade the hash to bcrypt
if (passwordHash.needRehash(user.password)) { if (passwordHash.needRehash(user.password)) {
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [ await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
passwordHash.generate(password), await passwordHash.generate(password),
user.id, user.id,
]); ]);
} }

View file

@ -1,4 +1,5 @@
const fs = require("fs"); const fs = require("fs");
const fsAsync = fs.promises;
const { R } = require("redbean-node"); const { R } = require("redbean-node");
const { setSetting, setting } = require("./util-server"); const { setSetting, setting } = require("./util-server");
const { log, sleep } = require("../src/util"); const { log, sleep } = require("../src/util");
@ -707,12 +708,12 @@ class Database {
/** /**
* Get the size of the database (SQLite only) * Get the size of the database (SQLite only)
* @returns {number} Size of database * @returns {Promise<number>} Size of database
*/ */
static getSize() { static async getSize() {
if (Database.dbConfig.type === "sqlite") { if (Database.dbConfig.type === "sqlite") {
log.debug("db", "Database.getSize()"); log.debug("db", "Database.getSize()");
let stats = fs.statSync(Database.sqlitePath); let stats = await fsAsync.stat(Database.sqlitePath);
log.debug("db", stats); log.debug("db", stats);
return stats.size; return stats.size;
} }

View file

@ -14,7 +14,7 @@ class User extends BeanModel {
*/ */
static async resetPassword(userID, newPassword) { static async resetPassword(userID, newPassword) {
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [ await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
passwordHash.generate(newPassword), await passwordHash.generate(newPassword),
userID userID
]); ]);
} }
@ -25,7 +25,7 @@ class User extends BeanModel {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async resetPassword(newPassword) { async resetPassword(newPassword) {
const hashedPassword = passwordHash.generate(newPassword); const hashedPassword = await passwordHash.generate(newPassword);
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [ await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
hashedPassword, hashedPassword,

View file

@ -5,10 +5,10 @@ const saltRounds = 10;
/** /**
* Hash a password * Hash a password
* @param {string} password Password to hash * @param {string} password Password to hash
* @returns {string} Hash * @returns {Promise<string>} Hash
*/ */
exports.generate = function (password) { exports.generate = function (password) {
return bcrypt.hashSync(password, saltRounds); return bcrypt.hash(password, saltRounds);
}; };
/** /**

View file

@ -674,7 +674,7 @@ let needSetup = false;
let user = R.dispense("user"); let user = R.dispense("user");
user.username = username; user.username = username;
user.password = passwordHash.generate(password); user.password = await passwordHash.generate(password);
await R.store(user); await R.store(user);
needSetup = false; needSetup = false;

View file

@ -20,7 +20,7 @@ module.exports.apiKeySocketHandler = (socket) => {
checkLogin(socket); checkLogin(socket);
let clearKey = nanoid(40); let clearKey = nanoid(40);
let hashedKey = passwordHash.generate(clearKey); let hashedKey = await passwordHash.generate(clearKey);
key["key"] = hashedKey; key["key"] = hashedKey;
let bean = await APIKey.save(key, socket.userID); let bean = await APIKey.save(key, socket.userID);

View file

@ -14,7 +14,7 @@ module.exports.databaseSocketHandler = (socket) => {
checkLogin(socket); checkLogin(socket);
callback({ callback({
ok: true, ok: true,
size: Database.getSize(), size: await Database.getSize(),
}); });
} catch (error) { } catch (error) {
callback({ callback({

View file

@ -4,7 +4,7 @@ const { sendInfo } = require("../client");
const { checkLogin } = require("../util-server"); const { checkLogin } = require("../util-server");
const GameResolver = require("gamedig/lib/GameResolver"); const GameResolver = require("gamedig/lib/GameResolver");
const { testChrome } = require("../monitor-types/real-browser-monitor-type"); const { testChrome } = require("../monitor-types/real-browser-monitor-type");
const fs = require("fs"); const fsAsync = require("fs").promises;
const path = require("path"); const path = require("path");
let gameResolver = new GameResolver(); let gameResolver = new GameResolver();
@ -90,7 +90,7 @@ module.exports.generalSocketHandler = (socket, server) => {
} }
}); });
socket.on("getPushExample", (language, callback) => { socket.on("getPushExample", async (language, callback) => {
try { try {
checkLogin(socket); checkLogin(socket);
if (!/^[a-z-]+$/.test(language)) { if (!/^[a-z-]+$/.test(language)) {
@ -106,13 +106,13 @@ module.exports.generalSocketHandler = (socket, server) => {
try { try {
let dir = path.join("./extra/push-examples", language); let dir = path.join("./extra/push-examples", language);
let files = fs.readdirSync(dir); let files = await fsAsync.readdir(dir);
for (let file of files) { for (let file of files) {
if (file.startsWith("index.")) { if (file.startsWith("index.")) {
callback({ callback({
ok: true, ok: true,
code: fs.readFileSync(path.join(dir, file), "utf8"), code: await fsAsync.readFile(path.join(dir, file), "utf8"),
}); });
return; return;
} }

View file

@ -51,7 +51,7 @@ exports.initJWTSecret = async () => {
jwtSecretBean.key = "jwtSecret"; jwtSecretBean.key = "jwtSecret";
} }
jwtSecretBean.value = passwordHash.generate(genSecret()); jwtSecretBean.value = await passwordHash.generate(genSecret());
await R.store(jwtSecretBean); await R.store(jwtSecretBean);
return jwtSecretBean; return jwtSecretBean;
}; };

View file

@ -75,11 +75,20 @@ export function currentLocale() {
if (locale in messages) { if (locale in messages) {
return locale; return locale;
} }
// some locales are further specified such as "en-US". // If the locale is a 2-letter code, we can try to find a regional variant
// If we only have a generic locale for this, we can use it too // e.g. "fr" may not be in the messages, but "fr-FR" is
const genericLocale = locale.split("-")[0]; if (locale.length === 2) {
if (genericLocale in messages) { const regionalLocale = `${locale}-${locale.toUpperCase()}`;
return genericLocale; if (regionalLocale in messages) {
return regionalLocale;
}
} else {
// Some locales are further specified such as "en-US".
// If we only have a generic locale for this, we can use it too
const genericLocale = locale.slice(0, 2);
if (genericLocale in messages) {
return genericLocale;
}
} }
} }
return "en"; return "en";

View file

@ -16,7 +16,7 @@
<span class="fs-4 title">{{ $t("Uptime Kuma") }}</span> <span class="fs-4 title">{{ $t("Uptime Kuma") }}</span>
</router-link> </router-link>
<a v-if="hasNewVersion" target="_blank" href="https://github.com/louislam/uptime-kuma/releases" class="btn btn-info me-3"> <a v-if="hasNewVersion" target="_blank" href="https://github.com/louislam/uptime-kuma/releases" class="btn btn-primary me-3">
<font-awesome-icon icon="arrow-alt-circle-up" /> {{ $t("New Update") }} <font-awesome-icon icon="arrow-alt-circle-up" /> {{ $t("New Update") }}
</a> </a>

View file

@ -9,7 +9,8 @@
<div>{{ monitor.id }}</div> <div>{{ monitor.id }}</div>
</div> </div>
</h1> </h1>
<p v-if="monitor.description">{{ monitor.description }}</p> <!-- eslint-disable-next-line vue/no-v-html-->
<p v-if="monitor.description" v-html="descriptionHTML"></p>
<div class="d-flex"> <div class="d-flex">
<div class="tags"> <div class="tags">
<Tag v-for="tag in monitor.tags" :key="tag.id" :item="tag" :size="'sm'" /> <Tag v-for="tag in monitor.tags" :key="tag.id" :item="tag" :size="'sm'" />
@ -285,6 +286,8 @@ import Tag from "../components/Tag.vue";
import CertificateInfo from "../components/CertificateInfo.vue"; import CertificateInfo from "../components/CertificateInfo.vue";
import { getMonitorRelativeURL } from "../util.ts"; import { getMonitorRelativeURL } from "../util.ts";
import { URL } from "whatwg-url"; import { URL } from "whatwg-url";
import DOMPurify from "dompurify";
import { marked } from "marked";
import { getResBaseURL } from "../util-frontend"; import { getResBaseURL } from "../util-frontend";
import { highlight, languages } from "prismjs/components/prism-core"; import { highlight, languages } from "prismjs/components/prism-core";
import "prismjs/components/prism-clike"; import "prismjs/components/prism-clike";
@ -399,6 +402,14 @@ export default {
screenshotURL() { screenshotURL() {
return getResBaseURL() + this.monitor.screenshot + "?time=" + this.cacheTime; return getResBaseURL() + this.monitor.screenshot + "?time=" + this.cacheTime;
},
descriptionHTML() {
if (this.monitor.description != null) {
return DOMPurify.sanitize(marked(this.monitor.description));
} else {
return "";
}
} }
}, },

View file

@ -157,12 +157,12 @@
<!-- Admin functions --> <!-- Admin functions -->
<div v-if="hasToken" class="mb-4"> <div v-if="hasToken" class="mb-4">
<div v-if="!enableEditMode"> <div v-if="!enableEditMode">
<button class="btn btn-info me-2" data-testid="edit-button" @click="edit"> <button class="btn btn-primary me-2" data-testid="edit-button" @click="edit">
<font-awesome-icon icon="edit" /> <font-awesome-icon icon="edit" />
{{ $t("Edit Status Page") }} {{ $t("Edit Status Page") }}
</button> </button>
<a href="/manage-status-page" class="btn btn-info"> <a href="/manage-status-page" class="btn btn-primary">
<font-awesome-icon icon="tachometer-alt" /> <font-awesome-icon icon="tachometer-alt" />
{{ $t("Go to Dashboard") }} {{ $t("Go to Dashboard") }}
</a> </a>