Compare commits

...

29 Commits

Author SHA1 Message Date
a007ec56f7 Update to 1.1.0 2023-11-19 17:44:30 +08:00
7bb0a1cb08 Minor 2023-11-19 17:36:19 +08:00
4df799b5b6 Added Turkish Language (#61)
* Added Turkish Language

* Add to list

---------

Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
2023-11-19 17:30:09 +08:00
03bc2b6a34 Added Fr language and added missing translation keys (#66)
* Added Fr language and added missing translation keys

* forgotten key

* forgotten key

* fix
2023-11-19 17:19:33 +08:00
53b052c1e5 Check TypeScript for backend (#64)
* Check Typescript

* Fix backend typescript issues

* Update
2023-11-18 15:54:43 +08:00
13c3dac44d ESLint, update vite to 5.0.0 and other dependencies (#63)
* Update vite to 5.0.0 and other dependencies

* Eslint

* Update workflow
2023-11-18 13:59:40 +08:00
5ce6b90546 Support compose.y[a]ml and docker-compose.y[a]ml (#55)
* Support compose.y[a]ml and docker-compose.y[a]ml

* using for-loop to iterate over supported compose filenames

* Fix lint

---------

Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
2023-11-18 13:36:57 +08:00
a488518f6e Add health status check (#58)
* set Health value to Status if existent

Check if Health has any value and save it to be displayed.
If Health is empty, continue as normal.

* add healthy and unhealthy status to be displayed

Check if status is either Running or Healthy to set span class to bg-primary,
and check if status is Unhealthy to set span class to bg-danger.

* Add lint to workflow

* Fix lint

---------

Co-authored-by: Thales <thcd@cock.li>
Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
2023-11-18 13:27:39 +08:00
8c4004f32d Update to 1.0.4 2023-11-17 01:04:14 +08:00
393bbcae79 Fix #52 2023-11-17 01:02:20 +08:00
9fbf94586b fix ci (#54) 2023-11-16 22:10:53 +08:00
0a46a7df1a Add Github Workflow (#38)
* Create ci..yml

* Rename ci..yml to ci.yml
2023-11-16 21:53:40 +08:00
d1732af529 Update README.md 2023-11-14 00:33:11 +08:00
87a6436f28 Update compose.yaml 2023-11-13 23:32:37 +08:00
ac75283b0f Update README.md 2023-11-13 23:31:34 +08:00
8d6160ec5b Update to 1.0.3 2023-11-13 20:48:23 +08:00
ecb16ae007 Update README.md 2023-11-13 20:38:42 +08:00
c296069a8d Update dependencies 2023-11-13 18:10:33 +08:00
d76442434f Fix #19 2023-11-13 18:10:33 +08:00
54e8484efd Update README.md 2023-11-13 13:47:30 +08:00
2cd10ad16d Remove --rmi 2023-11-13 13:38:24 +08:00
96a4f2fd0c Update README.md 2023-11-13 13:10:07 +08:00
700a24171b Add Badges (#13)
* Update README.md

* Update README.md

* Update README.md
2023-11-13 02:26:39 +08:00
6ce75a2df3 Update README.md for Podman 2023-11-13 02:04:10 +08:00
317c97650d Update to 1.0.2 2023-11-12 23:24:38 +08:00
9295583727 Fix #9 2023-11-12 23:09:31 +08:00
6dc998bedf Fix frontend version do not match 2023-11-12 16:47:05 +08:00
f5552b3344 Update README.md 2023-11-12 16:44:49 +08:00
b90fd35348 Merge pull request #5 from louislam/release-process
Release process
2023-11-12 16:43:30 +08:00
36 changed files with 1000 additions and 310 deletions

View File

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

60
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,60 @@
name: Node.js CI - Dockge
on:
push:
branches: [master]
paths-ignore:
- '*.md'
pull_request:
branches: [master]
paths-ignore:
- '*.md'
jobs:
ci:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [20.x] # Can be changed
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Code
uses: actions/checkout@v4
- run: git config --global core.autocrlf false # Mainly for Windows
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{matrix.node}}
- uses: pnpm/action-setup@v2
name: Install pnpm
with:
version: 8
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
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..

View File

@ -6,6 +6,8 @@
A fancy, easy-to-use and reactive self-hosted docker compose.yaml stack-oriented manager.
![GitHub Repo stars](https://img.shields.io/github/stars/louislam/dockge?logo=github) ![GitHub issues](https://img.shields.io/github/issues/louislam/dockge?logo=github) ![GitHub pull requests](https://img.shields.io/github/issues-pr/louislam/dockge?logo=github) ![Docker Pulls](https://img.shields.io/docker/pulls/louislam/dockge?logo=docker) ![Docker Image Version (latest semver)](https://img.shields.io/docker/v/louislam/dockge?logo=docker) ![GitHub last commit (branch)](https://img.shields.io/github/last-commit/louislam/dockge/master?logo=github) ![GitHub](https://img.shields.io/github/license/louislam/dockge?logo=github)
<img src="https://github.com/louislam/dockge/assets/1336778/26a583e1-ecb1-4a8d-aedf-76157d714ad7" width="900" alt="" />
View Video: https://youtu.be/AWAlOQeNpgU?t=48
@ -32,11 +34,12 @@ View Video: https://youtu.be/AWAlOQeNpgU?t=48
## 🔧 How to Install
Requirements:
- [Docker CE](https://docs.docker.com/engine/install/) 20+ is recommended
- [Docker Compose V2](https://docs.docker.com/compose/install/linux/)
- [Docker CE](https://docs.docker.com/engine/install/) 20+ is recommended / Podman
- (Docker only) [Docker Compose Plugin](https://docs.docker.com/compose/install/linux/)
- (Podman only) podman-docker (Debian: `apt install podman-docker`)
- OS:
- As long as you can run Docker CE, it should be fine, but:
- Debian/Raspbian Buster or lower is not supported, please upgrade to Bullseye
- As long as you can run Docker CE / Podman, it should be fine, but:
- Debian/Raspbian Buster or lower is not supported, please upgrade to Bullseye or higher
- Arch: armv7, arm64, amd64 (a.k.a x86_64)
### Basic
@ -55,7 +58,7 @@ curl https://raw.githubusercontent.com/louislam/dockge/master/compose.yaml --out
# Start Server
docker compose up -d
# If you are using docker-compose V1
# If you are using docker-compose V1 or Podman
# docker-compose up -d
```
@ -65,8 +68,6 @@ Dockge is now running on http://localhost:5001
If you want to store your stacks in another directory, you can change the `DOCKGE_STACKS_DIR` environment variable and volumes.
For example, if you want to store your stacks in `/my-stacks`:
```yaml
version: "3.8"
services:
@ -83,18 +84,20 @@ services:
# If you want to use private registries, you need to share the auth file with Dockge:
# - /root/.docker/:/root/.docker
# Your stacks directory in the host
# (The paths inside container must be the same as the host)
- /my-stacks:/my-stacks
# Your stacks directory in the host (The paths inside container must be the same as the host)
# ⚠️⚠️ If you did it wrong, your data could end up be written into a wrong path.
# ✔️✔️✔️✔️ CORRECT EXAMPLE: - /my-stacks:/my-stacks (Both paths match)
# ❌❌❌❌ WRONG EXAMPLE: - /docker:/my-stacks (Both paths do not match)
- /opt/stacks:/opt/stacks
environment:
# Tell Dockge where is your stacks directory
- DOCKGE_STACKS_DIR=/my-stacks
- DOCKGE_STACKS_DIR=/opt/stacks
```
## How to Update
```bash
cd /opt/stacks
cd /opt/dockge
docker compose pull
docker compose up -d
```
@ -119,20 +122,22 @@ docker compose up -d
If you love this project, please consider giving this project a ⭐.
## 🗣️ Discussion / Ask for Help
## 🗣️
Please go to https://github.com/louislam/dockge/discussions
### Bug Report
https://github.com/louislam/dockge/issues
### Ask for Help / Discussions
https://github.com/louislam/dockge/discussions
## FAQ
#### "Dockge"?
"Dockge" is a coinage word which is created by myself. I hope it sounds like `Badge` but replacing with `Dock` - `Dock-ge`.
"Dockge" is a coinage word which is created by myself. I hope it sounds like `Dodge`.
The naming idea was coming from Twitch emotes like `sadge`, `bedge` or `wokege`. They are all ending with `-ge`.
If you are not comfortable with the pronunciation, you can call it `Dockage`.
#### Can I manage a single container without `compose.yaml`?
The main objective of Dockge is that try to use docker `compose.yaml` for everything. If you want to manage a single container, you can just use Portainer or Docker CLI.

View File

@ -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));
}
@ -80,14 +86,17 @@ export class Database {
* @param {boolean} noLog Should logs not be output?
* @returns {Promise<void>}
*/
static async connect(autoloadModels = true, noLog = false) {
static async connect(autoloadModels = true) {
const acquireConnectionTimeout = 120 * 1000;
let dbConfig;
let dbConfig : DBConfig;
try {
dbConfig = this.readDBConfig();
Database.dbConfig = dbConfig;
} catch (err) {
if (err instanceof Error) {
log.warn("db", err.message);
}
dbConfig = {
type: "sqlite",
};
@ -176,6 +185,7 @@ export class Database {
directory: Database.knexMigrationsPath,
});
} catch (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);
@ -186,6 +196,7 @@ export class Database {
}
}
}
}
/**
* Special handle, because tarn.js throw a promise reject that cannot be caught

View File

@ -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) {
if (e instanceof Error) {
log.error("server", "Failed to prepare your database: " + e.message);
}
process.exit(1);
}
@ -291,7 +293,7 @@ export class DockgeServer {
}
// Run every 5 seconds
const job = Cron("*/2 * * * * *", {
Cron("*/2 * * * * *", {
protect: true, // Enabled over-run protection.
}, () => {
log.debug("server", "Cron job running");
@ -376,8 +378,10 @@ export class DockgeServer {
return process.env.TZ;
}
} catch (e) {
if (e instanceof Error) {
log.warn("timezone", e.message + " in process.env.TZ");
}
}
const timezone = await Settings.get("serverTimezone");
@ -389,8 +393,10 @@ export class DockgeServer {
return timezone;
}
} catch (e) {
if (e instanceof Error) {
log.warn("timezone", e.message + " in settings");
}
}
// Guess
try {

View File

@ -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 "";
}

View File

@ -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<boolean>} 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) {

View File

@ -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";

View File

@ -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<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
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<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` = ? ", [
key,
@ -95,15 +95,15 @@ export class Settings {
/**
* 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` = ? ", [
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<void>}
*/
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;
}
}
}

View File

@ -1,12 +1,11 @@
import { SocketHandler } from "../socket-handler.js";
import { Socket } from "socket.io";
import { DockgeServer } from "../dockge-server";
import { log } from "../log";
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";
@ -43,11 +42,13 @@ export class MainSocketHandler extends SocketHandler {
});
} catch (e) {
if (e instanceof Error) {
callback({
ok: false,
msg: e.message,
});
}
}
});
// Login by token
@ -57,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);
@ -91,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,
@ -149,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) {
@ -211,11 +217,13 @@ export class MainSocketHandler extends SocketHandler {
});
} catch (e) {
if (e instanceof Error) {
callback({
ok: false,
msg: e.message,
});
}
}
});
socket.on("getSettings", async (callback) => {
@ -229,11 +237,13 @@ export class MainSocketHandler extends SocketHandler {
});
} catch (e) {
if (e instanceof Error) {
callback({
ok: false,
msg: e.message,
});
}
}
});
socket.on("setSettings", async (data, currentPassword, callback) => {
@ -262,22 +272,24 @@ export class MainSocketHandler extends SocketHandler {
server.sendInfo(socket);
} catch (e) {
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") {
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

View File

@ -38,11 +38,13 @@ export class TerminalSocketHandler extends SocketHandler {
throw new Error("Terminal not found or it is not a Interactive Terminal.");
}
} catch (e) {
if (e instanceof Error) {
errorCallback({
ok: false,
msg: e.message,
});
}
}
});
// Main Terminal

