Browse Source

Merge branch 'master' into add-turkish

Louis Lam 1 year ago
parent
commit
6c35c1fa2e

+ 3 - 0
.eslintrc.cjs

@@ -92,6 +92,9 @@ module.exports = {
         "one-var": [ "error", "never" ],
         "one-var": [ "error", "never" ],
         "max-statements-per-line": [ "error", { "max": 1 }],
         "max-statements-per-line": [ "error", { "max": 1 }],
         "@typescript-eslint/ban-ts-comment": "off",
         "@typescript-eslint/ban-ts-comment": "off",
+        "@typescript-eslint/no-unused-vars": [ "warn", {
+            "args": "none"
+        }],
         "prefer-const" : "off",
         "prefer-const" : "off",
     },
     },
 };
 };

+ 10 - 2
.github/workflows/ci.yml

@@ -15,12 +15,15 @@ jobs:
     strategy:
     strategy:
       matrix:
       matrix:
         os: [ubuntu-latest, windows-latest, macos-latest]
         os: [ubuntu-latest, windows-latest, macos-latest]
-        node: [18.x, 20.x] # Can be changed
+        node: [20.x] # Can be changed
     runs-on: ${{ matrix.os }}
     runs-on: ${{ matrix.os }}
     steps:
     steps:
       - name: Checkout Code
       - name: Checkout Code
         uses: actions/checkout@v4
         uses: actions/checkout@v4
 
 
+      - run: git config --global core.autocrlf false  # Mainly for Windows
+      - uses: actions/checkout@v3
+
       - name: Setup Node.js
       - name: Setup Node.js
         uses: actions/setup-node@v3
         uses: actions/setup-node@v3
         with:
         with:
@@ -48,5 +51,10 @@ jobs:
       - name: Install dependencies
       - name: Install dependencies
         run: pnpm install
         run: pnpm install
 
 
+      - name: Lint
+        run: pnpm run lint
+
+      - name: Check Typescript
+        run: pnpm run check-ts
       # more things can be add later like tests etc..
       # more things can be add later like tests etc..
-      
+

+ 25 - 14
backend/database.ts

@@ -5,6 +5,7 @@ import fs from "fs";
 import path from "path";
 import path from "path";
 import knex from "knex";
 import knex from "knex";
 
 
+// @ts-ignore
 import Dialect from "knex/lib/dialects/sqlite3/index.js";
 import Dialect from "knex/lib/dialects/sqlite3/index.js";
 
 
 import sqlite from "@louislam/sqlite3";
 import sqlite from "@louislam/sqlite3";
@@ -12,6 +13,11 @@ import { sleep } from "./util-common";
 
 
 interface DBConfig {
 interface DBConfig {
     type?: "sqlite" | "mysql";
     type?: "sqlite" | "mysql";
+    hostname?: string;
+    port?: string;
+    database?: string;
+    username?: string;
+    password?: string;
 }
 }
 
 
 export class Database {
 export class Database {
@@ -19,7 +25,7 @@ export class Database {
      * SQLite file path (Default: ./data/dockge.db)
      * SQLite file path (Default: ./data/dockge.db)
      * @type {string}
      * @type {string}
      */
      */
-    static sqlitePath;
+    static sqlitePath : string;
 
 
     static noReject = true;
     static noReject = true;
 
 
@@ -51,7 +57,7 @@ export class Database {
      * @typedef {string|undefined} envString
      * @typedef {string|undefined} envString
      * @returns {{type: "sqlite"} | {type:envString, hostname:envString, port:envString, database:envString, username:envString, password:envString}} Database config
      * @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 dbConfigString = fs.readFileSync(path.join(this.server.config.dataDir, "db-config.json")).toString("utf-8");
         const dbConfig = JSON.parse(dbConfigString);
         const dbConfig = JSON.parse(dbConfigString);
 
 
@@ -67,10 +73,10 @@ export class Database {
 
 
     /**
     /**
      * @typedef {string|undefined} envString
      * @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}
      * @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));
         fs.writeFileSync(path.join(this.server.config.dataDir, "db-config.json"), JSON.stringify(dbConfig, null, 4));
     }
     }
 
 
@@ -80,14 +86,17 @@ export class Database {
      * @param {boolean} noLog Should logs not be output?
      * @param {boolean} noLog Should logs not be output?
      * @returns {Promise<void>}
      * @returns {Promise<void>}
      */
      */
-    static async connect(autoloadModels = true, noLog = false) {
+    static async connect(autoloadModels = true) {
         const acquireConnectionTimeout = 120 * 1000;
         const acquireConnectionTimeout = 120 * 1000;
-        let dbConfig;
+        let dbConfig : DBConfig;
         try {
         try {
             dbConfig = this.readDBConfig();
             dbConfig = this.readDBConfig();
             Database.dbConfig = dbConfig;
             Database.dbConfig = dbConfig;
         } catch (err) {
         } catch (err) {
-            log.warn("db", err.message);
+            if (err instanceof Error) {
+                log.warn("db", err.message);
+            }
+
             dbConfig = {
             dbConfig = {
                 type: "sqlite",
                 type: "sqlite",
             };
             };
@@ -176,13 +185,15 @@ export class Database {
                 directory: Database.knexMigrationsPath,
                 directory: Database.knexMigrationsPath,
             });
             });
         } catch (e) {
         } 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;
+                }
             }
             }
         }
         }
     }
     }

+ 13 - 7
backend/dockge-server.ts

