Compare commits

...

53 Commits

Author SHA1 Message Date
Louis Lam f45e0253f1 Speed up by using node --run instead of npm run 2026-06-24 20:13:07 +08:00
Louis Lam 7cce7df233 Implement DisableAuth and implement doubleCheckPassword() 2026-06-13 19:46:45 +08:00
Louis Lam 8d29fdec37 Improve login/logout without refresh the page 2026-06-12 12:08:02 +08:00
Louis Lam ec49c7c456 Prepare a fake session for DisableAuth (getDisableAuthSession) 2026-06-09 12:44:08 +08:00
Louis Lam 413b546529 Merge branch '3.0.X' into dev-better-auth 2026-06-09 11:45:43 +08:00
Louis Lam 28ee14d30d chore: Merge master 2026-06-09 11:45:01 +08:00
Louis Lam 1a36862abc Merge branch 'master' into 3.0.X
# Conflicts:
#	package-lock.json
#	package.json
2026-06-09 11:44:04 +08:00
Louis Lam 17626d1a24 Add 2fa plugin 2026-06-08 11:56:34 +08:00
Louis Lam 5222eef990 Handle remember me, improve logout 2026-06-08 11:46:14 +08:00
Louis Lam 45a3bac95f Add some plugins 2026-06-02 12:00:36 +08:00
Louis Lam 0de5ed1e8a Migrate change password 2026-06-02 11:51:49 +08:00
Louis Lam 9755823b93 WIP 2026-05-30 12:18:27 +08:00
Louis Lam 0d17551ed3 WIP 2026-05-28 12:36:40 +08:00
Louis Lam 5ad681edf1 WIP 2026-05-26 19:14:17 +08:00
Louis Lam ddd1a2a075 WIP 2026-05-26 12:34:41 +08:00
Louis Lam 18ec60e4d7 WIP 2026-05-26 12:33:49 +08:00
Louis Lam 13042aa990 WIP 2026-05-26 12:24:31 +08:00
Louis Lam eaa501f37d Correct TypeScript context (src uses vue LSP, other just use normal Typescript LSP) 2026-05-26 11:15:28 +08:00
Louis Lam d96753fd41 WIP: Expose better-auth's HTTP API 2026-05-26 02:11:51 +08:00
Louis Lam af886113ff Create user via better-auth 2026-05-26 01:42:58 +08:00
Louis Lam 219a4d3e10 Create user via better-auth 2026-05-26 01:18:50 +08:00
Louis Lam be3bd8563a Merge branch '3.0.X' into dev-better-auth
# Conflicts:
#	package-lock.json
#	package.json
#	src/util.ts
2026-05-26 00:41:57 +08:00
Louis Lam 3e2eb5bd0c Merge branch 'master' into 3.0.X 2026-05-26 00:39:06 +08:00
Louis Lam a0473769db chore: [3.0.X] Update Node.js to 26 (#7409) 2026-05-17 18:26:52 +08:00
Louis Lam 0632ca1ae8 chore: [3.0.X] Merge changes from master (#7408) 2026-05-17 15:40:53 +08:00
Louis Lam deaed046b0 chore: [3.0.X] Merge from master 2026-05-17 15:38:31 +08:00
Louis Lam 3abbeecddf Merge branch 'master' into 3.0.X-merge
# Conflicts:
#	package-lock.json
#	package.json
#	src/util.js
2026-05-17 15:36:42 +08:00
Louis Lam df23942f65 chore: remove @aws-sdk, @azure packages [3.0.X] (#7102) 2026-03-06 22:35:46 +08:00
Louis Lam f154fcf8da chore: merge changes to 3.0.X (#7104) 2026-03-06 22:30:07 +08:00
Louis Lam 7a16d803d3 Manual merge 2026-03-06 22:28:23 +08:00
Louis Lam 73d3573198 Merge branch 'master' into 3.0.X-merge
# Conflicts:
#	.github/workflows/validate.yml
#	extra/rdap-dns.json
#	extra/release/final.mjs
#	package-lock.json
#	package.json
#	src/util.js
2026-03-06 22:26:21 +08:00
Dream 9df8a957c7 fix: Explicitly pushing status=down now bypasses retry logic and goes directly to DOWN (#6595)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-02-07 09:52:25 +01:00
Louis Lam fcb0af3fd1 WIP 2026-01-25 09:01:07 +08:00
Louis Lam e5c679332e Connect our database using kysely-knex adapter 2026-01-25 08:06:17 +08:00
Louis Lam 998f4e81fa Update 2026-01-25 07:20:29 +08:00
Louis Lam cea9278755 Merge branch '3.0.X' into dev-better-auth
# Conflicts:
#	.eslintrc.js
#	package-lock.json
2026-01-25 07:18:50 +08:00
Louis Lam baaf14c594 chore: Merge Changes from master to 3.0.X (#6806) 2026-01-25 07:15:54 +08:00
Louis Lam b3388f5bb7 Regenerate package-lock.json 2026-01-25 07:06:43 +08:00
Louis Lam 3f1866f658 Merge branch 'master' into 3.0.X-merge
# Conflicts:
#	.github/workflows/auto-test.yml
#	CONTRIBUTING.md
#	extra/build-healthcheck.js
#	extra/release/beta.mjs
#	extra/release/lib.mjs
#	extra/sort-contributors.js
#	package-lock.json
#	package.json
#	src/util.js
#	tsconfig-backend.json
2026-01-25 07:02:20 +08:00
Louis Lam 57f5414d79 [3.0.0] Merge changes from master (#6634) 2026-01-07 16:02:20 +08:00
Louis Lam 0b174ef25a Match new playwright version 2026-01-07 16:01:07 +08:00
Louis Lam 9cfa0f483d Update lock file 2026-01-07 15:55:31 +08:00
Louis Lam f28cba8388 Merge branch 'master' into 3.0.X-merge
# Conflicts:
#	.github/workflows/auto-test.yml
#	.github/workflows/validate.yml
#	CONTRIBUTING.md
#	package-lock.json
#	package.json
#	src/util.js
2026-01-07 15:53:59 +08:00
Louis Lam c631cd3373 WIP 2025-11-10 19:28:01 +08:00
Louis Lam 77a31f1fbe WIP 2025-11-10 04:05:45 +08:00
Louis Lam fb0b8b484f WIP 2025-11-10 04:04:39 +08:00
Louis Lam b50d496e75 WIP 2025-11-10 03:10:39 +08:00
Louis Lam fe1cd3f2da Merge branch '3.0.X' into dev-better-auth 2025-11-10 03:08:50 +08:00
Louis Lam ac5781d711 Update playwright from ~1.39.0 to ~1.56.1 (#6321) 2025-11-08 02:58:58 +08:00
Louis Lam fa2bc8eda6 Merge branch '3.0.X' into dev-better-auth 2025-11-05 22:05:07 +08:00
Louis Lam 93fc8e463f [3.0.0] Project Upgrade (#6310) 2025-11-05 21:54:55 +08:00
Louis Lam 492b8f51ad schema 2025-10-29 00:42:26 +08:00
Louis Lam c515b6d043 wip 2025-10-25 12:24:17 +08:00
54 changed files with 2950 additions and 4029 deletions
+10
View File
@@ -104,9 +104,19 @@ module.exports = {
extends: ["plugin:@typescript-eslint/recommended"],
rules: {
"jsdoc/require-returns-type": "off",
"jsdoc/require-returns": [
"warn",
{
forceRequireReturn: false,
forceReturnsWithAsync: false,
},
],
"jsdoc/require-param-type": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/ban-ts-comment": "off",
"prefer-const": "off",
"@typescript-eslint/no-unused-vars": "warn",
eqeqeq: "off",
},
},
],
+8 -43
View File
@@ -6,7 +6,7 @@ concurrency:
on:
push:
branches: [master, 1.23.X, 3.0.0]
branches: [master, 1.23.X, 3.0.X]
pull_request:
permissions: {}
@@ -21,11 +21,7 @@ jobs:
matrix:
os: [macos-latest, ubuntu-22.04, windows-latest, ubuntu-22.04-arm]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
node: [20, 24]
# Also test non-LTS, but only on Ubuntu.
include:
- os: ubuntu-22.04
node: 25
node: [26]
steps:
- run: git config --global core.autocrlf false # Mainly for Windows
@@ -63,37 +59,6 @@ jobs:
HEADLESS_TEST: 1
JUST_FOR_TEST: ${{ secrets.JUST_FOR_TEST }}
# As a lot of dev dependencies are not supported on ARMv7, we have to test it separately and just test if `npm ci --production` works
armv7-simple-test:
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
fail-fast: false
matrix:
node: [20, 22]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: { persist-credentials: false }
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
with:
platforms: linux/arm/v7
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1
- name: Test on ARMv7 using Docker with QEMU
run: |
docker run --rm --platform linux/arm/v7 \
-v $PWD:/workspace \
-w /workspace \
arm32v7/node:${{ matrix.node }} \
npm clean-install --no-fund --production
check-linters:
runs-on: ubuntu-latest
permissions:
@@ -112,19 +77,19 @@ jobs:
# path: node_modules
# key: node-modules-${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }}
- name: Use Node.js 20
- name: Use Node.js 24
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version: 20
node-version: 24
- run: npm clean-install --no-fund
- run: npm run lint:prod
e2e-test:
runs-on: ubuntu-22.04-arm
runs-on: ubuntu-latest
permissions:
contents: read
env:
PLAYWRIGHT_VERSION: ~1.39.0
PLAYWRIGHT_VERSION: ~1.60.0
steps:
- run: git config --global core.autocrlf false # Mainly for Windows
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
@@ -141,14 +106,14 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version: 22
node-version: 26
- run: npm clean-install --no-fund
- name: Rebuild native modules for ARM64
run: npm rebuild @louislam/sqlite3
- name: Install Playwright ${{ env.PLAYWRIGHT_VERSION }}
run: npx playwright@${{ env.PLAYWRIGHT_VERSION }} install
run: npx playwright@${{ env.PLAYWRIGHT_VERSION }} install --with-deps
- run: npm run build
- run: npm run test-e2e
+1
View File
@@ -7,6 +7,7 @@ on:
branches:
- master
- 1.23.X
- 3.0.X
workflow_dispatch:
permissions: {}
+6
View File
@@ -0,0 +1,6 @@
{
"framework": "none",
"src/**": {
"framework": "vue"
}
}
+4 -5
View File
@@ -288,7 +288,7 @@ you can finally start the app. The goal is to make the Uptime Kuma installation
as easy as installing a mobile app.
- Easy to install for non-Docker users
- no native build dependency is needed (for `x86_64`/`armv7`/`arm64`)
- no native build dependency is needed (for `x86_64`/`arm64`)
- no extra configuration and
- no extra effort required to get it running
@@ -469,7 +469,7 @@ We have a few procedures we follow. These are documented here:
- <details><summary><b>Set up a Docker Builder</b> (click to expand)</summary>
<p>
- amd64, armv7 using local.
- amd64 using local.
- arm64 using remote arm64 cpu, as the emulator is too slow and can no longer
pass the `npm ci` command.
1. Add the public key to the remote server.
@@ -483,7 +483,7 @@ We have a few procedures we follow. These are documented here:
3. Create a new builder.
```bash
docker buildx create --name kuma-builder --platform linux/amd64,linux/arm/v7
docker buildx create --name kuma-builder --platform linux/amd64
docker buildx use kuma-builder
docker buildx inspect --bootstrap
```
@@ -516,8 +516,7 @@ We have a few procedures we follow. These are documented here:
These Items need to be checked:
- [ ] Check all tags is fine on
<https://hub.docker.com/r/louislam/uptime-kuma/tags>
- [ ] Try the Docker image with tag 1.X.X (Clean install / amd64 / arm64 /
armv7)
- [ ] Try the Docker image with tag 1.X.X (Clean install / amd64 / arm64)
- [ ] Try clean installation with Node.js
</p>
+1 -1
View File
@@ -59,7 +59,7 @@ export default defineConfig({
// Run your local dev server before starting the tests.
webServer: {
command: `node extra/remove-playwright-test-data.js && cross-env NODE_ENV=development node server/server.js --port=${port} --data-dir=./data/playwright-test`,
command: `node extra/remove-playwright-test-data.js && cross-env NODE_ENV=development node --import=tsx server/server.js --port=${port} --data-dir=./data/playwright-test`,
url,
reuseExistingServer: false,
cwd: "../",
@@ -0,0 +1,56 @@
/*
* The schema from: https://www.better-auth.com/docs/concepts/database#core-schema
*/
exports.up = function (knex) {
return knex.schema
.createTable("better_auth_user", (t) => {
t.string("id").primary();
t.string("name").notNullable();
t.string("email").notNullable();
t.boolean("emailVerified").notNullable();
t.string("image");
t.timestamp("createdAt").notNullable();
t.timestamp("updatedAt").notNullable();
})
.createTable("better_auth_session", (t) => {
t.string("id").primary();
t.string("userId").notNullable().references("id").inTable("better_auth_user");
t.string("token").notNullable();
t.timestamp("expiresAt").notNullable();
t.string("ipAddress");
t.string("userAgent");
t.timestamp("createdAt").notNullable();
t.timestamp("updatedAt").notNullable();
})
.createTable("better_auth_account", (t) => {
t.string("id").primary();
t.string("userId").notNullable().references("id").inTable("better_auth_user");
t.string("accountId").notNullable();
t.string("providerId").notNullable();
t.string("accessToken");
t.string("refreshToken");
t.timestamp("accessTokenExpiresAt");
t.timestamp("refreshTokenExpiresAt");
t.string("scope");
t.string("idToken");
t.string("password");
t.timestamp("createdAt").notNullable();
t.timestamp("updatedAt").notNullable();
})
.createTable("better_auth_verification", (t) => {
t.string("id").primary();
t.string("identifier").notNullable();
t.string("value").notNullable();
t.timestamp("expiresAt").notNullable();
t.timestamp("createdAt").notNullable();
t.timestamp("updatedAt").notNullable();
});
};
exports.down = function (knex) {
return knex.schema
.dropTableIfExists("better_auth_verification")
.dropTableIfExists("better_auth_account")
.dropTableIfExists("better_auth_session")
.dropTableIfExists("better_auth_user");
};
@@ -0,0 +1,16 @@
/*
* The schema from: https://better-auth.com/docs/plugins/username#schema
*/
exports.up = function (knex) {
return knex.schema.table("better_auth_user", (t) => {
t.string("username").unique();
t.string("displayUsername");
});
};
exports.down = function (knex) {
return knex.schema.table("better_auth_user", (t) => {
t.dropColumn("username");
t.dropColumn("displayUsername");
});
};
@@ -0,0 +1,28 @@
/*
* The schema from: https://better-auth.com/docs/plugins/admin#schema
*/
exports.up = function (knex) {
return knex.schema
.table("better_auth_user", function (table) {
table.text("role");
table.boolean("banned");
table.text("banReason");
table.timestamp("banExpires");
})
.table("better_auth_session", function (table) {
table.text("impersonatedBy");
});
};
exports.down = function (knex) {
return knex.schema
.table("better_auth_user", function (table) {
table.dropColumn("role");
table.dropColumn("banned");
table.dropColumn("banReason");
table.dropColumn("banExpires");
})
.table("better_auth_session", function (table) {
table.dropColumn("impersonatedBy");
});
};
@@ -0,0 +1,68 @@
const tables = [
{
name: "docker_host",
onDelete: "SET NULL",
},
{
name: "proxy",
onDelete: "SET NULL",
},
{
name: "monitor",
onDelete: "SET NULL",
},
{
name: "maintenance",
onDelete: "SET NULL",
},
{
name: "notification",
onDelete: "SET NULL",
},
{
name: "api_key",
onDelete: "CASCADE",
},
{
name: "remote_browser",
onDelete: "SET NULL",
},
];
exports.up = async function (knex) {
for (const table of tables) {
await knex.schema.alterTable(table.name, (t) => {
t.dropForeign("user_id");
t.dropColumn("user_id");
});
await knex.schema.alterTable(table.name, (t) => {
t.string("user_id", 255).nullable();
});
await knex.schema.alterTable(table.name, (t) => {
t.foreign("user_id")
.references("id")
.inTable("better_auth_user")
.onDelete(table.onDelete)
.onUpdate("CASCADE");
});
}
};
exports.down = async function (knex) {
for (const table of tables) {
await knex.schema.alterTable(table.name, (t) => {
t.dropForeign("user_id");
t.dropColumn("user_id");
});
await knex.schema.alterTable(table.name, (t) => {
t.integer("user_id").unsigned().nullable();
});
await knex.schema.alterTable(table.name, (t) => {
t.foreign("user_id").references("id").inTable("user").onDelete(table.onDelete).onUpdate("CASCADE");
});
}
};
@@ -0,0 +1,27 @@
/*
* The schema from: https://better-auth.com/docs/plugins/2fa#schema
*/
exports.up = function (knex) {
return knex.schema
.createTable("better_auth_twoFactor", (t) => {
t.string("id").primary();
t.string("secret").notNullable();
t.string("backupCodes").notNullable();
t.boolean("verified").notNullable();
t.string("userId")
.notNullable()
.references("id")
.inTable("better_auth_user")
.onDelete("CASCADE")
.onUpdate("CASCADE");
})
.table("better_auth_user", (t) => {
t.boolean("twoFactorEnabled");
});
};
exports.down = function (knex) {
return knex.schema.dropTableIfExists("better_auth_twoFactor").table("better_auth_user", (t) => {
t.dropColumn("twoFactorEnabled");
});
};
+2 -9
View File
@@ -1,22 +1,15 @@
############################################
# Build in Golang
# Run npm run build-healthcheck-armv7 in the host first, another it will be super slow where it is building the armv7 healthcheck
############################################
FROM golang:1-buster
FROM golang:1-trixie
WORKDIR /app
ARG TARGETPLATFORM
COPY ./extra/ ./extra/
## Switch to archive.debian.org
RUN sed -i '/^deb/s/^/#/' /etc/apt/sources.list \
&& echo "deb http://archive.debian.org/debian buster main contrib non-free" | tee -a /etc/apt/sources.list \
&& echo "deb http://archive.debian.org/debian-security buster/updates main contrib non-free" | tee -a /etc/apt/sources.list \
&& echo "deb http://archive.debian.org/debian buster-updates main contrib non-free" | tee -a /etc/apt/sources.list
# Compile healthcheck.go
RUN apt update && \
apt --yes --no-install-recommends install curl && \
curl -sL https://deb.nodesource.com/setup_18.x | bash && \
curl -sL https://deb.nodesource.com/setup_24.x | bash && \
apt --yes --no-install-recommends install nodejs && \
node ./extra/build-healthcheck.js $TARGETPLATFORM && \
apt --yes remove nodejs
+4 -6
View File
@@ -1,5 +1,5 @@
# Download Apprise deb package
FROM node:22-bookworm-slim AS download-apprise
FROM node:26-trixie-slim AS download-apprise
WORKDIR /app
COPY ./extra/download-apprise.mjs ./download-apprise.mjs
RUN apt update && \
@@ -9,7 +9,7 @@ RUN apt update && \
# Base Image (Slim)
# If the image changed, the second stage image should be changed too
FROM node:22-bookworm-slim AS base2-slim
FROM node:26-trixie-slim AS base3-slim
ARG TARGETPLATFORM
# Specify --no-install-recommends to skip unused dependencies, make the base much smaller!
@@ -35,7 +35,6 @@ RUN apt update && \
apt --yes autoremove
# apprise = for notifications (Install from the deb package, as the stable one is too old) (workaround for #4867)
# Switching to testing repo is no longer working, as the testing repo is not bookworm anymore.
# python3-paho-mqtt (#4859)
# TODO: no idea how to delete the deb file after installation as it becomes a layer already
COPY --from=download-apprise /app/apprise.deb ./apprise.deb
@@ -47,7 +46,7 @@ RUN apt update && \
# Install cloudflared
RUN curl https://pkg.cloudflare.com/cloudflare-main.gpg --output /usr/share/keyrings/cloudflare-main.gpg && \
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared bookworm main' | tee /etc/apt/sources.list.d/cloudflared.list && \
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main' | tee /etc/apt/sources.list.d/cloudflared.list && \
apt update && \
apt install --yes --no-install-recommends cloudflared && \
cloudflared version && \
@@ -67,8 +66,7 @@ RUN curl -fsSL https://letsencrypt.org/certs/gen-y/root-ye.pem -o /usr/local/sha
# Full Base Image
# MariaDB, Chromium and fonts
# Make sure to reuse the slim image here. Uncomment the above line if you want to build it from scratch.
# FROM base2-slim AS base2
FROM louislam/uptime-kuma:base2-slim AS base2
FROM louislam/uptime-kuma:base3-slim AS base3
ENV UPTIME_KUMA_ENABLE_EMBEDDED_MARIADB=1
RUN apt update && \
apt --yes --no-install-recommends install chromium fonts-indic fonts-noto fonts-noto-cjk mariadb-server && \
+1 -1
View File
@@ -3,7 +3,7 @@ version: "3.8"
services:
uptime-kuma:
container_name: uptime-kuma-dev
image: louislam/uptime-kuma:nightly2
image: louislam/uptime-kuma:nightly3
volumes:
#- ./data:/app/data
- ../server:/app/server
+6 -6
View File
@@ -1,16 +1,16 @@
ARG BASE_IMAGE=louislam/uptime-kuma:base2
ARG BASE_IMAGE=louislam/uptime-kuma:base3
############################################
# Build in Golang
# Run npm run build-healthcheck-armv7 in the host first, otherwise it will be super slow where it is building the armv7 healthcheck
# Check file: builder-go.dockerfile
############################################
FROM louislam/uptime-kuma:builder-go AS build_healthcheck
FROM louislam/uptime-kuma:builder-go3 AS build_healthcheck
############################################
# Build in Node.js
############################################
FROM louislam/uptime-kuma:base2 AS build
FROM louislam/uptime-kuma:base3 AS build
USER node
WORKDIR /app
@@ -59,7 +59,7 @@ USER node
############################################
# Build an image for testing pr
############################################
FROM louislam/uptime-kuma:base2 AS pr-test2
FROM louislam/uptime-kuma:base3 AS pr-test2
WORKDIR /app
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1
@@ -92,7 +92,7 @@ CMD ["npm", "run", "start-pr-test"]
############################################
# Upload the artifact to Github
############################################
FROM louislam/uptime-kuma:base2 AS upload-artifact
FROM louislam/uptime-kuma:base3 AS upload-artifact
WORKDIR /
RUN apt update && \
apt --yes install curl file
-18
View File
@@ -1,5 +1,4 @@
const childProcess = require("child_process");
const fs = require("fs");
const platform = process.argv[2];
if (!platform) {
@@ -7,22 +6,5 @@ if (!platform) {
process.exit(1);
}
if (platform === "linux/arm/v7") {
console.log("Arch: armv7");
if (fs.existsSync("./extra/healthcheck-armv7")) {
fs.renameSync("./extra/healthcheck-armv7", "./extra/healthcheck");
console.log("Already built in the host, skip.");
process.exit(0);
} else {
console.log(
"prebuilt not found, it will be slow! You should execute `npm run build-healthcheck-armv7` before build."
);
}
} else {
if (fs.existsSync("./extra/healthcheck-armv7")) {
fs.rmSync("./extra/healthcheck-armv7");
}
}
const output = childProcess.execSync("go build -x -o ./extra/healthcheck ./extra/healthcheck.go").toString("utf8");
console.log(output);
-55
View File
@@ -1,55 +0,0 @@
/*
* ⚠️ ⚠️ ⚠️ ⚠️ Due to the weird issue in Portainer that the healthcheck script is still pointing to this script for unknown reason.
* IT CANNOT BE DROPPED, even though it looks like it is not used.
* See more: https://github.com/louislam/uptime-kuma/issues/2774#issuecomment-1429092359
*
* ⚠️ Deprecated: Changed to healthcheck.go, it will be deleted in the future.
* This script should be run after a period of time (180s), because the server may need some time to prepare.
*/
const FBSD = /^freebsd/.test(process.platform);
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
let client;
const sslKey = process.env.UPTIME_KUMA_SSL_KEY || process.env.SSL_KEY || undefined;
const sslCert = process.env.UPTIME_KUMA_SSL_CERT || process.env.SSL_CERT || undefined;
if (sslKey && sslCert) {
client = require("https");
} else {
client = require("http");
}
// If host is omitted, the server will accept connections on the unspecified IPv6 address (::) when IPv6 is available and the unspecified IPv4 address (0.0.0.0) otherwise.
// Dual-stack support for (::)
let hostname = process.env.UPTIME_KUMA_HOST;
// Also read HOST if not *BSD, as HOST is a system environment variable in FreeBSD
if (!hostname && !FBSD) {
hostname = process.env.HOST;
}
const port = parseInt(process.env.UPTIME_KUMA_PORT || process.env.PORT || 3001);
let options = {
host: hostname || "127.0.0.1",
port: port,
timeout: 28 * 1000,
};
let request = client.request(options, (res) => {
console.log(`Health Check OK [Res Code: ${res.statusCode}]`);
if (res.statusCode === 302) {
process.exit(0);
} else {
process.exit(1);
}
});
request.on("error", function (err) {
console.error("Health Check ERROR");
process.exit(1);
});
request.end();
-3
View File
@@ -1,10 +1,7 @@
const pkg = require("../package.json");
const fs = require("fs");
const util = require("../src/util");
const dayjs = require("dayjs");
util.polyfill();
const oldVersion = pkg.version;
const newVersion = oldVersion + "-nightly-" + dayjs().format("YYYYMMDDHHmmss");
+2 -2
View File
@@ -61,14 +61,14 @@ if (!dryRun) {
repoNames,
["beta-slim-rootless", ver(version, "slim-rootless")],
"rootless",
"BASE_IMAGE=louislam/uptime-kuma:base2-slim"
"BASE_IMAGE=louislam/uptime-kuma:base3-slim"
);
// Build full image (rootless)
buildImage(repoNames, ["beta-rootless", ver(version, "rootless")], "rootless");
// Build slim image
buildImage(repoNames, ["beta-slim", ver(version, "slim")], "release", "BASE_IMAGE=louislam/uptime-kuma:base2-slim");
buildImage(repoNames, ["beta-slim", ver(version, "slim")], "release", "BASE_IMAGE=louislam/uptime-kuma:base3-slim");
// Build full image
buildImage(repoNames, ["beta", version], "release");
+6 -6
View File
@@ -59,24 +59,24 @@ if (!dryRun) {
// Build slim image (rootless)
buildImage(
repoNames,
["2-slim-rootless", ver(version, "slim-rootless")],
["3-slim-rootless", ver(version, "slim-rootless")],
"rootless",
"BASE_IMAGE=louislam/uptime-kuma:base2-slim"
"BASE_IMAGE=louislam/uptime-kuma:base3-slim"
);
// Build full image (rootless)
buildImage(repoNames, ["2-rootless", ver(version, "rootless")], "rootless");
buildImage(repoNames, ["3-rootless", ver(version, "rootless")], "rootless");
// Build slim image
buildImage(
repoNames,
["next-slim", "2-slim", ver(version, "slim")],
["next-slim", "3-slim", ver(version, "slim")],
"release",
"BASE_IMAGE=louislam/uptime-kuma:base2-slim"
"BASE_IMAGE=louislam/uptime-kuma:base3-slim"
);
// Build full image
buildImage(repoNames, ["next", "2", version], "release");
buildImage(repoNames, ["next", "3", version], "release");
} else {
console.log("Dry run mode - skipping image build and push.");
}
+1 -1
View File
@@ -64,7 +64,7 @@ export function buildImage(
target,
buildArgs = "",
dockerfile = "docker/dockerfile",
platform = "linux/amd64,linux/arm64,linux/arm/v7"
platform = "linux/amd64,linux/arm64"
) {
let args = ["buildx", "build", "-f", dockerfile, "--platform", platform];
+2 -61
View File
@@ -1,64 +1,5 @@
console.log("== Uptime Kuma Remove 2FA Tool ==");
console.log("Loading the database");
console.log("TODO");
const Database = require("../server/database");
const { R } = require("redbean-node");
const readline = require("readline");
const TwoFA = require("../server/2fa");
const args = require("args-parser")(process.argv);
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const main = async () => {
Database.initDataDir(args);
await Database.connect();
try {
// No need to actually reset the password for testing, just make sure no connection problem. It is ok for now.
if (!process.env.TEST_BACKEND) {
const user = await R.findOne("user");
if (!user) {
throw new Error("user not found, have you installed?");
}
console.log("Found user: " + user.username);
let ans = await question("Are you sure want to remove 2FA? [y/N]");
if (ans.toLowerCase() === "y") {
await TwoFA.disable2FA(user.id);
console.log("2FA has been removed successfully.");
}
}
} catch (e) {
console.error("Error: " + e.message);
}
await Database.close();
rl.close();
console.log("Finished.");
};
/**
* Ask question of user
* @param {string} question Question to ask
* @returns {Promise<string>} Users response
*/
function question(question) {
return new Promise((resolve) => {
rl.question(question, (answer) => {
resolve(answer);
});
});
}
if (!process.env.TEST_BACKEND) {
main();
}
module.exports = {
main,
};
// TODO
+2 -3
View File
@@ -4,7 +4,6 @@ const Database = require("../server/database");
const { R } = require("redbean-node");
const readline = require("readline");
const { passwordStrength } = require("check-password-strength");
const { initJWTSecret } = require("../server/util-server");
const User = require("../server/model/user");
const { io } = require("socket.io-client");
const { localWebSocketURL } = require("../server/config");
@@ -62,8 +61,8 @@ const main = async () => {
if (!("dry-run" in args)) {
await User.resetPassword(user.id, password);
// Reset all sessions by reset jwt secret
await initJWTSecret();
// TODO: Reset all sessions by reset jwt secret
// await initJWTSecret();
// Disconnect all other socket clients of the user
await disconnectAllSocketClients(user.username, password);
+1954 -2462
View File
File diff suppressed because it is too large Load Diff
+38 -36
View File
@@ -1,53 +1,49 @@
{
"name": "uptime-kuma",
"version": "2.4.0",
"version": "3.0.0-beta.0",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/louislam/uptime-kuma.git"
},
"engines": {
"node": ">= 20.4.0"
"node": ">= 26.2.0"
},
"scripts": {
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
"lint:js-prod": "npm run lint:js",
"lint:js-prod": "node --run lint:js",
"lint-fix:js": "eslint --ext \".js,.vue\" --fix --ignore-path .gitignore .",
"lint:style": "stylelint \"**/*.{vue,css,scss}\" --ignore-path .gitignore",
"lint-fix:style": "stylelint \"**/*.{vue,css,scss}\" --fix --ignore-path .gitignore",
"lint": "npm run lint:js && npm run lint:style",
"lint": "node --run lint:js && node --run lint:style",
"fmt": "prettier --write \"**/*.{js,ts,vue,css,scss,json,md,yml,yaml}\"",
"lint:prod": "npm run lint:js-prod && npm run lint:style",
"dev": "concurrently -k -r \"wait-on tcp:3000 && npm run start-server-dev \" \"npm run start-frontend-dev\"",
"lint:prod": "node --run lint:js-prod && node --run lint:style",
"dev": "concurrently -k -r \"wait-on tcp:3000 && node --run start-server-dev \" \"node --run start-frontend-dev\"",
"start-frontend-dev": "cross-env NODE_ENV=development vite --host --config ./config/vite.config.js",
"start-frontend-devcontainer": "cross-env NODE_ENV=development DEVCONTAINER=1 vite --host --config ./config/vite.config.js",
"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:watch": "cross-env NODE_ENV=development node --watch server/server.js",
"start": "node --run start-server",
"start-server": "tsx server/server.js",
"start-server-dev": "cross-env NODE_ENV=development tsx server/server.js",
"start-server-dev:watch": "cross-env NODE_ENV=development tsx --watch server/server.js",
"build": "vite build --config ./config/vite.config.js",
"test": "npm run test-backend && npm run test-e2e",
"test-with-build": "npm run build && npm test",
"test-backend": "node test/test-backend.mjs",
"test-backend-22": "cross-env TEST_BACKEND=1 node --test --test-reporter=spec \"test/backend-test/**/*.js\"",
"test-backend-20": "cross-env TEST_BACKEND=1 node --test --test-reporter=spec test/backend-test",
"test": "node --run test-backend && node --run test-e2e",
"test-with-build": "node --run build && npm test",
"test-backend": "cross-env TEST_BACKEND=1 node --import=tsx --test --test-reporter=spec \"test/backend-test/**/*.js\"",
"test-e2e": "playwright test --config ./config/playwright.config.js",
"test-e2e-ui": "playwright test --config ./config/playwright.config.js --ui --ui-port=51063",
"playwright-codegen": "playwright codegen localhost:3000 --save-storage=./private/e2e-auth.json",
"playwright-show-report": "playwright show-report ./private/playwright-report",
"tsc": "tsc --project ./tsconfig-backend.json",
"vite-preview-dist": "vite preview --host --config ./config/vite.config.js",
"build-docker-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base2 --target base2 . --push",
"build-docker-base-slim": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:base2-slim --target base2-slim . --push",
"build-docker-builder-go": "docker buildx build -f docker/builder-go.dockerfile --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:builder-go . --push",
"build-docker-nightly-local": "npm run build && docker build -f docker/dockerfile -t louislam/uptime-kuma:nightly2 --target nightly .",
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test2 --target pr-test2 . --push",
"build-docker-base": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:base3 --target base3 . --push",
"build-docker-base-slim": "docker buildx build -f docker/debian-base.dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:base3-slim --target base3-slim . --push",
"build-docker-builder-go": "docker buildx build -f docker/builder-go.dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:builder-go3 . --push",
"build-docker-nightly-local": "node --run build && docker build -f docker/dockerfile -t louislam/uptime-kuma:nightly3 --target nightly .",
"build-docker-pr-test": "docker buildx build -f docker/dockerfile --platform linux/amd64,linux/arm64 -t louislam/uptime-kuma:pr-test3 --target pr-test3 . --push",
"upload-artifacts": "node extra/release/upload-artifacts.mjs",
"upload-artifacts-beta": "node extra/release/upload-artifacts-beta.mjs",
"setup": "git checkout 2.4.0 && npm ci --omit dev --no-audit && npm run download-dist",
"setup": "git checkout 2.4.0 && npm ci --omit dev --no-audit && node --run download-dist",
"download-dist": "node extra/download-dist.js",
"mark-as-nightly": "node extra/mark-as-nightly.js",
"reset-password": "node extra/reset-password.js",
"reset-password": "tsx extra/reset-password.ts",
"remove-2fa": "node extra/remove-2fa.js",
"simple-dns-server": "node extra/simple-dns-server.js",
"simple-mqtt-server": "node extra/simple-mqtt-server.js",
@@ -58,13 +54,11 @@
"release-beta": "node ./extra/release/beta.mjs",
"release-nightly": "node ./extra/release/nightly.mjs",
"git-remove-tag": "git tag -d",
"build-dist-and-restart": "npm run build && npm run start-server-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-dist-and-restart": "node --run build && node --run start-server-dev",
"start-pr-test": "node extra/checkout-pr.mjs && npm install && node --run dev",
"deploy-demo-server": "node extra/deploy-demo-server.js",
"quick-run-nightly": "docker run --rm --env NODE_ENV=development -p 3001:3001 louislam/uptime-kuma:nightly2",
"quick-run-nightly": "docker run --rm --env NODE_ENV=development -p 3001:3001 louislam/uptime-kuma:nightly3",
"start-dev-container": "cd docker && docker-compose -f docker-compose-dev.yml up --force-recreate",
"rebase-pr-to-1.23.X": "node extra/rebase-pr.js 1.23.X",
"reset-migrate-aggregate-table-state": "node extra/reset-migrate-aggregate-table-state.js",
"generate-changelog": "node ./extra/generate-changelog.mjs"
},
@@ -77,6 +71,7 @@
"axios": "~0.32.0",
"badge-maker": "~3.3.1",
"bcryptjs": "~2.4.3",
"better-auth": "~1.6.11",
"chardet": "~1.4.0",
"check-password-strength": "~2.0.10",
"cheerio": "~1.0.0",
@@ -88,7 +83,6 @@
"croner": "~8.1.2",
"dayjs": "~1.11.19",
"dev-null": "~0.1.1",
"dotenv": "~16.0.3",
"express": "~4.22.1",
"express-basic-auth": "~1.2.1",
"express-static-gzip": "~2.1.8",
@@ -110,6 +104,7 @@
"jwt-decode": "~3.1.2",
"kafkajs": "~2.2.4",
"knex": "~3.1.0",
"kysely-knex": "~0.2.0",
"limiter": "~2.1.0",
"liquidjs": "~10.26.0",
"marked": "~14.1.4",
@@ -124,13 +119,12 @@
"node-radius-utils": "~1.2.0",
"nodemailer": "~7.0.13",
"nostr-tools": "~2.20.0",
"notp": "~2.0.3",
"openid-client": "~5.7.1",
"oracledb": "~6.10.0",
"password-hash": "~1.2.2",
"pg": "~8.11.6",
"pg-connection-string": "~2.6.4",
"playwright-core": "~1.39.0",
"playwright-core": "~1.60.0",
"prom-client": "~13.2.0",
"prometheus-api-metrics": "~3.2.2",
"promisify-child-process": "~4.1.2",
@@ -146,12 +140,13 @@
"sqlstring": "~2.3.3",
"tar": "~6.2.1",
"tcp-ping": "~0.1.1",
"thirty-two": "~1.0.2",
"tldts": "~7.0.23",
"tough-cookie": "~4.1.4",
"validator": "~13.15.26",
"web-push": "~3.6.7",
"ws": "~8.19.0"
"tsx": "~4.22.3",
"ws": "~8.19.0",
"zod": "~4.4.3"
},
"devDependencies": {
"@actions/github": "~6.0.1",
@@ -159,7 +154,7 @@
"@fortawesome/free-regular-svg-icons": "~5.15.4",
"@fortawesome/free-solid-svg-icons": "~5.15.4",
"@fortawesome/vue-fontawesome": "~3.1.3",
"@playwright/test": "~1.39.0",
"@playwright/test": "~1.60.0",
"@popperjs/core": "~2.10.2",
"@testcontainers/hivemq": "^10.28.0",
"@testcontainers/mariadb": "^10.28.0",
@@ -207,7 +202,7 @@
"terser": "~5.15.1",
"test": "~3.3.0",
"testcontainers": "^11.12.0",
"typescript": "~4.4.4",
"typescript": "~6.0.3",
"v-pagination-3": "~0.1.7",
"vite": "~5.4.21",
"vite-plugin-compression": "^0.5.1",
@@ -226,6 +221,13 @@
"wait-on": "^7.2.0",
"whatwg-url": "~12.0.1"
},
"overrides": {
"@aws-sdk/credential-providers": "npm:useless-module@1.0.0",
"tedious": {
"@azure/identity": "npm:useless-module@1.0.0",
"@azure/keyvault-keys": "npm:useless-module@1.0.0"
}
},
"allowScripts": {
"@louislam/sqlite3@15.1.6": true,
"@fortawesome/fontawesome-common-types@0.2.36": true,
-14
View File
@@ -1,14 +0,0 @@
const { R } = require("redbean-node");
class TwoFA {
/**
* Disable 2FA for specified user
* @param {number} userID ID of user to disable
* @returns {Promise<void>}
*/
static async disable2FA(userID) {
return await R.exec("UPDATE `user` SET twofa_status = 0 WHERE id = ? ", [userID]);
}
}
module.exports = TwoFA;
+224
View File
@@ -0,0 +1,224 @@
import { betterAuth, Session } from "better-auth";
// @ts-ignore
import * as Database from "./database.js";
import { genSecret, log } from "../src/util";
import { R } from "redbean-node";
import { KyselyKnexDialect, MySQL2ColdDialect, SQLite3ColdDialect } from "kysely-knex";
import { username } from "better-auth/plugins";
import { admin } from "better-auth/plugins";
import { Socket } from "socket.io";
import { haveIBeenPwned } from "better-auth/plugins";
import { twoFactor } from "better-auth/plugins";
type BetterAuthUser = ReturnType<typeof createAuthInstance>["$Infer"]["Session"]["user"];
let authInstance: ReturnType<typeof createAuthInstance>;
/**
*
*/
export function auth() {
if (authInstance) {
return authInstance;
}
authInstance = createAuthInstance();
return authInstance;
}
/**
*
*/
function createAuthInstance() {
// Check if Database.initDataDir() has been called before using this function
if (!Database.dataDir) {
throw new Error(
"Database data directory is not initialized. Please call Database.initDataDir() before using auth."
);
}
const knex = R.knex;
const kyselySubDialect = Database.dbConfig.type.includes("mariadb")
? new MySQL2ColdDialect()
: new SQLite3ColdDialect();
const database = new KyselyKnexDialect({
kyselySubDialect,
knex,
});
return betterAuth({
database,
secret: getAuthSecret(),
// Should be handled in Express.js, check better-auth-router.ts
trustedOrigins: ["*"],
emailAndPassword: {
revokeSessionsOnPasswordReset: true,
enabled: true,
disableSignUp: false,
},
rateLimit: {
// Seconds
window: 60,
// Requests per window
max: 10,
},
plugins: [
// Enable login by username
username(),
// Enable user management API (used for creating the first admin user)
admin(),
// Check if the password has been pwned in data breaches
haveIBeenPwned(),
twoFactor({
schema: {
twoFactor: {
modelName: "better_auth_twoFactor",
},
},
}),
// It is not suitable for "Disable Auth", because it can not turn on/off after init.
//anonymous(),
],
user: {
modelName: "better_auth_user",
},
account: {
modelName: "better_auth_account",
},
session: {
modelName: "better_auth_session",
},
verification: {
modelName: "better_auth_verification",
},
});
}
/**
* Get the authentication secret for better-auth
* @returns The authentication secret
*/
export function getAuthSecret() {
const env = process.env.UPTIME_KUMA_AUTH_SECRET;
if (env) {
return env;
}
if (!Database.dbConfig.authSecret) {
Database.dbConfig.authSecret = genSecret();
Database.writeDBConfig(Database.dbConfig);
}
return Database.dbConfig.authSecret;
}
/**
* Get session from cookie
* @param cookie Cookie string
* @returns Session Object
*/
export function getSession(cookie: string) {
log.info("auth", "Logged in with httpOnly cookie session");
const context = {
headers: createHeaders(cookie),
};
return authInstance.api.getSession(context);
}
/**
* Create Headers object with cookie for API calls
* @param cookie Cookie string
* @returns Headers object
*/
export function createHeaders(cookie: string) {
const headers = new Headers();
headers.set("cookie", cookie || "");
return headers;
}
/**
* Unfortunatety, there is no way to get a user object from better-auth api, so we have to craft a session object here.
* @returns Crafted Session Object
*/
export async function getDisableAuthSession(): ReturnType<typeof getSession> {
log.info("auth", "Logged in with Disable Auth");
const obj = await R.getRow("SELECT * FROM better_auth_user LIMIT 1");
if (!obj) {
throw new Error("Unexpected Error: No user found in the database.");
}
const user = {
id: obj.id as string,
createdAt: obj.createdAt as Date,
updatedAt: obj.updatedAt as Date,
email: obj.email as string,
emailVerified: obj.emailVerified === 1,
name: obj.name as string,
image: obj.image as string | null,
username: obj.username as string | null,
displayUsername: obj.displayUsername as string | null,
banned: obj.banned === 1,
role: obj.role as string | null,
banReason: obj.banReason as string | null,
banExpires: obj.banExpires as Date | null,
twoFactorEnabled: obj.twoFactorEnabled === 1,
};
return {
user,
session: {
id: "disable-auth",
userId: user.id,
ipAddress: null,
userAgent: null,
createdAt: new Date(),
updatedAt: new Date(),
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
token: "disable-auth",
},
};
}
/**
* Check Login (Better Auth New!)
* @param socket Socket.IO Socket
* @throws Error if not logged in
*/
export function checkLogin(socket: Socket) {
// @ts-ignore
if (!socket.session) {
throw new Error("You are not logged in.");
}
}
/**
* For logged-in users, double-check the password
* @param cookie Cookie string
* @param currentPassword Password to verify
* @throws Error if the password is incorrect or the user is not found
*/
export async function doubleCheckPassword(cookie: string, currentPassword: string): Promise<void> {
const { status } = await authInstance.api.verifyPassword({
body: {
password: currentPassword,
},
headers: createHeaders(cookie),
});
if (!status) {
throw new Error("Incorrect current password");
}
}
/**
* TODO
*/
export async function migrateUser() {
// TODO: User have to input pwd one time to migrate, or we can not get the original password hash to create a better-auth user
// TODO: Disable Auth may need to directly create a user in the database
}
+5 -5
View File
@@ -19,7 +19,7 @@ async function sendNotificationList(socket) {
const timeLogger = new TimeLogger();
let result = [];
let list = await R.find("notification", " user_id = ? ", [socket.userID]);
let list = await R.find("notification");
for (let bean of list) {
let notificationObject = bean.export();
@@ -104,7 +104,7 @@ async function sendImportantHeartbeatList(socket, monitorID, toUser = false, ove
async function sendProxyList(socket) {
const timeLogger = new TimeLogger();
const list = await R.find("proxy", " user_id = ? ", [socket.userID]);
const list = await R.find("proxy");
io.to(socket.userID).emit(
"proxyList",
list.map((bean) => bean.export())
@@ -124,7 +124,7 @@ async function sendAPIKeyList(socket) {
const timeLogger = new TimeLogger();
let result = [];
const list = await R.find("api_key", "user_id=?", [socket.userID]);
const list = await R.find("api_key");
for (let bean of list) {
result.push(bean.toPublicJSON());
@@ -171,7 +171,7 @@ async function sendDockerHostList(socket) {
const timeLogger = new TimeLogger();
let result = [];
let list = await R.find("docker_host", " user_id = ? ", [socket.userID]);
let list = await R.find("docker_host");
for (let bean of list) {
result.push(bean.toJSON());
@@ -193,7 +193,7 @@ async function sendRemoteBrowserList(socket) {
const timeLogger = new TimeLogger();
let result = [];
let list = await R.find("remote_browser", " user_id = ? ", [socket.userID]);
let list = await R.find("remote_browser");
for (let bean of list) {
result.push(bean.toJSON());
+4 -1
View File
@@ -123,6 +123,9 @@ class Database {
static noReject = true;
/**
* @type {Record<string, string>}
*/
static dbConfig = {};
static knexMigrationsPath = "./db/knex_migrations";
@@ -220,7 +223,7 @@ class Database {
/**
* @typedef {string|undefined} envString
* @param {{type: "sqlite"} | {type:envString, hostname:envString, port:envString, database:envString, username:envString, password:envString, socketPath:envString}} dbConfig the database configuration that should be written
* @param {Record<string, string>} dbConfig the database configuration that should be written
* @returns {void}
*/
static writeDBConfig(dbConfig) {
+1 -1
View File
@@ -1357,7 +1357,7 @@ class Monitor extends BeanModel {
* @param {Server} io Socket server instance
* @param {number} monitorID ID of monitor to send
* @param {number} userID ID of user to send to
* @returns {void}
* @returns {Promise<void>}
*/
static async sendStats(io, monitorID, userID) {
const hasClients = getTotalClientInRoom(io, userID) > 0;
-16
View File
@@ -31,22 +31,6 @@ class User extends BeanModel {
this.password = hashedPassword;
}
/**
* Create a new JWT for a user
* @param {User} user The User to create a JsonWebToken for
* @param {string} jwtSecret The key used to sign the JsonWebToken
* @returns {string} the JsonWebToken as a string
*/
static createJWT(user, jwtSecret) {
return jwt.sign(
{
username: user.username,
h: shake256(user.password, SHAKE256_LENGTH),
},
jwtSecret
);
}
}
module.exports = User;
@@ -274,6 +274,7 @@ class RealBrowserMonitorType extends MonitorType {
await page.waitForTimeout(monitor.screenshot_delay);
}
// TODO fix without jwtSecret
let filename = jwt.sign(monitor.id, server.jwtSecret) + ".png";
await page.screenshot({
-8
View File
@@ -61,15 +61,7 @@ const apiRateLimiter = new KumaRateLimiter({
errorMessage: "Too frequently, try again later.",
});
const twoFaRateLimiter = new KumaRateLimiter({
tokensPerInterval: 30,
interval: "minute",
fireImmediately: true,
errorMessage: "Too frequently, try again later.",
});
module.exports = {
loginRateLimiter,
apiRateLimiter,
twoFaRateLimiter,
};
+21 -2
View File
@@ -52,6 +52,10 @@ router.all("/api/push/:pushToken", async (request, response) => {
let statusString = request.query.status || "up";
const statusFromParam = statusString === "up" ? UP : DOWN;
// Check if status=down was explicitly provided (not defaulting to "up")
// When explicitly pushing down, bypass retry logic and go directly to DOWN
const isExplicitDown = request.query.status === "down";
// Validate ping value - max 100 billion ms (~3.17 years)
// Fits safely in both BIGINT and FLOAT(20,2)
const MAX_PING_MS = 100000000000;
@@ -85,7 +89,14 @@ router.all("/api/push/:pushToken", async (request, response) => {
msg = "Monitor under maintenance";
bean.status = MAINTENANCE;
} else {
determineStatus(statusFromParam, previousHeartbeat, monitor.maxretries, monitor.isUpsideDown(), bean);
determineStatus(
statusFromParam,
previousHeartbeat,
monitor.maxretries,
monitor.isUpsideDown(),
bean,
isExplicitDown
);
}
// Calculate uptime
@@ -571,13 +582,21 @@ router.get("/api/badge/:id/response", cache("5 minutes"), async (request, respon
* @param {number} maxretries - The maximum number of retries allowed.
* @param {boolean} isUpsideDown - Indicates if the monitor is upside down.
* @param {object} bean - The new heartbeat object.
* @param {boolean} isExplicitDown - If status=down was explicitly pushed, bypass retries.
* @returns {void}
*/
function determineStatus(status, previousHeartbeat, maxretries, isUpsideDown, bean) {
function determineStatus(status, previousHeartbeat, maxretries, isUpsideDown, bean, isExplicitDown) {
if (isUpsideDown) {
status = flipStatus(status);
}
// If status=down was explicitly pushed, bypass retry logic and go directly to DOWN
if (isExplicitDown && status === DOWN) {
bean.retries = 0;
bean.status = DOWN;
return;
}
if (previousHeartbeat) {
if (previousHeartbeat.status === UP && status === DOWN) {
// Going Down
+103
View File
@@ -0,0 +1,103 @@
import express from "express";
import { toNodeHandler } from "better-auth/node";
import { auth } from "../better-auth";
import { R } from "redbean-node";
import { log } from "../../src/util";
// @ts-ignore
import { allowDevOrigin } from "../util-server.js";
import { generalErrorResponse } from "../util2";
let processingSetup = false;
let hasUser = false;
let expired = false;
const expiredMsg = "Setup has expired. Please restart the server to try again.";
setTimeout(
() => {
expired = true;
log.error("auth", expiredMsg);
},
1000 * 60 * 10
);
/**
* For testing: http://localhost:3001/api/auth/ok
* @returns Express Router with better-auth routes and setup route.
*/
export async function createBetterAuthRouter() {
const betterAuthRouter = express.Router();
const callback = toNodeHandler(auth());
betterAuthRouter.all("/api/auth/*", async (req, res) => {
allowDevOrigin(req, res);
return callback(req, res);
});
// First Setup
betterAuthRouter.post("/api/setup", async (req, res) => {
allowDevOrigin(req, res);
try {
if (expired) {
log.error("auth", expiredMsg);
throw new Error(expiredMsg);
}
if (processingSetup) {
throw new Error("Setup is already in progress. Please wait.");
}
try {
if (!(await needSetup())) {
throw new Error(
"Uptime Kuma has been initialized. If you want to run setup again, please delete the database."
);
}
processingSetup = true;
const username = req.body.username;
const password = req.body.password;
const user = await auth().api.createUser({
body: {
name: username,
email: `${username}@noreply.uptime-kuma.internal`,
password,
role: "admin",
data: {
username,
},
},
});
log.debug("auth", "First user created:", user);
hasUser = true;
res.json({ ok: true });
} finally {
processingSetup = false;
}
} catch (e) {
log.error("auth", "Setup error:", e instanceof Error ? e.message : e);
generalErrorResponse(res, e);
}
});
return betterAuthRouter;
}
/**
* @returns Whether setup is needed.
*/
export async function needSetup() {
if (expired) {
return false;
}
if (processingSetup) {
return false;
}
if (hasUser) {
return false;
}
hasUser = (await R.knex("better_auth_user").count("id as count").first()).count !== 0;
return !hasUser;
}
+44 -431
View File
@@ -3,6 +3,13 @@
* node "server/server.js"
* DO NOT require("./server") in other modules, it likely creates circular dependency!
*/
import { getRandomInt, isDev, log, sleep } from "../src/util";
import { auth, doubleCheckPassword, getDisableAuthSession, getSession } from "./better-auth";
import { createBetterAuthRouter, needSetup } from "./routers/better-auth-router";
import { betterAuthSocketHandler } from "./socket-handlers/better-auth-socket-handler";
import { loadEnvFile } from "node:process";
import * as fs from "fs";
console.log("Welcome to Uptime Kuma");
// As the log function need to use dayjs, it should be very top
@@ -12,14 +19,16 @@ dayjs.extend(require("./modules/dayjs/plugin/timezone"));
dayjs.extend(require("dayjs/plugin/customParseFormat"));
// Load environment variables from `.env`
require("dotenv").config();
try {
loadEnvFile();
} catch (_) {}
// Check Node.js Version
const nodeVersion = process.versions.node;
// Get the required Node.js version from package.json
const requiredNodeVersions = require("../package.json").engines.node;
const bannedNodeVersions = " < 18 || 20.0.* || 20.1.* || 20.2.* || 20.3.* ";
const bannedNodeVersions = "< 24";
console.log(`Your Node.js version: ${nodeVersion}`);
const semver = require("semver");
@@ -46,7 +55,6 @@ if (!semver.satisfies(nodeVersion, requiredNodeVersions)) {
}
const args = require("args-parser")(process.argv);
const { sleep, log, getRandomInt, genSecret, isDev } = require("../src/util");
const config = require("./config");
process.title = "uptime-kuma";
@@ -79,19 +87,12 @@ const express = require("express");
const expressStaticGzip = require("express-static-gzip");
log.debug("server", "Importing redbean-node");
const { R } = require("redbean-node");
log.debug("server", "Importing jsonwebtoken");
const jwt = require("jsonwebtoken");
log.debug("server", "Importing http-graceful-shutdown");
const gracefulShutdown = require("http-graceful-shutdown");
log.debug("server", "Importing prometheus-api-metrics");
const prometheusAPIMetrics = require("prometheus-api-metrics");
const { passwordStrength } = require("check-password-strength");
const TranslatableError = require("./translatable-error");
log.debug("server", "Importing 2FA Modules");
const notp = require("notp");
const base32 = require("thirty-two");
const { UptimeKumaServer } = require("./uptime-kuma-server");
const server = UptimeKumaServer.getInstance();
const io = (module.exports.io = server.io);
@@ -106,13 +107,10 @@ const {
getSettings,
setSettings,
setting,
initJWTSecret,
checkLogin,
doubleCheckPassword,
shake256,
SHAKE256_LENGTH,
allowDevAllOrigin,
printServerUrls,
allowDevOrigin,
} = require("./util-server");
log.debug("server", "Importing Notification");
@@ -126,12 +124,7 @@ const Database = require("./database");
log.debug("server", "Importing Background Jobs");
const { initBackgroundJobs, stopBackgroundJobs } = require("./jobs");
const { loginRateLimiter, twoFaRateLimiter } = require("./rate-limiter");
const { apiAuth } = require("./auth");
const { login } = require("./auth");
const passwordHash = require("./password-hash");
const { Prometheus } = require("./prometheus");
const { UptimeCalculator } = require("./uptime-calculator");
@@ -147,12 +140,6 @@ const disableFrameSameOrigin =
!!process.env.UPTIME_KUMA_DISABLE_FRAME_SAMEORIGIN || args["disable-frame-sameorigin"] || false;
const cloudflaredToken = args["cloudflared-token"] || process.env.UPTIME_KUMA_CLOUDFLARED_TOKEN || undefined;
// 2FA / notp verification defaults
const twoFAVerifyOptions = {
window: 1,
time: 30,
};
/**
* Run unit test after the server is ready
* @type {boolean}
@@ -173,7 +160,6 @@ const {
const { statusPageSocketHandler } = require("./socket-handlers/status-page-socket-handler");
const { databaseSocketHandler } = require("./socket-handlers/database-socket-handler");
const { remoteBrowserSocketHandler } = require("./socket-handlers/remote-browser-socket-handler");
const TwoFA = require("./2fa");
const StatusPage = require("./model/status_page");
const {
cloudflaredSocketHandler,
@@ -203,12 +189,6 @@ app.use(function (req, res, next) {
next();
});
/**
* Show Setup Page
* @type {boolean}
*/
let needSetup = false;
(async () => {
// Create a data directory
Database.initDataDir(args);
@@ -228,6 +208,9 @@ let needSetup = false;
process.exit(1);
}
// Init Better Auth
auth();
// Database should be ready now
await server.initAfterDatabaseReady();
server.entryPage = await Settings.get("entryPage");
@@ -276,6 +259,11 @@ let needSetup = false;
});
if (isDev) {
app.options("/*", async (request, response) => {
allowDevOrigin(request, response);
response.end();
});
app.use(express.urlencoded({ extended: true }));
app.post("/test-webhook", async (request, response) => {
log.debug("test", request.headers);
@@ -289,8 +277,6 @@ let needSetup = false;
response.send("OK");
});
const fs = require("fs");
app.get("/_e2e/take-sqlite-snapshot", async (request, response) => {
await Database.close();
try {
@@ -358,6 +344,10 @@ let needSetup = false;
const statusPageRouter = require("./routers/status-page-router");
app.use(statusPageRouter);
// better auth API Router
const betterAuthRouter = await createBetterAuthRouter();
app.use(betterAuthRouter);
// Universal Route Handler, must be at the end of all express routes.
app.get("*", async (_request, response) => {
if (_request.originalUrl.startsWith("/upload/")) {
@@ -371,351 +361,25 @@ let needSetup = false;
io.on("connection", async (socket) => {
await sendInfo(socket, true);
if (needSetup) {
if (await needSetup()) {
log.info("server", "Redirect to setup page");
socket.emit("setup");
}
// ***************************
// Public Socket API
// ***************************
socket.on("loginByToken", async (token, callback) => {
const clientIP = await server.getClientIP(socket);
log.info("auth", `Login by token. IP=${clientIP}`);
try {
let decoded = jwt.verify(token, server.jwtSecret);
log.info("auth", "Username from JWT: " + decoded.username);
let user = await R.findOne("user", " username = ? AND active = 1 ", [decoded.username]);
if (user) {
// Check if the password changed
if (decoded.h !== shake256(user.password, SHAKE256_LENGTH)) {
throw new Error("The token is invalid due to password change or old token");
}
log.debug("auth", "afterLogin");
await afterLogin(socket, user);
log.debug("auth", "afterLogin ok");
log.info("auth", `Successfully logged in user ${decoded.username}. IP=${clientIP}`);
callback({
ok: true,
});
} else {
log.info("auth", `Inactive or deleted user ${decoded.username}. IP=${clientIP}`);
callback({
ok: false,
msg: "authUserInactiveOrDeleted",
msgi18n: true,
});
}
} catch (error) {
log.error("auth", `Invalid token. IP=${clientIP}`);
if (error.message) {
log.error("auth", error.message, `IP=${clientIP}`);
}
callback({
ok: false,
msg: "authInvalidToken",
msgi18n: true,
});
}
});
socket.on("login", async (data, callback) => {
const clientIP = await server.getClientIP(socket);
log.info("auth", `Login by username + password. IP=${clientIP}`);
// Checking
if (typeof callback !== "function") {
return;
}
if (!data) {
return;
}
// Login Rate Limit
if (!(await loginRateLimiter.pass(callback))) {
log.info("auth", `Too many failed requests for user ${data.username}. IP=${clientIP}`);
return;
}
let user = await login(data.username, data.password);
if (user) {
if (user.twofa_status === 0) {
await afterLogin(socket, user);
log.info("auth", `Successfully logged in user ${data.username}. IP=${clientIP}`);
callback({
ok: true,
token: User.createJWT(user, server.jwtSecret),
});
}
if (user.twofa_status === 1 && !data.token) {
log.info("auth", `2FA token required for user ${data.username}. IP=${clientIP}`);
callback({
tokenRequired: true,
});
}
if (data.token) {
let verify = notp.totp.verify(data.token, user.twofa_secret, twoFAVerifyOptions);
if (user.twofa_last_token !== data.token && verify) {
await afterLogin(socket, user);
await R.exec("UPDATE `user` SET twofa_last_token = ? WHERE id = ? ", [
data.token,
socket.userID,
]);
log.info("auth", `Successfully logged in user ${data.username}. IP=${clientIP}`);
callback({
ok: true,
token: User.createJWT(user, server.jwtSecret),
});
} else {
log.warn("auth", `Invalid token provided for user ${data.username}. IP=${clientIP}`);
callback({
ok: false,
msg: "authInvalidToken",
msgi18n: true,
});
}
}
} else {
log.warn("auth", `Incorrect username or password for user ${data.username}. IP=${clientIP}`);
callback({
ok: false,
msg: "authIncorrectCreds",
msgi18n: true,
});
}
});
socket.on("logout", async (callback) => {
// Rate Limit
if (!(await loginRateLimiter.pass(callback))) {
return;
}
socket.leave(socket.userID);
socket.userID = null;
if (typeof callback === "function") {
callback();
}
});
socket.on("prepare2FA", async (currentPassword, callback) => {
try {
if (!(await twoFaRateLimiter.pass(callback))) {
return;
}
checkLogin(socket);
await doubleCheckPassword(socket, currentPassword);
let user = await R.findOne("user", " id = ? AND active = 1 ", [socket.userID]);
if (user.twofa_status === 0) {
let newSecret = genSecret();
let encodedSecret = base32.encode(newSecret);
// Google authenticator doesn't like equal signs
// The fix is found at https://github.com/guyht/notp
// Related issue: https://github.com/louislam/uptime-kuma/issues/486
encodedSecret = encodedSecret.toString().replace(/=/g, "");
let uri = `otpauth://totp/Uptime%20Kuma:${user.username}?secret=${encodedSecret}`;
await R.exec("UPDATE `user` SET twofa_secret = ? WHERE id = ? ", [newSecret, socket.userID]);
callback({
ok: true,
uri: uri,
});
} else {
callback({
ok: false,
msg: "2faAlreadyEnabled",
msgi18n: true,
});
}
} catch (error) {
callback({
ok: false,
msg: error.message,
});
}
});
socket.on("save2FA", async (currentPassword, callback) => {
const clientIP = await server.getClientIP(socket);
try {
if (!(await twoFaRateLimiter.pass(callback))) {
return;
}
checkLogin(socket);
await doubleCheckPassword(socket, currentPassword);
await R.exec("UPDATE `user` SET twofa_status = 1 WHERE id = ? ", [socket.userID]);
log.info("auth", `Saved 2FA token. IP=${clientIP}`);
callback({
ok: true,
msg: "2faEnabled",
msgi18n: true,
});
} catch (error) {
log.error("auth", `Error changing 2FA token. IP=${clientIP}`);
callback({
ok: false,
msg: error.message,
});
}
});
socket.on("disable2FA", async (currentPassword, callback) => {
const clientIP = await server.getClientIP(socket);
try {
if (!(await twoFaRateLimiter.pass(callback))) {
return;
}
checkLogin(socket);
await doubleCheckPassword(socket, currentPassword);
await TwoFA.disable2FA(socket.userID);
log.info("auth", `Disabled 2FA token. IP=${clientIP}`);
callback({
ok: true,
msg: "2faDisabled",
msgi18n: true,
});
} catch (error) {
log.error("auth", `Error disabling 2FA token. IP=${clientIP}`);
callback({
ok: false,
msg: error.message,
});
}
});
socket.on("verifyToken", async (token, currentPassword, callback) => {
try {
checkLogin(socket);
await doubleCheckPassword(socket, currentPassword);
let user = await R.findOne("user", " id = ? AND active = 1 ", [socket.userID]);
let verify = notp.totp.verify(token, user.twofa_secret, twoFAVerifyOptions);
if (user.twofa_last_token !== token && verify) {
callback({
ok: true,
valid: true,
});
} else {
callback({
ok: false,
msg: "authInvalidToken",
msgi18n: true,
valid: false,
});
}
} catch (error) {
callback({
ok: false,
msg: error.message,
});
}
});
socket.on("twoFAStatus", async (callback) => {
try {
checkLogin(socket);
let user = await R.findOne("user", " id = ? AND active = 1 ", [socket.userID]);
if (user.twofa_status === 1) {
callback({
ok: true,
status: true,
});
} else {
callback({
ok: true,
status: false,
});
}
} catch (error) {
callback({
ok: false,
msg: error.message,
});
}
});
socket.on("needSetup", async (callback) => {
callback(needSetup);
});
socket.on("setup", async (username, password, callback) => {
try {
if (passwordStrength(password).value === "Too weak") {
throw new TranslatableError("passwordTooWeak");
}
if ((await R.knex("user").count("id as count").first()).count !== 0) {
throw new Error(
"Uptime Kuma has been initialized. If you want to run setup again, please delete the database."
);
}
let user = R.dispense("user");
user.username = username;
user.password = await passwordHash.generate(password);
await R.store(user);
needSetup = false;
callback({
ok: true,
msg: "successAdded",
msgi18n: true,
});
} catch (e) {
callback({
ok: false,
msg: e.message,
msgi18n: !!e.msgi18n,
});
}
});
let session;
if (!(await Settings.get("disableAuth"))) {
session = await getSession(socket.request.headers.cookie);
} else {
session = await getDisableAuthSession();
}
if (session) {
socket.userID = session.user.id;
socket.session = session;
socket.emit("session", session.user.username);
log.debug("auth", `Session active:`, session.session.ipAddress, session.user.username);
}
// ***************************
// Auth Only API
@@ -1421,38 +1085,6 @@ let needSetup = false;
}
});
socket.on("changePassword", async (password, callback) => {
try {
checkLogin(socket);
if (!password.newPassword) {
throw new Error("Invalid new password");
}
if (passwordStrength(password.newPassword).value === "Too weak") {
throw new TranslatableError("passwordTooWeak");
}
let user = await doubleCheckPassword(socket, password.currentPassword);
await user.resetPassword(password.newPassword);
server.disconnectAllSocketClients(user.id, socket.id);
callback({
ok: true,
token: User.createJWT(user, server.jwtSecret),
msg: "successAuthChangePassword",
msgi18n: true,
});
} catch (e) {
callback({
ok: false,
msg: e.message,
msgi18n: !!e.msgi18n,
});
}
});
socket.on("getSettings", async (callback) => {
try {
checkLogin(socket);
@@ -1485,7 +1117,7 @@ let needSetup = false;
// Enabled Auth + Want to Enable Auth => No Check
const currentDisabledAuth = await setting("disableAuth");
if (!currentDisabledAuth && data.disableAuth) {
await doubleCheckPassword(socket, currentPassword);
await doubleCheckPassword(socket.request.headers.cookie, currentPassword);
}
// Log out all clients if enabling auth
@@ -1707,6 +1339,8 @@ let needSetup = false;
}
});
betterAuthSocketHandler(socket);
// Status Page Socket Handler for admin only
statusPageSocketHandler(socket);
cloudflaredSocketHandler(socket);
@@ -1725,11 +1359,8 @@ let needSetup = false;
// Better do anything after added all socket handlers here
// ***************************
log.debug("auth", "check auto login");
if (await setting("disableAuth")) {
log.info("auth", "Disabled Auth: auto login to admin");
await afterLogin(socket, await R.findOne("user"));
socket.emit("autoLogin");
if (session) {
await afterLogin(socket, session.user);
} else {
socket.emit("loginRequired");
log.debug("auth", "need auth");
@@ -1850,24 +1481,6 @@ async function initDatabase(testMode = false) {
// Patch the database
await Database.patch(port, hostname);
let jwtSecretBean = await R.findOne("setting", " `key` = ? ", ["jwtSecret"]);
if (!jwtSecretBean) {
log.info("server", "JWT secret is not found, generate one.");
jwtSecretBean = await initJWTSecret();
log.info("server", "Stored JWT secret into database");
} else {
log.debug("server", "Load JWT secret from database.");
}
// If there is no record in user table, it is a new Uptime Kuma instance, need to setup
if ((await R.knex("user").count("id as count").first()).count === 0) {
log.info("server", "No user, need setup");
needSetup = true;
}
server.jwtSecret = jwtSecretBean.value;
}
/**
+2 -2
View File
@@ -29,10 +29,10 @@ class Settings {
// Start cache clear if not started yet
if (!Settings.cacheCleaner) {
Settings.cacheCleaner = setInterval(() => {
log.debug("settings", "Cache Cleaner is just started.");
//log.debug("settings", "Cache Cleaner is just started.");
for (key in Settings.cacheList) {
if (Date.now() - Settings.cacheList[key].timestamp > 60 * 1000) {
log.debug("settings", "Cache Cleaner deleted: " + key);
//log.debug("settings", "Cache Cleaner deleted: " + key);
delete Settings.cacheList[key];
}
}
@@ -0,0 +1,12 @@
import { Socket } from "socket.io";
import { needSetup } from "../routers/better-auth-router";
/**
* For better-auth, or setup
* @param socket Socket.io
*/
export function betterAuthSocketHandler(socket: Socket): void {
socket.on("needSetup", async (callback) => {
callback(needSetup());
});
}
@@ -1,7 +1,8 @@
const { checkLogin, setSetting, setting, doubleCheckPassword } = require("../util-server");
const { checkLogin, setSetting, setting } = require("../util-server");
const { CloudflaredTunnel } = require("node-cloudflared-tunnel");
const { UptimeKumaServer } = require("../uptime-kuma-server");
const { log } = require("../../src/util");
const { doubleCheckPassword } = require("../better-auth");
const io = UptimeKumaServer.getInstance().io;
const prefix = "cloudflared_";
@@ -74,7 +75,7 @@ module.exports.cloudflaredSocketHandler = (socket) => {
checkLogin(socket);
const disabledAuth = await setting("disableAuth");
if (!disabledAuth) {
await doubleCheckPassword(socket, currentPassword);
await doubleCheckPassword(socket.request.headers.cookie, currentPassword);
}
cloudflared.stop();
} catch (error) {
+25 -14
View File
@@ -4,7 +4,7 @@ const fs = require("fs");
const http = require("http");
const { Server } = require("socket.io");
const { R } = require("redbean-node");
const { log, isDev } = require("../src/util");
const { log, isDev, devOriginList } = require("../src/util");
const Database = require("./database");
const util = require("util");
const { Settings } = require("./settings");
@@ -54,12 +54,6 @@ class UptimeKumaServer {
*/
static monitorTypeList = {};
/**
* Use for decode the auth object
* @type {null}
*/
jwtSecret = null;
/**
* Get the current instance of the server if it exists, otherwise
* create a new instance.
@@ -137,12 +131,15 @@ class UptimeKumaServer {
let cors = undefined;
if (isDev) {
cors = {
origin: "*",
origin: devOriginList,
credentials: true,
methods: ["GET", "POST"],
};
}
this.io = new Server(this.httpServer, {
cors,
cookie: true,
allowRequest: async (req, callback) => {
let transport;
// It should be always true, but just in case, because this property is not documented
@@ -217,7 +214,7 @@ class UptimeKumaServer {
* @returns {Promise<object>} List of monitors
*/
async sendMonitorList(socket) {
let list = await this.getMonitorJSONList(socket.userID);
let list = await this.getMonitorJSONList();
this.io.to(socket.userID).emit("monitorList", list);
return list;
}
@@ -253,16 +250,30 @@ class UptimeKumaServer {
*
* Generated by Trelent
*/
async getMonitorJSONList(userID, monitorID = null) {
let query = " user_id = ? ";
let queryParams = [userID];
async getMonitorJSONList(userID = null, monitorID = null) {
let queryKeys = [];
let queryParams = [];
if (userID) {
queryKeys.push(" user_id = ? ");
queryParams.push(userID);
}
if (monitorID) {
query += "AND id = ? ";
queryKeys.push(" id = ? ");
queryParams.push(monitorID);
}
let monitorList = await R.find("monitor", query + "ORDER BY weight DESC, name", queryParams);
const query = queryKeys.join(" AND ");
const orderBy = "ORDER BY weight DESC, name";
let monitorList;
if (queryKeys.length === 0) {
monitorList = await R.findAll("monitor", orderBy, queryParams);
} else {
monitorList = await R.find("monitor", query + orderBy, queryParams);
}
const monitorData = monitorList.map((monitor) => ({
id: monitor.id,
+32 -52
View File
@@ -1,15 +1,15 @@
import { checkLogin as betterAuthCheckLogin } from "./better-auth";
const ping = require("@louislam/ping");
const { R } = require("redbean-node");
const {
log,
genSecret,
badgeConstants,
PING_PACKET_SIZE_DEFAULT,
PING_GLOBAL_TIMEOUT_DEFAULT,
PING_COUNT_DEFAULT,
PING_PER_REQUEST_TIMEOUT_DEFAULT,
} = require("../src/util");
const passwordHash = require("./password-hash");
const iconv = require("iconv-lite");
const chardet = require("chardet");
const chroma = require("chroma-js");
@@ -35,31 +35,6 @@ const { Kafka, SASLOptions } = require("kafkajs");
const crypto = require("crypto");
const isWindows = process.platform === /^win/.test(process.platform);
/**
* Init or reset JWT secret
* @returns {Promise<Bean>} JWT secret
*/
exports.initJWTSecret = async () => {
let jwtSecretBean = await R.findOne("setting", " `key` = ? ", ["jwtSecret"]);
if (!jwtSecretBean) {
jwtSecretBean = R.dispense("setting");
jwtSecretBean.key = "jwtSecret";
}
jwtSecretBean.value = await passwordHash.generate(genSecret());
await R.store(jwtSecretBean);
return jwtSecretBean;
};
/**
* Decodes a jwt and returns the payload portion without verifying the jwt.
* @param {string} jwt The input jwt as a string
* @returns {object} Decoded jwt payload object
*/
exports.decodeJwt = (jwt) => {
return JSON.parse(Buffer.from(jwt.split(".")[1], "base64").toString());
};
/**
* Gets an Access Token from an oidc/oauth2 provider
@@ -614,6 +589,7 @@ exports.getTotalClientInRoom = (io, roomName) => {
};
/**
* @deprecated Use allowDevOrigin
* Allow CORS all origins if development
* @param {object} res Response object from axios
* @returns {void}
@@ -625,6 +601,7 @@ exports.allowDevAllOrigin = (res) => {
};
/**
* @deprecated Use allowOrigin
* Allow CORS all origins
* @param {object} res Response object from axios
* @returns {void}
@@ -636,37 +613,40 @@ exports.allowAllOrigin = (res) => {
};
/**
* Allow CORS all origins if development
* @param {Request} req Express request object
* @param {Response} res Express response object
* @returns {void}
*/
exports.allowDevOrigin = (req, res) => {
if (process.env.NODE_ENV === "development") {
exports.allowOrigin(req, res);
}
};
/**
* Allow CORS all origins
* Since Allow-Credentials is set to true, the Access-Control-Allow-Origin cannot be *, so we will set it to the request origin.
* @param {Request} req Express request object
* @param {Response} res Response object
* @returns {void}
*/
exports.allowOrigin = (req, res) => {
res.header("Access-Control-Allow-Origin", req.get("origin"));
res.header("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE, OPTIONS");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
res.header("Access-Control-Allow-Credentials", "true");
};
/**
* @deprecated Use better-auth's checkLogin
* Check if a user is logged in
* @param {Socket} socket Socket instance
* @returns {void}
* @throws The user is not logged in
*/
exports.checkLogin = (socket) => {
if (!socket.userID) {
throw new Error("You are not logged in.");
}
};
/**
* For logged-in users, double-check the password
* @param {Socket} socket Socket.io instance
* @param {string} currentPassword Password to validate
* @returns {Promise<Bean>} User
* @throws The current password is not a string
* @throws The provided password is not correct
*/
exports.doubleCheckPassword = async (socket, currentPassword) => {
if (typeof currentPassword !== "string") {
throw new Error("Wrong data type?");
}
let user = await R.findOne("user", " id = ? AND active = 1 ", [socket.userID]);
if (!user || !passwordHash.verify(currentPassword, user.password)) {
throw new Error("Incorrect current password");
}
return user;
betterAuthCheckLogin(socket);
};
/**
+29
View File
@@ -0,0 +1,29 @@
import { ZodError } from "zod";
import { Response } from "express";
/**
* @param res Express response
* @param e Any error
*/
export function generalErrorResponse(res: Response, e: unknown) {
if (e instanceof ZodError) {
let message = "";
for (const issue of e.issues) {
message += `${issue.path.join(".")}: ${issue.message}\n`;
}
res.status(400).json({
ok: false,
msg: message,
});
} else if (e instanceof Error) {
res.status(400).json({
ok: false,
msg: e.message,
});
} else {
res.status(400).json({
ok: false,
msg: "Unknown error",
});
}
}
+53
View File
@@ -0,0 +1,53 @@
import { createAuthClient } from "better-auth/vue";
import { twoFactorClient, usernameClient } from "better-auth/client/plugins";
import { reconnectSocket } from "./mixins/socket";
export const baseURL =
process.env.NODE_ENV === "development" || localStorage.dev === "dev"
? location.protocol + "//" + location.hostname + ":3001"
: "";
export const authClient = createAuthClient({
baseURL,
plugins: [usernameClient(), twoFactorClient()],
});
authClient.signIn;
/**
* @returns Check if the user is logged in
*/
export async function isLoggedIn() {
const session = await authClient.getSession();
return session.data !== null;
}
/**
* @param username Username
* @param password Password
* @param remember Remember Me
*/
export async function login(username: string, password: string, remember: boolean = true) {
const { error } = await authClient.signIn.username({
username,
password,
rememberMe: remember,
});
if (error) {
throw new Error(error.message);
}
reconnectSocket();
}
/**
* @param onSuccess
*/
export async function logout(onSuccess = () => {}) {
await authClient.signOut({
fetchOptions: {
onSuccess,
},
});
}
+9 -9
View File
@@ -73,6 +73,8 @@
</template>
<script>
import { login } from "../auth-client";
export default {
data() {
return {
@@ -108,18 +110,16 @@ export default {
* Submit the user details and attempt to log in
* @returns {void}
*/
submit() {
async submit() {
this.processing = true;
this.$root.login(this.username, this.password, this.token, (res) => {
try {
await login(this.username, this.password, this.$root.remember);
} catch (e) {
console.error(e);
} finally {
this.processing = false;
if (res.tokenRequired) {
this.tokenRequired = true;
} else {
this.res = res;
}
});
}
},
},
};
+15 -15
View File
@@ -146,6 +146,7 @@
<script>
import Confirm from "../../components/Confirm.vue";
import TwoFADialog from "../../components/TwoFADialog.vue";
import { authClient } from "../../auth-client";
export default {
components: {
@@ -185,26 +186,25 @@ export default {
methods: {
/**
* Check new passwords match before saving them
* @returns {void}
* @returns {Promise<void>}
*/
savePassword() {
async savePassword() {
if (this.password.newPassword !== this.password.repeatNewPassword) {
this.invalidPassword = true;
} else {
this.$root.getSocket().emit("changePassword", this.password, (res) => {
this.$root.toastRes(res);
if (res.ok) {
this.password.currentPassword = "";
this.password.newPassword = "";
this.password.repeatNewPassword = "";
// Update token of the current session
if (res.token) {
this.$root.storage().token = res.token;
this.$root.socket.token = res.token;
}
}
const { error } = await authClient.changePassword({
currentPassword: this.password.currentPassword,
newPassword: this.password.newPassword,
});
if (error) {
this.$root.toastRes({ ok: false, msg: error.message });
} else {
this.$root.toastRes({ ok: true, msg: this.$t("successAuthChangePassword") });
this.password.currentPassword = "";
this.password.newPassword = "";
this.password.repeatNewPassword = "";
}
}
},
+2 -6
View File
@@ -1,14 +1,10 @@
import axios from "axios";
import { getDevContainerServerHostname, isDevContainer } from "../util-frontend";
import { baseURL } from "../auth-client";
const env = process.env.NODE_ENV || "production";
// change the axios base url for development
if (env === "development" && isDevContainer()) {
axios.defaults.baseURL = location.protocol + "//" + getDevContainerServerHostname();
} else if (env === "development" || localStorage.dev === "dev") {
axios.defaults.baseURL = location.protocol + "//" + location.hostname + ":3001";
}
axios.defaults.baseURL = baseURL;
export default {
data() {
+47 -153
View File
@@ -1,20 +1,16 @@
import { io } from "socket.io-client";
import { useToast } from "vue-toastification";
import jwtDecode from "jwt-decode";
import Favico from "favico.js";
import dayjs from "dayjs";
import mitt from "mitt";
import { DOWN, MAINTENANCE, PENDING, UP } from "../util.ts";
import {
getDevContainerServerHostname,
isDevContainer,
getToastSuccessTimeout,
getToastErrorTimeout,
} from "../util-frontend.js";
import { getToastSuccessTimeout, getToastErrorTimeout } from "../util-frontend.js";
import { logout as betterAuthLogout } from "../auth-client";
const toast = useToast();
let socket;
let disconnectTimeout;
const noSocketIOPages = [
/^\/status-page$/, // /status-page
@@ -31,7 +27,6 @@ export default {
return {
info: {},
socket: {
token: null,
firstConnect: true,
connected: false,
connectCount: 0,
@@ -108,16 +103,16 @@ export default {
let url;
const env = process.env.NODE_ENV || "production";
if (env === "development" && isDevContainer()) {
url = protocol + getDevContainerServerHostname();
} else if (env === "development" || localStorage.dev === "dev") {
if (env === "development" || localStorage.dev === "dev") {
url = protocol + location.hostname + ":3001";
} else {
// Connect to the current url
url = undefined;
}
socket = io(url);
socket = io(url, {
withCredentials: true,
});
socket.on("info", (info) => {
this.info = info;
@@ -127,6 +122,11 @@ export default {
this.$router.push("/setup");
});
socket.on("session", (username) => {
this.loggedIn = true;
this.username = username;
});
socket.on("autoLogin", (monitorID, data) => {
this.loggedIn = true;
this.storage().token = "autoLogin";
@@ -135,13 +135,7 @@ export default {
});
socket.on("loginRequired", () => {
let token = this.storage().token;
if (token && token !== "autoLogin") {
this.loginByToken(token);
} else {
this.$root.storage().removeItem("token");
this.allowLoginDialog = true;
}
this.allowLoginDialog = true;
});
socket.on("monitorList", (data) => {
@@ -267,12 +261,19 @@ export default {
socket.on("disconnect", () => {
console.log("disconnect");
this.connectionErrorMsg = `${this.$t("Lost connection to the socket server.")} ${this.$t("Reconnecting...")}`;
this.socket.connected = false;
// As we are using cookie based auth, we have to reconnect the socket
// avoid noise by add a delay
disconnectTimeout = setTimeout(() => {
this.connectionErrorMsg = `${this.$t("Lost connection to the socket server.")} ${this.$t("Reconnecting...")}`;
this.socket.connected = false;
}, 2000);
});
socket.on("connect", () => {
console.log("Connected to the socket server");
clearTimeout(disconnectTimeout);
this.socket.connectCount++;
this.socket.connected = true;
this.showReverseProxyGuide = false;
@@ -326,19 +327,6 @@ export default {
return this.remember ? localStorage : sessionStorage;
},
/**
* Get payload of JWT cookie
* @returns {(object | undefined)} JWT payload
*/
getJWTPayload() {
const jwtToken = this.$root.storage().token;
if (jwtToken && jwtToken !== "autoLogin") {
return jwtDecode(jwtToken);
}
return undefined;
},
/**
* Get current socket
* @returns {Socket} Current socket
@@ -395,129 +383,19 @@ export default {
toast.error(this.$t(msg));
},
/**
* Callback for login
* @callback loginCB
* @param {object} res Response object
*/
/**
* Send request to log user in
* @param {string} username Username to log in with
* @param {string} password Password to log in with
* @param {string} token User token
* @param {loginCB} callback Callback to call with result
* @returns {void}
*/
login(username, password, token, callback) {
socket.emit(
"login",
{
username,
password,
token,
},
(res) => {
if (res.tokenRequired) {
callback(res);
}
if (res.ok) {
this.storage().token = res.token;
this.socket.token = res.token;
this.loggedIn = true;
this.username = this.getJWTPayload()?.username;
// Trigger Chrome Save Password
history.pushState({}, "");
}
callback(res);
}
);
},
/**
* Log in using a token
* @param {string} token Token to log in with
* @returns {void}
*/
loginByToken(token) {
socket.emit("loginByToken", token, (res) => {
this.allowLoginDialog = true;
if (!res.ok) {
this.logout();
} else {
this.loggedIn = true;
this.username = this.getJWTPayload()?.username;
}
});
},
/**
* Log out of the web application
* @returns {void}
*/
logout() {
socket.emit("logout", () => {});
this.storage().removeItem("token");
this.socket.token = null;
this.loggedIn = false;
this.username = null;
this.clearData();
},
/**
* Callback for general socket requests
* @callback socketCB
* @param {object} res Result of operation
*/
/**
* Prepare 2FA configuration
* @param {socketCB} callback Callback for socket response
* @returns {void}
*/
prepare2FA(callback) {
socket.emit("prepare2FA", callback);
},
/**
* Save the current 2FA configuration
* @param {any} secret Unused
* @param {socketCB} callback Callback for socket response
* @returns {void}
*/
save2FA(secret, callback) {
socket.emit("save2FA", callback);
},
/**
* Disable 2FA for this user
* @param {socketCB} callback Callback for socket response
* @returns {void}
*/
disable2FA(callback) {
socket.emit("disable2FA", callback);
},
/**
* Verify the provided 2FA token
* @param {string} token Token to verify
* @param {socketCB} callback Callback for socket response
* @returns {void}
*/
verifyToken(token, callback) {
socket.emit("verifyToken", token, callback);
},
/**
* Get current 2FA status
* @param {socketCB} callback Callback for socket response
* @returns {void}
*/
twoFAStatus(callback) {
socket.emit("twoFAStatus", callback);
async logout() {
await betterAuthLogout(() => {
console.log("Logged out");
this.loggedIn = false;
this.username = null;
this.allowLoginDialog = false;
this.clearData();
reconnectSocket();
});
},
/**
@@ -869,7 +747,13 @@ export default {
// Reload the SPA if the server version is changed.
"info.version"(to, from) {
// No need to refresh, when the version is not obtained, which means it is not logged in.
if (!to) {
return;
}
if (from && from !== to) {
console.log(`Server version changed from ${from} to ${to}, reloading the page`);
window.location.reload();
}
},
@@ -892,3 +776,13 @@ export default {
},
},
};
/**
*
*/
export function reconnectSocket() {
if (socket) {
socket.disconnect();
socket.connect();
}
}
+28 -15
View File
@@ -73,6 +73,9 @@
</template>
<script>
import { checkFetch } from "../util";
import { authClient, baseURL, login } from "../auth-client";
export default {
data() {
return {
@@ -84,8 +87,6 @@ export default {
},
watch: {},
mounted() {
// TODO: Check if it is a database setup
this.$root.getSocket().emit("needSetup", (needSetup) => {
if (!needSetup) {
this.$router.push("/");
@@ -97,7 +98,7 @@ export default {
* Submit form data for processing
* @returns {void}
*/
submit() {
async submit() {
this.processing = true;
if (this.password !== this.repeatPassword) {
@@ -106,19 +107,31 @@ export default {
return;
}
this.$root.getSocket().emit("setup", this.username, this.password, (res) => {
try {
const response = await fetch(baseURL + "/api/setup", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username: this.username,
password: this.password,
}),
});
await checkFetch(response);
// Login
await login(this.username, this.password);
} catch (error) {
this.$root.toastRes({
ok: false,
msg: error.message,
msgi18n: false,
});
} finally {
this.processing = false;
this.$root.toastRes(res);
if (res.ok) {
this.processing = true;
this.$root.login(this.username, this.password, "", () => {
this.processing = false;
this.$router.push("/");
});
}
});
}
},
},
};
+2 -30
View File
@@ -2,6 +2,7 @@ import dayjs from "dayjs";
import { getTimeZones } from "@vvo/tzdb";
import { localeDirection, currentLocale } from "./i18n";
import { POSITION } from "vue-toastification";
import { baseURL } from "./auth-client";
/**
* Returns the offset from UTC in hours for the current locale.
@@ -76,36 +77,7 @@ export function setPageLocale() {
* @returns {string} Base URL
*/
export function getResBaseURL() {
const env = process.env.NODE_ENV;
if (env === "development" && isDevContainer()) {
return location.protocol + "//" + getDevContainerServerHostname();
} else if (env === "development" || localStorage.dev === "dev") {
return location.protocol + "//" + location.hostname + ":3001";
} else {
return "";
}
}
/**
* Are we currently running in a dev container?
* @returns {boolean} Running in dev container?
*/
export function isDevContainer() {
// eslint-disable-next-line no-undef
return typeof DEVCONTAINER === "string" && DEVCONTAINER === "1";
}
/**
* Supports GitHub Codespaces only currently
* @returns {string} Dev container server hostname
*/
export function getDevContainerServerHostname() {
if (!isDevContainer()) {
return "";
}
// eslint-disable-next-line no-undef
return CODESPACE_NAME + "-3001." + GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN;
return baseURL;
}
/**
-494
View File
@@ -1,494 +0,0 @@
"use strict";
/*!
// Common Util for frontend and backend
//
// DOT NOT MODIFY util.js!
// Need to run "npm run tsc" to compile if there are any changes.
//
// Backend uses the compiled file util.js
// Frontend uses util.ts
*/
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
exports.CONSOLE_STYLE_FgLightBlue = exports.CONSOLE_STYLE_FgLightGreen = exports.CONSOLE_STYLE_FgOrange = exports.CONSOLE_STYLE_FgGray = exports.CONSOLE_STYLE_FgWhite = exports.CONSOLE_STYLE_FgCyan = exports.CONSOLE_STYLE_FgMagenta = exports.CONSOLE_STYLE_FgBlue = exports.CONSOLE_STYLE_FgYellow = exports.CONSOLE_STYLE_FgGreen = exports.CONSOLE_STYLE_FgRed = exports.CONSOLE_STYLE_FgBlack = exports.CONSOLE_STYLE_Hidden = exports.CONSOLE_STYLE_Reverse = exports.CONSOLE_STYLE_Blink = exports.CONSOLE_STYLE_Underscore = exports.CONSOLE_STYLE_Dim = exports.CONSOLE_STYLE_Bright = exports.CONSOLE_STYLE_Reset = exports.RESPONSE_BODY_LENGTH_MAX = exports.RESPONSE_BODY_LENGTH_DEFAULT = exports.PING_PER_REQUEST_TIMEOUT_DEFAULT = exports.PING_PER_REQUEST_TIMEOUT_MAX = exports.PING_PER_REQUEST_TIMEOUT_MIN = exports.PING_COUNT_DEFAULT = exports.PING_COUNT_MAX = exports.PING_COUNT_MIN = exports.PING_GLOBAL_TIMEOUT_DEFAULT = exports.PING_GLOBAL_TIMEOUT_MAX = exports.PING_GLOBAL_TIMEOUT_MIN = exports.PING_PACKET_SIZE_DEFAULT = exports.PING_PACKET_SIZE_MAX = exports.PING_PACKET_SIZE_MIN = exports.INCIDENT_PAGE_SIZE = exports.MIN_INTERVAL_SECOND = exports.MAX_INTERVAL_SECOND = exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = exports.SQL_DATETIME_FORMAT = exports.SQL_DATE_FORMAT = exports.STATUS_PAGE_MAINTENANCE = exports.STATUS_PAGE_PARTIAL_DOWN = exports.STATUS_PAGE_ALL_UP = exports.STATUS_PAGE_ALL_DOWN = exports.MAINTENANCE = exports.PENDING = exports.UP = exports.DOWN = exports.appName = exports.isNode = exports.isDev = void 0;
exports.TYPES_WITH_DOMAIN_EXPIRY_SUPPORT_VIA_FIELD = exports.evaluateJsonQuery = exports.intHash = exports.localToUTC = exports.utcToLocal = exports.utcToISODateTime = exports.isoToUTCDateTime = exports.parseTimeFromTimeObject = exports.parseTimeObject = exports.getMonitorRelativeURL = exports.genSecret = exports.getCryptoRandomInt = exports.getRandomInt = exports.getRandomArbitrary = exports.TimeLogger = exports.polyfill = exports.log = exports.debug = exports.ucfirst = exports.sleep = exports.flipStatus = exports.badgeConstants = exports.CONSOLE_STYLE_BgGray = exports.CONSOLE_STYLE_BgWhite = exports.CONSOLE_STYLE_BgCyan = exports.CONSOLE_STYLE_BgMagenta = exports.CONSOLE_STYLE_BgBlue = exports.CONSOLE_STYLE_BgYellow = exports.CONSOLE_STYLE_BgGreen = exports.CONSOLE_STYLE_BgRed = exports.CONSOLE_STYLE_BgBlack = exports.CONSOLE_STYLE_FgPink = exports.CONSOLE_STYLE_FgBrown = exports.CONSOLE_STYLE_FgViolet = void 0;
const dayjs_1 = require("dayjs");
const jsonata = require("jsonata");
exports.isDev = process.env.NODE_ENV === "development";
exports.isNode = typeof process !== "undefined" && ((_a = process === null || process === void 0 ? void 0 : process.versions) === null || _a === void 0 ? void 0 : _a.node);
const dayjs = exports.isNode ? require("dayjs") : dayjs_1.default;
exports.appName = "Uptime Kuma";
exports.DOWN = 0;
exports.UP = 1;
exports.PENDING = 2;
exports.MAINTENANCE = 3;
exports.STATUS_PAGE_ALL_DOWN = 0;
exports.STATUS_PAGE_ALL_UP = 1;
exports.STATUS_PAGE_PARTIAL_DOWN = 2;
exports.STATUS_PAGE_MAINTENANCE = 3;
exports.SQL_DATE_FORMAT = "YYYY-MM-DD";
exports.SQL_DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ss";
exports.SQL_DATETIME_FORMAT_WITHOUT_SECOND = "YYYY-MM-DD HH:mm";
exports.MAX_INTERVAL_SECOND = 2073600;
exports.MIN_INTERVAL_SECOND = 1;
exports.INCIDENT_PAGE_SIZE = 10;
exports.PING_PACKET_SIZE_MIN = 1;
exports.PING_PACKET_SIZE_MAX = 65500;
exports.PING_PACKET_SIZE_DEFAULT = 56;
exports.PING_GLOBAL_TIMEOUT_MIN = 1;
exports.PING_GLOBAL_TIMEOUT_MAX = 300;
exports.PING_GLOBAL_TIMEOUT_DEFAULT = 10;
exports.PING_COUNT_MIN = 1;
exports.PING_COUNT_MAX = 100;
exports.PING_COUNT_DEFAULT = 1;
exports.PING_PER_REQUEST_TIMEOUT_MIN = 1;
exports.PING_PER_REQUEST_TIMEOUT_MAX = 60;
exports.PING_PER_REQUEST_TIMEOUT_DEFAULT = 2;
exports.RESPONSE_BODY_LENGTH_DEFAULT = 1024;
exports.RESPONSE_BODY_LENGTH_MAX = 1024 * 1024;
exports.CONSOLE_STYLE_Reset = "\x1b[0m";
exports.CONSOLE_STYLE_Bright = "\x1b[1m";
exports.CONSOLE_STYLE_Dim = "\x1b[2m";
exports.CONSOLE_STYLE_Underscore = "\x1b[4m";
exports.CONSOLE_STYLE_Blink = "\x1b[5m";
exports.CONSOLE_STYLE_Reverse = "\x1b[7m";
exports.CONSOLE_STYLE_Hidden = "\x1b[8m";
exports.CONSOLE_STYLE_FgBlack = "\x1b[30m";
exports.CONSOLE_STYLE_FgRed = "\x1b[31m";
exports.CONSOLE_STYLE_FgGreen = "\x1b[32m";
exports.CONSOLE_STYLE_FgYellow = "\x1b[33m";
exports.CONSOLE_STYLE_FgBlue = "\x1b[34m";
exports.CONSOLE_STYLE_FgMagenta = "\x1b[35m";
exports.CONSOLE_STYLE_FgCyan = "\x1b[36m";
exports.CONSOLE_STYLE_FgWhite = "\x1b[37m";
exports.CONSOLE_STYLE_FgGray = "\x1b[90m";
exports.CONSOLE_STYLE_FgOrange = "\x1b[38;5;208m";
exports.CONSOLE_STYLE_FgLightGreen = "\x1b[38;5;119m";
exports.CONSOLE_STYLE_FgLightBlue = "\x1b[38;5;117m";
exports.CONSOLE_STYLE_FgViolet = "\x1b[38;5;141m";
exports.CONSOLE_STYLE_FgBrown = "\x1b[38;5;130m";
exports.CONSOLE_STYLE_FgPink = "\x1b[38;5;219m";
exports.CONSOLE_STYLE_BgBlack = "\x1b[40m";
exports.CONSOLE_STYLE_BgRed = "\x1b[41m";
exports.CONSOLE_STYLE_BgGreen = "\x1b[42m";
exports.CONSOLE_STYLE_BgYellow = "\x1b[43m";
exports.CONSOLE_STYLE_BgBlue = "\x1b[44m";
exports.CONSOLE_STYLE_BgMagenta = "\x1b[45m";
exports.CONSOLE_STYLE_BgCyan = "\x1b[46m";
exports.CONSOLE_STYLE_BgWhite = "\x1b[47m";
exports.CONSOLE_STYLE_BgGray = "\x1b[100m";
const consoleModuleColors = [
exports.CONSOLE_STYLE_FgCyan,
exports.CONSOLE_STYLE_FgGreen,
exports.CONSOLE_STYLE_FgLightGreen,
exports.CONSOLE_STYLE_FgBlue,
exports.CONSOLE_STYLE_FgLightBlue,
exports.CONSOLE_STYLE_FgMagenta,
exports.CONSOLE_STYLE_FgOrange,
exports.CONSOLE_STYLE_FgViolet,
exports.CONSOLE_STYLE_FgBrown,
exports.CONSOLE_STYLE_FgPink,
];
const consoleLevelColors = {
info: exports.CONSOLE_STYLE_FgCyan,
warn: exports.CONSOLE_STYLE_FgYellow,
error: exports.CONSOLE_STYLE_FgRed,
debug: exports.CONSOLE_STYLE_FgGray,
};
exports.badgeConstants = {
naColor: "#999",
defaultUpColor: "#66c20a",
defaultWarnColor: "#eed202",
defaultDownColor: "#c2290a",
defaultPendingColor: "#f8a306",
defaultMaintenanceColor: "#1747f5",
defaultPingColor: "blue",
defaultStyle: "flat",
defaultPingValueSuffix: "ms",
defaultPingLabelSuffix: "h",
defaultUptimeValueSuffix: "%",
defaultUptimeLabelSuffix: "h",
defaultCertExpValueSuffix: " days",
defaultCertExpLabelSuffix: "h",
defaultCertExpireWarnDays: "14",
defaultCertExpireDownDays: "7",
};
function flipStatus(s) {
if (s === exports.UP) {
return exports.DOWN;
}
if (s === exports.DOWN) {
return exports.UP;
}
return s;
}
exports.flipStatus = flipStatus;
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
exports.sleep = sleep;
function ucfirst(str) {
if (!str) {
return str;
}
const firstLetter = str.substr(0, 1);
return firstLetter.toUpperCase() + str.substr(1);
}
exports.ucfirst = ucfirst;
function debug(msg) {
exports.log.log("", "debug", msg);
}
exports.debug = debug;
class Logger {
constructor() {
this.hideLog = {
info: [],
warn: [],
error: [],
debug: [],
};
if (typeof process !== "undefined" && process.env.UPTIME_KUMA_HIDE_LOG) {
const list = process.env.UPTIME_KUMA_HIDE_LOG.split(",").map((v) => v.toLowerCase());
for (const pair of list) {
const values = pair.split(/_(.*)/s);
if (values.length >= 2) {
this.hideLog[values[0]].push(values[1]);
}
}
this.debug("server", "UPTIME_KUMA_HIDE_LOG is set");
this.debug("server", this.hideLog);
}
}
log(module, level, ...msg) {
if (level === "debug" && !exports.isDev) {
return;
}
if (this.hideLog[level] && this.hideLog[level].includes(module.toLowerCase())) {
return;
}
module = module.toUpperCase();
const levelLabel = level.toUpperCase();
let now;
if (dayjs.tz) {
now = dayjs.tz(new Date()).format();
}
else {
now = dayjs().format();
}
if (process.env.UPTIME_KUMA_LOG_FORMAT === "json") {
const msgString = msg
.map((m) => {
if (typeof m === "string") {
return m;
}
else {
try {
return JSON.stringify(m);
}
catch (_a) {
return String(m);
}
}
})
.join(" ");
console.log(JSON.stringify({
time: now,
module: module,
level: level,
msg: msgString,
}));
return;
}
const levelColor = consoleLevelColors[level];
const moduleColor = consoleModuleColors[intHash(module, consoleModuleColors.length)];
let timePart;
let modulePart;
let levelPart;
if (exports.isNode) {
switch (level) {
case "debug":
timePart = exports.CONSOLE_STYLE_FgGray + now + exports.CONSOLE_STYLE_Reset;
break;
default:
timePart = exports.CONSOLE_STYLE_FgCyan + now + exports.CONSOLE_STYLE_Reset;
break;
}
modulePart = "[" + moduleColor + module + exports.CONSOLE_STYLE_Reset + "]";
levelPart = levelColor + `${levelLabel}:` + exports.CONSOLE_STYLE_Reset;
}
else {
timePart = now;
modulePart = `[${module}]`;
levelPart = `${levelLabel}:`;
}
switch (level) {
case "error":
console.error(timePart, modulePart, levelPart, ...msg);
break;
case "warn":
console.warn(timePart, modulePart, levelPart, ...msg);
break;
case "info":
console.info(timePart, modulePart, levelPart, ...msg);
break;
case "debug":
if (exports.isDev) {
console.debug(timePart, modulePart, levelPart, ...msg);
}
break;
default:
console.log(timePart, modulePart, levelPart, ...msg);
break;
}
}
info(module, ...msg) {
this.log(module, "info", ...msg);
}
warn(module, ...msg) {
this.log(module, "warn", ...msg);
}
error(module, ...msg) {
this.log(module, "error", ...msg);
}
debug(module, ...msg) {
this.log(module, "debug", ...msg);
}
exception(module, exception, ...msg) {
this.log(module, "error", ...msg, exception);
}
}
exports.log = new Logger();
function polyfill() {
if (!String.prototype.replaceAll) {
String.prototype.replaceAll = function (str, newStr) {
if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") {
return this.replace(str, newStr);
}
return this.replace(new RegExp(str, "g"), newStr);
};
}
}
exports.polyfill = polyfill;
class TimeLogger {
constructor() {
this.startTime = dayjs().valueOf();
}
print(name) {
if (exports.isDev && process.env.TIMELOGGER === "1") {
console.log(name + ": " + (dayjs().valueOf() - this.startTime) + "ms");
}
}
}
exports.TimeLogger = TimeLogger;
function getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min;
}
exports.getRandomArbitrary = getRandomArbitrary;
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
exports.getRandomInt = getRandomInt;
const getRandomBytes = (typeof window !== "undefined" && window.crypto
?
function () {
return (numBytes) => {
const randomBytes = new Uint8Array(numBytes);
for (let i = 0; i < numBytes; i += 65536) {
window.crypto.getRandomValues(randomBytes.subarray(i, i + Math.min(numBytes - i, 65536)));
}
return randomBytes;
};
}
:
function () {
return require("crypto").randomBytes;
})();
function getCryptoRandomInt(min, max) {
const range = max - min;
if (range >= Math.pow(2, 32)) {
console.log("Warning! Range is too large.");
}
let tmpRange = range;
let bitsNeeded = 0;
let bytesNeeded = 0;
let mask = 1;
while (tmpRange > 0) {
if (bitsNeeded % 8 === 0) {
bytesNeeded += 1;
}
bitsNeeded += 1;
mask = (mask << 1) | 1;
tmpRange = tmpRange >>> 1;
}
const randomBytes = getRandomBytes(bytesNeeded);
let randomValue = 0;
for (let i = 0; i < bytesNeeded; i++) {
randomValue |= randomBytes[i] << (8 * i);
}
randomValue = randomValue & mask;
if (randomValue <= range) {
return min + randomValue;
}
else {
return getCryptoRandomInt(min, max);
}
}
exports.getCryptoRandomInt = getCryptoRandomInt;
function genSecret(length = 64) {
let secret = "";
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const charsLength = chars.length;
for (let i = 0; i < length; i++) {
secret += chars.charAt(getCryptoRandomInt(0, charsLength - 1));
}
return secret;
}
exports.genSecret = genSecret;
function getMonitorRelativeURL(id) {
return "/dashboard/" + id;
}
exports.getMonitorRelativeURL = getMonitorRelativeURL;
function parseTimeObject(time) {
if (!time) {
return {
hours: 0,
minutes: 0,
};
}
const array = time.split(":");
if (array.length < 2) {
throw new Error("parseVueDatePickerTimeFormat: Invalid Time");
}
const obj = {
hours: parseInt(array[0]),
minutes: parseInt(array[1]),
seconds: 0,
};
if (array.length >= 3) {
obj.seconds = parseInt(array[2]);
}
return obj;
}
exports.parseTimeObject = parseTimeObject;
function parseTimeFromTimeObject(obj) {
if (!obj) {
return obj;
}
let result = "";
result += obj.hours.toString().padStart(2, "0") + ":" + obj.minutes.toString().padStart(2, "0");
if (obj.seconds) {
result += ":" + obj.seconds.toString().padStart(2, "0");
}
return result;
}
exports.parseTimeFromTimeObject = parseTimeFromTimeObject;
function isoToUTCDateTime(input) {
return dayjs(input).utc().format(exports.SQL_DATETIME_FORMAT);
}
exports.isoToUTCDateTime = isoToUTCDateTime;
function utcToISODateTime(input) {
return dayjs.utc(input).toISOString();
}
exports.utcToISODateTime = utcToISODateTime;
function utcToLocal(input, format = exports.SQL_DATETIME_FORMAT) {
return dayjs.utc(input).local().format(format);
}
exports.utcToLocal = utcToLocal;
function localToUTC(input, format = exports.SQL_DATETIME_FORMAT) {
return dayjs(input).utc().format(format);
}
exports.localToUTC = localToUTC;
function intHash(str, length = 10) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash += str.charCodeAt(i);
}
return ((hash % length) + length) % length;
}
exports.intHash = intHash;
async function evaluateJsonQuery(data, jsonPath, jsonPathOperator, expectedValue) {
let response;
try {
response = JSON.parse(data);
}
catch (_a) {
response =
(typeof data === "object" || typeof data === "number") && !Buffer.isBuffer(data) ? data : data.toString();
}
try {
response = jsonPath ? await jsonata(jsonPath).evaluate(response) : response;
if (response === null || response === undefined) {
throw new Error("Empty or undefined response. Check query syntax and response structure");
}
if (Array.isArray(response)) {
const responseStr = JSON.stringify(response);
const truncatedResponse = responseStr.length > 25 ? responseStr.substring(0, 25) + "...]" : responseStr;
throw new Error("JSON query returned the array " +
truncatedResponse +
", but a primitive value is required. " +
"Modify your query to return a single value via [0] to get the first element or use an aggregation like $count(), $sum() or $boolean().");
}
if (typeof response === "object" || response instanceof Date || typeof response === "function") {
throw new Error(`The post-JSON query evaluated response from the server is of type ${typeof response}, which cannot be directly compared to the expected value`);
}
let jsonQueryExpression;
switch (jsonPathOperator) {
case ">":
case ">=":
case "<":
case "<=":
jsonQueryExpression = `$number($.value) ${jsonPathOperator} $number($.expected)`;
break;
case "!=":
jsonQueryExpression = "$.value != $.expected";
break;
case "==":
jsonQueryExpression = "$.value = $.expected";
break;
case "contains":
jsonQueryExpression = "$contains($.value, $.expected)";
break;
default:
throw new Error(`Invalid condition ${jsonPathOperator}`);
}
const expression = jsonata(jsonQueryExpression);
const status = await expression.evaluate({
value: response.toString(),
expected: expectedValue.toString(),
});
if (status === undefined) {
throw new Error("Query evaluation returned undefined. Check query syntax and the structure of the response data");
}
return {
status,
response,
};
}
catch (err) {
response = JSON.stringify(response);
response = response && response.length > 50 ? `${response.substring(0, 100)}… (truncated)` : response;
throw new Error(`Error evaluating JSON query: ${err.message}. Response from server was: ${response}`);
}
}
exports.evaluateJsonQuery = evaluateJsonQuery;
exports.TYPES_WITH_DOMAIN_EXPIRY_SUPPORT_VIA_FIELD = {
http: "url",
keyword: "url",
"json-query": "url",
"real-browser": "url",
"websocket-upgrade": "url",
port: "hostname",
ping: "hostname",
"grpc-keyword": "grpcUrl",
dns: "hostname",
smtp: "hostname",
snmp: "hostname",
gamedig: "hostname",
steam: "hostname",
mqtt: "hostname",
radius: "hostname",
"tailscale-ping": "hostname",
"sip-options": "hostname",
};
+43 -20
View File
@@ -1,12 +1,6 @@
/* eslint-disable camelcase */
/*!
// Common Util for frontend and backend
//
// DOT NOT MODIFY util.js!
// Need to run "npm run tsc" to compile if there are any changes.
//
// Backend uses the compiled file util.js
// Frontend uses util.ts
*/
import dayjsFrontend from "dayjs";
@@ -17,7 +11,7 @@ import * as timezone from "dayjs/plugin/timezone";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import * as utc from "dayjs/plugin/utc";
import * as jsonata from "jsonata";
import jsonata from "jsonata";
export const isDev = process.env.NODE_ENV === "development";
export const isNode = typeof process !== "undefined" && process?.versions?.node;
@@ -28,6 +22,13 @@ export const isNode = typeof process !== "undefined" && process?.versions?.node;
*/
const dayjs = isNode ? require("dayjs") : dayjsFrontend;
export const devOriginList = [
"http://127.0.0.1:3000",
"http://127.0.0.1:3001",
"http://localhost:3000",
"http://localhost:3001",
];
export const appName = "Uptime Kuma";
export const DOWN = 0;
export const UP = 1;
@@ -137,11 +138,6 @@ const consoleLevelColors = {
debug: CONSOLE_STYLE_FgGray,
} as const;
/**
* Flip the status of s
* @param s input status: UP or DOWN
* @returns {number} UP or DOWN
*/
export const badgeConstants = {
naColor: "#999",
defaultUpColor: "#66c20a",
@@ -164,8 +160,8 @@ export const badgeConstants = {
/**
* Flip the status of s between UP and DOWN if this is possible
* @param s {number} status
* @returns {number} flipped status
* @param s status
* @returns flipped status
*/
export function flipStatus(s: number) {
if (s === UP) {
@@ -182,7 +178,6 @@ export function flipStatus(s: number) {
/**
* Delays for specified number of seconds
* @param ms Number of milliseconds to sleep for
* @returns {Promise<void>} Promise that resolves after ms
*/
export function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
@@ -191,7 +186,7 @@ export function sleep(ms: number) {
/**
* PHP's ucfirst
* @param str string input
* @returns {string} string with first letter capitalized
* @returns string with first letter capitalized
*/
export function ucfirst(str: string) {
if (!str) {
@@ -205,7 +200,6 @@ export function ucfirst(str: string) {
/**
* @deprecated Use log.debug (https://github.com/louislam/uptime-kuma/pull/910)
* @param msg Message to write
* @returns {void}
*/
export function debug(msg: unknown) {
log.log("", "debug", msg);
@@ -254,7 +248,6 @@ class Logger {
* @param module The module the log comes from
* @param level Log level. One of info, warn, error, debug.
* @param msg Message to write
* @returns {void}
*/
log(module: string, level: LogLevel, ...msg: unknown[]) {
if (level === "debug" && !isDev) {
@@ -457,7 +450,7 @@ export class TimeLogger {
* Returns a random number between min (inclusive) and max (exclusive)
* @param min minumim value, inclusive
* @param max maximum value, exclusive
* @returns {number} Random number
* @returns Random number
*/
export function getRandomArbitrary(min: number, max: number) {
return Math.random() * (max - min) + min;
@@ -637,7 +630,7 @@ export function isoToUTCDateTime(input: string) {
/**
* @param input valid datetime string
* @returns {string} ISO DateTime string
* @returns ISO DateTime string
*/
export function utcToISODateTime(input: string) {
return dayjs.utc(input).toISOString();
@@ -795,3 +788,33 @@ export const TYPES_WITH_DOMAIN_EXPIRY_SUPPORT_VIA_FIELD = {
"tailscale-ping": "hostname",
"sip-options": "hostname",
} as const;
/**
* @param res Response object from fetch
*/
export async function checkFetch(res: Response): Promise<void> {
let data;
try {
if (!res.ok) {
data = await res.json();
}
} catch (e) {
throw new Error("Failed to fetch without message: " + res.status);
}
if (data) {
if (data.msg) {
throw new Error(data.msg);
} else {
throw new Error(JSON.stringify(data));
}
}
const contentType = res.headers.get("content-type");
// if response is not in json type
if (!contentType || !contentType.startsWith("application/json")) {
throw new Error("Response is not in JSON format");
}
}
-12
View File
@@ -1,12 +0,0 @@
import * as childProcess from "child_process";
const version = parseInt(process.version.slice(1).split(".")[0]);
/**
* Since Node.js 22 introduced a different "node --test" command with glob, we need to run different test commands based on the Node.js version.
*/
if (version < 22) {
childProcess.execSync("npm run test-backend-20", { stdio: "inherit" });
} else {
childProcess.execSync("npm run test-backend-22", { stdio: "inherit" });
}
-7
View File
@@ -1,7 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"esModuleInterop": false
},
"files": ["./src/util.ts"]
}
+2 -2
View File
@@ -2,8 +2,8 @@
"compileOnSave": true,
"compilerOptions": {
"newLine": "LF",
"target": "es2018",
"module": "commonjs",
"target": "esnext",
"module": "esnext",
"lib": ["es2020", "DOM"],
"declaration": false,
"removeComments": true,