View File

@ -24,6 +24,7 @@ export class Stack {
protected _status: number = UNKNOWN;
protected _composeYAML?: string;
protected _configFilePath?: string;
protected _composeFileName: string = "compose.yaml";
protected server: DockgeServer;
protected combinedTerminal? : Terminal;
@ -34,6 +35,15 @@ export class Stack {
this.name = name;
this.server = server;
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 {
@ -50,6 +60,7 @@ export class Stack {
status: this._status,
tags: [],
isManagedByDockge: this.isManagedByDockge,
composeFileName: this._composeFileName,
};
}
@ -72,9 +83,9 @@ export class Stack {
}
validate() {
// Check name, allows [a-z][A-Z][0-9] _ - only
if (!this.name.match(/^[a-zA-Z0-9_-]+$/)) {
throw new ValidationError("Stack name can only contain [a-z][A-Z][0-9] _ - only");
// Check name, allows [a-z][0-9] _ - only
if (!this.name.match(/^[a-z0-9_-]+$/)) {
throw new ValidationError("Stack name can only contain [a-z][0-9] _ - only");
}
// Check YAML format
@ -84,7 +95,7 @@ export class Stack {
get composeYAML() : string {
if (this._composeYAML === undefined) {
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) {
this._composeYAML = "";
}
@ -135,7 +146,7 @@ export class Stack {
}
// 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> {
@ -149,7 +160,7 @@ export class Stack {
async delete(socket?: DockgeSocket) : Promise<number> {
const terminalName = getComposeTerminalName(this.name);
let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "down", "--remove-orphans", "--rmi", "all" ], this.path);
let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "down", "--remove-orphans" ], this.path);
if (exitCode !== 0) {
throw new Error("Failed to delete, please check the terminal output for more information.");
}
@ -177,13 +188,20 @@ export class Stack {
for (let filename of filenameList) {
try {
// Check if it is a directory
let stat = fs.statSync(path.join(stacksDir, filename));
if (!stat.isDirectory()) {
continue;
}
let stack = this.getStack(server, filename);
stack._status = CREATED_FILE;
stackList.set(filename, stack);
} catch (e) {
if (e instanceof Error) {
log.warn("getStackList", `Failed to get stack ${filename}, error: ${e.message}`);
}
}
}
// Cache by copying
this.managedStackList = new Map(stackList);
@ -346,7 +364,11 @@ export class Stack {
for (let line of lines) {
try {
let obj = JSON.parse(line);
if (obj.Health === "") {
statusList.set(obj.Service, obj.State);
} else {
statusList.set(obj.Service, obj.Health);
}
} catch (e) {
}
}

View File

@ -54,9 +54,11 @@ export class Terminal {
try {
this.ptyProcess?.resize(this.cols, this.rows);
} catch (e) {
if (e instanceof Error) {
log.debug("Terminal", "Failed to resize terminal: " + e.message);
}
}
}
get cols() {
return this._cols;
@ -67,9 +69,11 @@ export class Terminal {
try {
this.ptyProcess?.resize(this.cols, this.rows);
} catch (e) {
if (e instanceof Error) {
log.debug("Terminal", "Failed to resize terminal: " + e.message);
}
}
}
public start() {
if (this._ptyProcess) {
@ -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);
}

View File

@ -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();

View File

@ -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;

View File

@ -4,14 +4,14 @@
*/
export class LimitQueue<T> extends Array<T> {
__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();

View File

@ -7,14 +7,16 @@ services:
# Host Port : Container Port
- 5001:5001
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./data:/app/data
# If you want to use private registries, you need to share the auth file with Dockge:
# - /root/.docker/:/root/.docker
# Docker Socket
- /var/run/docker.sock:/var/run/docker.sock
# Dockge Config
- ./data:/app/data
# Your stacks directory in the host (The paths inside container must be the same as the host)
# ⚠️⚠️ If you did it wrong, your data could end up be written into a wrong path.
# ✔️✔️✔️✔️ CORRECT: - /my-stacks:/my-stacks (Both paths match)
# ❌❌❌❌ WRONG: - /docker:/my-stacks (Both paths do not match)
- /opt/stacks:/opt/stacks
environment:
# Tell Dockge where is your stacks directory

View File

@ -5,7 +5,7 @@
<li v-for="(value, index) in array" :key="index" class="list-group-item">
<select v-model="array[index]" class="no-bg domain-input">
<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>
<font-awesome-icon icon="times" class="action remove ms-2 me-3 text-danger" @click="remove(index)" />

View File

@ -9,7 +9,7 @@
<div v-if="!isEditMode">
<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>
</a>
</div>
@ -27,7 +27,7 @@
<div v-if="isEditMode" class="mt-2">
<button class="btn btn-normal me-2" @click="showConfig = !showConfig">
<font-awesome-icon icon="edit" />
Edit
{{ $t("Edit") }}
</button>
<button v-if="false" class="btn btn-normal me-2">Rename</button>
<button class="btn btn-danger me-2" @click="remove">
@ -179,8 +179,10 @@ export default defineComponent({
},
bgStyle() {
if (this.status === "running") {
if (this.status === "running" || this.status === "healthy") {
return "bg-primary";
} else if (this.status === "unhealthy") {
return "bg-danger";
} else {
return "bg-secondary";
}

View File

@ -1,6 +1,6 @@
<template>
<div>
<h5>Internal Networks</h5>
<h5>{{ $t("Internal Networks") }}</h5>
<ul class="list-group">
<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..." />
@ -10,10 +10,10 @@
<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">
No External Networks
{{ $t("No External Networks") }}
</div>
<div v-for="(networkName, index) in externalNetworkList" :key="networkName" class="form-check form-switch my-3">
@ -32,7 +32,7 @@
class="form-control"
@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") }}
</button>
</div>

View File

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

View File

@ -47,10 +47,10 @@
<input
v-model="settings.primaryHostname"
class="form-control"
placeholder="localhost"
placeholder="(Unset: Follow current hostname)"
/>
<button class="btn btn-outline-primary" type="button" @click="autoGetPrimaryHostname">
{{ $t("Auto Get") }}
{{ $t("autoGet") }}
</button>
</div>
@ -68,13 +68,13 @@
</template>
<script>
import HiddenInput from "../../components/HiddenInput.vue";
import dayjs from "dayjs";
import { timezoneList } from "../../util-frontend";
export default {
components: {
HiddenInput,
},
data() {

View File

@ -3,7 +3,8 @@ import { createI18n } from "vue-i18n/dist/vue-i18n.esm-browser.prod.js";
import en from "./lang/en.json";
const languageList = {
"fr": "Français",
"tr": "Türkçe",
};
let messages = {

View File

@ -1,7 +1,10 @@
{
"languageName": "English",
"Create your admin account": "Create your admin account",
"authIncorrectCreds": "Incorrect username or password.",
"PasswordsDoNotMatch": "Passwords do not match.",
"Repeat Password": "Repeat Password",
"Create": "Create",
"signedInDisp": "Signed in as {0}",
"signedInDispDisabled": "Auth Disabled.",
"home": "Home",
@ -43,11 +46,49 @@
"addContainer": "Add Container",
"addNetwork": "Add Network",
"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.",
"autoGet": "Auto Get",
"add": "Add",
"Edit": "Edit",
"applyToYAML": "Apply to YAML",
"createExternalNetwork": "Create",
"addInternalNetwork": "Add"
"addInternalNetwork": "Add",
"Save": "Save",
"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"
}

94
frontend/src/lang/fr.json Normal file
View File

@ -0,0 +1,94 @@
{
"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"
}

53
frontend/src/lang/tr.json Normal file
View File

@ -0,0 +1,53 @@
{
"languageName": "Türkçe",
"authIncorrectCreds": "Yanlış kullanıcı adı veya parola.",
"PasswordsDoNotMatch": "Parolalar eşleşmiyor.",
"signedInDisp": "{0} olarak oturum açıldı",
"signedInDispDisabled": "Yetkilendirme Devre Dışı.",
"home": "Anasayfa",
"console": "Konsol",
"registry": "Kayıt Defteri",
"compose": "Compose",
"addFirstStackMsg": "İlk yığınınızı oluşturun!",
"stackName" : "Yığın Adı",
"deployStack": "Dağıtmak",
"deleteStack": "Sil",
"stopStack": "Dudur",
"restartStack": "Yeniden Başlat",
"updateStack": "Güncelle",
"startStack": "Başlat",
"editStack": "Düzenle",
"discardStack": ıkar",
"saveStackDraft": "Kaydet",
"notAvailableShort" : "N/A",
"deleteStackMsg": "Bu yığını silmek istediğinizden emin misiniz?",
"stackNotManagedByDockgeMsg": "Bu yığın Dockge tarafından yönetilmemektedir.",
"primaryHostname": "Birincil Ana Bilgisayar Adı",
"general": "Genel",
"container": "Konteyner | Konteynerler",
"scanFolder": "Yığınlar Klasörünü Tara",
"dockerImage": "Görüntü",
"restartPolicyUnlessStopped": "Durdurulana Kadar",
"restartPolicyAlways": "Her zaman",
"restartPolicyOnFailure": "Başarısızlıkta",
"restartPolicyNo": "Hayır",
"environmentVariable": "Ortam Değişkeni | Ortam Değişkenleri",
"restartPolicy": "Yeniden Başlatma Politikası",
"containerName": "Konteyner Adı",
"port": "Port | Portlar",
"volume": "Disk Bölümü | Disk Bölümleri",
"network": "Ağ | Ağlar",
"dependsOn": "Konteyner Bağımlılığı | Konteyner Bağımlılıkları",
"addListItem": "{0} Ekle",
"deleteContainer": "Sil",
"addContainer": "Konteyner Ekle",
"addNetwork": "Ağ Ekle",
"disableauth.message1": "<strong>Kimlik doğrulamayı devre dışı</strong> bırakmak istediğinizden emin misiniz?",
"disableauth.message2": "Cloudflare Access, Authelia veya diğer kimlik doğrulama mekanizmaları gibi Uptime Kuma'nın önünde <strong>üçüncü taraf kimlik doğrulaması uygulamak</strong> istediğiniz senaryolar için tasarlanmıştır.",
"passwordNotMatchMsg": "Tekrarlanan parola eşleşmiyor.",
"autoGet": "Otomatik Al",
"add": "Ekle",
"applyToYAML": "YAML'ye uygulayın",
"createExternalNetwork": "Oluştur",
"addInternalNetwork": "Ekle"
}

View File

@ -68,9 +68,10 @@
<h4 class="mb-3">{{ $t("general") }}</h4>
<div class="shadow-box big-padding mb-3">
<!-- Stack Name -->
<div class="mb-3">
<div>
<label for="name" class="form-label">{{ $t("stackName") }}</label>
<input id="name" v-model="stack.name" type="text" class="form-control" required>
<input id="name" v-model="stack.name" type="text" class="form-control" required @blur="stackNameToLowercase">
<div class="form-text">{{ $t("Lowercase only") }}</div>
</div>
</div>
</div>
@ -117,7 +118,7 @@
</div>
</div>
<div class="col-lg-6">
<h4 class="mb-3">compose.yaml</h4>
<h4 class="mb-3">{{ stack.composeFileName }}</h4>
<!-- YAML editor -->
<div class="shadow-box mb-3 editor-box" :class="{'edit-mode' : isEditMode}">
@ -582,6 +583,10 @@ export default {
});
},
stackNameToLowercase() {
this.stack.name = this.stack?.name?.toLowerCase();
},
}
};
</script>

View File

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

View File

@ -22,12 +22,12 @@
</div>
</div>
<h2 class="mb-3">Docker Run</h2>
<h2 class="mb-3">{{ $t("Docker Run") }}</h2>
<div class="mb-3">
<textarea id="name" v-model="dockerRunCommand" type="text" class="form-control docker-run" required placeholder="docker run ..."></textarea>
</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>
</transition>
<router-view ref="child" />

View File

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

View File

@ -680,6 +680,10 @@ code {
}
}
.form-text {
color: $dark-font-color3;
}
// Vue Prism Editor bug - workaround
// https://github.com/koca/vue-prism-editor/issues/87
/*

View File

@ -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.

View File

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

View File

@ -1,14 +1,15 @@
{
"name": "dockge",
"version": "1.0.1",
"version": "1.1.0",
"type": "module",
"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",
"release-final": "tsx ./extra/test-docker.ts && pnpm run build:frontend && tsx extra/update-version.ts && npm run build:docker",
"release-final": "tsx ./extra/test-docker.ts && tsx extra/update-version.ts && pnpm run build:frontend && npm run build:docker",
"build:frontend": "vite build --config ./frontend/vite.config.ts",
"build:docker-base": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/dockge:base -f ./docker/Base.Dockerfile . --push",
"build:docker": "node ./extra/env2arg.js docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/dockge:latest -t louislam/dockge:1 -t louislam/dockge:$VERSION --target release -f ./docker/Dockerfile . --push",
@ -17,15 +18,14 @@
"mark-as-nightly": "tsx ./extra/mark-as-nightly.ts"
},
"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",
"bcryptjs": "~2.4.3",
"check-password-strength": "~2.0.7",
"command-exists": "~1.2.9",
"compare-versions": "~6.1.0",
"composerize": "~1.4.1",
"croner": "~7.0.4",
"croner": "~7.0.5",
"dayjs": "~1.11.10",
"express": "~4.18.2",
"express-static-gzip": "~2.1.7",
@ -34,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",
@ -45,17 +45,19 @@
"yaml": "~2.3.4"
},
"devDependencies": {
"@fontsource/jetbrains-mono": "^5.0.17",
"@fortawesome/fontawesome-svg-core": "6.4.2",
"@fortawesome/free-regular-svg-icons": "6.4.2",
"@fortawesome/free-solid-svg-icons": "6.4.2",
"@fortawesome/vue-fontawesome": "3.0.3",
"@types/bootstrap": "~5.2.8",
"@types/bcryptjs": "^2.4.6",
"@types/bootstrap": "~5.2.9",
"@types/command-exists": "~1.2.3",
"@types/express": "~4.17.21",
"@types/jsonwebtoken": "~9.0.4",
"@types/jsonwebtoken": "~9.0.5",
"@typescript-eslint/eslint-plugin": "~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-vue-next": "~0.14.10",
"cross-env": "~7.0.3",
@ -66,7 +68,7 @@
"sass": "~1.68.0",
"typescript": "~5.2.2",
"unplugin-vue-components": "~0.25.2",
"vite": "~4.5.0",
"vite": "~5.0.0",
"vite-plugin-compression": "~0.5.1",
"vue": "~3.3.8",
"vue-eslint-parser": "~9.3.2",
@ -75,7 +77,7 @@
"vue-qrcode": "~2.2.0",
"vue-router": "~4.2.5",
"vue-toastification": "2.0.0-rc.5",
"xterm": "~5.4.0-beta.37",
"xterm": "5.4.0-beta.37",
"xterm-addon-web-links": "~0.9.0"
}
}

646
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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