mirror of
https://github.com/louislam/uptime-kuma.git
synced 2026-06-28 04:24:25 +00:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f45e0253f1 | |||
| 7cce7df233 | |||
| 8d29fdec37 | |||
| ec49c7c456 | |||
| 413b546529 | |||
| 28ee14d30d | |||
| 1a36862abc | |||
| 17626d1a24 | |||
| 5222eef990 | |||
| 45a3bac95f | |||
| 0de5ed1e8a | |||
| 9755823b93 | |||
| 0d17551ed3 | |||
| 5ad681edf1 | |||
| ddd1a2a075 | |||
| 18ec60e4d7 | |||
| 13042aa990 | |||
| eaa501f37d | |||
| d96753fd41 | |||
| af886113ff | |||
| 219a4d3e10 | |||
| be3bd8563a | |||
| 3e2eb5bd0c | |||
| a0473769db | |||
| 0632ca1ae8 | |||
| deaed046b0 | |||
| 3abbeecddf | |||
| df23942f65 | |||
| f154fcf8da | |||
| 7a16d803d3 | |||
| 73d3573198 | |||
| 9df8a957c7 | |||
| fcb0af3fd1 | |||
| e5c679332e | |||
| 998f4e81fa | |||
| cea9278755 | |||
| baaf14c594 | |||
| b3388f5bb7 | |||
| 3f1866f658 | |||
| 57f5414d79 | |||
| 0b174ef25a | |||
| 9cfa0f483d | |||
| f28cba8388 | |||
| c631cd3373 | |||
| 77a31f1fbe | |||
| fb0b8b484f | |||
| b50d496e75 | |||
| fe1cd3f2da | |||
| ac5781d711 | |||
| fa2bc8eda6 | |||
| 93fc8e463f | |||
| 492b8f51ad | |||
| c515b6d043 |
@@ -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",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,6 +7,7 @@ on:
|
||||
branches:
|
||||
- master
|
||||
- 1.23.X
|
||||
- 3.0.X
|
||||
workflow_dispatch:
|
||||
permissions: {}
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"framework": "none",
|
||||
"src/**": {
|
||||
"framework": "vue"
|
||||
}
|
||||
}
|
||||
+4
-5
@@ -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>
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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 && \
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Generated
+1954
-2462
File diff suppressed because it is too large
Load Diff
+38
-36
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 = "";
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": false
|
||||
},
|
||||
"files": ["./src/util.ts"]
|
||||
}
|
||||
+2
-2
@@ -2,8 +2,8 @@
|
||||
"compileOnSave": true,
|
||||
"compilerOptions": {
|
||||
"newLine": "LF",
|
||||
"target": "es2018",
|
||||
"module": "commonjs",
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"lib": ["es2020", "DOM"],
|
||||
"declaration": false,
|
||||
"removeComments": true,
|
||||
|
||||
Reference in New Issue
Block a user