diff --git a/.eslintrc.js b/.eslintrc.js index 5da22e72d7..01a3135ef1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -162,6 +162,8 @@ module.exports = { "jsdoc/require-param-type": "off", "@typescript-eslint/no-explicit-any": "off", "prefer-const": "off", + "@typescript-eslint/no-unused-vars": "warn", + "eqeqeq": "off", } } ] diff --git a/db/knex_migrations/2025-10-29-0000-better-auth.js b/db/knex_migrations/2025-10-29-0000-better-auth.js new file mode 100644 index 0000000000..bb12880148 --- /dev/null +++ b/db/knex_migrations/2025-10-29-0000-better-auth.js @@ -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("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("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"); +}; diff --git a/package-lock.json b/package-lock.json index fa58807471..cdb8665506 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "axios": "~0.30.0", "badge-maker": "~3.3.1", "bcryptjs": "~2.4.3", + "better-auth": "~1.4.0-beta.18", "chardet": "~1.4.0", "check-password-strength": "^2.0.5", "cheerio": "~1.0.0-rc.12", @@ -1278,6 +1279,46 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/@better-auth/core": { + "version": "1.4.0-beta.18", + "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.4.0-beta.18.tgz", + "integrity": "sha512-2jukNv/JMsTbB49z/tr8cqmvNQgy/lfnoihYQsyNAJwtWhub0vQ/OCvAn0QTD1S+EH5ACqLQEQJDydSRWdGAwA==", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "zod": "^4.1.5" + }, + "peerDependencies": { + "@better-auth/utils": "0.3.0", + "@better-fetch/fetch": "1.1.18", + "better-call": "1.0.26", + "jose": "^6.1.0", + "kysely": "^0.28.5", + "nanostores": "^1.0.1" + } + }, + "node_modules/@better-auth/telemetry": { + "version": "1.4.0-beta.18", + "resolved": "https://registry.npmjs.org/@better-auth/telemetry/-/telemetry-1.4.0-beta.18.tgz", + "integrity": "sha512-IaakqUqzwzwp9kSzZbAnrQUIwHSkGXhV4ON1mmJOgZ3Yl+ti8/HcV+83RFZXPtThUs3C5h0Km6Q0t4M5LSr69A==", + "dependencies": { + "@better-auth/utils": "0.3.0", + "@better-fetch/fetch": "1.1.18" + }, + "peerDependencies": { + "@better-auth/core": "1.4.0-beta.18" + } + }, + "node_modules/@better-auth/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==", + "license": "MIT" + }, + "node_modules/@better-fetch/fetch": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.18.tgz", + "integrity": "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==" + }, "node_modules/@csstools/css-parser-algorithms": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.7.1.tgz", @@ -3845,6 +3886,12 @@ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, "node_modules/@szmarczak/http-timer": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", @@ -5485,6 +5532,97 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/better-auth": { + "version": "1.4.0-beta.18", + "resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.4.0-beta.18.tgz", + "integrity": "sha512-cniYRN7KalxQwfGfEsGDrvivnAJ9OmcxaB4WMasWYTBuWPWg+ttLWLzWhWY2CUQfSRBzYK+Aao3lB2ppF6rs0Q==", + "license": "MIT", + "dependencies": { + "@better-auth/core": "1.4.0-beta.18", + "@better-auth/telemetry": "1.4.0-beta.18", + "@better-auth/utils": "0.3.0", + "@better-fetch/fetch": "1.1.18", + "@noble/ciphers": "^2.0.0", + "@noble/hashes": "^2.0.0", + "@standard-schema/spec": "^1.0.0", + "better-call": "1.0.26", + "defu": "^6.1.4", + "jose": "^6.1.0", + "kysely": "^0.28.5", + "nanostores": "^1.0.1", + "zod": "^4.1.5" + }, + "peerDependenciesMeta": { + "@lynx-js/react": { + "optional": true + }, + "@sveltejs/kit": { + "optional": true + }, + "next": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "solid-js": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/better-auth/node_modules/@noble/ciphers": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.0.1.tgz", + "integrity": "sha512-xHK3XHPUW8DTAobU+G0XT+/w+JLM7/8k1UFdB5xg/zTFPnFCobhftzw8wl4Lw2aq/Rvir5pxfZV5fEazmeCJ2g==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/better-auth/node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/better-auth/node_modules/jose": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.0.tgz", + "integrity": "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/better-call": { + "version": "1.0.26", + "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.0.26.tgz", + "integrity": "sha512-/5AaTPC8IRXV5yWxpAI7eR2RNiGHq4q/mjBm9DiikIkmJhzuqO1Ub66oYYqJ3eBFF+6BfdtZFRnuW0me8T0Emg==", + "dependencies": { + "@better-auth/utils": "^0.3.0", + "@better-fetch/fetch": "^1.1.4", + "rou3": "^0.5.1", + "set-cookie-parser": "^2.7.1" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -7016,6 +7154,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "license": "MIT" + }, "node_modules/delay": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", @@ -10678,6 +10822,15 @@ "dev": true, "license": "MIT" }, + "node_modules/kysely": { + "version": "0.28.8", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.8.tgz", + "integrity": "sha512-QUOgl5ZrS9IRuhq5FvOKFSsD/3+IA6MLE81/bOOTRA/YQpKDza2sFdN5g6JCB9BOpqMJDGefLCQ9F12hRS13TA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/lazystream": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", @@ -11655,6 +11808,21 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/nanostores": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/nanostores/-/nanostores-1.0.1.tgz", + "integrity": "sha512-kNZ9xnoJYKg/AfxjrVL4SS0fKX++4awQReGqWnwTRHxeHGZ1FJFVgTqr/eMrNQdp0Tz7M7tG/TDaX8QfHDwVCw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": "^20.0.0 || >=22.0.0" + } + }, "node_modules/native-duplexpair": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/native-duplexpair/-/native-duplexpair-1.0.0.tgz", @@ -13889,6 +14057,12 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/rou3": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/rou3/-/rou3-0.5.1.tgz", + "integrity": "sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==", + "license": "MIT" + }, "node_modules/rtlcss": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-3.5.0.tgz", @@ -14188,6 +14362,12 @@ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "license": "ISC" }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", diff --git a/package.json b/package.json index a625ed853c..251a316a95 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "axios": "~0.30.0", "badge-maker": "~3.3.1", "bcryptjs": "~2.4.3", + "better-auth": "~1.4.0-beta.18", "chardet": "~1.4.0", "check-password-strength": "^2.0.5", "cheerio": "~1.0.0-rc.12", diff --git a/server/better-auth.ts b/server/better-auth.ts new file mode 100644 index 0000000000..9f5c990475 --- /dev/null +++ b/server/better-auth.ts @@ -0,0 +1,55 @@ +import { betterAuth } from "better-auth"; +import { DatabaseSync } from "node:sqlite"; +import * as Database from "./database.js"; +import { genSecret, log } from "../src/util"; +import { createPool } from "mysql2/promise"; + +// 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."); +} + +let database: DatabaseSync; + +// Better Auth is not supported with knex, so we need to create a separate connection here +if (Database.dbConfig.type == "sqlite") { + log.debug("better-auth", "Initializing better-auth with SQLite database at", Database.sqlitePath); + database = new DatabaseSync(Database.sqlitePath); + +} else if (Database.dbConfig.type == "mariadb") { + + // TODO + +} else if (Database.dbConfig.type == "embedded-mariadb") { + log.debug("better-auth", "Initializing better-auth with MariaDB database"); + + // TODO + +} + +export const auth = betterAuth({ + database, + secret: getAuthSecret(), + trustedOrigins: [ "*" ], + emailAndPassword: { + enabled: true, + }, +}); + +/** + * 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; +} diff --git a/server/server.js b/server/server.js index 6d4149ee71..5fbbdf390a 100644 --- a/server/server.js +++ b/server/server.js @@ -188,6 +188,9 @@ let needSetup = false; process.exit(1); } + // Init Better Auth + const { auth } = await import("./better-auth"); + // Database should be ready now await server.initAfterDatabaseReady(); server.entryPage = await Settings.get("entryPage"); diff --git a/src/util.ts b/src/util.ts index 6bf9501b58..742f408d1a 100644 --- a/src/util.ts +++ b/src/util.ts @@ -242,7 +242,7 @@ class Logger { * @param msg Message to write * @returns {void} */ - log(module: string, level: string, ...msg: unknown[]) { + private log(module: string, level: string, ...msg: unknown[]) { if (level === "DEBUG" && !isDev) { return; }