@@ -60,7 +60,7 @@ export class DockgeServer {
      */
      */
     needSetup = false;
     needSetup = false;
 
 
-    jwtSecret? : string;
+    jwtSecret : string = "";
 
 
     stacksDir : string = "";
     stacksDir : string = "";
 
 
@@ -129,7 +129,7 @@ export class DockgeServer {
         this.config.sslKey = args.sslKey || process.env.DOCKGE_SSL_KEY || undefined;
         this.config.sslKey = args.sslKey || process.env.DOCKGE_SSL_KEY || undefined;
         this.config.sslCert = args.sslCert || process.env.DOCKGE_SSL_CERT || 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.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.hostname = args.hostname || process.env.DOCKGE_HOSTNAME || undefined;
         this.config.dataDir = args.dataDir || process.env.DOCKGE_DATA_DIR || "./data/";
         this.config.dataDir = args.dataDir || process.env.DOCKGE_DATA_DIR || "./data/";
         this.config.stacksDir = args.stacksDir || process.env.DOCKGE_STACKS_DIR || defaultStacksDir;
         this.config.stacksDir = args.stacksDir || process.env.DOCKGE_STACKS_DIR || defaultStacksDir;
@@ -218,7 +218,7 @@ export class DockgeServer {
             log.debug("auth", "check auto login");
             log.debug("auth", "check auto login");
             if (await Settings.get("disableAuth")) {
             if (await Settings.get("disableAuth")) {
                 log.info("auth", "Disabled Auth: auto login to admin");
                 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");
                 socket.emit("autoLogin");
             } else {
             } else {
                 log.debug("auth", "need auth");
                 log.debug("auth", "need auth");
@@ -253,7 +253,9 @@ export class DockgeServer {
         try {
         try {
             await Database.init(this);
             await Database.init(this);
         } catch (e) {
         } 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);
             process.exit(1);
         }
         }
 
 
@@ -291,7 +293,7 @@ export class DockgeServer {
             }
             }
 
 
             // Run every 5 seconds
             // Run every 5 seconds
-            const job = Cron("*/2 * * * * *", {
+            Cron("*/2 * * * * *", {
                 protect: true,  // Enabled over-run protection.
                 protect: true,  // Enabled over-run protection.
             }, () => {
             }, () => {
                 log.debug("server", "Cron job running");
                 log.debug("server", "Cron job running");
@@ -376,7 +378,9 @@ export class DockgeServer {
                 return process.env.TZ;
                 return process.env.TZ;
             }
             }
         } catch (e) {
         } 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");
         const timezone = await Settings.get("serverTimezone");
@@ -389,7 +393,9 @@ export class DockgeServer {
                 return timezone;
                 return timezone;
             }
             }
         } catch (e) {
         } catch (e) {
-            log.warn("timezone", e.message + " in settings");
+            if (e instanceof Error) {
+                log.warn("timezone", e.message + " in settings");
+            }
         }
         }
 
 
         // Guess
         // Guess

+ 2 - 2
backend/password-hash.ts

@@ -17,7 +17,7 @@ export function generatePasswordHash(password : string) {
  * @param {string} hash Hash to verify against
  * @param {string} hash Hash to verify against
  * @returns {boolean} Does the password match the hash?
  * @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);
     return bcrypt.compareSync(password, hash);
 }
 }
 
 
@@ -37,7 +37,7 @@ export const SHAKE256_LENGTH = 16;
  * @param {number} len Output length of the hash
  * @param {number} len Output length of the hash
  * @returns {string} The hashed data in hex format
  * @returns {string} The hashed data in hex format
  */
  */
