Compare commits

..

46 Commits
wip ... 1.1.0

Author SHA1 Message Date
Louis Lam
a007ec56f7 Update to 1.1.0 2023-11-19 17:44:30 +08:00
Louis Lam
7bb0a1cb08 Minor 2023-11-19 17:36:19 +08:00
Furkan İ
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
Cyril59310
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
Louis Lam
53b052c1e5 Check TypeScript for backend (#64)
* Check Typescript

* Fix backend typescript issues

* Update
2023-11-18 15:54:43 +08:00
Louis Lam
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
broetchenrackete36
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
ThalesC
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
Louis Lam
8c4004f32d Update to 1.0.4 2023-11-17 01:04:14 +08:00
Louis Lam
393bbcae79 Fix #52 2023-11-17 01:02:20 +08:00
DevMirza
9fbf94586b fix ci (#54) 2023-11-16 22:10:53 +08:00
DevMirza
0a46a7df1a Add Github Workflow (#38)
* Create ci..yml

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

* Update README.md

* Update README.md
2023-11-13 02:26:39 +08:00
Louis Lam
6ce75a2df3 Update README.md for Podman 2023-11-13 02:04:10 +08:00
Louis Lam
317c97650d Update to 1.0.2 2023-11-12 23:24:38 +08:00
Louis Lam
9295583727 Fix #9 2023-11-12 23:09:31 +08:00
Louis Lam
6dc998bedf Fix frontend version do not match 2023-11-12 16:47:05 +08:00
Louis Lam
f5552b3344 Update README.md 2023-11-12 16:44:49 +08:00
Louis Lam
b90fd35348 Merge pull request #5 from louislam/release-process
Release process
2023-11-12 16:43:30 +08:00
Louis Lam
3dca9e735a Update to 1.0.1 2023-11-12 16:39:48 +08:00
Louis Lam
200ba0ca07 Update 2023-11-12 16:39:21 +08:00
Louis Lam
dd58a9cbc4 Update 2023-11-12 16:31:38 +08:00
Louis Lam
7f41cc099c Update 2023-11-12 16:29:17 +08:00
Louis Lam
4ce696181b Add release process 2023-11-12 16:27:02 +08:00
Louis Lam
959dbba776 Set a title with hostname 2023-11-12 16:14:35 +08:00
Muhammed Hussein karimi
ffa978eea1 feat: Using monospace fonts in editors (#4)
*  feat: Using monospace fonts in editors

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>

* Update README

---------

Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev>
Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
2023-11-12 15:52:31 +08:00
Louis Lam
9fd0c6416a Update README.md 2023-11-12 13:41:15 +08:00
Louis Lam
e6fc623758 Update README.md 2023-11-12 12:51:36 +08:00
Louis Lam
209dedf682 Update README.md 2023-11-12 02:38:20 +08:00
Louis Lam
cf49a2ef2a Update README.md 2023-11-12 02:11:03 +08:00
Louis Lam
c5d3b23af2 Update README.md 2023-11-12 01:55:11 +08:00
Louis Lam
b12056aa83 Update README.md 2023-11-12 00:34:21 +08:00
Louis Lam
6ad11277e0 Update README 2023-11-12 00:27:05 +08:00
Louis Lam
ab48866ae6 Update Docker 2023-11-11 23:43:53 +08:00
Louis Lam
d55d7c62a2 Fix 2023-11-11 23:43:25 +08:00
Louis Lam
6749e343ba Init (#1) 2023-11-11 22:18:37 +08:00
41 changed files with 1144 additions and 308 deletions

View File

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

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

@@ -4,13 +4,19 @@
# Dockge # Dockge
A fancy, easy-to-use and reactive docker `compose.yaml` stack-oriented manager. 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="" /> <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
## ⭐ Features ## ⭐ Features
- Manage `compose.yaml` - Manage `compose.yaml`
- Create/Edit/Start/Stop/Restart/Delete
- Update Docker Images
- Interactive Editor for `compose.yaml` - Interactive Editor for `compose.yaml`
- Interactive Web Terminal - Interactive Web Terminal
- Reactive - Reactive
@@ -18,20 +24,28 @@ A fancy, easy-to-use and reactive docker `compose.yaml` stack-oriented manager.
- Easy-to-use & fancy UI - Easy-to-use & fancy UI
- If you love Uptime Kuma's UI/UX, you will love this too - If you love Uptime Kuma's UI/UX, you will love this too
- Convert `docker run ...` commands into `compose.yaml` - Convert `docker run ...` commands into `compose.yaml`
- File based structure
- Dockge won't kidnap your compose files, they stored on your drive as usual. You can interact with them using normal `docker compose` commands
<img src="https://github.com/louislam/dockge/assets/1336778/cc071864-592e-4909-b73a-343a57494002" width=300 />
![](https://github.com/louislam/dockge/assets/1336778/89fc1023-b069-42c0-a01c-918c495f1a6a)
## 🔧 How to Install ## 🔧 How to Install
Requirements: Requirements:
- [Docker CE](https://docs.docker.com/engine/install/) 20+ is recommended - [Docker CE](https://docs.docker.com/engine/install/) 20+ is recommended / Podman
- [Docker Compose V2](https://docs.docker.com/compose/install/linux/) - (Docker only) [Docker Compose Plugin](https://docs.docker.com/compose/install/linux/)
- (Podman only) podman-docker (Debian: `apt install podman-docker`)
- OS: - OS:
- As long as you can run Docker CE, it should be fine, but: - 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 - Debian/Raspbian Buster or lower is not supported, please upgrade to Bullseye or higher
- Arch: armv7, arm64, amd64 (a.k.a x86_64) - Arch: armv7, arm64, amd64 (a.k.a x86_64)
### Basic ### Basic
Default stacks directory is `/opt/stacks`. - Default Stacks Directory: `/opt/stacks`
- Default Port: 5001
``` ```
# Create a directory that stores your stacks and stores dockge's compose.yaml # Create a directory that stores your stacks and stores dockge's compose.yaml
@@ -44,16 +58,16 @@ curl https://raw.githubusercontent.com/louislam/dockge/master/compose.yaml --out
# Start Server # Start Server
docker compose up -d docker compose up -d
# If you are using docker-compose V1 # If you are using docker-compose V1 or Podman
# docker-compose up -d # docker-compose up -d
``` ```
Dockge is now running on http://localhost:5001
### Advanced ### Advanced
If you want to store your stacks in another directory, you can change the `DOCKGE_STACKS_DIR` environment variable and volumes. If you want to store your stacks in another directory, you can change the `DOCKGE_STACKS_DIR` environment variable and volumes.
For exmaples, if you want to store your stacks in `/my-stacks`:
```yaml ```yaml
version: "3.8" version: "3.8"
services: services:
@@ -61,27 +75,45 @@ services:
image: louislam/dockge:1 image: louislam/dockge:1
restart: unless-stopped restart: unless-stopped
ports: ports:
# Host Port:Container Port
- 5001:5001 - 5001:5001
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- ./data:/app/data - ./data:/app/data
# Your stacks directory in the host # If you want to use private registries, you need to share the auth file with Dockge:
# (The paths inside container must be the same as the host) # - /root/.docker/:/root/.docker
- /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: environment:
# Tell Dockge where is your stacks directory # Tell Dockge where is your stacks directory
- DOCKGE_STACKS_DIR=/my-stacks - DOCKGE_STACKS_DIR=/opt/stacks
``` ```
## How to Update ## How to Update
```bash ```bash
cd /opt/stacks cd /opt/dockge
docker compose pull docker compose pull
docker compose up -d docker compose up -d
``` ```
## Screenshots
![](https://github.com/louislam/dockge/assets/1336778/e7ff0222-af2e-405c-b533-4eab04791b40)
![](https://github.com/louislam/dockge/assets/1336778/7139e88c-77ed-4d45-96e3-00b66d36d871)
![](https://github.com/louislam/dockge/assets/1336778/f019944c-0e87-405b-a1b8-625b35de1eeb)
![](https://github.com/louislam/dockge/assets/1336778/a4478d23-b1c4-4991-8768-1a7cad3472e3)
## Motivations ## Motivations
- I have been using Portainer for some time, but for the stack management, I am sometimes not satisfied with it. For example, sometimes when I try to deploy a stack, the loading icon keeps spinning for a few minutes without progress. And sometimes error messages are not clear. - I have been using Portainer for some time, but for the stack management, I am sometimes not satisfied with it. For example, sometimes when I try to deploy a stack, the loading icon keeps spinning for a few minutes without progress. And sometimes error messages are not clear.
@@ -90,16 +122,22 @@ docker compose up -d
If you love this project, please consider giving this project a ⭐. If you love this project, please consider giving this project a ⭐.
## 🗣️
### Bug Report
https://github.com/louislam/dockge/issues
### Ask for Help / Discussions
https://github.com/louislam/dockge/discussions
## FAQ ## FAQ
#### "Dockge"? #### "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`. 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`? #### 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. 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 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 instanceof Error) {
if (e.message.includes("the following files are missing:")) { // Allow missing patch files for downgrade or testing pr.
log.warn("db", e.message); if (e.message.includes("the following files are missing:")) {
log.warn("db", "Database migration failed, you may be downgrading Dockge."); log.warn("db", e.message);
} else { log.warn("db", "Database migration failed, you may be downgrading Dockge.");
log.error("db", "Database migration failed"); } else {
throw e; log.error("db", "Database migration failed");
throw e;
}
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 * @param key Key of setting to retrieve
* @returns {Promise<any>} Value * @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 key Key of setting to set
* @param {any} value Value to set to * @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 * @param type The type of setting
* @returns {Promise<Bean>} Settings * @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 type Type of settings to set
* @param {object} data Values of settings * @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;
} }
} }
} }

View File

@@ -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({ if (e instanceof Error) {
ok: false, callback({
msg: e.message, 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({ if (e instanceof Error) {
ok: false, callback({
msg: e.message, ok: false,
}); msg: e.message,
});
}
} }
}); });
@@ -229,10 +237,12 @@ export class MainSocketHandler extends SocketHandler {
}); });
} catch (e) { } catch (e) {
callback({ if (e instanceof Error) {
ok: false, callback({
msg: e.message, 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({ if (e instanceof Error) {
ok: false, callback({
msg: e.message, 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

View File

@@ -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({ if (e instanceof Error) {
ok: false, errorCallback({
msg: e.message, ok: false,
}); msg: e.message,
});
}
} }
}); });

View File

@@ -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,
}; };
} }
@@ -72,9 +83,9 @@ export class Stack {
} }
validate() { validate() {
// Check name, allows [a-z][A-Z][0-9] _ - only // Check name, allows [a-z][0-9] _ - only
if (!this.name.match(/^[a-zA-Z0-9_-]+$/)) { if (!this.name.match(/^[a-z0-9_-]+$/)) {
throw new ValidationError("Stack name can only contain [a-z][A-Z][0-9] _ - only"); throw new ValidationError("Stack name can only contain [a-z][0-9] _ - only");
} }
// Check YAML format // Check YAML format
@@ -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> {
@@ -149,7 +160,7 @@ export class Stack {
async delete(socket?: DockgeSocket) : Promise<number> { async delete(socket?: DockgeSocket) : Promise<number> {
const terminalName = getComposeTerminalName(this.name); 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) { if (exitCode !== 0) {
throw new Error("Failed to delete, please check the terminal output for more information."); throw new Error("Failed to delete, please check the terminal output for more information.");
} }
@@ -177,11 +188,18 @@ export class Stack {
for (let filename of filenameList) { for (let filename of filenameList) {
try { 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); let stack = this.getStack(server, filename);
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}`);
}
} }
} }
@@ -346,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) {
} }
} }

View File

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

View File

@@ -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();
@@ -86,8 +91,8 @@ export const TERMINAL_COLS = 105;
export const TERMINAL_ROWS = 10; export const TERMINAL_ROWS = 10;
export const PROGRESS_TERMINAL_ROWS = 8; export const PROGRESS_TERMINAL_ROWS = 8;
export const COMBINED_TERMINAL_COLS = 56; export const COMBINED_TERMINAL_COLS = 58;
export const COMBINED_TERMINAL_ROWS = 15; export const COMBINED_TERMINAL_ROWS = 20;
export const ERROR_TYPE_VALIDATION = 1; export const ERROR_TYPE_VALIDATION = 1;

View File

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

View File

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

View File

@@ -7,11 +7,16 @@ services:
# Host Port : Container Port # Host Port : Container Port
- 5001:5001 - 5001:5001
volumes: volumes:
# Docker Socket
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
# Dockge Config
- ./data:/app/data - ./data:/app/data
# 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) # 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 - /opt/stacks:/opt/stacks
environment: environment:
# Tell Dockge where is your stacks directory # Tell Dockge where is your stacks directory

20
extra/env2arg.js Normal file
View File

@@ -0,0 +1,20 @@
#!/usr/bin/env node
import childProcess from "child_process";
let env = process.env;
let cmd = process.argv[2];
let args = process.argv.slice(3);
let replacedArgs = [];
for (let arg of args) {
for (let key in env) {
arg = arg.replaceAll(`$${key}`, env[key]);
}
replacedArgs.push(arg);
}
let child = childProcess.spawn(cmd, replacedArgs);
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);

9
extra/test-docker.ts Normal file
View File

@@ -0,0 +1,9 @@
// Check if docker is running
import { exec } from "child_process";
exec("docker ps", (err, stdout, stderr) => {
if (err) {
console.error("Docker is not running. Please start docker and try again.");
process.exit(1);
}
});

64
extra/update-version.ts Normal file
View File

@@ -0,0 +1,64 @@
import pkg from "../package.json";
import childProcess from "child_process";
import fs from "fs";
const newVersion = process.env.VERSION;
console.log("New Version: " + newVersion);
if (! newVersion) {
console.error("invalid version");
process.exit(1);
}
const exists = tagExists(newVersion);
if (! exists) {
// Process package.json
pkg.version = newVersion;
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n");
commit(newVersion);
tag(newVersion);
} else {
console.log("version exists");
}
/**
* Commit updated files
* @param {string} version Version to update to
*/
function commit(version) {
let msg = "Update to " + version;
let res = childProcess.spawnSync("git", [ "commit", "-m", msg, "-a" ]);
let stdout = res.stdout.toString().trim();
console.log(stdout);
if (stdout.includes("no changes added to commit")) {
throw new Error("commit error");
}
}
/**
* Create a tag with the specified version
* @param {string} version Tag to create
*/
function tag(version) {
let res = childProcess.spawnSync("git", [ "tag", version ]);
console.log(res.stdout.toString().trim());
}
/**
* Check if a tag exists for the specified version
* @param {string} version Version to check
* @returns {boolean} Does the tag already exist
*/
function tagExists(version) {
if (! version) {
throw new Error("invalid version");
}
let res = childProcess.spawnSync("git", [ "tag", "-l", version ]);
return res.stdout.toString().trim() === version;
}

View File

@@ -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)" />

View File

@@ -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">
@@ -115,7 +115,7 @@
{{ $tc("network", 2) }} {{ $tc("network", 2) }}
</label> </label>
<div v-if="networkList.length === 0 && service.networks.length > 0" class="text-warning mb-3"> <div v-if="networkList.length === 0 && service.networks && service.networks.length > 0" class="text-warning mb-3">
No networks available. You need to add internal networks or enable external networks in the right side first. No networks available. You need to add internal networks or enable external networks in the right side first.
</div> </div>
@@ -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";
} }

View File

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

View File

@@ -77,8 +77,8 @@ export default {
} }
this.terminal = new Terminal({ this.terminal = new Terminal({
fontSize: 16, fontSize: 14,
fontFamily: "monospace", fontFamily: "'JetBrains Mono', monospace",
cursorBlink, cursorBlink,
cols: this.cols, cols: this.cols,
rows: this.rows, rows: this.rows,

View File

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

View File

@@ -47,10 +47,10 @@
<input <input
v-model="settings.primaryHostname" v-model="settings.primaryHostname"
class="form-control" class="form-control"
placeholder="localhost" placeholder="(Unset: Follow current hostname)"
/> />
<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() {

View File

@@ -3,7 +3,8 @@ 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",
"tr": "Türkçe",
}; };
let messages = { let messages = {

View File

@@ -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,49 @@
"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": "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

@@ -13,6 +13,7 @@ import Toast, { POSITION, useToast } from "vue-toastification";
import "xterm/lib/xterm.js"; import "xterm/lib/xterm.js";
// CSS // CSS
import "@fontsource/jetbrains-mono";
import "vue-toastification/dist/index.css"; import "vue-toastification/dist/index.css";
import "xterm/css/xterm.css"; import "xterm/css/xterm.css";
import "./styles/main.scss"; import "./styles/main.scss";
@@ -22,6 +23,9 @@ import socket from "./mixins/socket";
import lang from "./mixins/lang"; import lang from "./mixins/lang";
import theme from "./mixins/theme"; import theme from "./mixins/theme";
// Set Title
document.title = document.title + " - " + location.host;
const app = createApp(rootApp()); const app = createApp(rootApp());
app.use(Toast, { app.use(Toast, {

View File

@@ -68,9 +68,10 @@
<h4 class="mb-3">{{ $t("general") }}</h4> <h4 class="mb-3">{{ $t("general") }}</h4>
<div class="shadow-box big-padding mb-3"> <div class="shadow-box big-padding mb-3">
<!-- Stack Name --> <!-- Stack Name -->
<div class="mb-3"> <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> <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> </div>
</div> </div>
@@ -117,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}">
@@ -582,6 +583,10 @@ export default {
}); });
}, },
stackNameToLowercase() {
this.stack.name = this.stack?.name?.toLowerCase();
},
} }
}; };
</script> </script>
@@ -592,6 +597,8 @@ export default {
} }
.editor-box { .editor-box {
font-family: 'JetBrains Mono', monospace;
font-size: 14px;
&.edit-mode { &.edit-mode {
background-color: #2c2f38 !important; background-color: #2c2f38 !important;
} }

View File

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

View File

@@ -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" />
@@ -227,5 +227,7 @@ table {
.docker-run { .docker-run {
background-color: $dark-bg !important; background-color: $dark-bg !important;
border: none; border: none;
font-family: 'JetBrains Mono', monospace;
font-size: 15px;
} }
</style> </style>

View File

@@ -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"),

View File

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

View File

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

View File

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

View File

@@ -1,29 +1,31 @@
{ {
"name": "dockge", "name": "dockge",
"version": "1.0.0", "version": "1.1.0",
"type": "module", "type": "module",
"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",
"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: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-base": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/dockge:base -f ./docker/Base.Dockerfile . --push",
"build:docker": "pnpm run build:frontend && docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/dockge:latest -t louislam/dockge:1 -t louislam/dockge:1.0.0 -f ./docker/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",
"build:docker-nightly": "pnpm run build:frontend && docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/dockge:nightly --target nightly -f ./docker/Dockerfile . --push", "build:docker-nightly": "pnpm run build:frontend && docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/dockge:nightly --target nightly -f ./docker/Dockerfile . --push",
"start-docker": "docker run --rm -p 5001:5001 --name dockge louislam/dockge:latest", "start-docker": "docker run --rm -p 5001:5001 --name dockge louislam/dockge:latest",
"mark-as-nightly": "tsx ./extra/mark-as-nightly.ts" "mark-as-nightly": "tsx ./extra/mark-as-nightly.ts"
}, },
"dependencies": { "dependencies": {
"@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",
"command-exists": "~1.2.9", "command-exists": "~1.2.9",
"compare-versions": "~6.1.0", "compare-versions": "~6.1.0",
"composerize": "~1.4.1", "composerize": "~1.4.1",
"croner": "~7.0.4", "croner": "~7.0.5",
"dayjs": "~1.11.10", "dayjs": "~1.11.10",
"express": "~4.18.2", "express": "~4.18.2",
"express-static-gzip": "~2.1.7", "express-static-gzip": "~2.1.7",
@@ -32,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", "mysql2": "~3.6.3",
"redbean-node": "0.3.2", "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",
@@ -43,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/bootstrap": "~5.2.8", "@types/bcryptjs": "^2.4.6",
"@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.4", "@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",
@@ -64,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",
@@ -73,7 +77,7 @@
"vue-qrcode": "~2.2.0", "vue-qrcode": "~2.2.0",
"vue-router": "~4.2.5", "vue-router": "~4.2.5",
"vue-toastification": "2.0.0-rc.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" "xterm-addon-web-links": "~0.9.0"
} }
} }

645
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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