mirror of
https://github.com/louislam/uptime-kuma.git
synced 2026-10-13 08:52:17 -05:00
Merge remote-tracking branch 'origin/master' into merge
# Conflicts: # package-lock.json # package.json
This commit is contained in:
131 files changed
+7037
-5458
No files matched your search
@@ -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",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
name: Setup Atlas Cloud provider
|
||||
description: Write the Atlas Cloud provider config so opencode can use the models listed in .github/opencode-models.json
|
||||
|
||||
inputs:
|
||||
api-key:
|
||||
description: Atlas Cloud API key
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Write opencode provider config
|
||||
shell: bash
|
||||
env:
|
||||
ATLASCLOUD_API_KEY: ${{ inputs.api-key }}
|
||||
run: |
|
||||
mkdir -p ~/.config/opencode
|
||||
jq -n \
|
||||
--arg key "$ATLASCLOUD_API_KEY" \
|
||||
--slurpfile models .github/opencode-models.json \
|
||||
'{
|
||||
provider: {
|
||||
atlascloud: {
|
||||
npm: "@ai-sdk/openai-compatible",
|
||||
name: "Atlas Cloud",
|
||||
options: {
|
||||
baseURL: "https://api.atlascloud.ai/v1",
|
||||
apiKey: $key
|
||||
},
|
||||
models: ($models[0] | map({ (. | sub("^atlascloud/"; "")): { name: (. | split("/") | last) } }) | add)
|
||||
}
|
||||
}
|
||||
}' > ~/.config/opencode/opencode.json
|
||||
@@ -1,6 +1,6 @@
|
||||
# Copilot Instructions for Uptime Kuma
|
||||
|
||||
Warning: Only maintainers of Uptime Kuma can use this instructions, for other contributors, must read AGENTS.md and CLAUDE.md to avoid to get banned because of AI slop.
|
||||
Warning: Only maintainers of Uptime Kuma can use these instructions. Other contributors must read AGENTS.md and CLAUDE.md. If the git user name does not match a maintainer (see the "For Maintainers" section in AGENTS.md), ignore this file to avoid being banned for AI slop.
|
||||
|
||||
## Copilot's Goals/Tasks
|
||||
|
||||
@@ -8,52 +8,57 @@ Warning: Only maintainers of Uptime Kuma can use this instructions, for other co
|
||||
- Do not show "Pull Request Overview"
|
||||
- You do not have to reply if there are no issues
|
||||
|
||||
## Keep These Instructions in Sync
|
||||
|
||||
- If a PR/commit changes anything documented here (commands, paths, architecture, test files, workflows), update the related content in this file in the same PR/commit, so it does not drift.
|
||||
|
||||
## Repository Overview
|
||||
|
||||
**Uptime Kuma** is a self-hosted monitoring tool for HTTP(s), TCP, DNS, Docker, etc. Built with Vue 3 (frontend) and Node.js/Express (backend), using Socket.IO for real-time communication.
|
||||
|
||||
- **Languages**: JavaScript, Vue 3, TypeScript (limited), HTML, CSS/SCSS
|
||||
- **Backend**: Node.js >= 20.4, Express.js, Socket.IO, SQLite
|
||||
- **Languages**: JavaScript, TypeScript, Vue 3, HTML, CSS/SCSS
|
||||
- **Backend**: Node.js >= 26.2.0, Express.js, Socket.IO, SQLite (also MariaDB/MySQL), Better Auth
|
||||
- **Frontend**: Vue 3, Vite, Bootstrap 5, Chart.js
|
||||
- **Package Manager**: npm with `legacy-peer-deps=true` (.npmrc)
|
||||
- **Package Manager**: npm with `legacy-peer-deps=true` and `min-release-age=7` (.npmrc)
|
||||
|
||||
## Build & Validation Commands
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js >= 20.4.0, npm >= 9.3, Git
|
||||
- Node.js >= 26.2.0 (see `engines` in package.json; CI runs Node 26), a recent npm, Git
|
||||
|
||||
### Essential Command Sequence
|
||||
|
||||
1. **Install Dependencies**:
|
||||
|
||||
```bash
|
||||
npm ci # Use npm ci NOT npm install (~60-90 seconds)
|
||||
npm ci # Use npm ci NOT npm install
|
||||
```
|
||||
|
||||
2. **Linting** (required before committing):
|
||||
|
||||
```bash
|
||||
npm run lint # Both linters (~15-30 seconds)
|
||||
npm run lint # ESLint + Stylelint
|
||||
npm run lint:prod # For production (zero warnings)
|
||||
```
|
||||
|
||||
3. **Build Frontend**:
|
||||
|
||||
```bash
|
||||
npm run build # Takes ~90-120 seconds, builds to dist/
|
||||
npm run build # Builds to dist/
|
||||
```
|
||||
|
||||
4. **Run Tests**:
|
||||
|
||||
```bash
|
||||
npm run test-backend # Backend tests (~50-60 seconds)
|
||||
npm test # All tests
|
||||
npm run test-backend # Backend unit tests (node:test, run through tsx)
|
||||
npm test # Backend + Playwright E2E
|
||||
```
|
||||
|
||||
### Development Workflow
|
||||
|
||||
```bash
|
||||
npm run dev # Starts frontend (port 3000) and backend (port 3001)
|
||||
npm run dev # Frontend on port 3000, backend on port 3001
|
||||
```
|
||||
|
||||
## Project Architecture
|
||||
@@ -68,65 +73,85 @@ npm run dev # Starts frontend (port 3000) and backend (port 3001)
|
||||
│ ├── notification-providers/ Notification integrations
|
||||
│ ├── routers/ Express routers
|
||||
│ ├── socket-handlers/ Socket.IO event handlers
|
||||
│ ├── server.js Server entry point
|
||||
│ └── uptime-kuma-server.js Main server logic
|
||||
│ ├── modules/ Shared backend modules
|
||||
│ ├── util/ Backend utilities
|
||||
│ ├── server.js Server entry point (executed via tsx)
|
||||
│ ├── uptime-kuma-server.js Main server logic
|
||||
│ └── better-auth.ts Authentication (Better Auth)
|
||||
├── src/ Frontend source code (Vue 3 SPA)
|
||||
│ ├── components/ Vue components
|
||||
│ ├── pages/ Page components
|
||||
│ ├── lang/ i18n translations
|
||||
│ ├── router.js Vue Router configuration
|
||||
│ └── main.js Frontend entry point
|
||||
│ ├── lang/ i18n translations
|
||||
│ ├── router.js Vue Router configuration
|
||||
│ └── main.js Frontend entry point
|
||||
├── db/ Database related
|
||||
│ ├── knex_migrations/ Knex migration files
|
||||
│ └── kuma.db SQLite database (gitignored)
|
||||
│ └── kuma.db SQLite database
|
||||
├── test/ Test files
|
||||
│ ├── backend-test/ Backend unit tests
|
||||
│ └── e2e/ Playwright E2E tests
|
||||
├── config/ Build configuration
|
||||
│ ├── vite.config.js Vite build config
|
||||
│ └── playwright.config.js Playwright test config
|
||||
│ ├── backend-test/ Backend unit tests (.ts and .js)
|
||||
│ └── e2e/ Playwright E2E tests
|
||||
├── vite.config.mjs Vite build/dev config
|
||||
├── playwright.config.js Playwright test config
|
||||
├── tsconfig.json TypeScript config
|
||||
├── dist/ Frontend build output (gitignored)
|
||||
├── data/ App data directory (gitignored)
|
||||
├── public/ Static frontend assets (dev only)
|
||||
├── public/ Static frontend assets
|
||||
├── docker/ Docker build files
|
||||
└── extra/ Utility scripts
|
||||
```
|
||||
|
||||
### Backend Architecture
|
||||
|
||||
- `server/model/` — RedBean models mapped to tables. `monitor.js` holds the monitor loop and handles the core types inline: `http`, `keyword`, `json-query`, `ping`, `push`, `docker`, `radius`, `kafka-producer`.
|
||||
- `server/monitor-types/` — one class per monitor type, registered in `server/uptime-kuma-server.js` (`monitorTypeList`).
|
||||
- `server/notification-providers/` — one file per provider (110+), registered in `server/notification.js`.
|
||||
- `server/socket-handlers/` — Socket.IO event handlers (most of the real-time API surface).
|
||||
- `server/routers/` — Express routers: `api-router.js` (push/REST API), `status-page-router.js`, `better-auth-router.ts`.
|
||||
- `server/modules/`, `server/util/`, `server/utils/knex/` — shared helpers.
|
||||
- `server/better-auth.ts` + `src/auth-client.ts` — authentication (Better Auth, v3).
|
||||
|
||||
### Key Configuration Files
|
||||
|
||||
- **package.json**: Scripts, dependencies, Node.js version requirement
|
||||
- **.eslintrc.js**: ESLint rules (4 spaces, double quotes, unix line endings, JSDoc required)
|
||||
- **.eslintrc.js**: ESLint rules (4 spaces, double quotes, JSDoc required)
|
||||
- **.stylelintrc**: Stylelint rules (4 spaces indentation)
|
||||
- **.editorconfig**: Editor settings (4 spaces, LF, UTF-8)
|
||||
- **tsconfig-backend.json**: TypeScript config for backend (only src/util.ts)
|
||||
- **.npmrc**: `legacy-peer-deps=true` (required for dependency resolution)
|
||||
- **tsconfig.json**: TypeScript config
|
||||
- **.npmrc**: `legacy-peer-deps=true`, `min-release-age=7`
|
||||
- **.gitignore**: Excludes node_modules, dist, data, tmp, private
|
||||
|
||||
### Code Style (strictly enforced by linters)
|
||||
|
||||
- 4 spaces indentation, double quotes, Unix line endings (LF), semicolons required
|
||||
- Prettier enforces `printWidth: 120`; run `npm run fmt` to format (also run automatically in CI)
|
||||
- **Comments**: keep them short (one concise line explaining what/why); no multi-line essays or restating the code
|
||||
- **Commits**: one short sentence describing the change; no long body
|
||||
- **Naming**: JavaScript/TypeScript (camelCase), SQLite (snake_case), CSS/SCSS (kebab-case)
|
||||
- JSDoc required for all functions/methods
|
||||
- JSDoc is required on every function declaration and method (`jsdoc/require-jsdoc`); `*.ts` relaxes some JSDoc and `any` rules
|
||||
- Notable ESLint rules: `no-var`, `one-var: never`, `max-statements-per-line: 1`, `jsdoc/require-throws`
|
||||
- **Module system**: no `"type": "module"` in package.json — `*.js` is CommonJS (`require`), `*.mjs`/`*.mts` are ESM, and `*.ts` is executed via `tsx`
|
||||
|
||||
## CI/CD Workflows
|
||||
|
||||
**auto-test.yml** (runs on PR/push to master/1.23.X):
|
||||
**auto-test.yml** (push to master/1.23.X/3.0.X and every PR):
|
||||
|
||||
- Linting, building, backend tests on multiple OS/Node versions (15 min timeout)
|
||||
- E2E Playwright tests
|
||||
- Builds and runs backend tests on macOS, Ubuntu, Windows, and Ubuntu arm (Node 26)
|
||||
- `check-linters` runs `npm run lint:prod` on Node 24
|
||||
- `e2e-test` runs `npm run build` then `npm run test-e2e` (Playwright)
|
||||
|
||||
**validate.yml**: Validates JSON/YAML files, language files, knex migrations
|
||||
**validate.yml** (pushes to master, PRs to master/1.23.X/3.0.X):
|
||||
|
||||
- Validates JSON/YAML files and runs `extra/check-lang-json.js`, `extra/check-knex-filenames.mjs`, `extra/check-package-json.mjs`
|
||||
|
||||
**PR Requirements**: All linters pass, tests pass, code follows style guidelines
|
||||
|
||||
## Common Issues
|
||||
|
||||
1. **npm install vs npm ci**: Always use `npm ci` for reproducible builds
|
||||
2. **TypeScript errors**: `npm run tsc` shows 1400+ errors - ignore them, they don't affect builds
|
||||
3. **Stylelint warnings**: Deprecation warnings are expected, ignore them
|
||||
4. **Test failures**: Always run `npm run build` before running tests
|
||||
5. **Port conflicts**: Dev server uses ports 3000 and 3001
|
||||
6. **First run**: Server shows "db-config.json not found" - this is expected, starts setup wizard
|
||||
2. **Stylelint warnings**: Deprecation warnings are expected, ignore them
|
||||
3. **E2E tests**: Run `npm run build` before `npm run test-e2e` in a clean checkout (or use `npm run test-e2e-local`)
|
||||
4. **Port conflicts**: Dev server uses ports 3000 (frontend) and 3001 (backend)
|
||||
5. **First run**: Server shows "db-config.json not found" - this is expected, starts setup wizard
|
||||
|
||||
## Translations
|
||||
|
||||
@@ -137,19 +162,23 @@ npm run dev # Starts frontend (port 3000) and backend (port 3001)
|
||||
## Database
|
||||
|
||||
- Primary: SQLite (also supports MariaDB/MySQL)
|
||||
- Migrations in `db/knex_migrations/` using Knex.js
|
||||
- Migrations in `db/knex_migrations/` using Knex.js; filenames are `YYYY-MM-DD-HHMM-description.js` (migration behavior is covered by `test/backend-test/test-migration.js`)
|
||||
- Filename format validated by CI: `node ./extra/check-knex-filenames.mjs`
|
||||
- v3 uses Better Auth for authentication
|
||||
|
||||
## Testing
|
||||
|
||||
- **Backend**: Node.js test runner, fast unit tests
|
||||
- **E2E**: Playwright (requires `npx playwright install` first time)
|
||||
- Test data in `data/playwright-test`
|
||||
- **Backend**: Node.js test runner via tsx (`test/backend-test/**/*.{ts,js}`). Use `describe()`/`test()` from `node:test` and one test per scenario (see `test/backend-test/README.md`).
|
||||
- Database monitor tests use Testcontainers; `npm run test-e2e-local` sets `SKIP_TESTCONTAINER=1` to skip them.
|
||||
- **E2E**: Playwright specs in `test/e2e/specs/*.spec.js`; `setup-process.once.js` is a one-time setup project every spec depends on. Requires `npx playwright install`.
|
||||
- Test data lives in `data/playwright-test`; reports go to `private/playwright-report`.
|
||||
|
||||
## Adding New Features
|
||||
|
||||
### New Notification Provider
|
||||
|
||||
There are 110+ existing providers in `server/notification-providers/`; copy an existing one as a template.
|
||||
|
||||
Files to modify:
|
||||
|
||||
1. `server/notification-providers/PROVIDER_NAME.js` (backend logic)
|
||||
@@ -161,6 +190,8 @@ Files to modify:
|
||||
|
||||
### New Monitor Type
|
||||
|
||||
Core types (`http`, `keyword`, `json-query`, `ping`, `push`, `docker`, `radius`, `kafka-producer`) are handled inline in `server/model/monitor.js`. All other types are `MonitorType` subclasses in `server/monitor-types/` registered via `UptimeKumaServer.monitorTypeList` in `server/uptime-kuma-server.js`.
|
||||
|
||||
Files to modify:
|
||||
|
||||
1. `server/monitor-types/MONITORING_TYPE.js` (backend logic)
|
||||
@@ -170,9 +201,10 @@ Files to modify:
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. **Trust these instructions** - based on testing. Search only if incomplete/incorrect
|
||||
2. **Dependencies**: 5 known vulnerabilities (3 moderate, 2 high) - acknowledged, don't fix without discussion
|
||||
3. **Git Branches**: `master` (v2 development), `1.23.X` (v1 maintenance)
|
||||
4. **Node Version**: >= 20.4.0 required
|
||||
1. **Trust these instructions** - Search only if incomplete/incorrect
|
||||
2. **Dependencies**: Run `npm run audit` (`npm audit --omit=dev`) instead of relying on a fixed vulnerability list
|
||||
3. **Git Branches**: `master` (v3 development), `3.0.X` (v3 release), `2.5.X` (v2), `1.23.X` (v1)
|
||||
4. **Node Version**: >= 26.2.0 required
|
||||
5. **Socket.IO**: Most backend logic in `server/socket-handlers/`, not REST
|
||||
6. **Never commit**: `data/`, `dist/`, `tmp/`, `private/`, `node_modules/`
|
||||
6. **Docker**: `docker/` + `compose.yaml`; image tags use the `3` suffix (`base3`, `nightly3`, `pr-test3`)
|
||||
7. **Never commit**: `data/`, `dist/`, `tmp/`, `private/`, `node_modules/`
|
||||
@@ -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,27 +21,24 @@ 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
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with: { persist-credentials: false }
|
||||
|
||||
# Tempoary disable until we understand TanStack npm supply-chain compromise
|
||||
# - name: Cache/Restore node_modules
|
||||
# uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
||||
# id: node-modules-cache
|
||||
# with:
|
||||
# path: node_modules
|
||||
# key: node-modules-${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }}
|
||||
# The cache is scoped to the pull request's merge ref, so it cannot poison
|
||||
# the base branch (the vector in the TanStack incident used pull_request_target).
|
||||
- name: Cache node_modules
|
||||
id: cache-node-modules
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: node_modules
|
||||
key: node-modules-${{ runner.os }}-${{ runner.arch }}-node${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }}
|
||||
|
||||
- name: Use Node.js ${{ matrix.node }}
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
@@ -51,49 +48,25 @@ jobs:
|
||||
# Print the npm version
|
||||
- run: npm --version
|
||||
|
||||
- run: npm clean-install --no-fund
|
||||
|
||||
- name: Rebuild native modules for ARM64
|
||||
if: matrix.os == 'ubuntu-22.04-arm'
|
||||
run: npm rebuild @louislam/sqlite3
|
||||
# Apprise is not pre-installed on the runners, so install it from upstream
|
||||
# (via uv) to run the Apprise notification provider test instead of skipping it.
|
||||
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
- name: Install Apprise
|
||||
run: uv tool install "apprise~=2.0.1" --with paho-mqtt --with "slixmpp>=1.16.0"
|
||||
- name: Add uv tools to PATH
|
||||
if: runner.os != 'Windows'
|
||||
run: echo "$(uv tool dir --bin)" >> "$GITHUB_PATH"
|
||||
- name: Verify Apprise
|
||||
run: apprise --version
|
||||
|
||||
- if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
||||
run: npm clean-install --no-fund
|
||||
- run: npm run build
|
||||
- run: npm run test-backend
|
||||
env:
|
||||
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:
|
||||
@@ -101,7 +74,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- run: git config --global core.autocrlf false # Mainly for Windows
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with: { persist-credentials: false }
|
||||
|
||||
# Tempoary disable until we understand TanStack npm supply-chain compromise
|
||||
@@ -112,22 +85,22 @@ jobs:
|
||||
# path: node_modules
|
||||
# key: node-modules-${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }}
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
- name: Use Node.js 24
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.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.61.1
|
||||
steps:
|
||||
- run: git config --global core.autocrlf false # Mainly for Windows
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with: { persist-credentials: false }
|
||||
|
||||
# Tempoary disable until we understand TanStack npm supply-chain compromise
|
||||
@@ -139,16 +112,29 @@ jobs:
|
||||
# key: node-modules-${{ runner.os }}-node${{ matrix.node }}-${{ hashFiles('**/package-lock.json') }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.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
|
||||
|
||||
- name: Upload Playwright Report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: playwright-report
|
||||
path: private/playwright-report
|
||||
retention-days: 14
|
||||
|
||||
- name: Upload Playwright Test Results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: playwright-test-results
|
||||
path: private/playwright-test-results
|
||||
retention-days: 14
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with: { persist-credentials: false }
|
||||
|
||||
# Tempoary disable until we understand TanStack npm supply-chain compromise
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
# key: node-modules-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
@@ -47,4 +47,4 @@ jobs:
|
||||
run: npm run tsc
|
||||
continue-on-error: true
|
||||
|
||||
- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27
|
||||
- uses: autofix-ci/action@c5b2d67aa2274e7b5a18224e8171550871fc7e4a
|
||||
@@ -14,30 +14,30 @@ jobs:
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with: { persist-credentials: false }
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ secrets.GHCR_USERNAME }}
|
||||
password: ${{ secrets.GHCR_TOKEN }}
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
|
||||
@@ -18,23 +18,23 @@ jobs:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with: { persist-credentials: false }
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Use Node.js 26
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 26
|
||||
|
||||
|
||||
@@ -18,28 +18,28 @@ jobs:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with: { persist-credentials: false }
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
|
||||
- name: Install cross-env
|
||||
run: npm install -g cross-env
|
||||
|
||||
@@ -17,15 +17,15 @@ jobs:
|
||||
node-version: [20]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with: { persist-credentials: false }
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm ci
|
||||
- name: Close incorrect issue
|
||||
run: node extra/close-incorrect-issue.js ${{ secrets.GITHUB_TOKEN }} ${{ github.event.issue.number }} "$ISSUE_USER_LOGIN"
|
||||
run: node extra/close-incorrect-issue.mjs ${{ secrets.GITHUB_TOKEN }} ${{ github.event.issue.number }} "$ISSUE_USER_LOGIN"
|
||||
env:
|
||||
ISSUE_USER_LOGIN: ${{ github.event.issue.user.login }}
|
||||
@@ -27,20 +27,20 @@ jobs:
|
||||
language: ["go", "javascript-typescript"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with: { persist-credentials: false }
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
||||
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
||||
uses: github/codeql-action/autobuild@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
||||
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
zizmor:
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
actions: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with: { persist-credentials: false }
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@e639db99335bc9038abc0e066dfcd72e23d26fb4 # v0.3.0
|
||||
uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Apply label
|
||||
uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3
|
||||
uses: eps1lon/actions-label-merge-conflict@0273be72a0bbd58fcd71d0d6c02c209b50d1e5e1 # v3.1.0
|
||||
with:
|
||||
dirtyLabel: "needs:resolve-merge-conflict"
|
||||
repoToken: "${{ secrets.GITHUB_TOKEN }}"
|
||||
@@ -22,16 +22,20 @@ jobs:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install opencode
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
run: curl -fsSL https://opencode.ai/v2/install | bash
|
||||
|
||||
- uses: $/.github/actions/setup-atlascloud
|
||||
with:
|
||||
api-key: ${{ secrets.ATLASCLOUD_API_KEY }}
|
||||
|
||||
- name: Check for duplicate issues
|
||||
env:
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
ATLASCLOUD_API_KEY: ${{ secrets.ATLASCLOUD_API_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
OPENCODE_PERMISSION: |
|
||||
{
|
||||
@@ -42,7 +46,10 @@ jobs:
|
||||
"webfetch": "deny"
|
||||
}
|
||||
run: |
|
||||
opencode run -m opencode/big-pickle "An issue has been submitted (or a duplicate check was requested on it):
|
||||
mapfile -t models < <(jq -r '.[]' .github/opencode-models.json)
|
||||
|
||||
PROMPT=$(cat <<'PROMPT_EOF'
|
||||
An issue has been submitted (or a duplicate check was requested on it):
|
||||
|
||||
Issue number: ${{ github.event.issue.number }}
|
||||
|
||||
@@ -68,4 +75,18 @@ jobs:
|
||||
|
||||
Feel free to ignore if none of these address your specific case.'
|
||||
|
||||
If no clear duplicates or related issues are found, do not comment."
|
||||
If no clear duplicates or related issues are found, do not comment.
|
||||
PROMPT_EOF
|
||||
)
|
||||
|
||||
for model in "${models[@]}"; do
|
||||
echo "Trying model: $model"
|
||||
if opencode run -m "$model" "$PROMPT"; then
|
||||
echo "Completed with model: $model"
|
||||
exit 0
|
||||
fi
|
||||
echo "::warning::Model $model failed, trying the next fallback model."
|
||||
done
|
||||
|
||||
echo "::error::All fallback models failed."
|
||||
exit 1
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: plbstl/first-contribution@4b2b042fffa26792504a18e49aa9543a87bec077 # v4.1.0
|
||||
- uses: plbstl/first-contribution@2c36bdb58684587f60549a69aaa3ec00b9d5f4fe # v4.3.3
|
||||
with:
|
||||
pr-reactions: rocket
|
||||
pr-opened-msg: >
|
||||
|
||||
@@ -17,16 +17,24 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- branch: master
|
||||
update_branch: npm-update
|
||||
- branch: 2.5.X
|
||||
update_branch: npm-update-2.5.X
|
||||
|
||||
steps:
|
||||
- name: Checkout master branch
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Checkout ${{ matrix.branch }} branch
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: master
|
||||
ref: ${{ matrix.branch }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
@@ -36,12 +44,12 @@ jobs:
|
||||
npm -v
|
||||
rm -f package-lock.json
|
||||
npm install --package-lock-only
|
||||
npm approve-scripts --all
|
||||
npm install-scripts approve --all
|
||||
|
||||
- name: Check if there are changes
|
||||
id: check_changes
|
||||
run: |
|
||||
if git diff --quiet package-lock.json; then
|
||||
if git diff --quiet package.json package-lock.json; then
|
||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
@@ -56,13 +64,13 @@ jobs:
|
||||
- name: Commit changes
|
||||
if: steps.check_changes.outputs.has_changes == 'true'
|
||||
run: |
|
||||
git add package-lock.json
|
||||
git add package.json package-lock.json
|
||||
git commit -m "chore: Update dependencies"
|
||||
|
||||
- name: Force push to npm-update branch
|
||||
- name: Force push to ${{ matrix.update_branch }} branch
|
||||
if: steps.check_changes.outputs.has_changes == 'true'
|
||||
run: |
|
||||
git push -f origin HEAD:npm-update
|
||||
git push -f origin HEAD:${{ matrix.update_branch }}
|
||||
|
||||
- name: Check if PR exists
|
||||
if: steps.check_changes.outputs.has_changes == 'true'
|
||||
@@ -70,7 +78,7 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
PR_EXISTS=$(gh pr list --base master --head npm-update --json number --jq 'length')
|
||||
PR_EXISTS=$(gh pr list --base ${{ matrix.branch }} --head ${{ matrix.update_branch }} --json number --jq 'length')
|
||||
if [ "$PR_EXISTS" -eq "0" ]; then
|
||||
echo "pr_exists=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
@@ -83,7 +91,7 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh pr create \
|
||||
--base master \
|
||||
--head npm-update \
|
||||
--base ${{ matrix.branch }} \
|
||||
--head ${{ matrix.update_branch }} \
|
||||
--title "chore: Update dependencies" \
|
||||
--body ""
|
||||
@@ -19,14 +19,14 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Check PR description
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const pr = context.payload.pull_request;
|
||||
const body = (pr && pr.body) ? pr.body : "";
|
||||
const requiredPhrase = "avoid unnecessary back and forth";
|
||||
|
||||
const exclude = ["UptimeKumaBot", "Copilot", "copilot-swe-agent"];
|
||||
const exclude = ["UptimeKumaBot", "Copilot", "copilot-swe-agent", "dependabot[bot]"];
|
||||
const excludeLower = exclude.map((e) => e.toLowerCase());
|
||||
const author = pr?.user?.login || "";
|
||||
|
||||
@@ -36,6 +36,14 @@ jobs:
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip maintainers/admins (repo owner, org members, and collaborators with write access)
|
||||
const exemptAssociations = ["OWNER", "MEMBER", "COLLABORATOR"];
|
||||
const association = pr?.author_association || "";
|
||||
if (exemptAssociations.includes(association)) {
|
||||
core.info(`PR #${pr.number} opened by maintainer/admin '${author}' (${association}), skipping template check.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip release PRs (created by the release workflow)
|
||||
const isReleasePR = (pr?.head?.ref || "").startsWith("release-");
|
||||
if (isReleasePR) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: prevent-file-change
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: Prevent file change
|
||||
uses: xalvarez/prevent-file-change-action@004d9f17c2e4a7afa037cda5f38dc55a5e9c9c06 # v1.9.1
|
||||
uses: xalvarez/prevent-file-change-action@dbc67c205c9ed02aa39c497b422c24f364c3e8dd # v3.0.1
|
||||
with:
|
||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Regex, /src/lang/*.json is not allowed to be changed, except for /src/lang/en.json
|
||||
|
||||
@@ -19,30 +19,30 @@ jobs:
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with: { persist-credentials: false }
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ secrets.GHCR_USERNAME }}
|
||||
password: ${{ secrets.GHCR_TOKEN }}
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Release version number (e.g., 2.1.0, or 2.1.0-beta.2 for beta)"
|
||||
description: "Release version number (e.g., 2.5.5, or 2.5.5-beta.2 for beta). NOTE: Releasing 3.0.0 is NOT supported yet, this workflow currently releases the 2.5.X branch only."
|
||||
required: true
|
||||
type: string
|
||||
previous_version:
|
||||
@@ -38,13 +38,16 @@ jobs:
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
# TODO: temporarily pinned to 2.5.X because master is 3.0.0 and not ready to be released yet.
|
||||
# Remove this ref (and RELEASE_BASE_BRANCH below) once 3.0.0 is ready.
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: 2.5.X
|
||||
persist-credentials: true
|
||||
fetch-depth: 0 # Fetch all history and tags for update-version
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
@@ -64,6 +67,7 @@ jobs:
|
||||
RELEASE_VERSION: ${{ inputs.version }}
|
||||
RELEASE_IS_BETA: ${{ inputs.is_beta }}
|
||||
RELEASE_PREVIOUS_VERSION: ${{ inputs.previous_version }}
|
||||
RELEASE_BASE_BRANCH: "2.5.X"
|
||||
DRY_RUN: ${{ inputs.dry_run }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
@@ -76,7 +80,7 @@ jobs:
|
||||
outputs:
|
||||
pr_number: ${{ steps.pr.outputs.pr_number }}
|
||||
|
||||
# 2. Squash merge the release PR into master
|
||||
# 2. Squash merge the release PR into the release base branch (2.5.X for now, normally master)
|
||||
# Re-runnable: skips if the PR is already merged
|
||||
merge-pr:
|
||||
needs: prepare
|
||||
@@ -84,12 +88,12 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
@@ -121,18 +125,24 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
# TODO: temporarily pinned to 2.5.X, see the prepare job
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: 2.5.X
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # Fetch all history and tags for changelog generation
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Install opencode
|
||||
run: curl -fsSL https://opencode.ai/install | bash
|
||||
run: curl -fsSL https://opencode.ai/v2/install | bash
|
||||
|
||||
- uses: $/.github/actions/setup-atlascloud
|
||||
with:
|
||||
api-key: ${{ secrets.ATLASCLOUD_API_KEY }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: node extra/release/install-deps.mjs
|
||||
@@ -142,10 +152,11 @@ jobs:
|
||||
RELEASE_VERSION: ${{ inputs.version }}
|
||||
RELEASE_IS_BETA: ${{ inputs.is_beta }}
|
||||
RELEASE_PREVIOUS_VERSION: ${{ inputs.previous_version }}
|
||||
RELEASE_BASE_BRANCH: "2.5.X"
|
||||
DRY_RUN: ${{ inputs.dry_run }}
|
||||
MERGE_SHA: ${{ needs.merge-pr.outputs.merge_sha }}
|
||||
GH_TOKEN: ${{ secrets.RELEASE_PAT }}
|
||||
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
|
||||
ATLASCLOUD_API_KEY: ${{ secrets.ATLASCLOUD_API_KEY }}
|
||||
run: node extra/release/create-draft-release.mjs
|
||||
|
||||
# 4. Build the frontend dist ONCE, upload it to the draft release and share
|
||||
@@ -157,16 +168,16 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
# Checkout the merged master code (merge_sha), not the workflow-dispatch SHA,
|
||||
# Checkout the merged release branch code (merge_sha), not the workflow-dispatch SHA,
|
||||
# so package.json is already bumped to the release version before building.
|
||||
# Otherwise the frontend FRONTEND_VERSION (from npm_package_version) would be stale.
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ needs.merge-pr.outputs.merge_sha || github.sha }}
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
@@ -182,20 +193,20 @@ jobs:
|
||||
run: node extra/release/build-and-upload-assets.mjs
|
||||
|
||||
- name: Upload dist as artifact
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: dist-${{ inputs.version }}
|
||||
path: ./dist
|
||||
retention-days: 90
|
||||
|
||||
- name: Upload dist.tar.gz as artifact
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: dist-tar-${{ inputs.version }}
|
||||
path: ./tmp/dist.tar.gz
|
||||
retention-days: 90
|
||||
|
||||
# 5. Build and push all docker images from the merged master, reusing the dist
|
||||
# 5. Build and push all docker images from the merged release branch, reusing the dist
|
||||
# built by build-and-upload-assets (the frontend is never rebuilt here)
|
||||
# Re-runnable: partial pushes are overwritten safely
|
||||
docker-images:
|
||||
@@ -204,13 +215,13 @@ jobs:
|
||||
timeout-minutes: 120
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ needs.merge-pr.outputs.merge_sha || github.sha }}
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
@@ -218,25 +229,25 @@ jobs:
|
||||
run: node extra/release/install-deps.mjs
|
||||
|
||||
- name: Download dist artifact
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: dist-${{ inputs.version }}
|
||||
path: ./dist
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ secrets.GHCR_USERNAME }}
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
actions: write
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||
- uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0
|
||||
with:
|
||||
stale-issue-message: |-
|
||||
We are clearing up our old `help`-issues and your issue has been open for 60 days with no activity.
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
exempt-issue-labels: "News,discussion,bug,doc,feature-request"
|
||||
exempt-issue-assignees: "louislam"
|
||||
operations-per-run: 200
|
||||
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||
- uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0
|
||||
with:
|
||||
stale-issue-message: |-
|
||||
This issue was marked as `cannot-reproduce` by a maintainer.
|
||||
|
||||
@@ -7,6 +7,7 @@ on:
|
||||
branches:
|
||||
- master
|
||||
- 1.23.X
|
||||
- 3.0.X
|
||||
workflow_dispatch:
|
||||
permissions: {}
|
||||
|
||||
@@ -17,12 +18,12 @@ jobs:
|
||||
contents: read
|
||||
pull-requests: write # enable write permissions for pull request comments
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with: { persist-credentials: false }
|
||||
|
||||
- name: json-yaml-validate
|
||||
id: json-yaml-validate
|
||||
uses: GrantBirki/json-yaml-validate@9bbaa8474e3af4e91f25eda8ac194fdc30564d96 # v4.0.0
|
||||
uses: GrantBirki/json-yaml-validate@9dbc6e6ff950c53200156e8afbc16ef1355f1c8d # v5.0.1
|
||||
with:
|
||||
comment: "true" # enable comment mode
|
||||
exclude_file: ".github/config/exclude.txt" # gitignore style file for exclusions
|
||||
@@ -33,10 +34,10 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with: { persist-credentials: false }
|
||||
- name: Use Node.js 25
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 25
|
||||
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
legacy-peer-deps=true
|
||||
min-release-age=14
|
||||
min-release-age=7
|
||||
min-release-age-exclude[]=redbean-node
|
||||
@@ -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>
|
||||
|
||||
@@ -126,6 +126,8 @@ I will assign requests/issues to the next milestone.
|
||||
|
||||
## ❤️ Sponsors
|
||||
|
||||
[](https://www.atlascloud.ai/?ref=UZCYBN)
|
||||
|
||||
Thank you so much! (GitHub Sponsors will be updated manually. OpenCollective sponsors will be updated automatically, the list will be cached by GitHub though. It may need some time to be updated)
|
||||
|
||||
<img src="https://uptime.kuma.pet/sponsors?v=6" alt="Uptime Kuma Sponsors" />
|
||||
|
||||
@@ -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,83 @@
|
||||
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) {
|
||||
try {
|
||||
await knex.schema.alterTable(table.name, (t) => {
|
||||
t.dropForeign("user_id");
|
||||
});
|
||||
} catch (error) {
|
||||
// Not sure why it only happens on Ubuntu 22.04 + Node.js 26
|
||||
// Error: Can't DROP 'docker_host_user_id_foreign'; check that column/key exists
|
||||
console.error(`Error dropping foreign key for table ${table.name}:`, error);
|
||||
|
||||
const columns = await knex(table.name).columnInfo();
|
||||
console.error(`Schema for ${table.name}:`, JSON.stringify(columns, null, 2));
|
||||
|
||||
// Try to ignore it, because the foreign key not existing, we can just drop user_id
|
||||
// throw error;
|
||||
}
|
||||
|
||||
await knex.schema.alterTable(table.name, (t) => {
|
||||
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");
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* The api key schema from: https://www.better-auth.com/docs/plugins/api-key/reference#schema
|
||||
* Plugin: @better-auth/api-key (modelName: better_auth_apikey)
|
||||
*/
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable("better_auth_apikey", (t) => {
|
||||
t.string("id").primary();
|
||||
t.string("configId").notNullable().defaultTo("default").index();
|
||||
t.string("name");
|
||||
t.string("start");
|
||||
t.string("prefix");
|
||||
t.string("key").notNullable().index();
|
||||
t.string("referenceId")
|
||||
.notNullable()
|
||||
.index()
|
||||
.references("id")
|
||||
.inTable("better_auth_user")
|
||||
.onDelete("CASCADE")
|
||||
.onUpdate("CASCADE");
|
||||
t.integer("refillInterval");
|
||||
t.integer("refillAmount");
|
||||
t.timestamp("lastRefillAt");
|
||||
t.boolean("enabled").defaultTo(true);
|
||||
t.boolean("rateLimitEnabled").defaultTo(true);
|
||||
t.bigInteger("rateLimitTimeWindow");
|
||||
t.integer("rateLimitMax");
|
||||
t.integer("requestCount").defaultTo(0);
|
||||
t.integer("remaining");
|
||||
t.timestamp("lastRequest");
|
||||
t.timestamp("expiresAt");
|
||||
t.timestamp("createdAt").notNullable();
|
||||
t.timestamp("updatedAt").notNullable();
|
||||
t.text("permissions");
|
||||
t.text("metadata");
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.dropTableIfExists("better_auth_apikey");
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
PRAGMA writable_schema = TRUE;
|
||||
|
||||
UPDATE
|
||||
SQLITE_MASTER
|
||||
SET
|
||||
sql = replace(sql,
|
||||
'monitor_id INTEGER NOT NULL',
|
||||
'monitor_id INTEGER NOT NULL REFERENCES [monitor] ([id]) ON DELETE CASCADE ON UPDATE CASCADE'
|
||||
)
|
||||
WHERE
|
||||
name = 'monitor_tls_info'
|
||||
AND type = 'table';
|
||||
|
||||
PRAGMA writable_schema = RESET;
|
||||
|
||||
COMMIT;
|
||||
@@ -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,15 +1,6 @@
|
||||
# Download Apprise deb package
|
||||
FROM node:22-bookworm-slim AS download-apprise
|
||||
WORKDIR /app
|
||||
COPY ./extra/download-apprise.mjs ./download-apprise.mjs
|
||||
RUN apt update && \
|
||||
apt --yes --no-install-recommends install curl && \
|
||||
npm install cheerio semver && \
|
||||
node ./download-apprise.mjs
|
||||
|
||||
# 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!
|
||||
@@ -34,20 +25,24 @@ RUN apt update && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
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
|
||||
# apprise = for notifications (installed from upstream PyPI via uv, since Debian's stable package is too old) (workaround for #4867)
|
||||
# paho-mqtt is installed for the mqtt:// plugin (#4859)
|
||||
# slixmpp is installed for the xmpp:// plugin (#7361)
|
||||
# APPRISE_VERSION is a PEP 440 specifier (uv/pip use PEP 440, not npm semver); ~=2.0.1 accepts >=2.0.1,<2.1.0
|
||||
ARG APPRISE_VERSION="~=2.0.1"
|
||||
RUN apt update && \
|
||||
apt --yes --no-install-recommends install ./apprise.deb python3-paho-mqtt && \
|
||||
apt --yes --no-install-recommends install python3 && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
rm -f apprise.deb && \
|
||||
apt --yes autoremove
|
||||
curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR=/usr/local/bin UV_NO_MODIFY_PATH=1 sh && \
|
||||
UV_TOOL_BIN_DIR=/usr/local/bin UV_TOOL_DIR=/opt/uv-tools uv tool install "apprise${APPRISE_VERSION}" \
|
||||
--with paho-mqtt \
|
||||
--with "slixmpp>=1.16.0" && \
|
||||
apt --yes autoremove && \
|
||||
apprise --version
|
||||
|
||||
# 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 +62,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,4 +1,4 @@
|
||||
const github = require("@actions/github");
|
||||
import * as github from "@actions/github";
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
@@ -37,7 +37,7 @@ const github = require("@actions/github");
|
||||
owner: issue.owner,
|
||||
repo: issue.repo,
|
||||
issue_number: issue.number,
|
||||
body: `@${username}: Hello! :wave:\n\nThis issue is being automatically closed because it does not follow the issue template. Please **DO NOT open blank issues and use our [issue-templates](https://github.com/louislam/uptime-kuma/issues/new/choose) instead**.\nBlank Issues do not contain the context necessary for a good discussions.`,
|
||||
body: `@${username}: Hello! :wave:\n\nThis issue is being automatically closed because it does not follow the issue template. Please **DO NOT open blank issues and use our [issue-templates](https://github.com/louislam/uptime-kuma/issues/new/choose) instead**.\nBlank Issues do not contain the context necessary for a good discussion.`,
|
||||
});
|
||||
|
||||
// Close the issue
|
||||
@@ -51,6 +51,7 @@ const github = require("@actions/github");
|
||||
console.log("Pass!");
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
console.error(e);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
})();
|
||||
@@ -1,9 +1,13 @@
|
||||
require("dotenv").config();
|
||||
const { loadEnvFile } = require("node:process");
|
||||
const { NodeSSH } = require("node-ssh");
|
||||
const readline = require("readline");
|
||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
||||
const prompt = (query) => new Promise((resolve) => rl.question(query, resolve));
|
||||
|
||||
try {
|
||||
loadEnvFile();
|
||||
} catch (_) {}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
console.log("SSH to demo server");
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Build and push (multi arch):
|
||||
# docker buildx build -f Dockerfile.latest-warning --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:latest -t louislam/uptime-kuma:1 --push .
|
||||
|
||||
FROM louislam/uptime-kuma:1.23.17-debian
|
||||
COPY docker-latest-warning.js /app/dist/docker-latest-warning.js
|
||||
RUN rm -f /app/dist/index.html.gz /app/dist/index.html.br \
|
||||
&& sed -i 's#</head>#<script src="/docker-latest-warning.js"></script></head>#' /app/dist/index.html \
|
||||
&& grep -q "docker-latest-warning.js" /app/dist/index.html
|
||||
@@ -0,0 +1,71 @@
|
||||
// Injected into the legacy `latest` / `1` Docker images.
|
||||
(() => {
|
||||
const isSetupPage = () => location.pathname === "/setup" || location.pathname.startsWith("/setup/");
|
||||
|
||||
let acknowledged = false;
|
||||
let overlay = null;
|
||||
let styleAdded = false;
|
||||
|
||||
const show = () => {
|
||||
if (acknowledged || overlay || !isSetupPage()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!styleAdded) {
|
||||
const style = document.createElement("style");
|
||||
style.textContent = ".uk-latest-warning { position: fixed; inset: 0; z-index: 2147483647; }";
|
||||
document.documentElement.appendChild(style);
|
||||
styleAdded = true;
|
||||
}
|
||||
|
||||
overlay = document.createElement("div");
|
||||
overlay.className =
|
||||
"uk-latest-warning d-flex align-items-center justify-content-center p-3 bg-dark text-white overflow-auto";
|
||||
overlay.innerHTML = `
|
||||
<div style="max-width: 640px;">
|
||||
<h3 class="mb-3">⚠️ This Uptime Kuma version is outdated!</h3>
|
||||
<p>
|
||||
Current Version: 1.23.17 (Outdated!)
|
||||
</p>
|
||||
<p>
|
||||
You are running the <code>latest</code> tag, which still points to Uptime Kuma v1.
|
||||
It is NO LONGER maintained and does not receive any bug or security fixes.
|
||||
</p>
|
||||
<p>
|
||||
Please switch to a recommended tag, to get the latest version and security updates.
|
||||
</p>
|
||||
<p class="mb-4">
|
||||
Read more:
|
||||
<a class="link-light" href="https://github.com/louislam/uptime-kuma/wiki/Docker-Tags" target="_blank" rel="noopener noreferrer">Recommended Docker Tags</a>
|
||||
</p>
|
||||
<button type="button" class="btn btn-danger">Ignore and continue with the old version</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
overlay.querySelector("button").addEventListener("click", () => {
|
||||
acknowledged = true;
|
||||
overlay.remove();
|
||||
overlay = null;
|
||||
});
|
||||
|
||||
document.documentElement.appendChild(overlay);
|
||||
};
|
||||
|
||||
// The app redirects to /setup with the client-side router, so watch for route changes.
|
||||
for (const method of ["pushState", "replaceState"]) {
|
||||
const original = history[method];
|
||||
history[method] = function () {
|
||||
const result = original.apply(history, arguments);
|
||||
show();
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
window.addEventListener("popstate", show);
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", show);
|
||||
} else {
|
||||
show();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "docker-latest-warning",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "docker build -f Dockerfile.latest-warning -t louislam/uptime-kuma:latest-warning-test .",
|
||||
"multi-build-and-push": "docker buildx build -f Dockerfile.latest-warning --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/uptime-kuma:latest -t louislam/uptime-kuma:1 --push ."
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
// Go to http://ftp.debian.org/debian/pool/main/a/apprise/ using fetch api, where it is a apache directory listing page
|
||||
// Use cheerio to parse the html and get the latest version of Apprise
|
||||
// call curl to download the latest version of Apprise
|
||||
// Target file: the latest version of Apprise, which the format is apprise_{VERSION}_all.deb
|
||||
|
||||
import * as cheerio from "cheerio";
|
||||
import semver from "semver";
|
||||
import * as childProcess from "child_process";
|
||||
|
||||
const baseURL = "http://ftp.debian.org/debian/pool/main/a/apprise/";
|
||||
const response = await fetch(baseURL);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch page of Apprise Debian repository.");
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
// Get all the links in the page
|
||||
const linkElements = $("a");
|
||||
|
||||
// Filter the links which match apprise_{VERSION}_all.deb
|
||||
const links = [];
|
||||
const pattern = /apprise_(.*?)_all.deb/;
|
||||
|
||||
for (let i = 0; i < linkElements.length; i++) {
|
||||
const link = linkElements[i];
|
||||
if (link.attribs.href.match(pattern) && !link.attribs.href.includes("~")) {
|
||||
links.push({
|
||||
filename: link.attribs.href,
|
||||
version: link.attribs.href.match(pattern)[1],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(links);
|
||||
|
||||
// semver compare and download
|
||||
let latestLink = {
|
||||
filename: "",
|
||||
version: "0.0.0",
|
||||
};
|
||||
|
||||
for (const link of links) {
|
||||
if (semver.gt(link.version, latestLink.version)) {
|
||||
latestLink = link;
|
||||
}
|
||||
}
|
||||
|
||||
const downloadURL = baseURL + latestLink.filename;
|
||||
console.log(`Downloading ${downloadURL}...`);
|
||||
let result = childProcess.spawnSync("curl", [ downloadURL, "--output", "apprise.deb" ]);
|
||||
console.log(result.stdout?.toString());
|
||||
console.error(result.stderr?.toString());
|
||||
process.exit(result.status !== null ? result.status : 1);
|
||||
@@ -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();
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Regenerate test/fixtures/v1-kuma.db.gz, a pristine Uptime Kuma 1.23.17 SQLite database.
|
||||
*
|
||||
* The migration test (test/backend-test/test-migration-v1-to-v3.ts) upgrades this real
|
||||
* 1.23.17 database straight to v3 to make sure the v1 -> v3 upgrade path keeps working.
|
||||
*
|
||||
* It is generated by booting the official 1.23.17 image once with an empty data dir,
|
||||
* letting it create and patch the database, then checkpointing the WAL so the file is standalone.
|
||||
*
|
||||
* Usage:
|
||||
* node extra/prepare-v1-test-db.mjs
|
||||
*
|
||||
* Requires Docker. Only run this when the v1 schema actually changed.
|
||||
*/
|
||||
import { execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import zlib from "node:zlib";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import BetterSqlite3 from "better-sqlite3";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.join(__dirname, "..");
|
||||
const fixturePath = path.join(repoRoot, "test/fixtures/v1-kuma.db.gz");
|
||||
const image = "louislam/uptime-kuma:1.23.17";
|
||||
const containerName = "uptime-kuma-v1-fixture";
|
||||
const adminPassword = "Kuma-V1-Test-Password";
|
||||
|
||||
/**
|
||||
* Run a command and inherit stdio.
|
||||
* @param {string} command Command to run
|
||||
* @param {string[]} args Arguments
|
||||
* @returns {void}
|
||||
*/
|
||||
function run(command, args) {
|
||||
console.log(`$ ${command} ${args.join(" ")}`);
|
||||
execFileSync(command, args, { stdio: "inherit" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy a file if it exists.
|
||||
* @param {string} from Source
|
||||
* @param {string} to Destination
|
||||
* @returns {void}
|
||||
*/
|
||||
function copyIfExists(from, to) {
|
||||
if (fs.existsSync(from)) {
|
||||
fs.copyFileSync(from, to);
|
||||
}
|
||||
}
|
||||
|
||||
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "uk-v1-fixture-"));
|
||||
const workDir = fs.mkdtempSync(path.join(os.tmpdir(), "uk-v1-work-"));
|
||||
|
||||
try {
|
||||
run("docker", ["rm", "-f", containerName]);
|
||||
run("docker", [
|
||||
"run",
|
||||
"-d",
|
||||
"--name",
|
||||
containerName,
|
||||
"-e",
|
||||
"UPTIME_KUMA_DB_TYPE=sqlite",
|
||||
"-v",
|
||||
`${dataDir}:/app/data`,
|
||||
image,
|
||||
]);
|
||||
|
||||
// Wait until the container finished patching the database and started the server
|
||||
const dbPath = path.join(dataDir, "kuma.db");
|
||||
const deadline = Date.now() + 120_000;
|
||||
while (Date.now() < deadline) {
|
||||
let logs = "";
|
||||
try {
|
||||
logs = execFileSync("docker", ["logs", containerName], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
||||
} catch (e) {
|
||||
logs = "";
|
||||
}
|
||||
if (logs.includes("Database Patched Successfully") && fs.existsSync(dbPath) && fs.statSync(dbPath).size > 0) {
|
||||
break;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
if (!fs.existsSync(dbPath)) {
|
||||
throw new Error("Timed out waiting for the v1 database to be created");
|
||||
}
|
||||
|
||||
run("docker", ["stop", containerName]);
|
||||
|
||||
// Like v2, the image keeps the schema in the WAL file, so copy all three files and checkpoint
|
||||
copyIfExists(dbPath, path.join(workDir, "kuma.db"));
|
||||
copyIfExists(`${dbPath}-wal`, path.join(workDir, "kuma.db-wal"));
|
||||
copyIfExists(`${dbPath}-shm`, path.join(workDir, "kuma.db-shm"));
|
||||
|
||||
const db = new BetterSqlite3(path.join(workDir, "kuma.db"));
|
||||
db.pragma("wal_checkpoint(TRUNCATE)");
|
||||
|
||||
// Seed a realistic v1 dataset (an admin with one of each owned resource), so the fixture
|
||||
// can be used to boot the server directly. v1 has no remote_browser table yet; the v3
|
||||
// migrations create it during the upgrade.
|
||||
const now = new Date().toISOString().slice(0, 19).replace("T", " ");
|
||||
const adminPasswordHash = await bcrypt.hash(adminPassword, 10);
|
||||
db.prepare("INSERT INTO user (username, password, active, timezone, twofa_status) VALUES (?, ?, 1, 'UTC', 0)").run(
|
||||
"admin",
|
||||
adminPasswordHash
|
||||
);
|
||||
db.prepare(
|
||||
"INSERT INTO monitor (name, active, user_id, interval, type, url, method, maxretries, maxredirects, accepted_statuscodes_json, retry_interval, created_date) VALUES ('Migrated Monitor', 0, 1, 60, 'http', 'https://example.com', 'GET', 0, 10, '[\"200-299\"]', 60, ?)"
|
||||
).run(now);
|
||||
db.prepare("INSERT INTO notification (name, active, user_id, is_default, config) VALUES ('Notif 1', 1, 1, 0, '{}')").run();
|
||||
db.prepare(
|
||||
"INSERT INTO maintenance (title, description, user_id, active, strategy, weekdays, days_of_month) VALUES ('Maintenance 1', '', 1, 1, 'manual', '[]', '[]')"
|
||||
).run();
|
||||
db.prepare(
|
||||
"INSERT INTO proxy (user_id, protocol, host, port, auth, active, `default`, created_date) VALUES (1, 'http', '127.0.0.1', 8080, 0, 1, 0, ?)"
|
||||
).run(now);
|
||||
db.prepare("INSERT INTO docker_host (user_id, docker_daemon, docker_type, name) VALUES (1, 'tcp://127.0.0.1:2375', 'socket', 'Docker 1')").run();
|
||||
db.prepare("INSERT INTO api_key (key, name, user_id, created_date, active) VALUES (?, 'API Key 1', 1, ?, 1)").run(
|
||||
await bcrypt.hash("test-api-key", 10),
|
||||
now
|
||||
);
|
||||
|
||||
// Keep the fixture compact and in rollback-journal mode so simply opening it does not create
|
||||
// -wal/-shm sidecar files. Set VACUUM for a compact file, then it is gzipped before being saved.
|
||||
db.pragma("journal_mode = DELETE");
|
||||
db.exec("VACUUM");
|
||||
db.close();
|
||||
|
||||
fs.mkdirSync(path.dirname(fixturePath), { recursive: true });
|
||||
fs.writeFileSync(fixturePath, zlib.gzipSync(fs.readFileSync(path.join(workDir, "kuma.db")), { level: 9 }));
|
||||
console.log(`Wrote ${fixturePath}`);
|
||||
} finally {
|
||||
try {
|
||||
run("docker", ["rm", "-f", containerName]);
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
fs.rmSync(dataDir, { recursive: true, force: true });
|
||||
fs.rmSync(workDir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Regenerate test/fixtures/v2-kuma.db, a pristine Uptime Kuma 2.5.3 SQLite database.
|
||||
*
|
||||
* The migration test (test/backend-test/test-migration-v2-to-v3.ts) upgrades this real
|
||||
* 2.5.3 database to v3 to make sure the upgrade path keeps working (see #7944).
|
||||
*
|
||||
* It is generated by booting the official 2.5.3 slim image once with an empty data dir,
|
||||
* letting it create the database, then checkpointing the WAL so the file is standalone.
|
||||
*
|
||||
* Usage:
|
||||
* node extra/prepare-v2-test-db.mjs
|
||||
*
|
||||
* Requires Docker. Only run this when the v2 schema actually changed.
|
||||
*/
|
||||
import { execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import zlib from "node:zlib";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import BetterSqlite3 from "better-sqlite3";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.join(__dirname, "..");
|
||||
const fixturePath = path.join(repoRoot, "test/fixtures/v2-kuma.db.gz");
|
||||
const image = "louislam/uptime-kuma:2.5.3-slim";
|
||||
const containerName = "uptime-kuma-v2-fixture";
|
||||
const adminPassword = "Kuma-V2-Test-Password";
|
||||
|
||||
/**
|
||||
* Run a command and inherit stdio.
|
||||
* @param {string} command Command to run
|
||||
* @param {string[]} args Arguments
|
||||
* @returns {void}
|
||||
*/
|
||||
function run(command, args) {
|
||||
console.log(`$ ${command} ${args.join(" ")}`);
|
||||
execFileSync(command, args, { stdio: "inherit" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy a file if it exists.
|
||||
* @param {string} from Source
|
||||
* @param {string} to Destination
|
||||
* @returns {void}
|
||||
*/
|
||||
function copyIfExists(from, to) {
|
||||
if (fs.existsSync(from)) {
|
||||
fs.copyFileSync(from, to);
|
||||
}
|
||||
}
|
||||
|
||||
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "uk-v2-fixture-"));
|
||||
const workDir = fs.mkdtempSync(path.join(os.tmpdir(), "uk-v2-work-"));
|
||||
|
||||
try {
|
||||
run("docker", ["rm", "-f", containerName]);
|
||||
run("docker", [
|
||||
"run",
|
||||
"-d",
|
||||
"--name",
|
||||
containerName,
|
||||
"-e",
|
||||
"UPTIME_KUMA_DB_TYPE=sqlite",
|
||||
"-v",
|
||||
`${dataDir}:/app/data`,
|
||||
image,
|
||||
]);
|
||||
|
||||
// Wait until the container created and migrated the database
|
||||
const dbPath = path.join(dataDir, "kuma.db");
|
||||
const deadline = Date.now() + 120_000;
|
||||
while (Date.now() < deadline) {
|
||||
if (fs.existsSync(dbPath) && fs.statSync(dbPath).size > 0) {
|
||||
const walPath = `${dbPath}-wal`;
|
||||
if (fs.existsSync(walPath) && fs.statSync(walPath).size > 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
if (!fs.existsSync(dbPath)) {
|
||||
throw new Error("Timed out waiting for the v2 database to be created");
|
||||
}
|
||||
|
||||
run("docker", ["stop", containerName]);
|
||||
|
||||
// The 2.5.3 image keeps the schema in the WAL file, so copy all three files and checkpoint
|
||||
copyIfExists(dbPath, path.join(workDir, "kuma.db"));
|
||||
copyIfExists(`${dbPath}-wal`, path.join(workDir, "kuma.db-wal"));
|
||||
copyIfExists(`${dbPath}-shm`, path.join(workDir, "kuma.db-shm"));
|
||||
|
||||
const db = new BetterSqlite3(path.join(workDir, "kuma.db"));
|
||||
db.pragma("wal_checkpoint(TRUNCATE)");
|
||||
|
||||
// Seed a realistic 2.5.3 dataset (an admin with one of each owned resource), so the fixture
|
||||
// can be used to boot the server directly.
|
||||
const now = new Date().toISOString().slice(0, 19).replace("T", " ");
|
||||
const adminPasswordHash = await bcrypt.hash(adminPassword, 10);
|
||||
db.prepare("INSERT INTO user (username, password, active, timezone, twofa_status) VALUES (?, ?, 1, 'UTC', 0)").run(
|
||||
"admin",
|
||||
adminPasswordHash
|
||||
);
|
||||
db.prepare(
|
||||
"INSERT INTO monitor (name, active, user_id, interval, type, url, method, maxretries, maxredirects, accepted_statuscodes_json, retry_interval, created_date) VALUES ('Migrated Monitor', 0, 1, 60, 'http', 'https://example.com', 'GET', 0, 10, '[\"200-299\"]', 60, ?)"
|
||||
).run(now);
|
||||
db.prepare("INSERT INTO notification (name, active, user_id, is_default, config) VALUES ('Notif 1', 1, 1, 0, '{}')").run();
|
||||
db.prepare(
|
||||
"INSERT INTO maintenance (title, description, user_id, active, strategy, weekdays, days_of_month) VALUES ('Maintenance 1', '', 1, 1, 'manual', '[]', '[]')"
|
||||
).run();
|
||||
db.prepare(
|
||||
"INSERT INTO proxy (user_id, protocol, host, port, auth, active, `default`, created_date) VALUES (1, 'http', '127.0.0.1', 8080, 0, 1, 0, ?)"
|
||||
).run(now);
|
||||
db.prepare("INSERT INTO docker_host (user_id, docker_daemon, docker_type, name) VALUES (1, 'tcp://127.0.0.1:2375', 'socket', 'Docker 1')").run();
|
||||
db.prepare("INSERT INTO remote_browser (name, url, user_id) VALUES ('Browser 1', 'http://127.0.0.1:9222', 1)").run();
|
||||
db.prepare("INSERT INTO api_key (key, name, user_id, created_date, active) VALUES (?, 'API Key 1', 1, ?, 1)").run(
|
||||
await bcrypt.hash("test-api-key", 10),
|
||||
now
|
||||
);
|
||||
|
||||
// Keep the fixture compact and in rollback-journal mode so simply opening it does not create
|
||||
// -wal/-shm sidecar files. Set VACUUM for a compact file, then it is gzipped before being saved.
|
||||
db.pragma("journal_mode = DELETE");
|
||||
db.exec("VACUUM");
|
||||
db.close();
|
||||
|
||||
fs.mkdirSync(path.dirname(fixturePath), { recursive: true });
|
||||
fs.writeFileSync(fixturePath, zlib.gzipSync(fs.readFileSync(path.join(workDir, "kuma.db")), { level: 9 }));
|
||||
console.log(`Wrote ${fixturePath}`);
|
||||
} finally {
|
||||
try {
|
||||
run("docker", ["rm", "-f", containerName]);
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
fs.rmSync(dataDir, { recursive: true, force: true });
|
||||
fs.rmSync(workDir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
// GitHub CLI (gh command) is required
|
||||
|
||||
import * as childProcess from "child_process";
|
||||
import * as fs from "fs";
|
||||
|
||||
const ignoreList = [
|
||||
"louislam",
|
||||
@@ -188,7 +189,8 @@ export async function generateChangelog(previousVersion, categorizedMap) {
|
||||
|
||||
/**
|
||||
* Generate Changelog using AI
|
||||
* The LLM API can be flaky, so it retries a few times before falling back to uncategorized.
|
||||
* The LLM API can be flaky, so it tries a list of models in order, falling back to the next model
|
||||
* on failure before falling back to uncategorized.
|
||||
* @param {string} previousVersion Previous Version Tag
|
||||
* @returns {Promise<string>} Changelog Content
|
||||
*/
|
||||
@@ -201,24 +203,26 @@ export async function generateChangelogAI(previousVersion) {
|
||||
|
||||
console.log(llmPrompt);
|
||||
|
||||
const maxAttempts = 3;
|
||||
const retryDelays = [15000, 45000];
|
||||
// Try each model in order (see .github/opencode-models.json), falling back to the next one on failure.
|
||||
const models = JSON.parse(
|
||||
await fs.promises.readFile(new URL("../../.github/opencode-models.json", import.meta.url), "utf8")
|
||||
);
|
||||
const retryDelayMs = 15000;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts && !categorizedMap; attempt++) {
|
||||
console.log(`Running opencode with the above prompt... (attempt ${attempt}/${maxAttempts})`);
|
||||
for (const model of models) {
|
||||
if (categorizedMap) {
|
||||
break;
|
||||
}
|
||||
console.log(`Running opencode with the above prompt... (model: ${model})`);
|
||||
|
||||
try {
|
||||
const result = childProcess.spawnSync(
|
||||
"opencode",
|
||||
["run", "-m", "opencode/big-pickle", "--format", "json", llmPrompt],
|
||||
{
|
||||
encoding: "utf-8",
|
||||
timeout: 300000,
|
||||
shell: true,
|
||||
cwd: process.cwd(),
|
||||
env: process.env,
|
||||
}
|
||||
);
|
||||
const result = childProcess.spawnSync("opencode", ["run", "-m", model, "--format", "json", llmPrompt], {
|
||||
encoding: "utf-8",
|
||||
timeout: 300000,
|
||||
shell: true,
|
||||
cwd: process.cwd(),
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
if (result.status === 0 && result.stdout) {
|
||||
// Parse NDJSON output: find "type":"text" line
|
||||
@@ -252,16 +256,17 @@ export async function generateChangelogAI(previousVersion) {
|
||||
console.warn("Failed to run opencode:", e.message);
|
||||
}
|
||||
|
||||
if (!categorizedMap && attempt < maxAttempts) {
|
||||
const delayMs = retryDelays[attempt - 1];
|
||||
console.warn(`Attempt ${attempt} failed. Retrying in ${delayMs / 1000}s...`);
|
||||
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||
if (!categorizedMap) {
|
||||
console.warn(`Model "${model}" failed. Trying next fallback in ${retryDelayMs / 1000}s...`);
|
||||
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
||||
}
|
||||
}
|
||||
|
||||
if (!categorizedMap) {
|
||||
categorizedMap = {};
|
||||
console.warn(`OpenCode unavailable after ${maxAttempts} attempts, using uncategorized fallback.`);
|
||||
console.warn(
|
||||
`OpenCode unavailable on all fallback models (${models.join(", ")}), using uncategorized fallback.`
|
||||
);
|
||||
}
|
||||
|
||||
return await generateChangelog(previousVersion, categorizedMap);
|
||||
|
||||
+16
-10
@@ -1,9 +1,13 @@
|
||||
import "dotenv/config";
|
||||
import { loadEnvFile } from "node:process";
|
||||
import * as childProcess from "child_process";
|
||||
import semver from "semver";
|
||||
import fs from "fs";
|
||||
import tar from "tar";
|
||||
|
||||
try {
|
||||
loadEnvFile();
|
||||
} catch (_) {}
|
||||
|
||||
// Support both the legacy RELEASE_DRY_RUN=1 format and DRY_RUN=true used by GitHub Actions workflows
|
||||
export const dryRun = process.env.RELEASE_DRY_RUN === "1" || process.env.DRY_RUN === "true";
|
||||
|
||||
@@ -121,7 +125,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];
|
||||
|
||||
@@ -386,13 +390,15 @@ export async function uploadReleaseAssets(version, files) {
|
||||
* @returns {void}
|
||||
*/
|
||||
export function buildAllImages(repoNames, version, isBeta) {
|
||||
const majorVersion = semver.parse(version).major;
|
||||
|
||||
if (isBeta) {
|
||||
// Build slim image (rootless)
|
||||
buildImage(
|
||||
repoNames,
|
||||
["beta-slim-rootless", ver(version, "slim-rootless")],
|
||||
"rootless",
|
||||
"BASE_IMAGE=louislam/uptime-kuma:base2-slim"
|
||||
`BASE_IMAGE=louislam/uptime-kuma:base${majorVersion}-slim`
|
||||
);
|
||||
|
||||
// Build full image (rootless)
|
||||
@@ -403,7 +409,7 @@ export function buildAllImages(repoNames, version, isBeta) {
|
||||
repoNames,
|
||||
["beta-slim", ver(version, "slim")],
|
||||
"release",
|
||||
"BASE_IMAGE=louislam/uptime-kuma:base2-slim"
|
||||
`BASE_IMAGE=louislam/uptime-kuma:base${majorVersion}-slim`
|
||||
);
|
||||
|
||||
// Build full image
|
||||
@@ -412,24 +418,24 @@ export function buildAllImages(repoNames, version, isBeta) {
|
||||
// Build slim image (rootless)
|
||||
buildImage(
|
||||
repoNames,
|
||||
["2-slim-rootless", ver(version, "slim-rootless")],
|
||||
[`next-slim-rootless`, `${majorVersion}-slim-rootless`, ver(version, "slim-rootless")],
|
||||
"rootless",
|
||||
"BASE_IMAGE=louislam/uptime-kuma:base2-slim"
|
||||
`BASE_IMAGE=louislam/uptime-kuma:base${majorVersion}-slim`
|
||||
);
|
||||
|
||||
// Build full image (rootless)
|
||||
buildImage(repoNames, ["next-rootless", "2-rootless", ver(version, "rootless")], "rootless");
|
||||
buildImage(repoNames, ["next-rootless", `${majorVersion}-rootless`, ver(version, "rootless")], "rootless");
|
||||
|
||||
// Build slim image
|
||||
buildImage(
|
||||
repoNames,
|
||||
["next-slim", "2-slim", ver(version, "slim")],
|
||||
["next-slim", `${majorVersion}-slim`, ver(version, "slim")],
|
||||
"release",
|
||||
"BASE_IMAGE=louislam/uptime-kuma:base2-slim"
|
||||
`BASE_IMAGE=louislam/uptime-kuma:base${majorVersion}-slim`
|
||||
);
|
||||
|
||||
// Build full image
|
||||
buildImage(repoNames, ["next", "2", version], "release");
|
||||
buildImage(repoNames, ["next", `${majorVersion}`, version], "release");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ checkDocker();
|
||||
buildDist();
|
||||
|
||||
// Build full image (rootless)
|
||||
buildImage(repoNames, [ "nightly2-rootless" ], "nightly-rootless");
|
||||
buildImage(repoNames, ["nightly3-rootless", "nightly-rootless"], "nightly-rootless");
|
||||
|
||||
// Build full image
|
||||
buildImage(repoNames, [ "nightly2" ], "nightly");
|
||||
buildImage(repoNames, ["nightly3", "nightly"], "nightly");
|
||||
+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
|
||||
@@ -1,155 +0,0 @@
|
||||
console.log("== Uptime Kuma Reset Password Tool ==");
|
||||
|
||||
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");
|
||||
const args = require("args-parser")(process.argv);
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
const main = async () => {
|
||||
if ("dry-run" in args) {
|
||||
console.log("Dry run mode, no changes will be made.");
|
||||
}
|
||||
|
||||
console.log("Connecting the database");
|
||||
|
||||
try {
|
||||
Database.initDataDir(args);
|
||||
await Database.connect(false, false, true);
|
||||
// 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);
|
||||
|
||||
while (true) {
|
||||
let password;
|
||||
let confirmPassword;
|
||||
|
||||
// When called with "--new-password" argument for unattended modification (e.g. npm run reset-password -- --new_password=secret)
|
||||
if ("new-password" in args) {
|
||||
console.log("Using password from argument");
|
||||
console.warn(
|
||||
"\x1b[31m%s\x1b[0m",
|
||||
"Warning: the password might be stored, in plain text, in your shell's history"
|
||||
);
|
||||
password = confirmPassword = args["new-password"] + "";
|
||||
if (passwordStrength(password).value === "Too weak") {
|
||||
throw new Error("Password is too weak, please use a stronger password.");
|
||||
}
|
||||
} else {
|
||||
password = await question("New Password: ");
|
||||
if (passwordStrength(password).value === "Too weak") {
|
||||
console.log("Password is too weak, please try again.");
|
||||
continue;
|
||||
}
|
||||
confirmPassword = await question("Confirm New Password: ");
|
||||
}
|
||||
|
||||
if (password === confirmPassword) {
|
||||
if (!("dry-run" in args)) {
|
||||
await User.resetPassword(user.id, password);
|
||||
|
||||
// Reset all sessions by reset jwt secret
|
||||
await initJWTSecret();
|
||||
|
||||
// Disconnect all other socket clients of the user
|
||||
await disconnectAllSocketClients(user.username, password);
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
console.log("Passwords do not match, please try again.");
|
||||
}
|
||||
}
|
||||
console.log("Password reset 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect all socket clients of the user
|
||||
* @param {string} username Username
|
||||
* @param {string} password Password
|
||||
* @returns {Promise<void>} Promise
|
||||
*/
|
||||
function disconnectAllSocketClients(username, password) {
|
||||
return new Promise((resolve) => {
|
||||
console.log("Connecting to " + localWebSocketURL + " to disconnect all other socket clients");
|
||||
|
||||
// Disconnect all socket connections
|
||||
const socket = io(localWebSocketURL, {
|
||||
reconnection: false,
|
||||
timeout: 5000,
|
||||
});
|
||||
socket.on("connect", () => {
|
||||
socket.emit(
|
||||
"login",
|
||||
{
|
||||
username,
|
||||
password,
|
||||
},
|
||||
(res) => {
|
||||
if (res.ok) {
|
||||
console.log("Logged in.");
|
||||
socket.emit("disconnectOtherSocketClients");
|
||||
} else {
|
||||
console.warn("Login failed.");
|
||||
console.warn("Please restart the server to disconnect all sessions.");
|
||||
}
|
||||
socket.close();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
socket.on("connect_error", function () {
|
||||
// The localWebSocketURL is not guaranteed to be working for some complicated Uptime Kuma setup
|
||||
// Ask the user to restart the server manually
|
||||
console.warn("Failed to connect to " + localWebSocketURL);
|
||||
console.warn("Please restart the server to disconnect all sessions manually.");
|
||||
resolve();
|
||||
});
|
||||
socket.on("disconnect", () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!process.env.TEST_BACKEND) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
main,
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
console.log("== Uptime Kuma Reset Password Tool ==");
|
||||
|
||||
import { loadEnvFile } from "node:process";
|
||||
import { auth } from "../server/better-auth";
|
||||
import { password as passwordInput, select } from "@inquirer/prompts";
|
||||
import { ExitPromptError } from "@inquirer/core";
|
||||
import { isDevEnv } from "../src/util";
|
||||
import { hashPassword } from "better-auth/crypto";
|
||||
|
||||
// @ts-ignore
|
||||
import Database from "../server/database.js";
|
||||
|
||||
// @ts-ignore No type package is available
|
||||
import parseArgs from "args-parser";
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv);
|
||||
try {
|
||||
loadEnvFile();
|
||||
} catch { }
|
||||
|
||||
console.log("Dev Environment:", isDevEnv());
|
||||
|
||||
Database.initDataDir(args);
|
||||
await Database.connect();
|
||||
|
||||
const context = await auth().$context;
|
||||
const internalAdapter = context.internalAdapter;
|
||||
|
||||
const users = await internalAdapter.listUsers();
|
||||
const choices = users.map((user) => {
|
||||
const userId = user.id;
|
||||
const username = user.name || user.email;
|
||||
|
||||
return {
|
||||
name: username,
|
||||
value: userId,
|
||||
description: "",
|
||||
};
|
||||
});
|
||||
|
||||
let userId: string;
|
||||
|
||||
while (true) {
|
||||
const selectedUserId = await select({
|
||||
message: "Select a username:",
|
||||
choices,
|
||||
});
|
||||
|
||||
if (!selectedUserId) {
|
||||
console.log("Invalid choice");
|
||||
continue;
|
||||
}
|
||||
|
||||
userId = selectedUserId;
|
||||
break;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const password = await passwordInput({
|
||||
message: "Enter new password:",
|
||||
mask: "*",
|
||||
});
|
||||
|
||||
try {
|
||||
const hash = await hashPassword(password); // We do that because the method under doesn't hash the password (it stores it in plain text)
|
||||
await internalAdapter.updatePassword(userId, hash);
|
||||
|
||||
console.log("Password reset succesfully!");
|
||||
break;
|
||||
} catch (_) {
|
||||
// Better Auth shows Cli error message in cli already, we don't need to
|
||||
}
|
||||
}
|
||||
|
||||
await cleanUp();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
async function cleanUp() {
|
||||
await Database.close();
|
||||
}
|
||||
|
||||
// Gracefully handle Ctrl+C
|
||||
process.on("SIGINT", async () => {
|
||||
console.log("\nGracefully shutting down from SIGINT (Ctrl+C)");
|
||||
await cleanUp();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
try {
|
||||
await main();
|
||||
} catch (e) {
|
||||
// Ignore prompt exit errors
|
||||
if (!(e instanceof ExitPromptError)) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { rmSync } from "fs";
|
||||
import { spawn } from "child_process";
|
||||
|
||||
const dataDir = "./data/playwright-test";
|
||||
const port = "3001";
|
||||
|
||||
console.log(`Cleaning up ${dataDir}...`);
|
||||
rmSync(dataDir, { recursive: true, force: true });
|
||||
|
||||
console.log(`Starting server on port ${port}...`);
|
||||
const proc = spawn("node", ["--import=tsx", "server/server.js", `--port=${port}`, `--data-dir=${dataDir}`], {
|
||||
stdio: "inherit",
|
||||
env: { ...process.env, NODE_ENV: "development" },
|
||||
});
|
||||
|
||||
proc.on("exit", (code) => {
|
||||
process.exit(code);
|
||||
});
|
||||
Generated
+3582
-3081
File diff suppressed because it is too large.
Load diff
+80
-74
@@ -1,54 +1,52 @@
|
||||
{
|
||||
"name": "uptime-kuma",
|
||||
"version": "2.5.6",
|
||||
"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\"",
|
||||
"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",
|
||||
"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-e2e": "playwright test --config ./config/playwright.config.js",
|
||||
"test-e2e-ui": "playwright test --config ./config/playwright.config.js --ui --ui-port=51063",
|
||||
"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",
|
||||
"start": "node --run start-server",
|
||||
"start-server": "node --import=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",
|
||||
"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/**/*.{ts,js}\"",
|
||||
"test-e2e": "playwright test",
|
||||
"test-e2e-local": "cross-env SKIP_TESTCONTAINER=1 npm run build && npm run test-e2e",
|
||||
"test-e2e-ui": "playwright test --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",
|
||||
"vite-preview-dist": "vite preview --host",
|
||||
"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.5.6 && npm ci --omit dev --no-audit && npm run download-dist",
|
||||
"setup": "git checkout 2.5.5 && npm ci --omit dev --no-audit && npm 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": "node --import=tsx extra/reset-password.mts",
|
||||
"remove-2fa": "node extra/remove-2fa.js",
|
||||
"start-clear-server": "node extra/start-clear-server.mjs",
|
||||
"simple-dns-server": "node extra/simple-dns-server.js",
|
||||
"simple-mqtt-server": "node extra/simple-mqtt-server.js",
|
||||
"simple-mongo": "docker run --rm -p 27017:27017 mongo",
|
||||
@@ -58,38 +56,38 @@
|
||||
"release-beta": "node ./extra/release/release.mjs --beta",
|
||||
"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/release/generate-changelog.mjs",
|
||||
"create-release": "node ./extra/release/create-release.mjs"
|
||||
"create-release": "node ./extra/release/create-release.mjs",
|
||||
"audit": "npm audit --omit=dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"@better-auth/api-key": "1.6.11",
|
||||
"@grpc/grpc-js": "~1.8.22",
|
||||
"@inquirer/core": "~11.2.1",
|
||||
"@inquirer/prompts": "~8.5.2",
|
||||
"@louislam/ping": "~0.4.4-mod.1",
|
||||
"@louislam/sqlite3": "15.1.6",
|
||||
"@vvo/tzdb": "~6.198.0",
|
||||
"args-parser": "~1.3.0",
|
||||
"axios": "~0.32.0",
|
||||
"axios": "~0.33.0",
|
||||
"badge-maker": "~3.3.1",
|
||||
"bcryptjs": "~2.4.3",
|
||||
"better-auth": "~1.6.11",
|
||||
"better-sqlite3": "~12.11.1",
|
||||
"chardet": "~1.4.0",
|
||||
"check-password-strength": "~2.0.10",
|
||||
"cheerio": "~1.0.0",
|
||||
"chroma-js": "~2.4.2",
|
||||
"command-exists": "~1.2.9",
|
||||
"compare-versions": "~3.6.0",
|
||||
"compression": "~1.8.1",
|
||||
"country-flag-emoji-polyfill": "~0.1.8",
|
||||
"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",
|
||||
@@ -107,38 +105,35 @@
|
||||
"isomorphic-ws": "~5.0.0",
|
||||
"jsesc": "~3.0.2",
|
||||
"jsonata": "~2.2.2",
|
||||
"jsonwebtoken": "~9.0.3",
|
||||
"jwt-decode": "~3.1.2",
|
||||
"kafkajs": "~2.2.4",
|
||||
"knex": "~3.1.0",
|
||||
"knex": "~3.3.0",
|
||||
"limiter": "~2.1.0",
|
||||
"liquidjs": "~10.26.0",
|
||||
"liquidjs": "~10.29.0",
|
||||
"marked": "~14.1.4",
|
||||
"mitt": "~3.0.1",
|
||||
"mongodb": "~4.17.2",
|
||||
"mqtt": "~4.3.8",
|
||||
"mssql": "~12.0.0",
|
||||
"mysql2": "~3.11.5",
|
||||
"mysql2": "~3.24.2",
|
||||
"nanoid": "~3.3.11",
|
||||
"net-snmp": "~3.26.1",
|
||||
"node-cloudflared-tunnel": "~1.0.10",
|
||||
"node-radius-utils": "~1.2.0",
|
||||
"nodemailer": "~7.0.13",
|
||||
"nodemailer": "~10.0.0",
|
||||
"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.61.1",
|
||||
"prom-client": "~13.2.0",
|
||||
"prometheus-api-metrics": "~3.2.2",
|
||||
"promisify-child-process": "~4.1.2",
|
||||
"protobufjs": "~7.2.6",
|
||||
"qs": "~6.14.2",
|
||||
"protobufjs": "~8.8.0",
|
||||
"qs": "~6.16.0",
|
||||
"radius": "~1.1.4",
|
||||
"redbean-node": "~0.3.3",
|
||||
"redbean-node": "~0.4.0",
|
||||
"redis": "~5.9.0",
|
||||
"semver": "~7.5.4",
|
||||
"socket.io": "~4.8.3",
|
||||
@@ -148,43 +143,44 @@
|
||||
"ssh2-sftp-client": "~12.0.1",
|
||||
"tar": "~6.2.1",
|
||||
"tcp-ping": "~0.1.1",
|
||||
"thirty-two": "~1.0.2",
|
||||
"tldts": "~7.0.23",
|
||||
"tough-cookie": "~4.1.4",
|
||||
"tsx": "~4.23.1",
|
||||
"unlimited-timeout": "~0.1.0",
|
||||
"validator": "~13.15.26",
|
||||
"web-push": "~3.6.7",
|
||||
"ws": "~8.19.0"
|
||||
"ws": "~8.21.3",
|
||||
"zod": "~4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "~6.0.1",
|
||||
"@actions/github": "~9.1.1",
|
||||
"@fortawesome/fontawesome-svg-core": "~1.2.36",
|
||||
"@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.61.1",
|
||||
"@popperjs/core": "~2.10.2",
|
||||
"@testcontainers/hivemq": "^10.28.0",
|
||||
"@testcontainers/mariadb": "^10.28.0",
|
||||
"@testcontainers/mssqlserver": "^10.28.0",
|
||||
"@testcontainers/mysql": "^11.12.0",
|
||||
"@testcontainers/oraclefree": "^11.13.0",
|
||||
"@testcontainers/postgresql": "^11.12.0",
|
||||
"@testcontainers/rabbitmq": "^10.28.0",
|
||||
"@testcontainers/hivemq": "10.28.0",
|
||||
"@testcontainers/mariadb": "10.28.0",
|
||||
"@testcontainers/mssqlserver": "10.28.0",
|
||||
"@testcontainers/mysql": "11.14.0",
|
||||
"@testcontainers/oraclefree": "11.14.0",
|
||||
"@testcontainers/postgresql": "11.14.0",
|
||||
"@testcontainers/rabbitmq": "10.28.0",
|
||||
"@types/better-sqlite3": "~7.6.13",
|
||||
"@types/bootstrap": "~5.1.13",
|
||||
"@types/node": "^20.19.33",
|
||||
"@types/node": "^26.1.1",
|
||||
"@types/web-push": "^3.6.4",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@vitejs/plugin-vue": "~5.0.5",
|
||||
"@vue/compiler-sfc": "~3.4.38",
|
||||
"@vitejs/plugin-vue": "~6.0.7",
|
||||
"@vue/compiler-sfc": "~3.5.39",
|
||||
"@vuepic/vue-datepicker": "~3.4.8",
|
||||
"aedes": "~1.0.0",
|
||||
"bootstrap": "5.1.3",
|
||||
"chart.js": "~4.2.1",
|
||||
"chartjs-adapter-dayjs-4": "~1.0.4",
|
||||
"concurrently": "^7.6.0",
|
||||
"core-js": "~3.26.1",
|
||||
"cronstrue": "~2.24.0",
|
||||
"cross-env": "~7.0.3",
|
||||
"dns2": "~2.0.5",
|
||||
@@ -202,8 +198,8 @@
|
||||
"prettier": "^3.8.1",
|
||||
"prismjs": "~1.30.0",
|
||||
"qrcode": "~1.5.4",
|
||||
"rollup-plugin-visualizer": "^5.14.0",
|
||||
"sass": "~1.42.1",
|
||||
"rollup-plugin-visualizer": "~7.0.1",
|
||||
"sass": "^1.101.0",
|
||||
"ssh2": "~1.17.0",
|
||||
"stylelint": "^15.11.0",
|
||||
"stylelint-config-prettier": "^9.0.5",
|
||||
@@ -213,11 +209,10 @@
|
||||
"testcontainers": "^11.12.0",
|
||||
"typescript": "~4.4.4",
|
||||
"v-pagination-3": "~0.1.7",
|
||||
"vite": "~5.4.21",
|
||||
"vite": "~8.1.2",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vue": "~3.5.28",
|
||||
"vue": "~3.5.39",
|
||||
"vue-chartjs": "~5.2.0",
|
||||
"vue-confirm-dialog": "~1.0.2",
|
||||
"vue-contenteditable": "~3.0.4",
|
||||
"vue-i18n": "~11.2.8",
|
||||
"vue-image-crop-upload": "~3.0.3",
|
||||
@@ -230,8 +225,16 @@
|
||||
"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"
|
||||
},
|
||||
"knex": "$knex"
|
||||
},
|
||||
"allowScripts": {
|
||||
"@louislam/sqlite3@15.1.6": true,
|
||||
"better-sqlite3@12.11.1": true,
|
||||
"@fortawesome/fontawesome-common-types@0.2.36": true,
|
||||
"@fortawesome/fontawesome-svg-core@1.2.36": true,
|
||||
"@fortawesome/free-regular-svg-icons@5.15.4": true,
|
||||
@@ -239,11 +242,14 @@
|
||||
"core-js@2.6.12": true,
|
||||
"core-js@3.26.1": true,
|
||||
"cpu-features@0.0.10": true,
|
||||
"protobufjs@7.5.4": true,
|
||||
"protobufjs@7.6.1": true,
|
||||
"protobufjs@7.2.6": true,
|
||||
"esbuild@0.21.5": true,
|
||||
"protobufjs@7.6.6": true,
|
||||
"oracledb@6.10.0": true,
|
||||
"ssh2@1.17.0": true,
|
||||
"vue-demi@0.12.5": true
|
||||
"vue-demi@0.12.5": true,
|
||||
"esbuild@0.28.2": true,
|
||||
"@parcel/watcher@2.6.0": true
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,25 @@
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
import { loadEnvFile } from "node:process";
|
||||
|
||||
const port = 30001;
|
||||
// Load environment variables from `.env` (same as server.js), so local-only options
|
||||
// such as HEADLESS_E2E_TESTS can be set there.
|
||||
try {
|
||||
loadEnvFile();
|
||||
} catch (_) {}
|
||||
|
||||
const port = 3001;
|
||||
|
||||
// CI is always headless. Locally, `HEADLESS_E2E_TESTS=1` (in `.env` or the environment) forces it too.
|
||||
const headless = !!process.env.CI || process.env.HEADLESS_E2E_TESTS === "1";
|
||||
export const url = `http://localhost:${port}`;
|
||||
|
||||
export default defineConfig({
|
||||
// Look for test files in the "tests" directory, relative to this configuration file.
|
||||
testDir: "../test/e2e/specs",
|
||||
outputDir: "../private/playwright-test-results",
|
||||
testDir: "test/e2e/specs",
|
||||
outputDir: "private/playwright-test-results",
|
||||
fullyParallel: false,
|
||||
locale: "en-US",
|
||||
maxFailures: 1,
|
||||
|
||||
// Fail the build on CI if you accidentally left test.only in the source code.
|
||||
forbidOnly: !!process.env.CI,
|
||||
@@ -24,7 +35,7 @@ export default defineConfig({
|
||||
[
|
||||
"html",
|
||||
{
|
||||
outputFolder: "../private/playwright-report",
|
||||
outputFolder: "private/playwright-report",
|
||||
open: "never",
|
||||
},
|
||||
],
|
||||
@@ -34,8 +45,14 @@ export default defineConfig({
|
||||
// Base URL to use in actions like `await page.goto('/')`.
|
||||
baseURL: url,
|
||||
|
||||
headless,
|
||||
|
||||
// Collect trace when retrying the failed test.
|
||||
trace: "on-first-retry",
|
||||
|
||||
launchOptions: {
|
||||
args: ["--start-minimized"],
|
||||
},
|
||||
},
|
||||
|
||||
// Configure projects for major browsers.
|
||||
@@ -59,9 +76,9 @@ 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: "../",
|
||||
cwd: "./",
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
+15
-8
@@ -5,9 +5,11 @@ const { log } = require("../src/util");
|
||||
const { loginRateLimiter, apiRateLimiter } = require("./rate-limiter");
|
||||
const { Settings } = require("./settings");
|
||||
const dayjs = require("dayjs");
|
||||
const { checkPassword } = require("./better-auth");
|
||||
|
||||
/**
|
||||
* Login to web app
|
||||
* @deprecated DO NOT CALL IT. Use Better Auth instead.
|
||||
* Old Login function, keep it for migration purposes.
|
||||
* @param {string} username Username to login with
|
||||
* @param {string} password Password to login with
|
||||
* @returns {Promise<(Bean|null)>} User or null if login failed
|
||||
@@ -107,14 +109,19 @@ function userAuthorizer(username, password, callback) {
|
||||
// Login Rate Limit
|
||||
loginRateLimiter.pass(null, 0).then((pass) => {
|
||||
if (pass) {
|
||||
exports.login(username, password).then((user) => {
|
||||
callback(null, user != null);
|
||||
checkPassword(username, password)
|
||||
.then((valid) => {
|
||||
callback(null, valid);
|
||||
|
||||
if (user == null) {
|
||||
log.warn("basic-auth", "Failed basic auth attempt: invalid username/password");
|
||||
loginRateLimiter.removeTokens(1);
|
||||
}
|
||||
});
|
||||
if (!valid) {
|
||||
log.warn("basic-auth", "Failed basic auth attempt: invalid username/password");
|
||||
loginRateLimiter.removeTokens(1);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
log.error("basic-auth", "Auth error:", e);
|
||||
callback(null, false);
|
||||
});
|
||||
} else {
|
||||
log.warn("basic-auth", "Failed basic auth attempt: rate limit exceeded");
|
||||
callback(null, false);
|
||||
|
||||
@@ -0,0 +1,405 @@
|
||||
import { betterAuth } from "better-auth";
|
||||
// @ts-ignore
|
||||
import * as Database from "./database.js";
|
||||
import { genSecret, log } from "../src/util";
|
||||
import { R } from "redbean-node";
|
||||
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";
|
||||
import { apiKey } from "@better-auth/api-key";
|
||||
import { createAuthMiddleware } from "better-auth/api";
|
||||
// @ts-ignore
|
||||
import * as oldAuth from "./auth.js";
|
||||
import { hasUser } from "./routers/better-auth-router";
|
||||
import { symmetricEncrypt } from "better-auth/crypto";
|
||||
// @ts-ignore
|
||||
import config from "./config.js";
|
||||
|
||||
export type BetterAuthUser = ReturnType<typeof createAuthInstance>["$Infer"]["Session"]["user"];
|
||||
|
||||
let authInstance: ReturnType<typeof createAuthInstance>;
|
||||
|
||||
/**
|
||||
* Whether the HIBP password check is enabled (`UPTIME_KUMA_DISABLE_LEAK_PWD_CHECK` / `--disable-leak-pwd-check`).
|
||||
* @returns True to run the check
|
||||
*/
|
||||
function isHaveIBeenPwnedEnabled() {
|
||||
const disableHibp = process.env.UPTIME_KUMA_DISABLE_LEAK_PWD_CHECK;
|
||||
const disabledByEnv = !!disableHibp && disableHibp !== "false" && disableHibp !== "0";
|
||||
return !disabledByEnv && !config.args["disable-leak-pwd-check"];
|
||||
}
|
||||
|
||||
// Shared haveIBeenPwned plugin options; `enabled` is turned off during legacy user migration (#7946).
|
||||
const haveIBeenPwnedOptions = {
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the singleton instance of better-auth
|
||||
* Mainly used for http and socket.io authentication
|
||||
* @param force If true, create a new instance even if one already exists. Should only be used for testing or special cases.
|
||||
* @returns The singleton instance of better-auth
|
||||
*/
|
||||
export function auth(force = false) {
|
||||
if (authInstance && !force) {
|
||||
return authInstance;
|
||||
}
|
||||
|
||||
authInstance = createAuthInstance();
|
||||
return authInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do staff without any auth
|
||||
* For internal usage only, the api should be a superset of auth().api
|
||||
* @returns Internal adapter of better-auth
|
||||
*/
|
||||
export async function authInternal() {
|
||||
return (await auth().$context).internalAdapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of better-auth
|
||||
* @throws Error if Database.initDataDir() has not been called
|
||||
* @returns A new instance
|
||||
*/
|
||||
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."
|
||||
);
|
||||
}
|
||||
|
||||
// Apply the UPTIME_KUMA_DISABLE_LEAK_PWD_CHECK / --disable-leak-pwd-check setting
|
||||
haveIBeenPwnedOptions.enabled = isHaveIBeenPwnedEnabled();
|
||||
if (!haveIBeenPwnedOptions.enabled) {
|
||||
log.warn("auth", "haveIBeenPwned password check is disabled (UPTIME_KUMA_DISABLE_LEAK_PWD_CHECK).");
|
||||
}
|
||||
|
||||
const authDatabase = Database.createAuthDatabase(Database.dbConfig);
|
||||
|
||||
return betterAuth({
|
||||
database: authDatabase,
|
||||
|
||||
// Just want to silent the warning message
|
||||
// As we don't use callback/redirect, it is not used
|
||||
baseURL: "http://localhost:3000",
|
||||
|
||||
secret: getAuthSecret(),
|
||||
// Should be handled in Express.js, check better-auth-router.ts
|
||||
trustedOrigins: ["*"],
|
||||
|
||||
// Since multiple users are not supported yet, disable these paths to prevent accidental usage
|
||||
disabledPaths: [
|
||||
"/admin/create-user",
|
||||
"/admin/get-user",
|
||||
"/admin/list-users",
|
||||
"/admin/update-user",
|
||||
"/admin/set-user-password",
|
||||
"/admin/set-role",
|
||||
"/admin/ban-user",
|
||||
"/admin/unban-user",
|
||||
"/admin/impersonate-user",
|
||||
"/admin/stop-impersonating",
|
||||
"/admin/remove-user",
|
||||
"/admin/list-user-sessions",
|
||||
"/admin/revoke-user-session",
|
||||
"/admin/revoke-user-sessions",
|
||||
"/admin/has-permission",
|
||||
],
|
||||
|
||||
emailAndPassword: {
|
||||
revokeSessionsOnPasswordReset: true,
|
||||
enabled: true,
|
||||
disableSignUp: true,
|
||||
},
|
||||
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(haveIBeenPwnedOptions),
|
||||
|
||||
twoFactor({
|
||||
schema: {
|
||||
twoFactor: {
|
||||
modelName: "better_auth_twoFactor",
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
apiKey({
|
||||
schema: {
|
||||
apikey: {
|
||||
modelName: "better_auth_apikey",
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
// 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",
|
||||
},
|
||||
|
||||
hooks: {
|
||||
before: createAuthMiddleware(async (ctx) => {
|
||||
if (ctx.path.startsWith("/sign-in/")) {
|
||||
const username = ctx.body?.username;
|
||||
const password = ctx.body?.password;
|
||||
|
||||
// Migrate legacy user from old user table to better-auth
|
||||
// Only do this when there is no user in better-auth
|
||||
if (!(await hasUser())) {
|
||||
await migrateUser(username, password);
|
||||
}
|
||||
}
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt a string using the BetterAuth encryption method
|
||||
* @param data The string to encrypt
|
||||
* @returns The encrypted string
|
||||
*/
|
||||
export async function betterAuthEncrypt(data: string): Promise<string> {
|
||||
const encrypted = await symmetricEncrypt({
|
||||
key: auth().options.secret,
|
||||
data,
|
||||
});
|
||||
|
||||
return encrypted;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate a legacy user from the old `user` table to better-auth tables.
|
||||
* @param username Legacy username
|
||||
* @param password Plain-text password from the login form
|
||||
*/
|
||||
export async function migrateUser(username: string, password: string) {
|
||||
const legacyUser = await oldAuth.login(username, password);
|
||||
if (legacyUser) {
|
||||
// Skip the HIBP check: the legacy user's existing password must not be rejected (#7946)
|
||||
haveIBeenPwnedOptions.enabled = false;
|
||||
try {
|
||||
// Create BetterAuth user with the same username and password
|
||||
const newUser = await auth().api.createUser({
|
||||
body: {
|
||||
name: username,
|
||||
email: `${username}@noreply.uptime-kuma.internal`,
|
||||
password,
|
||||
role: "admin",
|
||||
data: {
|
||||
username,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Migrate 2FA settings if they exist
|
||||
if (legacyUser.twofa_status) {
|
||||
log.info("auth", `Migrating 2FA settings for user: ${username}`);
|
||||
|
||||
const rowId = genSecret(32);
|
||||
const userId = newUser.user.id;
|
||||
|
||||
const encryptedSecret = await betterAuthEncrypt(legacyUser.twofa_secret);
|
||||
const encryptedBackupCodes = await betterAuthEncrypt(JSON.stringify([])); // Empty backup codes for migration, as we don't have the original backup codes
|
||||
|
||||
await R.knex("better_auth_twoFactor").insert({
|
||||
id: rowId,
|
||||
secret: encryptedSecret,
|
||||
backupCodes: encryptedBackupCodes,
|
||||
verified: true,
|
||||
userId: userId,
|
||||
});
|
||||
|
||||
await R.knex("better_auth_user").where({ id: userId }).update({
|
||||
twoFactorEnabled: true,
|
||||
});
|
||||
}
|
||||
log.info("auth", `Migrated legacy user: ${username}`);
|
||||
} catch (e) {
|
||||
log.error("auth", `Failed to migrate legacy user ${username}:`, e);
|
||||
} finally {
|
||||
haveIBeenPwnedOptions.enabled = isHaveIBeenPwnedEnabled();
|
||||
}
|
||||
} else {
|
||||
log.info("auth", `No legacy user found for username: ${username}, do not migrate.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated DO NOT USE THIS FUNCTION, just for backward compatibility
|
||||
* Check username / password without creating a session, mainly for basic auth
|
||||
* @param username Username
|
||||
* @param password Plain-text password
|
||||
* @returns True if the username and password are correct, false otherwise
|
||||
*/
|
||||
export async function checkPassword(username: string, password: string): Promise<boolean> {
|
||||
const { adapter, password: passwordVerifier } = await auth().$context;
|
||||
const internalAdapter = await authInternal();
|
||||
|
||||
const user = await adapter.findOne<Record<string, any>>({
|
||||
model: "user",
|
||||
where: [{ field: "username", value: username }],
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const accounts = await internalAdapter.findAccounts(user.id);
|
||||
const credentialAccount = accounts.find((ac) => ac.providerId === "credential");
|
||||
|
||||
if (!credentialAccount?.password) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return passwordVerifier.verify({
|
||||
hash: credentialAccount.password,
|
||||
password,
|
||||
});
|
||||
}
|
||||
+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());
|
||||
|
||||
+102
-38
@@ -1,18 +1,20 @@
|
||||
const fs = require("fs");
|
||||
const fsAsync = fs.promises;
|
||||
const { R } = require("redbean-node");
|
||||
const { setSetting, setting } = require("./util-server");
|
||||
const { log, sleep, isDev } = require("../src/util");
|
||||
const { log, sleep, isDevEnv } = require("../src/util");
|
||||
const knex = require("knex");
|
||||
const path = require("path");
|
||||
const { EmbeddedMariaDB } = require("./embedded-mariadb");
|
||||
const mysql = require("mysql2/promise");
|
||||
const { Pool } = mysql;
|
||||
const { Settings } = require("./settings");
|
||||
const { UptimeCalculator } = require("./uptime-calculator");
|
||||
const dayjs = require("dayjs");
|
||||
const { SimpleMigrationServer } = require("./utils/simple-migration-server");
|
||||
const BetterSqlite3Database = require("better-sqlite3");
|
||||
const KumaColumnCompiler = require("./utils/knex/lib/dialects/mysql2/schema/mysql2-columncompiler");
|
||||
const SqlString = require("sqlstring");
|
||||
const { auth } = require("./better-auth");
|
||||
|
||||
/**
|
||||
* Database & App Data Folder
|
||||
@@ -59,6 +61,16 @@ class Database {
|
||||
*/
|
||||
static patched = false;
|
||||
|
||||
/**
|
||||
* @type {BetterSqlite3Database.Database}
|
||||
*/
|
||||
static authSQLite = null;
|
||||
|
||||
/**
|
||||
* @type {Pool}
|
||||
*/
|
||||
static authMariaDB = null;
|
||||
|
||||
/**
|
||||
* SQLite only
|
||||
* Add patch filename in key
|
||||
@@ -123,6 +135,9 @@ class Database {
|
||||
|
||||
static noReject = true;
|
||||
|
||||
/**
|
||||
* @type {Record<string, string>}
|
||||
*/
|
||||
static dbConfig = {};
|
||||
|
||||
static knexMigrationsPath = "./db/knex_migrations";
|
||||
@@ -168,7 +183,7 @@ class Database {
|
||||
* @returns {string} The dev data dir, empty string if not in dev mode or in master branch
|
||||
*/
|
||||
static getDevDataDir() {
|
||||
if (isDev) {
|
||||
if (isDevEnv()) {
|
||||
const gitBranch = this.getCurrentGitBranch();
|
||||
|
||||
// HEAD means detached head. Don't handle this case, becasuse it is not common.
|
||||
@@ -220,7 +235,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) {
|
||||
@@ -299,9 +314,6 @@ class Database {
|
||||
fs.copyFileSync(Database.templatePath, Database.sqlitePath);
|
||||
}
|
||||
|
||||
const Dialect = require("knex/lib/dialects/sqlite3/index.js");
|
||||
Dialect.prototype._driver = () => require("@louislam/sqlite3");
|
||||
|
||||
// SQLite is actually multiple connections for WAL mode, so we can set it to a higher number.
|
||||
// See: https://github.com/knex/knex/issues/3176#issuecomment-3389054899
|
||||
let poolConfig = {
|
||||
@@ -320,7 +332,7 @@ class Database {
|
||||
}
|
||||
|
||||
config = {
|
||||
client: Dialect,
|
||||
client: "better-sqlite3",
|
||||
connection: {
|
||||
filename: Database.sqlitePath,
|
||||
acquireConnectionTimeout: acquireConnectionTimeout,
|
||||
@@ -330,9 +342,12 @@ class Database {
|
||||
...poolConfig,
|
||||
acquireTimeoutMillis: acquireConnectionTimeout,
|
||||
afterCreate: (rawConn, done) => {
|
||||
this.initSQLite(rawConn, testMode)
|
||||
.then(() => done(undefined, rawConn))
|
||||
.catch((err) => done(err, rawConn));
|
||||
try {
|
||||
this.initSQLite(rawConn, testMode);
|
||||
done(undefined, rawConn);
|
||||
} catch (err) {
|
||||
done(err, rawConn);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -438,39 +453,39 @@ class Database {
|
||||
} else if (dbConfig.type.endsWith("mariadb")) {
|
||||
await this.initMariaDB();
|
||||
}
|
||||
|
||||
// Also connect better-auth
|
||||
auth(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize SQLite for each connection
|
||||
* @param {any} rawConn The raw node-sqlite3 Database object
|
||||
* @param {import("better-sqlite3").Database} rawConn The raw better-sqlite3 Database object
|
||||
* @param {boolean} testMode Should the connection be started in test mode?
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async initSQLite(rawConn, testMode) {
|
||||
// Since rawConn.run is callback based, in order to avoid callback hell, wrap it in a promise
|
||||
const asyncRun = (sql) => {
|
||||
return new Promise((resolve, reject) => rawConn.run(sql, (err) => (err ? reject(err) : resolve())));
|
||||
};
|
||||
static initSQLite(rawConn, testMode) {
|
||||
// Required for legacy SQL patch files that use unsafe PRAGMAs (e.g. PRAGMA writable_schema)
|
||||
rawConn.unsafeMode(true);
|
||||
|
||||
if (testMode) {
|
||||
// Change to MEMORY
|
||||
await asyncRun("PRAGMA journal_mode = MEMORY");
|
||||
rawConn.exec("PRAGMA journal_mode = MEMORY");
|
||||
} else {
|
||||
// Change to WAL
|
||||
await asyncRun("PRAGMA journal_mode = WAL");
|
||||
rawConn.exec("PRAGMA journal_mode = WAL");
|
||||
}
|
||||
|
||||
await asyncRun("PRAGMA foreign_keys = ON");
|
||||
await asyncRun("PRAGMA cache_size = -12000");
|
||||
await asyncRun("PRAGMA auto_vacuum = INCREMENTAL");
|
||||
rawConn.exec("PRAGMA foreign_keys = ON");
|
||||
rawConn.exec("PRAGMA cache_size = -12000");
|
||||
rawConn.exec("PRAGMA auto_vacuum = INCREMENTAL");
|
||||
|
||||
// Avoid error "SQLITE_BUSY: database is locked" by allowing SQLITE to wait up to 5 seconds to do a write
|
||||
await asyncRun("PRAGMA busy_timeout = 5000");
|
||||
rawConn.exec("PRAGMA busy_timeout = 5000");
|
||||
|
||||
// This ensures that an operating system crash or power failure will not corrupt the database.
|
||||
// FULL synchronous is very safe, but it is also slower.
|
||||
// Read more: https://sqlite.org/pragma.html#pragma_synchronous
|
||||
await asyncRun("PRAGMA synchronous = NORMAL");
|
||||
rawConn.exec("PRAGMA synchronous = NORMAL");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -533,6 +548,41 @@ class Database {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a database connection for better-auth, because better-auth doesn't use knex, so it needs a separate connection.
|
||||
* @param {object} dbConfig The database configuration
|
||||
* @returns {Pool|BetterSqlite3Database.Database} The database connection
|
||||
*/
|
||||
static createAuthDatabase(dbConfig) {
|
||||
let database;
|
||||
if (dbConfig.type.includes("mariadb")) {
|
||||
database = mysql.createPool({
|
||||
host: dbConfig.hostname,
|
||||
port: Number(dbConfig.port),
|
||||
database: dbConfig.dbName,
|
||||
user: dbConfig.username,
|
||||
password: dbConfig.password,
|
||||
timezone: "Z",
|
||||
...(dbConfig.socketPath ? { socketPath: dbConfig.socketPath } : {}),
|
||||
});
|
||||
Database.authMariaDB = database;
|
||||
} else if (dbConfig.type === "embedded-mariadb") {
|
||||
let embeddedMariaDB = EmbeddedMariaDB.getInstance();
|
||||
database = mysql.createPool({
|
||||
socketPath: embeddedMariaDB.socketPath,
|
||||
user: embeddedMariaDB.username,
|
||||
database: "kuma",
|
||||
timezone: "Z",
|
||||
});
|
||||
Database.authMariaDB = database;
|
||||
} else {
|
||||
database = new BetterSqlite3Database(Database.sqlitePath);
|
||||
Database.initSQLite(database, false);
|
||||
Database.authSQLite = database;
|
||||
}
|
||||
return database;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* @returns {Promise<void>}
|
||||
@@ -545,7 +595,7 @@ class Database {
|
||||
* @deprecated
|
||||
*/
|
||||
static async patchSqlite() {
|
||||
let version = parseInt(await setting("database_version"));
|
||||
let version = parseInt(await Settings.get("database_version"));
|
||||
|
||||
if (!version) {
|
||||
version = 0;
|
||||
@@ -570,7 +620,7 @@ class Database {
|
||||
log.info("db", `Patching ${sqlFile}`);
|
||||
await Database.importSQLFile(sqlFile);
|
||||
log.info("db", `Patched ${sqlFile}`);
|
||||
await setSetting("database_version", i);
|
||||
await Settings.set("database_version", i);
|
||||
}
|
||||
} catch (ex) {
|
||||
await Database.close();
|
||||
@@ -599,7 +649,7 @@ class Database {
|
||||
*/
|
||||
static async patchSqlite2() {
|
||||
log.debug("db", "Database Patch 2.0 Process");
|
||||
let databasePatchedFiles = await setting("databasePatchedFiles");
|
||||
let databasePatchedFiles = await Settings.get("databasePatchedFiles");
|
||||
|
||||
if (!databasePatchedFiles) {
|
||||
databasePatchedFiles = {};
|
||||
@@ -629,7 +679,7 @@ class Database {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await setSetting("databasePatchedFiles", databasePatchedFiles);
|
||||
await Settings.set("databasePatchedFiles", databasePatchedFiles);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -641,7 +691,7 @@ class Database {
|
||||
// Fix 1.13.0 empty slug bug
|
||||
await R.exec("UPDATE status_page SET slug = 'empty-slug-recover' WHERE TRIM(slug) = ''");
|
||||
|
||||
let title = await setting("title");
|
||||
let title = await Settings.get("title");
|
||||
|
||||
if (title) {
|
||||
log.info("database", "Migrating Status Page");
|
||||
@@ -656,12 +706,12 @@ class Database {
|
||||
let statusPage = R.dispense("status_page");
|
||||
statusPage.slug = "default";
|
||||
statusPage.title = title;
|
||||
statusPage.description = await setting("description");
|
||||
statusPage.icon = await setting("icon");
|
||||
statusPage.theme = await setting("statusPageTheme");
|
||||
statusPage.published = !!(await setting("statusPagePublished"));
|
||||
statusPage.search_engine_index = !!(await setting("searchEngineIndex"));
|
||||
statusPage.show_tags = !!(await setting("statusPageTags"));
|
||||
statusPage.description = await Settings.get("description");
|
||||
statusPage.icon = await Settings.get("icon");
|
||||
statusPage.theme = await Settings.get("statusPageTheme");
|
||||
statusPage.published = !!(await Settings.get("statusPagePublished"));
|
||||
statusPage.search_engine_index = !!(await Settings.get("searchEngineIndex"));
|
||||
statusPage.show_tags = !!(await Settings.get("statusPageTags"));
|
||||
statusPage.password = null;
|
||||
|
||||
if (!statusPage.title) {
|
||||
@@ -685,10 +735,10 @@ class Database {
|
||||
await R.exec("DELETE FROM setting WHERE type = 'statusPage'");
|
||||
|
||||
// Migrate Entry Page if it is status page
|
||||
let entryPage = await setting("entryPage");
|
||||
let entryPage = await Settings.get("entryPage");
|
||||
|
||||
if (entryPage === "statusPage") {
|
||||
await setSetting("entryPage", "statusPage-default", "general");
|
||||
await Settings.set("entryPage", "statusPage-default", "general");
|
||||
}
|
||||
|
||||
log.info("database", "Migrating Status Page - Done");
|
||||
@@ -768,6 +818,8 @@ class Database {
|
||||
}
|
||||
|
||||
/**
|
||||
* Close Database
|
||||
* Warning: It is used for shutdown the application. After closed database, it cannot be connected again! Because Better-auth would not reconnect the database.
|
||||
* Special handle, because tarn.js throw a promise reject that cannot be caught
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@@ -786,7 +838,19 @@ class Database {
|
||||
|
||||
while (true) {
|
||||
Database.noReject = true;
|
||||
|
||||
await R.close();
|
||||
|
||||
// Also close auth db
|
||||
if (Database.authMariaDB) {
|
||||
await Database.authMariaDB.end();
|
||||
Database.authMariaDB = null;
|
||||
}
|
||||
if (Database.authSQLite) {
|
||||
Database.authSQLite.close();
|
||||
Database.authSQLite = null;
|
||||
}
|
||||
|
||||
await sleep(2000);
|
||||
|
||||
if (Database.noReject) {
|
||||
|
||||
+3
-4
@@ -22,7 +22,7 @@ class DockerHost {
|
||||
let bean;
|
||||
|
||||
if (dockerHostID) {
|
||||
bean = await R.findOne("docker_host", " id = ? AND user_id = ? ", [dockerHostID, userID]);
|
||||
bean = await R.findOne("docker_host", " id = ? ", [dockerHostID]);
|
||||
|
||||
if (!bean) {
|
||||
throw new Error("docker host not found");
|
||||
@@ -44,11 +44,10 @@ class DockerHost {
|
||||
/**
|
||||
* Delete a Docker host
|
||||
* @param {number} dockerHostID ID of the Docker host to delete
|
||||
* @param {number} userID ID of the user who created the Docker host
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async delete(dockerHostID, userID) {
|
||||
let bean = await R.findOne("docker_host", " id = ? AND user_id = ? ", [dockerHostID, userID]);
|
||||
static async delete(dockerHostID) {
|
||||
let bean = await R.findOne("docker_host", " id = ? ", [dockerHostID]);
|
||||
|
||||
if (!bean) {
|
||||
throw new Error("docker host not found");
|
||||
|
||||
+8
-11
@@ -51,7 +51,6 @@ const version = require("../../package.json").version;
|
||||
const apicache = require("../modules/apicache");
|
||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
const { DockerHost } = require("../docker");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const crypto = require("crypto");
|
||||
const { UptimeCalculator } = require("../uptime-calculator");
|
||||
const { CookieJar } = require("tough-cookie");
|
||||
@@ -117,7 +116,7 @@ class Monitor extends BeanModel {
|
||||
let screenshot = null;
|
||||
|
||||
if (this.type === "real-browser") {
|
||||
screenshot = "/screenshots/" + jwt.sign(this.id, UptimeKumaServer.getInstance().jwtSecret) + ".png";
|
||||
screenshot = "/screenshots/" + this.id + ".png";
|
||||
}
|
||||
|
||||
const path = preloadData.paths.get(this.id) || [];
|
||||
@@ -1316,7 +1315,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;
|
||||
@@ -1988,10 +1987,9 @@ class Monitor extends BeanModel {
|
||||
/**
|
||||
* Delete a monitor from the system
|
||||
* @param {number} monitorID ID of the monitor to delete
|
||||
* @param {number} userID ID of the user who owns the monitor
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async deleteMonitor(monitorID, userID) {
|
||||
static async deleteMonitor(monitorID) {
|
||||
const server = UptimeKumaServer.getInstance();
|
||||
|
||||
// Stop the monitor if it's running
|
||||
@@ -2001,31 +1999,30 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
|
||||
// Delete from database
|
||||
await R.exec("DELETE FROM monitor WHERE id = ? AND user_id = ? ", [monitorID, userID]);
|
||||
await R.exec("DELETE FROM monitor WHERE id = ? ", [monitorID]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively delete a monitor and all its descendants
|
||||
* @param {number} monitorID ID of the monitor to delete
|
||||
* @param {number} userID ID of the user who owns the monitor
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async deleteMonitorRecursively(monitorID, userID) {
|
||||
static async deleteMonitorRecursively(monitorID) {
|
||||
// Check if this monitor is a group
|
||||
const monitor = await R.findOne("monitor", " id = ? AND user_id = ? ", [monitorID, userID]);
|
||||
const monitor = await R.findOne("monitor", " id = ? ", [monitorID]);
|
||||
|
||||
if (monitor && monitor.type === "group") {
|
||||
// Get all children and delete them recursively
|
||||
const children = await Monitor.getChildren(monitorID);
|
||||
if (children && children.length > 0) {
|
||||
for (const child of children) {
|
||||
await Monitor.deleteMonitorRecursively(child.id, userID);
|
||||
await Monitor.deleteMonitorRecursively(child.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the monitor itself
|
||||
await Monitor.deleteMonitor(monitorID, userID);
|
||||
await Monitor.deleteMonitor(monitorID);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-48
@@ -1,52 +1,5 @@
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
const passwordHash = require("../password-hash");
|
||||
const { R } = require("redbean-node");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const { shake256, SHAKE256_LENGTH } = require("../util-server");
|
||||
|
||||
class User extends BeanModel {
|
||||
/**
|
||||
* Reset user password
|
||||
* Fix #1510, as in the context reset-password.js, there is no auto model mapping. Call this static function instead.
|
||||
* @param {number} userID ID of user to update
|
||||
* @param {string} newPassword Users new password
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async resetPassword(userID, newPassword) {
|
||||
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [
|
||||
await passwordHash.generate(newPassword),
|
||||
userID,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset this users password
|
||||
* @param {string} newPassword Users new password
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async resetPassword(newPassword) {
|
||||
const hashedPassword = await passwordHash.generate(newPassword);
|
||||
|
||||
await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [hashedPassword, this.id]);
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
class User extends BeanModel {}
|
||||
|
||||
module.exports = User;
|
||||
@@ -5,7 +5,6 @@ const { Settings } = require("../settings");
|
||||
const childProcess = require("child_process");
|
||||
const path = require("path");
|
||||
const Database = require("../database");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const config = require("../config");
|
||||
const { RemoteBrowser } = require("../remote-browser");
|
||||
const { commandExists } = require("../util-server");
|
||||
@@ -95,11 +94,10 @@ async function getBrowser() {
|
||||
/**
|
||||
* Get the current instance of the browser. If there isn't one, create it
|
||||
* @param {integer} remoteBrowserID Path to executable
|
||||
* @param {integer} userId User ID
|
||||
* @returns {Promise<Browser>} The browser
|
||||
*/
|
||||
async function getRemoteBrowser(remoteBrowserID, userId) {
|
||||
let remoteBrowser = await RemoteBrowser.get(remoteBrowserID, userId);
|
||||
async function getRemoteBrowser(remoteBrowserID) {
|
||||
let remoteBrowser = await RemoteBrowser.get(remoteBrowserID);
|
||||
log.debug("chromium", `Using remote browser: ${remoteBrowser.name} (${remoteBrowser.id})`);
|
||||
browser = await chromium.connect(remoteBrowser.url);
|
||||
return browser;
|
||||
@@ -250,9 +248,7 @@ class RealBrowserMonitorType extends MonitorType {
|
||||
* @inheritdoc
|
||||
*/
|
||||
async check(monitor, heartbeat, server) {
|
||||
const browser = monitor.remote_browser
|
||||
? await getRemoteBrowser(monitor.remote_browser, monitor.user_id)
|
||||
: await getBrowser();
|
||||
const browser = monitor.remote_browser ? await getRemoteBrowser(monitor.remote_browser) : await getBrowser();
|
||||
const context = await browser.newContext();
|
||||
try {
|
||||
const page = await context.newPage();
|
||||
@@ -275,7 +271,7 @@ class RealBrowserMonitorType extends MonitorType {
|
||||
await page.waitForTimeout(monitor.screenshot_delay);
|
||||
}
|
||||
|
||||
let filename = jwt.sign(monitor.id, server.jwtSecret) + ".png";
|
||||
let filename = monitor.id + ".png";
|
||||
|
||||
await page.screenshot({
|
||||
path: path.join(Database.screenshotDir, filename),
|
||||
|
||||
@@ -19,17 +19,15 @@ class Apprise extends NotificationProvider {
|
||||
encoding: "utf8",
|
||||
});
|
||||
|
||||
const output = s.stdout ? s.stdout.toString() : "ERROR: maybe apprise not found";
|
||||
|
||||
if (output) {
|
||||
if (!output.includes("ERROR")) {
|
||||
return okMsg;
|
||||
}
|
||||
// A successful run (exit code 0) may legitimately print nothing to
|
||||
// stdout, so empty output must not be treated as a failure (#5547).
|
||||
const output = s.stdout ? s.stdout.toString() : "";
|
||||
|
||||
if (output.includes("ERROR")) {
|
||||
throw new Error(output);
|
||||
} else {
|
||||
return "No output from apprise";
|
||||
}
|
||||
|
||||
return okMsg;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
const NotificationProvider = require("./notification-provider");
|
||||
const axios = require("axios");
|
||||
const https = require("https");
|
||||
|
||||
class Indigo extends NotificationProvider {
|
||||
name = "Indigo";
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) {
|
||||
const okMsg = "Sent Successfully.";
|
||||
const url = `${notification.indigoUrl.trim().replace(/\/*$/, "")}/v2/api/command`;
|
||||
const variableId = parseInt(notification.indigoVariableId);
|
||||
const actionGroupId = parseInt(notification.indigoActionGroupId);
|
||||
|
||||
if (!variableId && !actionGroupId) {
|
||||
throw new Error("Set an Indigo variable ID, an action group ID, or both.");
|
||||
}
|
||||
|
||||
// The variable is written first so the action group can read it.
|
||||
const commands = [];
|
||||
if (variableId) {
|
||||
commands.push({
|
||||
message: "indigo.variable.updateValue",
|
||||
objectId: variableId,
|
||||
parameters: { value: msg },
|
||||
});
|
||||
}
|
||||
if (actionGroupId) {
|
||||
commands.push({
|
||||
message: "indigo.actionGroup.execute",
|
||||
objectId: actionGroupId,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
let config = {
|
||||
headers: {
|
||||
Authorization: `Bearer ${notification.indigoApiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
};
|
||||
// The Indigo Web Server on a LAN uses a self-signed certificate;
|
||||
// the Reflector (NAME.indigodomo.net) has a valid one.
|
||||
if (notification.indigoIgnoreTlsError) {
|
||||
config.httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
||||
}
|
||||
config = this.getAxiosConfigWithProxy(config);
|
||||
|
||||
for (const command of commands) {
|
||||
const result = await axios.post(url, { id: "uptime-kuma", ...command }, config);
|
||||
if (result.data?.error || result.data?.validationErrors) {
|
||||
throw new Error(
|
||||
`Indigo rejected ${command.message}: ${result.data.error || JSON.stringify(result.data.validationErrors)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return okMsg;
|
||||
} catch (error) {
|
||||
this.throwGeneralAxiosError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Indigo;
|
||||
@@ -26,6 +26,7 @@ const Gorush = require("./notification-providers/gorush");
|
||||
const Gotify = require("./notification-providers/gotify");
|
||||
const GrafanaOncall = require("./notification-providers/grafana-oncall");
|
||||
const HomeAssistant = require("./notification-providers/home-assistant");
|
||||
const Indigo = require("./notification-providers/indigo");
|
||||
const HeiiOnCall = require("./notification-providers/heii-oncall");
|
||||
const Keep = require("./notification-providers/keep");
|
||||
const Kook = require("./notification-providers/kook");
|
||||
@@ -150,6 +151,7 @@ class Notification {
|
||||
new Gotify(),
|
||||
new GrafanaOncall(),
|
||||
new HomeAssistant(),
|
||||
new Indigo(),
|
||||
new HeiiOnCall(),
|
||||
new Keep(),
|
||||
new Kook(),
|
||||
@@ -274,7 +276,7 @@ class Notification {
|
||||
let bean;
|
||||
|
||||
if (notificationID) {
|
||||
bean = await R.findOne("notification", " id = ? AND user_id = ? ", [notificationID, userID]);
|
||||
bean = await R.findOne("notification", " id = ? ", [notificationID]);
|
||||
|
||||
if (!bean) {
|
||||
throw new Error("notification not found");
|
||||
@@ -294,7 +296,7 @@ class Notification {
|
||||
await R.store(bean);
|
||||
|
||||
if (applyExisting) {
|
||||
await applyNotificationEveryMonitor(bean.id, userID);
|
||||
await applyNotificationEveryMonitor(bean.id);
|
||||
}
|
||||
|
||||
return bean;
|
||||
@@ -303,11 +305,10 @@ class Notification {
|
||||
/**
|
||||
* Delete a notification
|
||||
* @param {number} notificationID ID of notification to delete
|
||||
* @param {number} userID ID of user who created notification
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async delete(notificationID, userID) {
|
||||
let bean = await R.findOne("notification", " id = ? AND user_id = ? ", [notificationID, userID]);
|
||||
static async delete(notificationID) {
|
||||
let bean = await R.findOne("notification", " id = ? ", [notificationID]);
|
||||
|
||||
if (!bean) {
|
||||
throw new Error("notification not found");
|
||||
@@ -328,11 +329,10 @@ class Notification {
|
||||
/**
|
||||
* Apply the notification to every monitor
|
||||
* @param {number} notificationID ID of notification to apply
|
||||
* @param {number} userID ID of user who created notification
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function applyNotificationEveryMonitor(notificationID, userID) {
|
||||
let monitors = await R.getAll("SELECT id FROM monitor WHERE user_id = ?", [userID]);
|
||||
async function applyNotificationEveryMonitor(notificationID) {
|
||||
let monitors = await R.getAll("SELECT id FROM monitor");
|
||||
|
||||
for (let i = 0; i < monitors.length; i++) {
|
||||
let checkNotification = await R.findOne("monitor_notification", " monitor_id = ? AND notification_id = ? ", [
|
||||
|
||||
+6
-8
@@ -21,7 +21,7 @@ class Proxy {
|
||||
let bean;
|
||||
|
||||
if (proxyID) {
|
||||
bean = await R.findOne("proxy", " id = ? AND user_id = ? ", [proxyID, userID]);
|
||||
bean = await R.findOne("proxy", " id = ? ", [proxyID]);
|
||||
|
||||
if (!bean) {
|
||||
throw new Error("proxy not found");
|
||||
@@ -55,7 +55,7 @@ class Proxy {
|
||||
await R.store(bean);
|
||||
|
||||
if (proxy.applyExisting) {
|
||||
await applyProxyEveryMonitor(bean.id, userID);
|
||||
await applyProxyEveryMonitor(bean.id);
|
||||
}
|
||||
|
||||
return bean;
|
||||
@@ -64,11 +64,10 @@ class Proxy {
|
||||
/**
|
||||
* Deletes proxy with given id and removes it from monitors
|
||||
* @param {number} proxyID ID of proxy to delete
|
||||
* @param {number} userID ID of proxy owner
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async delete(proxyID, userID) {
|
||||
const bean = await R.findOne("proxy", " id = ? AND user_id = ? ", [proxyID, userID]);
|
||||
static async delete(proxyID) {
|
||||
const bean = await R.findOne("proxy", " id = ? ", [proxyID]);
|
||||
|
||||
if (!bean) {
|
||||
throw new Error("proxy not found");
|
||||
@@ -179,12 +178,11 @@ class Proxy {
|
||||
/**
|
||||
* Applies given proxy id to monitors
|
||||
* @param {number} proxyID ID of proxy to apply
|
||||
* @param {number} userID ID of proxy owner
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function applyProxyEveryMonitor(proxyID, userID) {
|
||||
async function applyProxyEveryMonitor(proxyID) {
|
||||
// Find all monitors with id and proxy id
|
||||
const monitors = await R.getAll("SELECT id, proxy_id FROM monitor WHERE user_id = ?", [userID]);
|
||||
const monitors = await R.getAll("SELECT id, proxy_id FROM monitor");
|
||||
|
||||
// Update proxy id not match with given proxy id
|
||||
for (const monitor of monitors) {
|
||||
|
||||
@@ -72,15 +72,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,
|
||||
};
|
||||
@@ -4,11 +4,10 @@ class RemoteBrowser {
|
||||
/**
|
||||
* Gets remote browser from ID
|
||||
* @param {number} remoteBrowserID ID of the remote browser
|
||||
* @param {number} userID ID of the user who created the remote browser
|
||||
* @returns {Promise<Bean>} Remote Browser
|
||||
*/
|
||||
static async get(remoteBrowserID, userID) {
|
||||
let bean = await R.findOne("remote_browser", " id = ? AND user_id = ? ", [remoteBrowserID, userID]);
|
||||
static async get(remoteBrowserID) {
|
||||
let bean = await R.findOne("remote_browser", " id = ? ", [remoteBrowserID]);
|
||||
|
||||
if (!bean) {
|
||||
throw new Error("Remote browser not found");
|
||||
@@ -28,7 +27,7 @@ class RemoteBrowser {
|
||||
let bean;
|
||||
|
||||
if (remoteBrowserID) {
|
||||
bean = await R.findOne("remote_browser", " id = ? AND user_id = ? ", [remoteBrowserID, userID]);
|
||||
bean = await R.findOne("remote_browser", " id = ? ", [remoteBrowserID]);
|
||||
|
||||
if (!bean) {
|
||||
throw new Error("Remote browser not found");
|
||||
@@ -49,11 +48,10 @@ class RemoteBrowser {
|
||||
/**
|
||||
* Delete a Remote Browser
|
||||
* @param {number} remoteBrowserID ID of the Remote Browser to delete
|
||||
* @param {number} userID ID of the user who created the Remote Browser
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async delete(remoteBrowserID, userID) {
|
||||
let bean = await R.findOne("remote_browser", " id = ? AND user_id = ? ", [remoteBrowserID, userID]);
|
||||
static async delete(remoteBrowserID) {
|
||||
let bean = await R.findOne("remote_browser", " id = ? ", [remoteBrowserID]);
|
||||
|
||||
if (!bean) {
|
||||
throw new Error("Remote Browser not found");
|
||||
|
||||
@@ -51,6 +51,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;
|
||||
@@ -84,7 +88,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
|
||||
@@ -558,13 +569,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,121 @@
|
||||
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;
|
||||
let setupTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
const expiredMsg = "Setup has expired. Please restart the server to try again.";
|
||||
|
||||
/**
|
||||
* 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();
|
||||
betterAuthRouter.all("/api/auth/*", async (req, res) => {
|
||||
allowDevOrigin(req, res);
|
||||
return toNodeHandler(auth())(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;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export async function hasUser() {
|
||||
if (_hasUser) {
|
||||
return true;
|
||||
}
|
||||
const { count } = await R.knex("better_auth_user").count("id as count").first();
|
||||
return Number(count) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns Whether setup is needed.
|
||||
*/
|
||||
export async function needSetup() {
|
||||
if (!setupTimeout) {
|
||||
setupTimeout = setTimeout(
|
||||
() => {
|
||||
expired = true;
|
||||
log.error("auth", expiredMsg);
|
||||
},
|
||||
1000 * 60 * 10
|
||||
);
|
||||
}
|
||||
|
||||
if (expired) {
|
||||
return false;
|
||||
}
|
||||
if (processingSetup) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (await hasUser()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The user may be in the old user table, check that as well
|
||||
const oldUserCountRow = await R.knex("user").count("id as count").first();
|
||||
const hasOldUser = Number(oldUserCountRow.count) !== 0;
|
||||
return !hasOldUser;
|
||||
}
|
||||
+61
-477
@@ -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 "node: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";
|
||||
@@ -97,18 +105,10 @@ 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();
|
||||
@@ -117,20 +117,16 @@ const app = server.app;
|
||||
|
||||
log.debug("server", "Importing Monitor");
|
||||
const Monitor = require("./model/monitor");
|
||||
const User = require("./model/user");
|
||||
|
||||
log.debug("server", "Importing Settings");
|
||||
const {
|
||||
getSettings,
|
||||
setSettings,
|
||||
setting,
|
||||
initJWTSecret,
|
||||
checkLogin,
|
||||
doubleCheckPassword,
|
||||
shake256,
|
||||
SHAKE256_LENGTH,
|
||||
allowDevAllOrigin,
|
||||
printServerUrls,
|
||||
allowDevOrigin,
|
||||
} = require("./util-server");
|
||||
|
||||
log.debug("server", "Importing Notification");
|
||||
@@ -144,12 +140,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");
|
||||
|
||||
@@ -165,12 +156,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}
|
||||
@@ -191,7 +176,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,
|
||||
@@ -221,12 +205,6 @@ app.use(function (req, res, next) {
|
||||
next();
|
||||
});
|
||||
|
||||
/**
|
||||
* Show Setup Page
|
||||
* @type {boolean}
|
||||
*/
|
||||
let needSetup = false;
|
||||
|
||||
(async () => {
|
||||
// Create a data directory
|
||||
Database.initDataDir(args);
|
||||
@@ -246,6 +224,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");
|
||||
@@ -294,6 +275,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);
|
||||
@@ -301,23 +287,11 @@ let needSetup = false;
|
||||
response.send("OK");
|
||||
});
|
||||
|
||||
app.post("/test-x-www-form-urlencoded", async (request, response) => {
|
||||
log.debug("test", request.headers);
|
||||
log.debug("test", request.body);
|
||||
response.send("OK");
|
||||
});
|
||||
|
||||
const fs = require("fs");
|
||||
|
||||
app.get("/_e2e/take-sqlite-snapshot", async (request, response) => {
|
||||
await Database.close();
|
||||
try {
|
||||
fs.cpSync(Database.sqlitePath, `${Database.sqlitePath}.e2e-snapshot`);
|
||||
} catch (err) {
|
||||
throw new Error("Unable to copy SQLite DB.");
|
||||
}
|
||||
await Database.connect();
|
||||
|
||||
// Checkpoint WAL to flush all data to the main .db file, then copy.
|
||||
// No close/reopen needed — the file is consistent after checkpoint.
|
||||
await R.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
||||
fs.cpSync(Database.sqlitePath, `${Database.sqlitePath}.e2e-snapshot`);
|
||||
response.send("Snapshot taken.");
|
||||
});
|
||||
|
||||
@@ -336,6 +310,12 @@ let needSetup = false;
|
||||
|
||||
response.send("Snapshot restored.");
|
||||
});
|
||||
|
||||
app.post("/test-x-www-form-urlencoded", async (request, response) => {
|
||||
log.debug("test", request.headers);
|
||||
log.debug("test", request.body);
|
||||
response.send("OK");
|
||||
});
|
||||
}
|
||||
|
||||
// Robots.txt
|
||||
@@ -376,6 +356,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/")) {
|
||||
@@ -389,351 +373,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
|
||||
@@ -822,10 +480,6 @@ let needSetup = false;
|
||||
|
||||
let bean = await R.findOne("monitor", " id = ? ", [monitor.id]);
|
||||
|
||||
if (bean.user_id !== socket.userID) {
|
||||
throw new Error("Permission denied.");
|
||||
}
|
||||
|
||||
// Check if Parent is Descendant (would cause endless loop)
|
||||
if (monitor.parent !== null) {
|
||||
const childIDs = await Monitor.getAllChildrenIDs(monitor.id);
|
||||
@@ -1020,7 +674,7 @@ let needSetup = false;
|
||||
|
||||
log.info("monitor", `Get Monitor: ${monitorID} User ID: ${socket.userID}`);
|
||||
|
||||
let monitor = await R.findOne("monitor", " id = ? AND user_id = ? ", [monitorID, socket.userID]);
|
||||
let monitor = await R.findOne("monitor", " id = ? ", [monitorID]);
|
||||
const monitorData = [{ id: monitor.id, active: monitor.active }];
|
||||
const preloadData = await Monitor.preparePreloadData(monitorData);
|
||||
callback({
|
||||
@@ -1143,7 +797,7 @@ let needSetup = false;
|
||||
const startTime = Date.now();
|
||||
|
||||
// Check if this is a group monitor
|
||||
const monitor = await R.findOne("monitor", " id = ? AND user_id = ? ", [monitorID, socket.userID]);
|
||||
const monitor = await R.findOne("monitor", " id = ? ", [monitorID]);
|
||||
|
||||
// Log with context about deletion type
|
||||
if (monitor && monitor.type === "group") {
|
||||
@@ -1164,7 +818,7 @@ let needSetup = false;
|
||||
// Delete all child monitors recursively
|
||||
if (children && children.length > 0) {
|
||||
for (const child of children) {
|
||||
await Monitor.deleteMonitorRecursively(child.id, socket.userID);
|
||||
await Monitor.deleteMonitorRecursively(child.id);
|
||||
await server.sendDeleteMonitorFromList(socket, child.id);
|
||||
}
|
||||
}
|
||||
@@ -1182,7 +836,7 @@ let needSetup = false;
|
||||
}
|
||||
|
||||
// Delete the monitor itself
|
||||
await Monitor.deleteMonitor(monitorID, socket.userID);
|
||||
await Monitor.deleteMonitor(monitorID);
|
||||
|
||||
// Fix #2880
|
||||
apicache.clear();
|
||||
@@ -1449,38 +1103,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);
|
||||
@@ -1513,7 +1135,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
|
||||
@@ -1590,7 +1212,7 @@ let needSetup = false;
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
await Notification.delete(notificationID, socket.userID);
|
||||
await Notification.delete(notificationID);
|
||||
await sendNotificationList(socket);
|
||||
|
||||
callback({
|
||||
@@ -1735,6 +1357,8 @@ let needSetup = false;
|
||||
}
|
||||
});
|
||||
|
||||
betterAuthSocketHandler(socket);
|
||||
|
||||
// Status Page Socket Handler for admin only
|
||||
statusPageSocketHandler(socket);
|
||||
cloudflaredSocketHandler(socket);
|
||||
@@ -1753,11 +1377,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");
|
||||
@@ -1809,21 +1430,6 @@ async function updateMonitorNotification(monitorID, notificationIDList) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given user owns a specific monitor
|
||||
* @param {number} userID ID of user to check
|
||||
* @param {number} monitorID ID of monitor to check
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} The specified user does not own the monitor
|
||||
*/
|
||||
async function checkOwner(userID, monitorID) {
|
||||
let row = await R.getRow("SELECT id FROM monitor WHERE id = ? AND user_id = ? ", [monitorID, userID]);
|
||||
|
||||
if (!row) {
|
||||
throw new Error("You do not own this monitor.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called after user login
|
||||
* This function is used to send the heartbeat list of a monitor.
|
||||
@@ -1878,24 +1484,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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1905,11 +1493,9 @@ async function initDatabase(testMode = false) {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function startMonitor(userID, monitorID) {
|
||||
await checkOwner(userID, monitorID);
|
||||
|
||||
log.info("manage", `Resume Monitor: ${monitorID} User ID: ${userID}`);
|
||||
|
||||
await R.exec("UPDATE monitor SET active = 1 WHERE id = ? AND user_id = ? ", [monitorID, userID]);
|
||||
await R.exec("UPDATE monitor SET active = 1 WHERE id = ? ", [monitorID]);
|
||||
|
||||
let monitor = await R.findOne("monitor", " id = ? ", [monitorID]);
|
||||
|
||||
@@ -1938,11 +1524,9 @@ async function restartMonitor(userID, monitorID) {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function pauseMonitor(userID, monitorID) {
|
||||
await checkOwner(userID, monitorID);
|
||||
|
||||
log.info("manage", `Pause Monitor: ${monitorID} User ID: ${userID}`);
|
||||
|
||||
await R.exec("UPDATE monitor SET active = 0 WHERE id = ? AND user_id = ? ", [monitorID, userID]);
|
||||
await R.exec("UPDATE monitor SET active = 0 WHERE id = ? ", [monitorID]);
|
||||
|
||||
if (monitorID in server.monitorList) {
|
||||
await server.monitorList[monitorID].stop();
|
||||
|
||||
+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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ module.exports.apiKeySocketHandler = (socket) => {
|
||||
|
||||
log.debug("apikeys", `Deleted API Key: ${keyID} User ID: ${socket.userID}`);
|
||||
|
||||
await R.exec("DELETE FROM api_key WHERE id = ? AND user_id = ? ", [keyID, socket.userID]);
|
||||
await R.exec("DELETE FROM api_key WHERE id = ? ", [keyID]);
|
||||
|
||||
apicache.clear();
|
||||
|
||||
|
||||
@@ -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(await 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) {
|
||||
|
||||
@@ -34,7 +34,7 @@ module.exports.dockerSocketHandler = (socket) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
await DockerHost.delete(dockerHostID, socket.userID);
|
||||
await DockerHost.delete(dockerHostID);
|
||||
await sendDockerHostList(socket);
|
||||
|
||||
callback({
|
||||
|
||||
@@ -49,10 +49,6 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
||||
|
||||
let bean = server.getMaintenance(maintenance.id);
|
||||
|
||||
if (bean.user_id !== socket.userID) {
|
||||
throw new Error("Permission denied.");
|
||||
}
|
||||
|
||||
await Maintenance.jsonToBean(bean, maintenance);
|
||||
await R.store(bean);
|
||||
await bean.run(true);
|
||||
@@ -143,7 +139,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
||||
|
||||
log.debug("maintenance", `Get Maintenance: ${maintenanceID} User ID: ${socket.userID}`);
|
||||
|
||||
let bean = await R.findOne("maintenance", " id = ? AND user_id = ? ", [maintenanceID, socket.userID]);
|
||||
let bean = await R.findOne("maintenance", " id = ? ", [maintenanceID]);
|
||||
|
||||
callback({
|
||||
ok: true,
|
||||
@@ -232,7 +228,7 @@ module.exports.maintenanceSocketHandler = (socket) => {
|
||||
delete server.maintenanceList[maintenanceID];
|
||||
}
|
||||
|
||||
await R.exec("DELETE FROM maintenance WHERE id = ? AND user_id = ? ", [maintenanceID, socket.userID]);
|
||||
await R.exec("DELETE FROM maintenance WHERE id = ? ", [maintenanceID]);
|
||||
|
||||
apicache.clear();
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ module.exports.proxySocketHandler = (socket) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
await Proxy.delete(proxyID, socket.userID);
|
||||
await Proxy.delete(proxyID);
|
||||
await sendProxyList(socket);
|
||||
await Proxy.reloadProxy();
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ module.exports.remoteBrowserSocketHandler = (socket) => {
|
||||
try {
|
||||
checkLogin(socket);
|
||||
|
||||
await RemoteBrowser.delete(dockerHostID, socket.userID);
|
||||
await RemoteBrowser.delete(dockerHostID);
|
||||
await sendRemoteBrowserList(socket);
|
||||
|
||||
callback({
|
||||
|
||||
@@ -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");
|
||||
@@ -13,6 +13,7 @@ const childProcessAsync = require("promisify-child-process");
|
||||
const path = require("path");
|
||||
const axios = require("axios");
|
||||
const { isSSL, sslKey, sslCert, sslKeyPassphrase } = require("./config");
|
||||
const { getSession, getDisableAuthSession } = require("./better-auth");
|
||||
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`, put at the bottom of this file instead.
|
||||
|
||||
/**
|
||||
@@ -54,12 +55,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.
|
||||
@@ -141,12 +136,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
|
||||
@@ -204,8 +202,24 @@ class UptimeKumaServer {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async initAfterDatabaseReady() {
|
||||
// Static
|
||||
this.app.use("/screenshots", express.static(Database.screenshotDir));
|
||||
// Screenshots
|
||||
this.app.use("/screenshots", async (req, res, next) => {
|
||||
try {
|
||||
let session;
|
||||
if (!(await Settings.get("disableAuth"))) {
|
||||
session = await getSession(req.headers.cookie);
|
||||
} else {
|
||||
session = await getDisableAuthSession();
|
||||
}
|
||||
if (session) {
|
||||
express.static(Database.screenshotDir)(req, res, next);
|
||||
} else {
|
||||
res.sendStatus(401);
|
||||
}
|
||||
} catch (_) {
|
||||
res.sendStatus(401);
|
||||
}
|
||||
});
|
||||
|
||||
process.env.TZ = await this.getTimezone();
|
||||
dayjs.tz.setDefault(process.env.TZ);
|
||||
@@ -221,7 +235,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;
|
||||
}
|
||||
@@ -233,7 +247,7 @@ class UptimeKumaServer {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async sendUpdateMonitorIntoList(socket, monitorID) {
|
||||
let list = await this.getMonitorJSONList(socket.userID, monitorID);
|
||||
let list = await this.getMonitorJSONList(monitorID);
|
||||
if (list && list[monitorID]) {
|
||||
this.io.to(socket.userID).emit("updateMonitorIntoList", list);
|
||||
}
|
||||
@@ -250,23 +264,31 @@ class UptimeKumaServer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of monitors for the given user.
|
||||
* @param {string} userID - The ID of the user to get monitors for.
|
||||
* Get a list of monitors, optionally filtered to a single monitor.
|
||||
* @param {number} monitorID - The ID of monitor for.
|
||||
* @returns {Promise<object>} A promise that resolves to an object with monitor IDs as keys and monitor objects as values.
|
||||
*
|
||||
* Generated by Trelent
|
||||
*/
|
||||
async getMonitorJSONList(userID, monitorID = null) {
|
||||
let query = " user_id = ? ";
|
||||
let queryParams = [userID];
|
||||
async getMonitorJSONList(monitorID = null) {
|
||||
let queryKeys = [];
|
||||
let queryParams = [];
|
||||
|
||||
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,70 @@
|
||||
import { createAuthClient } from "better-auth/vue";
|
||||
import { twoFactorClient, usernameClient } from "better-auth/client/plugins";
|
||||
import { apiKeyClient } from "@better-auth/api-key/client";
|
||||
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(), apiKeyClient()],
|
||||
});
|
||||
/**
|
||||
* @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 { data, error } = await authClient.signIn.username({
|
||||
username,
|
||||
password,
|
||||
rememberMe: remember,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
|
||||
if ("twoFactorRedirect" in data) {
|
||||
return "twoFactorRequired";
|
||||
}
|
||||
|
||||
reconnectSocket();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param token Token
|
||||
*/
|
||||
export async function verifyTotp(token: string) {
|
||||
const { error } = await authClient.twoFactor.verifyTotp({
|
||||
code: token,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
|
||||
reconnectSocket();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param onSuccess
|
||||
*/
|
||||
export async function logout(onSuccess = () => {}) {
|
||||
await authClient.signOut({
|
||||
fetchOptions: {
|
||||
onSuccess,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
<h5 class="modal-title">
|
||||
{{ $t("Edit Incident") }}
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" :aria-label="$t('Close')"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form @submit.prevent="submit">
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { login, verifyTotp } from "../auth-client";
|
||||
import HiddenInput from "./HiddenInput.vue";
|
||||
|
||||
export default {
|
||||
@@ -110,18 +111,24 @@ 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) => {
|
||||
this.processing = false;
|
||||
|
||||
if (res.tokenRequired) {
|
||||
this.tokenRequired = true;
|
||||
} else {
|
||||
this.res = res;
|
||||
try {
|
||||
if (this.tokenRequired) {
|
||||
await verifyTotp(this.token);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
const result = await login(this.username, this.password, this.$root.remember);
|
||||
if (result === "twoFactorRequired") {
|
||||
this.tokenRequired = true;
|
||||
}
|
||||
} catch (e) {
|
||||
this.res = { ok: false, msg: e.message };
|
||||
} finally {
|
||||
this.processing = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -318,6 +318,7 @@ export default {
|
||||
// Home Automation - Smart home and IoT platforms
|
||||
let homeAutomation = {
|
||||
HomeAssistant: "Home Assistant",
|
||||
Indigo: "Indigo",
|
||||
};
|
||||
|
||||
// Other Integrations
|
||||
|
||||
@@ -73,22 +73,14 @@
|
||||
|
||||
<div v-if="uri && twoFAStatus == false" class="mt-3">
|
||||
<label for="basic-url" class="form-label">{{ $t("twoFAVerifyLabel") }}</label>
|
||||
<div class="input-group">
|
||||
<input
|
||||
v-model="token"
|
||||
type="text"
|
||||
maxlength="6"
|
||||
class="form-control"
|
||||
autocomplete="one-time-code"
|
||||
required
|
||||
/>
|
||||
<button class="btn btn-outline-primary" type="button" @click="verifyToken()">
|
||||
{{ $t("Verify Token") }}
|
||||
</button>
|
||||
</div>
|
||||
<p v-show="tokenValid" class="mt-2" style="color: green">
|
||||
{{ $t("tokenValidSettingsMsg") }}
|
||||
</p>
|
||||
<input
|
||||
v-model="token"
|
||||
type="text"
|
||||
maxlength="6"
|
||||
class="form-control"
|
||||
autocomplete="one-time-code"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -97,7 +89,7 @@
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
:disabled="processing || tokenValid == false"
|
||||
:disabled="processing"
|
||||
@click="confirmEnableTwoFA()"
|
||||
>
|
||||
<div v-if="processing" class="spinner-border spinner-border-sm me-1"></div>
|
||||
@@ -109,7 +101,7 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<Confirm ref="confirmEnableTwoFA" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="save2FA">
|
||||
<Confirm ref="confirmEnableTwoFA" btn-style="btn-danger" :yes-text="$t('Yes')" :no-text="$t('No')" @yes="enable2FA">
|
||||
{{ $t("confirmEnableTwoFAMsg") }}
|
||||
</Confirm>
|
||||
|
||||
@@ -128,6 +120,7 @@
|
||||
import { Modal } from "bootstrap";
|
||||
import Confirm from "./Confirm.vue";
|
||||
import VueQrcode from "vue-qrcode";
|
||||
import { authClient } from "../auth-client";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -140,8 +133,7 @@ export default {
|
||||
currentPassword: "",
|
||||
processing: false,
|
||||
uri: null,
|
||||
tokenValid: false,
|
||||
twoFAStatus: null,
|
||||
twoFAStatus: null as boolean | null,
|
||||
token: null,
|
||||
showURI: false,
|
||||
};
|
||||
@@ -179,88 +171,86 @@ export default {
|
||||
* Prepare 2FA configuration
|
||||
* @returns {void}
|
||||
*/
|
||||
prepare2FA() {
|
||||
async prepare2FA() {
|
||||
this.processing = true;
|
||||
|
||||
this.$root.getSocket().emit("prepare2FA", this.currentPassword, (res) => {
|
||||
this.processing = false;
|
||||
|
||||
if (res.ok) {
|
||||
this.uri = res.uri;
|
||||
} else {
|
||||
this.$root.toastError(res.msg);
|
||||
}
|
||||
const { data, error } = await authClient.twoFactor.enable({
|
||||
password: this.currentPassword!,
|
||||
issuer: "Uptime Kuma",
|
||||
});
|
||||
|
||||
this.processing = false;
|
||||
|
||||
if (!data || error) {
|
||||
this.$root.toastError(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentPassword = "";
|
||||
this.uri = data.totpURI;
|
||||
},
|
||||
|
||||
/**
|
||||
* Save the current 2FA configuration
|
||||
* Enable 2FA for this user
|
||||
* @returns {void}
|
||||
*/
|
||||
save2FA() {
|
||||
async enable2FA() {
|
||||
this.processing = true;
|
||||
|
||||
this.$root.getSocket().emit("save2FA", this.currentPassword, (res) => {
|
||||
this.processing = false;
|
||||
|
||||
if (res.ok) {
|
||||
this.$root.toastRes(res);
|
||||
this.getStatus();
|
||||
this.currentPassword = "";
|
||||
this.modal.hide();
|
||||
} else {
|
||||
this.$root.toastError(res.msg);
|
||||
}
|
||||
const { error } = await authClient.twoFactor.verifyTotp({
|
||||
code: this.token!,
|
||||
});
|
||||
|
||||
this.processing = false;
|
||||
|
||||
if (error) {
|
||||
this.$root.toastError(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
this.$root.toastSuccess(this.$t("2faEnabled"));
|
||||
this.twoFAStatus = true;
|
||||
this.uri = null;
|
||||
this.token = null;
|
||||
this.modal.hide();
|
||||
},
|
||||
|
||||
/**
|
||||
* Disable 2FA for this user
|
||||
* @returns {void}
|
||||
*/
|
||||
disable2FA() {
|
||||
async disable2FA(): Promise<void> {
|
||||
this.processing = true;
|
||||
|
||||
this.$root.getSocket().emit("disable2FA", this.currentPassword, (res) => {
|
||||
this.processing = false;
|
||||
|
||||
if (res.ok) {
|
||||
this.$root.toastRes(res);
|
||||
this.getStatus();
|
||||
this.currentPassword = "";
|
||||
this.modal.hide();
|
||||
} else {
|
||||
this.$root.toastError(res.msg);
|
||||
}
|
||||
const { error } = await authClient.twoFactor.disable({
|
||||
password: this.currentPassword!,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Verify the token generated by the user
|
||||
* @returns {void}
|
||||
*/
|
||||
verifyToken() {
|
||||
this.$root.getSocket().emit("verifyToken", this.token, this.currentPassword, (res) => {
|
||||
if (res.ok) {
|
||||
this.tokenValid = res.valid;
|
||||
} else {
|
||||
this.$root.toastError(res.msg);
|
||||
}
|
||||
});
|
||||
this.processing = false;
|
||||
|
||||
if (error) {
|
||||
this.$root.toastError(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
this.$root.toastSuccess(this.$t("2faDisabled"));
|
||||
this.twoFAStatus = false;
|
||||
this.currentPassword = "";
|
||||
this.modal.hide();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get current status of 2FA
|
||||
* @returns {void}
|
||||
*/
|
||||
getStatus() {
|
||||
this.$root.getSocket().emit("twoFAStatus", (res) => {
|
||||
if (res.ok) {
|
||||
this.twoFAStatus = res.status;
|
||||
} else {
|
||||
this.$root.toastError(res.msg);
|
||||
}
|
||||
});
|
||||
async getStatus() {
|
||||
const { data, error } = await authClient.getSession();
|
||||
if (error) {
|
||||
this.$root.toastError(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
this.twoFAStatus = data!.user.twoFactorEnabled ?? false;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<label for="indigo-url" class="form-label">
|
||||
{{ $t("Indigo URL") }}
|
||||
<span style="color: red"><sup>*</sup></span>
|
||||
</label>
|
||||
<input
|
||||
id="indigo-url"
|
||||
v-model="$parent.notification.indigoUrl"
|
||||
type="url"
|
||||
class="form-control"
|
||||
placeholder="https://your-reflector.indigodomo.net"
|
||||
required
|
||||
/>
|
||||
<div class="form-text">{{ $t("indigoUrlHelptext") }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input
|
||||
id="indigo-ignore-tls-error"
|
||||
v-model="$parent.notification.indigoIgnoreTlsError"
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label class="form-check-label" for="indigo-ignore-tls-error">
|
||||
{{ $t("Ignore TLS Error") }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-text">{{ $t("indigoIgnoreTlsErrorHelptext") }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="indigo-api-key" class="form-label">
|
||||
{{ $t("API Key") }}
|
||||
<span style="color: red"><sup>*</sup></span>
|
||||
</label>
|
||||
<HiddenInput
|
||||
id="indigo-api-key"
|
||||
v-model="$parent.notification.indigoApiKey"
|
||||
:required="true"
|
||||
autocomplete="new-password"
|
||||
></HiddenInput>
|
||||
<div class="form-text">{{ $t("indigoApiKeyHelptext") }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="indigo-variable-id" class="form-label">{{ $t("Variable ID") }}</label>
|
||||
<input
|
||||
id="indigo-variable-id"
|
||||
v-model="$parent.notification.indigoVariableId"
|
||||
type="number"
|
||||
class="form-control"
|
||||
min="1"
|
||||
/>
|
||||
<div class="form-text">{{ $t("indigoVariableIdHelptext") }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="indigo-action-group-id" class="form-label">{{ $t("Action Group ID") }}</label>
|
||||
<input
|
||||
id="indigo-action-group-id"
|
||||
v-model="$parent.notification.indigoActionGroupId"
|
||||
type="number"
|
||||
class="form-control"
|
||||
min="1"
|
||||
/>
|
||||
<div class="form-text">{{ $t("indigoActionGroupIdHelptext") }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HiddenInput from "../HiddenInput.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HiddenInput,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -25,6 +25,7 @@ import Gotify from "./Gotify.vue";
|
||||
import GrafanaOncall from "./GrafanaOncall.vue";
|
||||
import GtxMessaging from "./GtxMessaging.vue";
|
||||
import HomeAssistant from "./HomeAssistant.vue";
|
||||
import Indigo from "./Indigo.vue";
|
||||
import HeiiOnCall from "./HeiiOnCall.vue";
|
||||
import Keep from "./Keep.vue";
|
||||
import Kook from "./Kook.vue";
|
||||
@@ -139,6 +140,7 @@ const NotificationFormList = {
|
||||
gotify: Gotify,
|
||||
GrafanaOncall: GrafanaOncall,
|
||||
HomeAssistant: HomeAssistant,
|
||||
Indigo: Indigo,
|
||||
HeiiOnCall: HeiiOnCall,
|
||||
Keep: Keep,
|
||||
Kook: Kook,
|
||||
|
||||
@@ -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 = "";
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -220,7 +220,6 @@ export default {
|
||||
this.saveSettings(() => {
|
||||
this.password.currentPassword = "";
|
||||
this.$root.username = null;
|
||||
this.$root.socket.token = "autoLogin";
|
||||
}, this.password.currentPassword);
|
||||
},
|
||||
|
||||
@@ -231,7 +230,6 @@ export default {
|
||||
enableAuth() {
|
||||
this.settings.disableAuth = false;
|
||||
this.saveSettings();
|
||||
this.$root.storage().removeItem("token");
|
||||
location.reload();
|
||||
},
|
||||
|
||||
|
||||
@@ -526,6 +526,14 @@
|
||||
"wayToGetBaleToken": "You can get a token from {0}.",
|
||||
"Home Assistant URL": "Home Assistant URL",
|
||||
"Long-Lived Access Token": "Long-Lived Access Token",
|
||||
"Indigo URL": "Indigo URL",
|
||||
"indigoUrlHelptext": "Your Indigo Reflector address (https://NAME.indigodomo.net) or the Indigo Web Server on your network (https://IP:8176).",
|
||||
"indigoIgnoreTlsErrorHelptext": "Needed for the Indigo Web Server on your network, which uses a self-signed certificate. Not needed for the Reflector.",
|
||||
"indigoApiKeyHelptext": "An API key from your Indigo Account (Authorizations), or a local secret for the Web Server on your network.",
|
||||
"Variable ID": "Variable ID",
|
||||
"indigoVariableIdHelptext": "The alert message is written to this variable.",
|
||||
"Action Group ID": "Action Group ID",
|
||||
"indigoActionGroupIdHelptext": "Executed after the variable is updated. Set a variable, an action group, or both. Copy IDs from Indigo with right-click → Copy ID.",
|
||||
"Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token. ": "Long-Lived Access Token can be created by clicking on your profile name (bottom left) and scrolling to the bottom then click Create Token.",
|
||||
"Notification Action": "Notification Action",
|
||||
"default: notify all devices": "default: notify all devices",
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li v-if="$root.loggedIn && $root.socket.token !== 'autoLogin'">
|
||||
<li v-if="$root.loggedIn">
|
||||
<button class="dropdown-item" @click="$root.logout">
|
||||
<font-awesome-icon icon="sign-out-alt" />
|
||||
{{ $t("Logout") }}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
+46
-158
@@ -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,40 +103,32 @@ 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;
|
||||
});
|
||||
|
||||
socket.on("setup", (monitorID, data) => {
|
||||
socket.on("setup", () => {
|
||||
this.$router.push("/setup");
|
||||
});
|
||||
|
||||
socket.on("autoLogin", (monitorID, data) => {
|
||||
socket.on("session", (username) => {
|
||||
this.loggedIn = true;
|
||||
this.storage().token = "autoLogin";
|
||||
this.socket.token = "autoLogin";
|
||||
this.allowLoginDialog = false;
|
||||
this.username = username;
|
||||
});
|
||||
|
||||
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 +254,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 +320,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 +376,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 +740,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 +769,14 @@ export default {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Reconnect the socket.io connection.
|
||||
* Usage: Reconnect socket.io with auth-ed cookie, or drop the auth-ed session.
|
||||
*/
|
||||
export function reconnectSocket() {
|
||||
if (socket) {
|
||||
socket.disconnect();
|
||||
socket.connect();
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,12 @@ export default {
|
||||
components: {
|
||||
Confirm,
|
||||
},
|
||||
props: {
|
||||
calculatedHeight: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedStatusSlug: "",
|
||||
|
||||
@@ -25,11 +25,7 @@
|
||||
</router-link>
|
||||
|
||||
<!-- Logout Button -->
|
||||
<a
|
||||
v-if="$root.isMobile && $root.loggedIn && $root.socket.token !== 'autoLogin'"
|
||||
class="logout"
|
||||
@click.prevent="$root.logout"
|
||||
>
|
||||
<a v-if="$root.isMobile && $root.loggedIn" class="logout" @click.prevent="$root.logout">
|
||||
<div class="menu-item">
|
||||
<font-awesome-icon icon="sign-out-alt" />
|
||||
{{ $t("Logout") }}
|
||||
|
||||
+44
-17
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="form-container" data-cy="setup-form">
|
||||
<div v-if="ready" class="form-container">
|
||||
<div class="form">
|
||||
<form @submit.prevent="submit">
|
||||
<div>
|
||||
@@ -73,9 +73,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { checkFetch } from "../util";
|
||||
import { authClient, baseURL, login } from "../auth-client";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
ready: false,
|
||||
processing: false,
|
||||
username: "",
|
||||
password: "",
|
||||
@@ -83,21 +87,29 @@ export default {
|
||||
};
|
||||
},
|
||||
watch: {},
|
||||
mounted() {
|
||||
// TODO: Check if it is a database setup
|
||||
|
||||
async mounted() {
|
||||
this.$root.getSocket().emit("needSetup", (needSetup) => {
|
||||
console.log(needSetup);
|
||||
if (!needSetup) {
|
||||
this.$router.push("/");
|
||||
} else {
|
||||
this.ready = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Check if /setup-database-info is available, if so, redirect to it
|
||||
const res = await fetch(baseURL + "/setup-database-info");
|
||||
const data = await res.json();
|
||||
if (res.ok && data.needSetup) {
|
||||
this.$router.push("/setup-database");
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Submit form data for processing
|
||||
* @returns {void}
|
||||
*/
|
||||
submit() {
|
||||
async submit() {
|
||||
this.processing = true;
|
||||
|
||||
if (this.password !== this.repeatPassword) {
|
||||
@@ -106,19 +118,34 @@ 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);
|
||||
|
||||
// Redirect to home page
|
||||
this.$router.push("/");
|
||||
} 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("/");
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Loaded 100 of 131 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user