-export function shake256(data, len) {
+export function shake256(data : string, len : number) {
     if (!data) {
     if (!data) {
         return "";
         return "";
     }
     }

+ 10 - 4
backend/rate-limiter.ts

@@ -1,8 +1,14 @@
 // "limit" is bugged in Typescript, use "limiter-es6-compat" instead
 // "limit" is bugged in Typescript, use "limiter-es6-compat" instead
 // See https://github.com/jhurliman/node-rate-limiter/issues/80
 // 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";
 import { log } from "./log";
 
 
+export interface KumaRateLimiterOpts extends RateLimiterOpts {
+    errorMessage : string;
+}
+
+export type KumaRateLimiterCallback = (err : object) => void;
+
 class KumaRateLimiter {
 class KumaRateLimiter {
 
 
     errorMessage : string;
     errorMessage : string;
@@ -11,7 +17,7 @@ class KumaRateLimiter {
     /**
     /**
      * @param {object} config Rate limiter configuration object
      * @param {object} config Rate limiter configuration object
      */
      */
-    constructor(config) {
+    constructor(config : KumaRateLimiterOpts) {
         this.errorMessage = config.errorMessage;
         this.errorMessage = config.errorMessage;
         this.rateLimiter = new RateLimiter(config);
         this.rateLimiter = new RateLimiter(config);
     }
     }
@@ -24,11 +30,11 @@ class KumaRateLimiter {
 
 
     /**
     /**
      * Should the request be passed through
      * 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
      * @param {number} num Number of tokens to remove
      * @returns {Promise<boolean>} Should the request be allowed?
      * @returns {Promise<boolean>} Should the request be allowed?
      */
      */
-    async pass(callback, num = 1) {
+    async pass(callback : KumaRateLimiterCallback, num = 1) {
         const remainingRequests = await this.removeTokens(num);
         const remainingRequests = await this.removeTokens(num);
         log.info("rate-limit", "remaining requests: " + remainingRequests);
         log.info("rate-limit", "remaining requests: " + remainingRequests);
         if (remainingRequests < 0) {
         if (remainingRequests < 0) {

+ 1 - 1
backend/routers/main-router.ts

@@ -1,4 +1,4 @@
-import { DockgeServer } from "../dockgeServer";
+import { DockgeServer } from "../dockge-server";
 import { Router } from "../router";
 import { Router } from "../router";
 import express, { Express, Router as ExpressRouter } from "express";
 import express, { Express, Router as ExpressRouter } from "express";
 
 

+ 18 - 18
backend/settings.ts

@@ -1,5 +1,6 @@
 import { R } from "redbean-node";
 import { R } from "redbean-node";
 import { log } from "./log";
 import { log } from "./log";
+import { LooseObject } from "./util-common";
 
 
 export class Settings {
 export class Settings {
 
 
@@ -15,20 +16,19 @@ export class Settings {
      *             timestamp: 12345678
      *             timestamp: 12345678
      *         },
      *         },
      *     }
      *     }
-     * @type {{}}
      */
      */
-    static cacheList = {
+    static cacheList : LooseObject = {
 
 
     };
     };
 
 
-    static cacheCleaner = null;
+    static cacheCleaner? : NodeJS.Timeout;
 
 
     /**
     /**
      * Retrieve value of setting based on key
      * Retrieve value of setting based on key
-     * @param {string} key Key of setting to retrieve
-     * @returns {Promise<any>} 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
         // Start cache clear if not started yet
         if (!Settings.cacheCleaner) {
         if (!Settings.cacheCleaner) {
@@ -72,12 +72,12 @@ export class Settings {
 
 
     /**
     /**
      * Sets the specified setting to specified value
      * 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
      * @param {?string} type Type of setting
      * @returns {Promise<void>}
      * @returns {Promise<void>}
      */
      */
-    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` = ? ", [
         let bean = await R.findOne("setting", " `key` = ? ", [
             key,
             key,
@@ -95,15 +95,15 @@ export class Settings {
 
 
     /**
     /**
      * Get settings based on type
      * Get settings based on type
-     * @param {string} type The type of setting
-     * @returns {Promise<Bean>} 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` = ? ", [
         const list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [
             type,
             type,
         ]);
         ]);
 
 
-        const result = {};
+        const result : LooseObject = {};
 
 
         for (const row of list) {
         for (const row of list) {
             try {
             try {
@@ -118,11 +118,11 @@ export class Settings {
 
 
     /**
     /**
      * Set settings based on type
      * 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<void>}
      * @returns {Promise<void>}
      */
      */
-    static async setSettings(type, data) {
+    static async setSettings(type : string, data : LooseObject) {
         const keyList = Object.keys(data);
         const keyList = Object.keys(data);
 
 
         const promiseList = [];
         const promiseList = [];
@@ -154,7 +154,7 @@ export class Settings {
      * @param {string[]} keyList Keys to remove
      * @param {string[]} keyList Keys to remove
      * @returns {void}
      * @returns {void}
      */
      */
-    static deleteCache(keyList) {
+    static deleteCache(keyList : string[]) {
         for (const key of keyList) {
         for (const key of keyList) {
             delete Settings.cacheList[key];
             delete Settings.cacheList[key];
         }
         }
@@ -167,7 +167,7 @@ export class Settings {
     static stopCacheCleaner() {
     static stopCacheCleaner() {
         if (Settings.cacheCleaner) {
         if (Settings.cacheCleaner) {
             clearInterval(Settings.cacheCleaner);
             clearInterval(Settings.cacheCleaner);
-            Settings.cacheCleaner = null;
+            Settings.cacheCleaner = undefined;
         }
         }
     }
     }
 }
 }

+ 34 - 22
backend/socket-handlers/main-socket-handler.ts

@@ -1,12 +1,11 @@
 import { SocketHandler } from "../socket-handler.js";
 import { SocketHandler } from "../socket-handler.js";
-import { Socket } from "socket.io";
 import { DockgeServer } from "../dockge-server";
 import { DockgeServer } from "../dockge-server";
 import { log } from "../log";
 import { log } from "../log";
 import { R } from "redbean-node";
 import { R } from "redbean-node";
 import { loginRateLimiter, twoFaRateLimiter } from "../rate-limiter";
 import { loginRateLimiter, twoFaRateLimiter } from "../rate-limiter";
 import { generatePasswordHash, needRehashPassword, shake256, SHAKE256_LENGTH, verifyPassword } from "../password-hash";
 import { generatePasswordHash, needRehashPassword, shake256, SHAKE256_LENGTH, verifyPassword } from "../password-hash";
 import { User } from "../models/user";
 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 { passwordStrength } from "check-password-strength";
 import jwt from "jsonwebtoken";
 import jwt from "jsonwebtoken";
 import { Settings } from "../settings";
 import { Settings } from "../settings";
@@ -43,10 +42,12 @@ export class MainSocketHandler extends SocketHandler {
                 });
                 });
 
 
             } catch (e) {
             } catch (e) {
-                callback({
-                    ok: false,
-                    msg: e.message,
-                });
+                if (e instanceof Error) {
+                    callback({
+                        ok: false,
+                        msg: e.message,
+                    });
+                }
             }
             }
         });
         });
 
 
@@ -57,7 +58,7 @@ export class MainSocketHandler extends SocketHandler {
             log.info("auth", `Login by token. IP=${clientIP}`);
             log.info("auth", `Login by token. IP=${clientIP}`);
 
 
             try {
             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);
                 log.info("auth", "Username from JWT: " + decoded.username);
 
 
@@ -91,9 +92,13 @@ export class MainSocketHandler extends SocketHandler {
                     });
                     });
                 }
                 }
             } catch (error) {
             } catch (error) {
+                if (!(error instanceof Error)) {
+                    console.error("Unknown error:", error);
+                    return;
+                }
                 log.error("auth", `Invalid token. IP=${clientIP}`);
                 log.error("auth", `Invalid token. IP=${clientIP}`);
                 if (error.message) {
                 if (error.message) {
-                    log.error("auth", error.message, `IP=${clientIP}`);
+                    log.error("auth", error.message + ` IP=${clientIP}`);
                 }
                 }
                 callback({
                 callback({
                     ok: false,
                     ok: false,
@@ -149,6 +154,7 @@ export class MainSocketHandler extends SocketHandler {
                 }
                 }
 
 
                 if (data.token) {
                 if (data.token) {
+                    // @ts-ignore
                     const verify = notp.totp.verify(data.token, user.twofa_secret, twoFAVerifyOptions);
                     const verify = notp.totp.verify(data.token, user.twofa_secret, twoFAVerifyOptions);
 
 
                     if (user.twofa_last_token !== data.token && verify) {
                     if (user.twofa_last_token !== data.token && verify) {
@@ -211,10 +217,12 @@ export class MainSocketHandler extends SocketHandler {
                 });
                 });
 
 
             } catch (e) {
             } catch (e) {
-                callback({
-                    ok: false,
-                    msg: e.message,
-                });
+                if (e instanceof Error) {
+                    callback({
+                        ok: false,
+                        msg: e.message,
+                    });
+                }
             }
             }
         });
         });
 
 
@@ -229,10 +237,12 @@ export class MainSocketHandler extends SocketHandler {
                 });
                 });
 
 
             } catch (e) {
             } catch (e) {
-                callback({
-                    ok: false,
-                    msg: e.message,
-                });
+                if (e instanceof Error) {
+                    callback({
+                        ok: false,
+                        msg: e.message,
+                    });
+                }
             }
             }
         });
         });
 
 
