diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c79e917e..f7804245 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,5 +53,8 @@ jobs: - name: Lint run: pnpm run lint + + - name: Check Typescript + run: pnpm run check-ts # more things can be add later like tests etc.. diff --git a/backend/database.ts b/backend/database.ts index 814e61cb..a63bc18c 100644 --- a/backend/database.ts +++ b/backend/database.ts @@ -5,6 +5,7 @@ import fs from "fs"; import path from "path"; import knex from "knex"; +// @ts-ignore import Dialect from "knex/lib/dialects/sqlite3/index.js"; import sqlite from "@louislam/sqlite3"; @@ -12,6 +13,11 @@ import { sleep } from "./util-common"; interface DBConfig { type?: "sqlite" | "mysql"; + hostname?: string; + port?: string; + database?: string; + username?: string; + password?: string; } export class Database { @@ -19,7 +25,7 @@ export class Database { * SQLite file path (Default: ./data/dockge.db) * @type {string} */ - static sqlitePath; + static sqlitePath : string; static noReject = true; @@ -51,7 +57,7 @@ export class Database { * @typedef {string|undefined} envString * @returns {{type: "sqlite"} | {type:envString, hostname:envString, port:envString, database:envString, username:envString, password:envString}} Database config */ - static readDBConfig() { + static readDBConfig() : DBConfig { const dbConfigString = fs.readFileSync(path.join(this.server.config.dataDir, "db-config.json")).toString("utf-8"); const dbConfig = JSON.parse(dbConfigString); @@ -67,10 +73,10 @@ export class Database { /** * @typedef {string|undefined} envString - * @param {{type: "sqlite"} | {type:envString, hostname:envString, port:envString, database:envString, username:envString, password:envString}} dbConfig the database configuration that should be written + * @param dbConfig the database configuration that should be written * @returns {void} */ - static writeDBConfig(dbConfig) { + static writeDBConfig(dbConfig : DBConfig) { fs.writeFileSync(path.join(this.server.config.dataDir, "db-config.json"), JSON.stringify(dbConfig, null, 4)); } @@ -82,12 +88,15 @@ export class Database { */ static async connect(autoloadModels = true) { const acquireConnectionTimeout = 120 * 1000; - let dbConfig; + let dbConfig : DBConfig; try { dbConfig = this.readDBConfig(); Database.dbConfig = dbConfig; } catch (err) { - log.warn("db", err.message); + if (err instanceof Error) { + log.warn("db", err.message); + } + dbConfig = { type: "sqlite", }; @@ -176,13 +185,15 @@ export class Database { directory: Database.knexMigrationsPath, }); } catch (e) { - // Allow missing patch files for downgrade or testing pr. - if (e.message.includes("the following files are missing:")) { - log.warn("db", e.message); - log.warn("db", "Database migration failed, you may be downgrading Dockge."); - } else { - log.error("db", "Database migration failed"); - throw e; + if (e instanceof Error) { + // Allow missing patch files for downgrade or testing pr. + if (e.message.includes("the following files are missing:")) { + log.warn("db", e.message); + log.warn("db", "Database migration failed, you may be downgrading Dockge."); + } else { + log.error("db", "Database migration failed"); + throw e; + } } } } diff --git a/backend/dockge-server.ts b/backend/dockge-server.ts index ea781ffb..b757c46f 100644 --- a/backend/dockge-server.ts +++ b/backend/dockge-server.ts @@ -60,7 +60,7 @@ export class DockgeServer { */ needSetup = false; - jwtSecret? : string; + jwtSecret : string = ""; stacksDir : string = ""; @@ -129,7 +129,7 @@ export class DockgeServer { this.config.sslKey = args.sslKey || process.env.DOCKGE_SSL_KEY || undefined; this.config.sslCert = args.sslCert || process.env.DOCKGE_SSL_CERT || undefined; this.config.sslKeyPassphrase = args.sslKeyPassphrase || process.env.DOCKGE_SSL_KEY_PASSPHRASE || undefined; - this.config.port = args.port || parseInt(process.env.DOCKGE_PORT) || 5001; + this.config.port = args.port || Number(process.env.DOCKGE_PORT) || 5001; this.config.hostname = args.hostname || process.env.DOCKGE_HOSTNAME || undefined; this.config.dataDir = args.dataDir || process.env.DOCKGE_DATA_DIR || "./data/"; this.config.stacksDir = args.stacksDir || process.env.DOCKGE_STACKS_DIR || defaultStacksDir; @@ -218,7 +218,7 @@ export class DockgeServer { log.debug("auth", "check auto login"); if (await Settings.get("disableAuth")) { log.info("auth", "Disabled Auth: auto login to admin"); - this.afterLogin(socket as DockgeSocket, await R.findOne("user")); + this.afterLogin(socket as DockgeSocket, await R.findOne("user") as User); socket.emit("autoLogin"); } else { log.debug("auth", "need auth"); @@ -253,7 +253,9 @@ export class DockgeServer { try { await Database.init(this); } catch (e) { - log.error("server", "Failed to prepare your database: " + e.message); + if (e instanceof Error) { + log.error("server", "Failed to prepare your database: " + e.message); + } process.exit(1); } @@ -376,7 +378,9 @@ export class DockgeServer { return process.env.TZ; } } catch (e) { - log.warn("timezone", e.message + " in process.env.TZ"); + if (e instanceof Error) { + log.warn("timezone", e.message + " in process.env.TZ"); + } } const timezone = await Settings.get("serverTimezone"); @@ -389,7 +393,9 @@ export class DockgeServer { return timezone; } } catch (e) { - log.warn("timezone", e.message + " in settings"); + if (e instanceof Error) { + log.warn("timezone", e.message + " in settings"); + } } // Guess diff --git a/backend/password-hash.ts b/backend/password-hash.ts index fb8d42d3..3a174401 100644 --- a/backend/password-hash.ts +++ b/backend/password-hash.ts @@ -17,7 +17,7 @@ export function generatePasswordHash(password : string) { * @param {string} hash Hash to verify against * @returns {boolean} Does the password match the hash? */ -export function verifyPassword(password, hash) { +export function verifyPassword(password : string, hash : string) { return bcrypt.compareSync(password, hash); } @@ -37,7 +37,7 @@ export const SHAKE256_LENGTH = 16; * @param {number} len Output length of the hash * @returns {string} The hashed data in hex format */ -export function shake256(data, len) { +export function shake256(data : string, len : number) { if (!data) { return ""; } diff --git a/backend/rate-limiter.ts b/backend/rate-limiter.ts index 3c43abe7..9bfa8d28 100644 --- a/backend/rate-limiter.ts +++ b/backend/rate-limiter.ts @@ -1,8 +1,14 @@ // "limit" is bugged in Typescript, use "limiter-es6-compat" instead // See https://github.com/jhurliman/node-rate-limiter/issues/80 -import { RateLimiter } from "limiter-es6-compat"; +import { RateLimiter, RateLimiterOpts } from "limiter-es6-compat"; import { log } from "./log"; +export interface KumaRateLimiterOpts extends RateLimiterOpts { + errorMessage : string; +} + +export type KumaRateLimiterCallback = (err : object) => void; + class KumaRateLimiter { errorMessage : string; @@ -11,7 +17,7 @@ class KumaRateLimiter { /** * @param {object} config Rate limiter configuration object */ - constructor(config) { + constructor(config : KumaRateLimiterOpts) { this.errorMessage = config.errorMessage; this.rateLimiter = new RateLimiter(config); } @@ -24,11 +30,11 @@ class KumaRateLimiter { /** * Should the request be passed through - * @param {passCB} callback Callback function to call with decision + * @param callback Callback function to call with decision * @param {number} num Number of tokens to remove * @returns {Promise} Should the request be allowed? */ - async pass(callback, num = 1) { + async pass(callback : KumaRateLimiterCallback, num = 1) { const remainingRequests = await this.removeTokens(num); log.info("rate-limit", "remaining requests: " + remainingRequests); if (remainingRequests < 0) { diff --git a/backend/routers/main-router.ts b/backend/routers/main-router.ts index 8d791dbe..f882f948 100644 --- a/backend/routers/main-router.ts +++ b/backend/routers/main-router.ts @@ -1,4 +1,4 @@ -import { DockgeServer } from "../dockgeServer"; +import { DockgeServer } from "../dockge-server"; import { Router } from "../router"; import express, { Express, Router as ExpressRouter } from "express"; diff --git a/backend/settings.ts b/backend/settings.ts index fed94a70..c1703dcb 100644 --- a/backend/settings.ts +++ b/backend/settings.ts @@ -1,5 +1,6 @@ import { R } from "redbean-node"; import { log } from "./log"; +import { LooseObject } from "./util-common"; export class Settings { @@ -15,20 +16,19 @@ export class Settings { * timestamp: 12345678 * }, * } - * @type {{}} */ - static cacheList = { + static cacheList : LooseObject = { }; - static cacheCleaner = null; + static cacheCleaner? : NodeJS.Timeout; /** * Retrieve value of setting based on key - * @param {string} key Key of setting to retrieve - * @returns {Promise} Value + * @param key Key of setting to retrieve + * @returns Value */ - static async get(key) { + static async get(key : string) { // Start cache clear if not started yet if (!Settings.cacheCleaner) { @@ -72,12 +72,12 @@ export class Settings { /** * Sets the specified setting to specified value - * @param {string} key Key of setting to set - * @param {any} value Value to set to + * @param key Key of setting to set + * @param value Value to set to * @param {?string} type Type of setting * @returns {Promise} */ - static async set(key, value, type = null) { + static async set(key : string, value : object | string | number | boolean, type : string | null = null) { let bean = await R.findOne("setting", " `key` = ? ", [ key, @@ -95,15 +95,15 @@ export class Settings { /** * Get settings based on type - * @param {string} type The type of setting - * @returns {Promise} Settings + * @param type The type of setting + * @returns Settings */ - static async getSettings(type) { + static async getSettings(type : string) { const list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [ type, ]); - const result = {}; + const result : LooseObject = {}; for (const row of list) { try { @@ -118,11 +118,11 @@ export class Settings { /** * Set settings based on type - * @param {string} type Type of settings to set - * @param {object} data Values of settings + * @param type Type of settings to set + * @param data Values of settings * @returns {Promise} */ - static async setSettings(type, data) { + static async setSettings(type : string, data : LooseObject) { const keyList = Object.keys(data); const promiseList = []; @@ -154,7 +154,7 @@ export class Settings { * @param {string[]} keyList Keys to remove * @returns {void} */ - static deleteCache(keyList) { + static deleteCache(keyList : string[]) { for (const key of keyList) { delete Settings.cacheList[key]; } @@ -167,7 +167,7 @@ export class Settings { static stopCacheCleaner() { if (Settings.cacheCleaner) { clearInterval(Settings.cacheCleaner); - Settings.cacheCleaner = null; + Settings.cacheCleaner = undefined; } } } diff --git a/backend/socket-handlers/main-socket-handler.ts b/backend/socket-handlers/main-socket-handler.ts index 15478409..bfdb45d3 100644 --- a/backend/socket-handlers/main-socket-handler.ts +++ b/backend/socket-handlers/main-socket-handler.ts @@ -5,7 +5,7 @@ import { R } from "redbean-node"; import { loginRateLimiter, twoFaRateLimiter } from "../rate-limiter"; import { generatePasswordHash, needRehashPassword, shake256, SHAKE256_LENGTH, verifyPassword } from "../password-hash"; import { User } from "../models/user"; -import { checkLogin, DockgeSocket, doubleCheckPassword } from "../util-server"; +import { checkLogin, DockgeSocket, doubleCheckPassword, JWTDecoded } from "../util-server"; import { passwordStrength } from "check-password-strength"; import jwt from "jsonwebtoken"; import { Settings } from "../settings"; @@ -42,10 +42,12 @@ export class MainSocketHandler extends SocketHandler { }); } catch (e) { - callback({ - ok: false, - msg: e.message, - }); + if (e instanceof Error) { + callback({ + ok: false, + msg: e.message, + }); + } } }); @@ -56,7 +58,7 @@ export class MainSocketHandler extends SocketHandler { log.info("auth", `Login by token. IP=${clientIP}`); try { - const decoded = jwt.verify(token, server.jwtSecret); + const decoded = jwt.verify(token, server.jwtSecret) as JWTDecoded; log.info("auth", "Username from JWT: " + decoded.username); @@ -90,9 +92,13 @@ export class MainSocketHandler extends SocketHandler { }); } } catch (error) { + if (!(error instanceof Error)) { + console.error("Unknown error:", error); + return; + } log.error("auth", `Invalid token. IP=${clientIP}`); if (error.message) { - log.error("auth", error.message, `IP=${clientIP}`); + log.error("auth", error.message + ` IP=${clientIP}`); } callback({ ok: false, @@ -148,6 +154,7 @@ export class MainSocketHandler extends SocketHandler { } if (data.token) { + // @ts-ignore const verify = notp.totp.verify(data.token, user.twofa_secret, twoFAVerifyOptions); if (user.twofa_last_token !== data.token && verify) { @@ -210,10 +217,12 @@ export class MainSocketHandler extends SocketHandler { }); } catch (e) { - callback({ - ok: false, - msg: e.message, - }); + if (e instanceof Error) { + callback({ + ok: false, + msg: e.message, + }); + } } }); @@ -228,10 +237,12 @@ export class MainSocketHandler extends SocketHandler { }); } catch (e) { - callback({ - ok: false, - msg: e.message, - }); + if (e instanceof Error) { + callback({ + ok: false, + msg: e.message, + }); + } } }); @@ -261,22 +272,24 @@ export class MainSocketHandler extends SocketHandler { server.sendInfo(socket); } catch (e) { - callback({ - ok: false, - msg: e.message, - }); + if (e instanceof Error) { + callback({ + ok: false, + msg: e.message, + }); + } } }); } - async login(username : string, password : string) { + async login(username : string, password : string) : Promise { if (typeof username !== "string" || typeof password !== "string") { return null; } const user = await R.findOne("user", " username = ? AND active = 1 ", [ username, - ]); + ]) as User; if (user && verifyPassword(password, user.password)) { // Upgrade the hash to bcrypt diff --git a/backend/socket-handlers/terminal-socket-handler.ts b/backend/socket-handlers/terminal-socket-handler.ts index f647dfb4..0a0485b5 100644 --- a/backend/socket-handlers/terminal-socket-handler.ts +++ b/backend/socket-handlers/terminal-socket-handler.ts @@ -38,10 +38,12 @@ export class TerminalSocketHandler extends SocketHandler { throw new Error("Terminal not found or it is not a Interactive Terminal."); } } catch (e) { - errorCallback({ - ok: false, - msg: e.message, - }); + if (e instanceof Error) { + errorCallback({ + ok: false, + msg: e.message, + }); + } } }); diff --git a/backend/stack.ts b/backend/stack.ts index af1b745e..fde543ae 100644 --- a/backend/stack.ts +++ b/backend/stack.ts @@ -197,7 +197,9 @@ export class Stack { stack._status = CREATED_FILE; stackList.set(filename, stack); } catch (e) { - log.warn("getStackList", `Failed to get stack ${filename}, error: ${e.message}`); + if (e instanceof Error) { + log.warn("getStackList", `Failed to get stack ${filename}, error: ${e.message}`); + } } } diff --git a/backend/terminal.ts b/backend/terminal.ts index 2a31f177..66831b9f 100644 --- a/backend/terminal.ts +++ b/backend/terminal.ts @@ -54,7 +54,9 @@ export class Terminal { try { this.ptyProcess?.resize(this.cols, this.rows); } catch (e) { - log.debug("Terminal", "Failed to resize terminal: " + e.message); + if (e instanceof Error) { + log.debug("Terminal", "Failed to resize terminal: " + e.message); + } } } @@ -67,7 +69,9 @@ export class Terminal { try { this.ptyProcess?.resize(this.cols, this.rows); } catch (e) { - log.debug("Terminal", "Failed to resize terminal: " + e.message); + if (e instanceof Error) { + log.debug("Terminal", "Failed to resize terminal: " + e.message); + } } } @@ -85,7 +89,7 @@ export class Terminal { // On Data this._ptyProcess.onData((data) => { - this.buffer.push(data); + this.buffer.pushItem(data); if (this.server.io) { this.server.io.to(this.name).emit("terminalWrite", this.name, data); } diff --git a/backend/util-common.ts b/backend/util-common.ts index ad963d70..a254b8d9 100644 --- a/backend/util-common.ts +++ b/backend/util-common.ts @@ -12,6 +12,11 @@ dayjs.extend(utc); dayjs.extend(timezone); dayjs.extend(relativeTime); +export interface LooseObject { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any +} + let randomBytes : (numBytes: number) => Uint8Array; initRandomBytes(); diff --git a/backend/util-server.ts b/backend/util-server.ts index 241782c7..04d34dbc 100644 --- a/backend/util-server.ts +++ b/backend/util-server.ts @@ -6,6 +6,11 @@ import { ERROR_TYPE_VALIDATION } from "./util-common"; import { R } from "redbean-node"; import { verifyPassword } from "./password-hash"; +export interface JWTDecoded { + username : string; + h? : string; +} + export interface DockgeSocket extends Socket { userID: number; consoleTerminal? : Terminal; diff --git a/backend/utils/limit-queue.ts b/backend/utils/limit-queue.ts index 8a1a95d0..29130510 100644 --- a/backend/utils/limit-queue.ts +++ b/backend/utils/limit-queue.ts @@ -4,14 +4,14 @@ */ export class LimitQueue extends Array { __limit; - __onExceed = null; + __onExceed? : (item : T | undefined) => void; constructor(limit: number) { super(); this.__limit = limit; } - push(value : T) { + pushItem(value : T) { super.push(value); if (this.length > this.__limit) { const item = this.shift(); diff --git a/frontend/src/util-frontend.ts b/frontend/src/util-frontend.ts index e55ebf05..8b1f113d 100644 --- a/frontend/src/util-frontend.ts +++ b/frontend/src/util-frontend.ts @@ -10,7 +10,7 @@ import { POSITION } from "vue-toastification"; * * Generated by Trelent */ -function getTimezoneOffset(timeZone) { +function getTimezoneOffset(timeZone : string) { const now = new Date(); const tzString = now.toLocaleString("en-US", { timeZone, @@ -124,33 +124,6 @@ export function hostNameRegexPattern(mqtt = false) { return `${ipRegexPattern}|${hostNameRegexPattern}`; } -/** - * Get the tag color options - * Shared between components - * @param {any} self Component - * @returns {object[]} Colour options - */ -export function colorOptions(self) { - return [ - { name: self.$t("Gray"), - color: "#4B5563" }, - { name: self.$t("Red"), - color: "#DC2626" }, - { name: self.$t("Orange"), - color: "#D97706" }, - { name: self.$t("Green"), - color: "#059669" }, - { name: self.$t("Blue"), - color: "#2563EB" }, - { name: self.$t("Indigo"), - color: "#4F46E5" }, - { name: self.$t("Purple"), - color: "#7C3AED" }, - { name: self.$t("Pink"), - color: "#DB2777" }, - ]; -} - /** * Loads the toast timeout settings from storage. * @returns {object} The toast plugin options object. diff --git a/package.json b/package.json index b8b2e565..3423654c 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "scripts": { "fmt": "eslint \"**/*.{ts,vue}\" --fix", "lint": "eslint \"**/*.{ts,vue}\"", + "check-ts": "tsc --noEmit", "start": "tsx ./backend/index.ts", "dev:backend": "cross-env NODE_ENV=development tsx watch ./backend/index.ts", "dev:frontend": "cross-env NODE_ENV=development vite --host --config ./frontend/vite.config.ts", @@ -33,8 +34,8 @@ "jwt-decode": "~3.1.2", "knex": "~2.5.1", "limiter-es6-compat": "~2.1.2", - "mysql2": "^3.6.3", - "redbean-node": "0.3.2", + "mysql2": "~3.6.3", + "redbean-node": "~0.3.3", "socket.io": "~4.7.2", "socket.io-client": "~4.7.2", "timezones-list": "~3.0.2", @@ -49,6 +50,7 @@ "@fortawesome/free-regular-svg-icons": "6.4.2", "@fortawesome/free-solid-svg-icons": "6.4.2", "@fortawesome/vue-fontawesome": "3.0.3", + "@types/bcryptjs": "^2.4.6", "@types/bootstrap": "~5.2.9", "@types/command-exists": "~1.2.3", "@types/express": "~4.17.21", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7ed40fa3..1b045867 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,11 +54,11 @@ dependencies: specifier: ~2.1.2 version: 2.1.2 mysql2: - specifier: ^3.6.3 + specifier: ~3.6.3 version: 3.6.3 redbean-node: - specifier: 0.3.2 - version: 0.3.2(mysql2@3.6.3) + specifier: ~0.3.3 + version: 0.3.3(mysql2@3.6.3) socket.io: specifier: ~4.7.2 version: 4.7.2 @@ -97,6 +97,9 @@ devDependencies: '@fortawesome/vue-fontawesome': specifier: 3.0.3 version: 3.0.3(@fortawesome/fontawesome-svg-core@6.4.2)(vue@3.3.8) + '@types/bcryptjs': + specifier: ^2.4.6 + version: 2.4.6 '@types/bootstrap': specifier: ~5.2.9 version: 5.2.9 @@ -1013,6 +1016,10 @@ packages: dev: false optional: true + /@types/bcryptjs@2.4.6: + resolution: {integrity: sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==} + dev: true + /@types/body-parser@1.19.5: resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} dependencies: @@ -3847,8 +3854,8 @@ packages: resolve: 1.22.8 dev: false - /redbean-node@0.3.2(mysql2@3.6.3): - resolution: {integrity: sha512-39VMxPWPpPicRlU4FSJJnJuUMoxw5/4envFthHtKnLe+3qWTBje3RMrJTFZcQGLruWQ/s2LgeYzdd+d0O+p+uQ==} + /redbean-node@0.3.3(mysql2@3.6.3): + resolution: {integrity: sha512-0J59/QlShdWs1h0lsFHRfb8NwjvgIYTQKwYrvq6FykRmeX1cG2u8AgHEIRVBrm56mtKLRASVy/8ykk6fSntLdw==} dependencies: '@types/node': 20.3.3 await-lock: 2.2.2 diff --git a/tsconfig.json b/tsconfig.json index 6eff920c..e7e38fb2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,10 @@ "module": "ESNext", "target": "ESNext", "strict": true, - "moduleResolution": "bundler" - } + "moduleResolution": "bundler", + "skipLibCheck": true + }, + "include": [ + "backend/**/*" + ], }