@@ -262,22 +272,24 @@ export class MainSocketHandler extends SocketHandler {
                 server.sendInfo(socket);
                 server.sendInfo(socket);
 
 
             } catch (e) {
             } 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<User | null> {
         if (typeof username !== "string" || typeof password !== "string") {
         if (typeof username !== "string" || typeof password !== "string") {
             return null;
             return null;
         }
         }
 
 
         const user = await R.findOne("user", " username = ? AND active = 1 ", [
         const user = await R.findOne("user", " username = ? AND active = 1 ", [
             username,
             username,
-        ]);
+        ]) as User;
 
 
         if (user && verifyPassword(password, user.password)) {
         if (user && verifyPassword(password, user.password)) {
             // Upgrade the hash to bcrypt
             // Upgrade the hash to bcrypt

+ 6 - 4
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.");
                     throw new Error("Terminal not found or it is not a Interactive Terminal.");
                 }
                 }
             } catch (e) {
             } catch (e) {
-                errorCallback({
-                    ok: false,
-                    msg: e.message,
-                });
+                if (e instanceof Error) {
+                    errorCallback({
+                        ok: false,
+                        msg: e.message,
+                    });
+                }
             }
             }
         });
         });
 
 

+ 21 - 4
backend/stack.ts

@@ -24,6 +24,7 @@ export class Stack {
     protected _status: number = UNKNOWN;
     protected _status: number = UNKNOWN;
     protected _composeYAML?: string;
     protected _composeYAML?: string;
     protected _configFilePath?: string;
     protected _configFilePath?: string;
+    protected _composeFileName: string = "compose.yaml";
     protected server: DockgeServer;
     protected server: DockgeServer;
 
 
     protected combinedTerminal? : Terminal;
     protected combinedTerminal? : Terminal;
@@ -34,6 +35,15 @@ export class Stack {
         this.name = name;
         this.name = name;
         this.server = server;
         this.server = server;
         this._composeYAML = composeYAML;
         this._composeYAML = composeYAML;
+
+        // Check if compose file name is different from compose.yaml
+        const supportedFileNames = [ "compose.yaml", "compose.yml", "docker-compose.yml", "docker-compose.yaml" ];
+        for (const filename of supportedFileNames) {
+            if (fs.existsSync(path.join(this.path, filename))) {
+                this._composeFileName = filename;
+                break;
+            }
+        }
     }
     }
 
 
     toJSON() : object {
     toJSON() : object {
@@ -50,6 +60,7 @@ export class Stack {
             status: this._status,
             status: this._status,
             tags: [],
             tags: [],
             isManagedByDockge: this.isManagedByDockge,
             isManagedByDockge: this.isManagedByDockge,
+            composeFileName: this._composeFileName,
         };
         };
     }
     }
 
 
@@ -84,7 +95,7 @@ export class Stack {
     get composeYAML() : string {
     get composeYAML() : string {
         if (this._composeYAML === undefined) {
         if (this._composeYAML === undefined) {
             try {
             try {
-                this._composeYAML = fs.readFileSync(path.join(this.path, "compose.yaml"), "utf-8");
+                this._composeYAML = fs.readFileSync(path.join(this.path, this._composeFileName), "utf-8");
             } catch (e) {
             } catch (e) {
                 this._composeYAML = "";
                 this._composeYAML = "";
             }
             }
@@ -135,7 +146,7 @@ export class Stack {
         }
         }
 
 
         // Write or overwrite the compose.yaml
         // Write or overwrite the compose.yaml
-        fs.writeFileSync(path.join(dir, "compose.yaml"), this.composeYAML);
+        fs.writeFileSync(path.join(dir, this._composeFileName), this.composeYAML);
     }
     }
 
 
     async deploy(socket? : DockgeSocket) : Promise<number> {
     async deploy(socket? : DockgeSocket) : Promise<number> {
@@ -186,7 +197,9 @@ export class Stack {
                     stack._status = CREATED_FILE;
                     stack._status = CREATED_FILE;
                     stackList.set(filename, stack);
                     stackList.set(filename, stack);
                 } catch (e) {
                 } 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}`);
+                    }
                 }
                 }
             }
             }
 
 
@@ -351,7 +364,11 @@ export class Stack {
         for (let line of lines) {
         for (let line of lines) {
             try {
             try {
                 let obj = JSON.parse(line);
                 let obj = JSON.parse(line);
-                statusList.set(obj.Service, obj.State);
+                if (obj.Health === "") {
+                    statusList.set(obj.Service, obj.State);
+                } else {
+                    statusList.set(obj.Service, obj.Health);
+                }
             } catch (e) {
             } catch (e) {
             }
             }
         }
         }

+ 7 - 3
backend/terminal.ts

@@ -54,7 +54,9 @@ export class Terminal {
         try {
         try {
             this.ptyProcess?.resize(this.cols, this.rows);
             this.ptyProcess?.resize(this.cols, this.rows);
         } catch (e) {
         } 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 {
         try {
             this.ptyProcess?.resize(this.cols, this.rows);
             this.ptyProcess?.resize(this.cols, this.rows);
         } catch (e) {
         } 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
         // On Data
         this._ptyProcess.onData((data) => {
         this._ptyProcess.onData((data) => {
-            this.buffer.push(data);
+            this.buffer.pushItem(data);
             if (this.server.io) {
             if (this.server.io) {
                 this.server.io.to(this.name).emit("terminalWrite", this.name, data);
                 this.server.io.to(this.name).emit("terminalWrite", this.name, data);
             }
             }

+ 5 - 0
backend/util-common.ts

@@ -12,6 +12,11 @@ dayjs.extend(utc);
 dayjs.extend(timezone);
 dayjs.extend(timezone);
 dayjs.extend(relativeTime);
 dayjs.extend(relativeTime);
 
 
+export interface LooseObject {
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    [key: string]: any
+}
+
 let randomBytes : (numBytes: number) => Uint8Array;
 let randomBytes : (numBytes: number) => Uint8Array;
 initRandomBytes();
 initRandomBytes();
 
 

+ 5 - 0
backend/util-server.ts

@@ -6,6 +6,11 @@ import { ERROR_TYPE_VALIDATION } from "./util-common";
 import { R } from "redbean-node";
 import { R } from "redbean-node";
 import { verifyPassword } from "./password-hash";
 import { verifyPassword } from "./password-hash";
 
 
+export interface JWTDecoded {
+    username : string;
+    h? : string;
+}
+
 export interface DockgeSocket extends Socket {
 export interface DockgeSocket extends Socket {
     userID: number;
     userID: number;
     consoleTerminal? : Terminal;
     consoleTerminal? : Terminal;

+ 2 - 2
backend/utils/limit-queue.ts

@@ -4,14 +4,14 @@
  */
  */
 export class LimitQueue<T> extends Array<T> {
 export class LimitQueue<T> extends Array<T> {
     __limit;
     __limit;
-    __onExceed = null;
+    __onExceed? : (item : T | undefined) => void;
 
 
     constructor(limit: number) {
     constructor(limit: number) {
         super();
         super();
         this.__limit = limit;
         this.__limit = limit;
     }
     }
 
 
-    push(value : T) {
+    pushItem(value : T) {
         super.push(value);
         super.push(value);
         if (this.length > this.__limit) {
         if (this.length > this.__limit) {
             const item = this.shift();
             const item = this.shift();

+ 1 - 1
frontend/src/components/ArraySelect.vue

@@ -5,7 +5,7 @@
                 <li v-for="(value, index) in array" :key="index" class="list-group-item">
                 <li v-for="(value, index) in array" :key="index" class="list-group-item">
                     <select v-model="array[index]" class="no-bg domain-input">
                     <select v-model="array[index]" class="no-bg domain-input">
                         <option value="">Select a network...</option>
                         <option value="">Select a network...</option>
-                        <option v-for="option in options" :value="option">{{ option }}</option>
+                        <option v-for="option in options" :key="option" :value="option">{{ option }}</option>
                     </select>
                     </select>
 
 
                     <font-awesome-icon icon="times" class="action remove ms-2 me-3 text-danger" @click="remove(index)" />
                     <font-awesome-icon icon="times" class="action remove ms-2 me-3 text-danger" @click="remove(index)" />

+ 5 - 3
frontend/src/components/Container.vue

@@ -9,7 +9,7 @@
                 <div v-if="!isEditMode">
                 <div v-if="!isEditMode">
                     <span class="badge me-1" :class="bgStyle">{{ status }}</span>
                     <span class="badge me-1" :class="bgStyle">{{ status }}</span>
 
 
-                    <a v-for="port in service.ports" :href="parsePort(port).url" target="_blank">
+                    <a v-for="port in service.ports" :key="port" :href="parsePort(port).url" target="_blank">
                         <span class="badge me-1 bg-secondary">{{ parsePort(port).display }}</span>
                         <span class="badge me-1 bg-secondary">{{ parsePort(port).display }}</span>
                     </a>
                     </a>
                 </div>
                 </div>
@@ -27,7 +27,7 @@
         <div v-if="isEditMode" class="mt-2">
         <div v-if="isEditMode" class="mt-2">
             <button class="btn btn-normal me-2" @click="showConfig = !showConfig">
             <button class="btn btn-normal me-2" @click="showConfig = !showConfig">
                 <font-awesome-icon icon="edit" />
                 <font-awesome-icon icon="edit" />
-                Edit
+                {{ $t("Edit") }}
             </button>
             </button>
             <button v-if="false" class="btn btn-normal me-2">Rename</button>
             <button v-if="false" class="btn btn-normal me-2">Rename</button>
             <button class="btn btn-danger me-2" @click="remove">
             <button class="btn btn-danger me-2" @click="remove">
@@ -179,8 +179,10 @@ export default defineComponent({
         },
         },
 
 
         bgStyle() {
         bgStyle() {
-            if (this.status === "running") {
+            if (this.status === "running" || this.status === "healthy") {
                 return "bg-primary";
                 return "bg-primary";
+            } else if (this.status === "unhealthy") {
+                return "bg-danger";
             } else {
             } else {
                 return "bg-secondary";
                 return "bg-secondary";
             }
             }

+ 4 - 4
frontend/src/components/NetworkInput.vue

@@ -1,6 +1,6 @@
 <template>
 <template>
     <div>
     <div>
-        <h5>Internal Networks</h5>
+        <h5>{{ $t("Internal Networks") }}</h5>
         <ul class="list-group">
         <ul class="list-group">
             <li v-for="(networkRow, index) in networkList" :key="index" class="list-group-item">
             <li v-for="(networkRow, index) in networkList" :key="index" class="list-group-item">
                 <input v-model="networkRow.key" type="text" class="no-bg domain-input" placeholder="Network name..." />
                 <input v-model="networkRow.key" type="text" class="no-bg domain-input" placeholder="Network name..." />
@@ -10,10 +10,10 @@
 
 
         <button class="btn btn-normal btn-sm mt-3 me-2" @click="addField">{{ $t("addInternalNetwork") }}</button>
         <button class="btn btn-normal btn-sm mt-3 me-2" @click="addField">{{ $t("addInternalNetwork") }}</button>
 
 
-        <h5 class="mt-3">External Networks</h5>
+        <h5 class="mt-3">{{ $t("External Networks") }}</h5>
 
 
         <div v-if="externalNetworkList.length === 0">
         <div v-if="externalNetworkList.length === 0">
-            No External Networks
+            {{ $t("No External Networks") }}
         </div>
         </div>
 
 
         <div v-for="(networkName, index) in externalNetworkList" :key="networkName" class="form-check form-switch my-3">
         <div v-for="(networkName, index) in externalNetworkList" :key="networkName" class="form-check form-switch my-3">
@@ -32,7 +32,7 @@
                 class="form-control"
                 class="form-control"
                 @keyup.enter="createExternelNetwork"
                 @keyup.enter="createExternelNetwork"
             />
             />
-            <button class="btn btn-normal btn-sm  me-2" type="button" @click="">
+            <button class="btn btn-normal btn-sm  me-2" type="button">
                 {{ $t("createExternalNetwork") }}
                 {{ $t("createExternalNetwork") }}
             </button>
             </button>
         </div>
         </div>

+ 0 - 1
frontend/src/components/Uptime.vue

@@ -19,7 +19,6 @@ export default {
 
 
     computed: {
     computed: {
         uptime() {
         uptime() {
-            return "0.00%";
             return this.$t("notAvailableShort");
             return this.$t("notAvailableShort");
         },
         },
 
 

+ 3 - 3
frontend/src/components/settings/General.vue

@@ -50,7 +50,7 @@
                         placeholder="localhost"
                         placeholder="localhost"
                     />
                     />
                     <button class="btn btn-outline-primary" type="button" @click="autoGetPrimaryHostname">
                     <button class="btn btn-outline-primary" type="button" @click="autoGetPrimaryHostname">
-                        {{ $t("Auto Get") }}
+                        {{ $t("autoGet") }}
                     </button>
                     </button>
                 </div>
                 </div>
 
 
@@ -68,13 +68,13 @@
 </template>
 </template>
 
 
 <script>
 <script>
-import HiddenInput from "../../components/HiddenInput.vue";
+
 import dayjs from "dayjs";
 import dayjs from "dayjs";
 import { timezoneList } from "../../util-frontend";
 import { timezoneList } from "../../util-frontend";
 
 
 export default {
 export default {
     components: {
     components: {
-        HiddenInput,
+
     },
     },
 
 
     data() {
     data() {

+ 1 - 0
frontend/src/i18n.ts

@@ -3,6 +3,7 @@ import { createI18n } from "vue-i18n/dist/vue-i18n.esm-browser.prod.js";
 import en from "./lang/en.json";
 import en from "./lang/en.json";
 
 
 const languageList = {
 const languageList = {
+    "fr": "Français",
 
 
 };
 };
 
 

+ 45 - 2
frontend/src/lang/en.json

@@ -1,7 +1,10 @@
 {
 {
     "languageName": "English",
     "languageName": "English",
+    "Create your admin account": "Create your admin account",
     "authIncorrectCreds": "Incorrect username or password.",
     "authIncorrectCreds": "Incorrect username or password.",
     "PasswordsDoNotMatch": "Passwords do not match.",
     "PasswordsDoNotMatch": "Passwords do not match.",
+    "Repeat Password": "Repeat Password",
+    "Create": "Create",
     "signedInDisp": "Signed in as {0}",
     "signedInDisp": "Signed in as {0}",
     "signedInDispDisabled": "Auth Disabled.",
     "signedInDispDisabled": "Auth Disabled.",
     "home": "Home",
     "home": "Home",
@@ -43,11 +46,51 @@
     "addContainer": "Add Container",
     "addContainer": "Add Container",
     "addNetwork": "Add Network",
     "addNetwork": "Add Network",
     "disableauth.message1": "Are you sure want to <strong>disable authentication</strong>?",
     "disableauth.message1": "Are you sure want to <strong>disable authentication</strong>?",
-    "disableauth.message2": "It is designed for scenarios <strong>where you intend to implement third-party authentication</strong> in front of Uptime Kuma such as Cloudflare Access, Authelia or other authentication mechanisms.",
+    "disableauth.message2": "It is designed for scenarios <strong>where you intend to implement third-party authentication</strong> in front of Dockge such as Cloudflare Access, Authelia or other authentication mechanisms.",
     "passwordNotMatchMsg": "The repeat password does not match.",
     "passwordNotMatchMsg": "The repeat password does not match.",
     "autoGet": "Auto Get",
     "autoGet": "Auto Get",
     "add": "Add",
     "add": "Add",
+    "Edit": "Edit",
     "applyToYAML": "Apply to YAML",
     "applyToYAML": "Apply to YAML",
     "createExternalNetwork": "Create",
     "createExternalNetwork": "Create",
-    "addInternalNetwork": "Add"
+    "addInternalNetwork": "Add",
+    "Save": "Sauvegarder",
+    "Language": "Language",
+    "Current User": "Current User",
+    "Change Password": "Change Password",
+    "Current Password": "Current Password",
+    "New Password": "New Password",
+    "Repeat New Password": "Repeat New Password",
+    "Update Password": "Update Password",
+    "Advanced": "Advanced",
+    "Please use this option carefully!": "Please use this option carefully!",
+    "Enable Auth": "Enable Auth",
+    "Disable Auth": "Disable Auth",
+    "I understand, please disable": "I understand, please disable",
+    "Leave": "Leave",
+    "Frontend Version": "Frontend Version",
+    "Check Update On GitHub": "Check Update On GitHub",
+    "Show update if available": "Show update if available",
+    "Also check beta release": "Also check beta release",
+    "Remember me": "Remember me",
+    "Login": "Login",
+    "Username": "Username",
+    "Password": "Password",
+    "Settings": "Settings",
+    "Logout": "Logout",
+    "Lowercase only": "Lowercase only",
+    "Convert to Compose": "Convert to Compose",
+    "Docker Run": "Docker Run",
+    "active": "active",
+    "exited": "exited",
+    "inactive": "inactive",
+    "Appearance": "Appearance",
+    "Security": "Security",
+    "About": "About",
+    "Allowed commands:": "Allowed commands:",
+    "Internal Networks": "Internal Networks",
+    "External Networks": "External Networks",
+    "No External Networks": "No External Networks"
+
+
 }
 }

+ 95 - 0
frontend/src/lang/fr.json

@@ -0,0 +1,95 @@
+{
+    "languageName": "Francais",
+    "Create your admin account": "Créez votre compte administrateur",
+    "authIncorrectCreds": "identifiant ou mot de passe incorrect.",
+    "Repeat Password": "Répéter le mot de passe",
+    "PasswordsDoNotMatch": "Les mots de passe ne correspondent pas.",
+    "Create": "Créer",
+    "signedInDisp": "Connecté en tant que {0}",
+    "signedInDispDisabled": "Authentification désactivée.",
+    "home": "Accueil",
+    "console": "Console",
+    "registry": "Registre",
+    "compose": "Compose",
+    "addFirstStackMsg": "Créez votre première pile!",
+    "stackName" : "Nom de la pile",
+    "deployStack": "Déployer",
+    "deleteStack": "Supprimer",
+    "stopStack": "Arrêter",
+    "restartStack": "Redémarrer",
+    "updateStack": "Mettre à jour",
+    "startStack": "Démarrer",
+    "editStack": "Modifier",
+    "discardStack": "Ignorer",
+    "saveStackDraft": "Sauvegarder",
+    "notAvailableShort" : "N/A",
+    "deleteStackMsg": "Êtes-vous sûr de vouloir supprimer cette pile ?",
+    "stackNotManagedByDockgeMsg": "Cette pile n'est pas gérée par Dockge.",
+    "primaryHostname": "Nom d'hôte principal",
+    "general": "Générale",
+    "container": "Conteneur | Conteneurs",
+    "scanFolder": "Analyser le dossier des piles",
+    "dockerImage": "Image",
+    "restartPolicyUnlessStopped": "Sauf arrêt",
+    "restartPolicyAlways": "Toujours",
+    "restartPolicyOnFailure": "En cas d'échec",
+    "restartPolicyNo": "Non",
+    "environmentVariable": "Variable d'environnement | Variables d'environnement",
+    "restartPolicy": "Politique de redémarrage",
+    "containerName": "Nom du conteneur",
+    "port": "Port | Ports",
+    "volume": "Volume | Volumes",
+    "network": "Réseau | Réseaux",
+    "dependsOn": "Dépendance du conteneur | Dépendances du conteneur",
+    "addListItem": "Ajouter {0}",
+    "deleteContainer": "Supprimer",
+    "addContainer": "Ajouter un conteneur",
+    "addNetwork": "Ajouter un réseau",
+    "disableauth.message1": "Voulez-vous vraiment <strong>désactiver l'authentification</strong> ?",
+    "disableauth.message2": "Il est conçu pour les scénarios <strong>dans lesquels vous avez l'intention d'implémenter une authentification tierce</strong> devant Dockge, comme Cloudflare Access, Authelia ou d'autres mécanismes d'authentification.",
+    "passwordNotMatchMsg": "Le mot de passe de confirmation ne correspond pas.",
+    "autoGet": "Obtention automatique",
+    "add": "Ajouter",
+    "Edit": "Modifier",
+    "applyToYAML": "Appliquer à YAML",
+    "createExternalNetwork": "Créer",
+    "addInternalNetwork": "Ajouter",
+    "Save": "Enregistrer",
+    "Language": "Langue",
+    "Current User": "Utilisateur Actuel",
+    "Change Password": "Changer le Mot de Passe",
+    "Current Password": "Mot de passe actuel",
+    "New Password": "Nouveau Mot de Passe",
+    "Repeat New Password": "Répéter le Nouveau Mot de Passe",
+    "Update Password": "Mettre à Jour le Mot de Passe",
+    "Advanced": "Avancé",
+    "Please use this option carefully!": "Veuillez utiliser cette option avec précaution !",
+    "Enable Auth": "Activer l'Authentification",
+    "Disable Auth": "Désactiver l'Authentification",
+    "I understand, please disable": "Je comprends, veuillez désactiver",
+    "Leave": "Quitter",
+    "Frontend Version": "Version Frontend",
+    "Check Update On GitHub": "Vérifier la Mise à Jour sur GitHub",
+    "Show update if available": "Afficher la mise à jour si disponible",
+    "Also check beta release": "Vérifier également la version bêta",
+    "Remember me": "Se souvenir de moi",
+    "Login": "Connexion",
+    "Username": "Nom d'utilisateur",
+    "Password": "Mot de Passe",
+    "Settings": "Paramètres",
+    "Logout": "Déconnexion",
+    "Lowercase only": "Minuscules uniquement",
+    "Convert to Compose": "Convertir en Compose",
+    "Docker Run": "Exécution Docker",
+    "active": "actif",
+    "exited": "arrêté",
+    "inactive": "inactif",
+    "Appearance": "Apparence",
+    "Security": "Sécurité",
+    "About": "À propos",
+    "Allowed commands:": "Commandes autorisées:",
+    "Internal Networks": "Réseaux Internes",
+    "External Networks": "Réseaux Externes",
+    "No External Networks": "Aucun Réseau Externe"
+
+}

+ 2 - 2
frontend/src/pages/Compose.vue

@@ -71,7 +71,7 @@
                             <div>
                             <div>
                                 <label for="name" class="form-label">{{ $t("stackName") }}</label>
                                 <label for="name" class="form-label">{{ $t("stackName") }}</label>
                                 <input id="name" v-model="stack.name" type="text" class="form-control" required @blur="stackNameToLowercase">
                                 <input id="name" v-model="stack.name" type="text" class="form-control" required @blur="stackNameToLowercase">
-                                <div class="form-text">Lowercase only</div>
+                                <div class="form-text">{{ $t("Lowercase only") }}</div>
                             </div>
                             </div>
                         </div>
                         </div>
                     </div>
                     </div>
@@ -118,7 +118,7 @@
                     </div>
                     </div>
                 </div>
                 </div>
                 <div class="col-lg-6">
                 <div class="col-lg-6">
-                    <h4 class="mb-3">compose.yaml</h4>
+                    <h4 class="mb-3">{{ stack.composeFileName }}</h4>
 
 
                     <!-- YAML editor -->
                     <!-- YAML editor -->
                     <div class="shadow-box mb-3 editor-box" :class="{'edit-mode' : isEditMode}">
                     <div class="shadow-box mb-3 editor-box" :class="{'edit-mode' : isEditMode}">

+ 1 - 1
frontend/src/pages/Console.vue

@@ -5,7 +5,7 @@
 
 
             <div>
             <div>
                 <p>
                 <p>
-                    Allowed commands:
+                    {{ $t("Allowed commands:") }}
                     <template v-for="(command, index) in allowedCommandList" :key="command">
                     <template v-for="(command, index) in allowedCommandList" :key="command">
                         <code>{{ command }}</code>
                         <code>{{ command }}</code>
 
 

+ 2 - 2
frontend/src/pages/DashboardHome.vue

@@ -22,12 +22,12 @@
                 </div>
                 </div>
             </div>
             </div>
 
 
-            <h2 class="mb-3">Docker Run</h2>
+            <h2 class="mb-3">{{ $t("Docker Run") }}</h2>
             <div class="mb-3">
             <div class="mb-3">
                 <textarea id="name" v-model="dockerRunCommand" type="text" class="form-control docker-run" required placeholder="docker run ..."></textarea>
                 <textarea id="name" v-model="dockerRunCommand" type="text" class="form-control docker-run" required placeholder="docker run ..."></textarea>
             </div>
             </div>
 
 
-            <button class="btn-normal btn" @click="convertDockerRun">Convert to Compose</button>
+            <button class="btn-normal btn" @click="convertDockerRun">{{ $t("Convert to Compose") }}</button>
         </div>
         </div>
     </transition>
     </transition>
     <router-view ref="child" />
     <router-view ref="child" />

+ 1 - 1
frontend/src/pages/Settings.vue

@@ -75,7 +75,7 @@ export default {
         subMenus() {
         subMenus() {
             return {
             return {
                 general: {
                 general: {
-                    title: this.$t("General"),
+                    title: this.$t("general"),
                 },
                 },
                 appearance: {
                 appearance: {
                     title: this.$t("Appearance"),
                     title: this.$t("Appearance"),

+ 1 - 28
frontend/src/util-frontend.ts

@@ -10,7 +10,7 @@ import { POSITION } from "vue-toastification";
  *
  *
  * Generated by Trelent
  * Generated by Trelent
  */
  */
-function getTimezoneOffset(timeZone) {
+function getTimezoneOffset(timeZone : string) {
     const now = new Date();
     const now = new Date();
     const tzString = now.toLocaleString("en-US", {
     const tzString = now.toLocaleString("en-US", {
         timeZone,
         timeZone,
@@ -124,33 +124,6 @@ export function hostNameRegexPattern(mqtt = false) {
     return `${ipRegexPattern}|${hostNameRegexPattern}`;
     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.
  * Loads the toast timeout settings from storage.
  * @returns {object} The toast plugin options object.
  * @returns {object} The toast plugin options object.

+ 1 - 0
frontend/src/vite-env.d.ts

@@ -1,3 +1,4 @@
+/* eslint-disable */
 /// <reference types="vite/client" />
 /// <reference types="vite/client" />
 
 
 declare module "*.vue" {
 declare module "*.vue" {

+ 8 - 6
package.json

@@ -5,6 +5,7 @@
     "scripts": {
     "scripts": {
         "fmt": "eslint \"**/*.{ts,vue}\" --fix",
         "fmt": "eslint \"**/*.{ts,vue}\" --fix",
         "lint": "eslint \"**/*.{ts,vue}\"",
         "lint": "eslint \"**/*.{ts,vue}\"",
+        "check-ts": "tsc --noEmit",
         "start": "tsx ./backend/index.ts",
         "start": "tsx ./backend/index.ts",
         "dev:backend": "cross-env NODE_ENV=development tsx watch ./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",
         "dev:frontend": "cross-env NODE_ENV=development vite --host --config ./frontend/vite.config.ts",
@@ -17,8 +18,7 @@
         "mark-as-nightly": "tsx ./extra/mark-as-nightly.ts"
         "mark-as-nightly": "tsx ./extra/mark-as-nightly.ts"
     },
     },
     "dependencies": {
     "dependencies": {
-        "@fontsource/jetbrains-mono": "^5.0.17",
-        "@homebridge/node-pty-prebuilt-multiarch": "~0.11.10",
+        "@homebridge/node-pty-prebuilt-multiarch": "~0.11.11",
         "@louislam/sqlite3": "~15.1.6",
         "@louislam/sqlite3": "~15.1.6",
         "bcryptjs": "~2.4.3",
         "bcryptjs": "~2.4.3",
         "check-password-strength": "~2.0.7",
         "check-password-strength": "~2.0.7",
@@ -34,8 +34,8 @@
         "jwt-decode": "~3.1.2",
         "jwt-decode": "~3.1.2",
         "knex": "~2.5.1",
         "knex": "~2.5.1",
         "limiter-es6-compat": "~2.1.2",
         "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": "~4.7.2",
         "socket.io-client": "~4.7.2",
         "socket.io-client": "~4.7.2",
         "timezones-list": "~3.0.2",
         "timezones-list": "~3.0.2",
@@ -45,17 +45,19 @@
         "yaml": "~2.3.4"
         "yaml": "~2.3.4"
     },
     },
     "devDependencies": {
     "devDependencies": {
+        "@fontsource/jetbrains-mono": "^5.0.17",
         "@fortawesome/fontawesome-svg-core": "6.4.2",
         "@fortawesome/fontawesome-svg-core": "6.4.2",
         "@fortawesome/free-regular-svg-icons": "6.4.2",
         "@fortawesome/free-regular-svg-icons": "6.4.2",
         "@fortawesome/free-solid-svg-icons": "6.4.2",
         "@fortawesome/free-solid-svg-icons": "6.4.2",
         "@fortawesome/vue-fontawesome": "3.0.3",
         "@fortawesome/vue-fontawesome": "3.0.3",
+        "@types/bcryptjs": "^2.4.6",
         "@types/bootstrap": "~5.2.9",
         "@types/bootstrap": "~5.2.9",
         "@types/command-exists": "~1.2.3",
         "@types/command-exists": "~1.2.3",
         "@types/express": "~4.17.21",
         "@types/express": "~4.17.21",
         "@types/jsonwebtoken": "~9.0.5",
         "@types/jsonwebtoken": "~9.0.5",
         "@typescript-eslint/eslint-plugin": "~6.8.0",
         "@typescript-eslint/eslint-plugin": "~6.8.0",
         "@typescript-eslint/parser": "~6.8.0",
         "@typescript-eslint/parser": "~6.8.0",
-        "@vitejs/plugin-vue": "~4.3.4",
+        "@vitejs/plugin-vue": "~4.5.0",
         "bootstrap": "5.3.2",
         "bootstrap": "5.3.2",
         "bootstrap-vue-next": "~0.14.10",
         "bootstrap-vue-next": "~0.14.10",
         "cross-env": "~7.0.3",
         "cross-env": "~7.0.3",
@@ -66,7 +68,7 @@
         "sass": "~1.68.0",
         "sass": "~1.68.0",
         "typescript": "~5.2.2",
         "typescript": "~5.2.2",
         "unplugin-vue-components": "~0.25.2",
         "unplugin-vue-components": "~0.25.2",
-        "vite": "~4.5.0",
+        "vite": "~5.0.0",
         "vite-plugin-compression": "~0.5.1",
         "vite-plugin-compression": "~0.5.1",
         "vue": "~3.3.8",
         "vue": "~3.3.8",
         "vue-eslint-parser": "~9.3.2",
         "vue-eslint-parser": "~9.3.2",

File diff suppressed because it is too large
+ 438 - 72
pnpm-lock.yaml


+ 6 - 2
tsconfig.json

@@ -3,6 +3,10 @@
         "module": "ESNext",
         "module": "ESNext",
         "target": "ESNext",
         "target": "ESNext",
         "strict": true,
         "strict": true,
-        "moduleResolution": "bundler"
-    }
+        "moduleResolution": "bundler",
+        "skipLibCheck": true
+    },
+    "include": [
+        "backend/**/*"
+    ],
 }
 }

Some files were not shown because too many files changed in this diff