mirror of
https://github.com/louislam/dockge.git
synced 2025-08-16 00:57:59 +02:00
Compare commits
27 Commits
Author | SHA1 | Date | |
---|---|---|---|
d8d25563ba | |||
83d58cd363 | |||
caa82bbad5 | |||
2530cac989 | |||
06307956ca | |||
9b9234434e | |||
a12c6dc033 | |||
eb6db8b31e | |||
015e4c21f9 | |||
7a63d59ef8 | |||
ecbbdae7ab | |||
e7abbcbefa | |||
59adcc148c | |||
5e9065a4d6 | |||
35a79ea8a6 | |||
0c9fc4ead2 | |||
1c5ff7914b | |||
f7c66a476c | |||
5daa6fd788 | |||
c2ec9ac7f4 | |||
21e736459e | |||
d7f4873405 | |||
2ed739b1b9 | |||
314630724b | |||
e67d08b7b3 | |||
7d1da2ad99 | |||
5f70fa6baf |
@ -92,9 +92,6 @@ 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
60
.github/workflows/ci.yml
vendored
@ -1,60 +0,0 @@
|
|||||||
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..
|
|
||||||
|
|
72
README.md
72
README.md
@ -4,19 +4,13 @@
|
|||||||
|
|
||||||
# Dockge
|
# Dockge
|
||||||
|
|
||||||
A fancy, easy-to-use and reactive self-hosted docker compose.yaml stack-oriented manager.
|
A fancy, easy-to-use and reactive docker `compose.yaml` stack-oriented manager.
|
||||||
|
|
||||||
      
|
|
||||||
|
|
||||||
<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
|
||||||
@ -24,28 +18,20 @@ View Video: https://youtu.be/AWAlOQeNpgU?t=48
|
|||||||
- 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 />
|
|
||||||
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## 🔧 How to Install
|
## 🔧 How to Install
|
||||||
|
|
||||||
Requirements:
|
Requirements:
|
||||||
- [Docker CE](https://docs.docker.com/engine/install/) 20+ is recommended / Podman
|
- [Docker CE](https://docs.docker.com/engine/install/) 20+ is recommended
|
||||||
- (Docker only) [Docker Compose Plugin](https://docs.docker.com/compose/install/linux/)
|
- [Docker Compose V2](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 / Podman, it should be fine, but:
|
- 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 or higher
|
- Debian/Raspbian Buster or lower is not supported, please upgrade to Bullseye
|
||||||
- Arch: armv7, arm64, amd64 (a.k.a x86_64)
|
- Arch: armv7, arm64, amd64 (a.k.a x86_64)
|
||||||
|
|
||||||
### Basic
|
### Basic
|
||||||
|
|
||||||
- Default Stacks Directory: `/opt/stacks`
|
Default stacks directory is `/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
|
||||||
@ -58,16 +44,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 or Podman
|
# If you are using docker-compose V1
|
||||||
# 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:
|
||||||
@ -75,45 +61,27 @@ 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
|
||||||
|
|
||||||
# If you want to use private registries, you need to share the auth file with Dockge:
|
# Your stacks directory in the host
|
||||||
# - /root/.docker/:/root/.docker
|
# (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:
|
environment:
|
||||||
# Tell Dockge where is your stacks directory
|
# Tell Dockge where is your stacks directory
|
||||||
- DOCKGE_STACKS_DIR=/opt/stacks
|
- DOCKGE_STACKS_DIR=/my-stacks
|
||||||
```
|
```
|
||||||
|
|
||||||
## How to Update
|
## How to Update
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /opt/dockge
|
cd /opt/stacks
|
||||||
docker compose pull
|
docker compose pull
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
## Screenshots
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
## 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.
|
||||||
@ -122,22 +90,16 @@ 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 `Dodge`.
|
"Dockge" is a coinage word which is created by myself. I hope it sounds like `Badge` but replacing with `Dock` - `Dock-ge`.
|
||||||
|
|
||||||
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.
|
||||||
|
@ -5,7 +5,6 @@ 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";
|
||||||
@ -13,11 +12,6 @@ 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 {
|
||||||
@ -25,7 +19,7 @@ export class Database {
|
|||||||
* SQLite file path (Default: ./data/dockge.db)
|
* SQLite file path (Default: ./data/dockge.db)
|
||||||
* @type {string}
|
* @type {string}
|
||||||
*/
|
*/
|
||||||
static sqlitePath : string;
|
static sqlitePath;
|
||||||
|
|
||||||
static noReject = true;
|
static noReject = true;
|
||||||
|
|
||||||
@ -57,7 +51,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() : DBConfig {
|
static readDBConfig() {
|
||||||
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);
|
||||||
|
|
||||||
@ -73,10 +67,10 @@ export class Database {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {string|undefined} envString
|
* @typedef {string|undefined} envString
|
||||||
* @param dbConfig the database configuration that should be written
|
* @param {{type: "sqlite"} | {type:envString, hostname:envString, port:envString, database:envString, username:envString, password:envString}} dbConfig the database configuration that should be written
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
static writeDBConfig(dbConfig : DBConfig) {
|
static writeDBConfig(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));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,17 +80,14 @@ 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) {
|
static async connect(autoloadModels = true, noLog = false) {
|
||||||
const acquireConnectionTimeout = 120 * 1000;
|
const acquireConnectionTimeout = 120 * 1000;
|
||||||
let dbConfig : DBConfig;
|
let dbConfig;
|
||||||
try {
|
try {
|
||||||
dbConfig = this.readDBConfig();
|
dbConfig = this.readDBConfig();
|
||||||
Database.dbConfig = dbConfig;
|
Database.dbConfig = dbConfig;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Error) {
|
log.warn("db", err.message);
|
||||||
log.warn("db", err.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
dbConfig = {
|
dbConfig = {
|
||||||
type: "sqlite",
|
type: "sqlite",
|
||||||
};
|
};
|
||||||
@ -185,15 +176,13 @@ export class Database {
|
|||||||
directory: Database.knexMigrationsPath,
|
directory: Database.knexMigrationsPath,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
// Allow missing patch files for downgrade or testing pr.
|
||||||
// Allow missing patch files for downgrade or testing pr.
|
if (e.message.includes("the following files are missing:")) {
|
||||||
if (e.message.includes("the following files are missing:")) {
|
log.warn("db", e.message);
|
||||||
log.warn("db", e.message);
|
log.warn("db", "Database migration failed, you may be downgrading Dockge.");
|
||||||
log.warn("db", "Database migration failed, you may be downgrading Dockge.");
|
} else {
|
||||||
} else {
|
log.error("db", "Database migration failed");
|
||||||
log.error("db", "Database migration failed");
|
throw e;
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 || Number(process.env.DOCKGE_PORT) || 5001;
|
this.config.port = args.port || parseInt(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") as User);
|
this.afterLogin(socket as DockgeSocket, await R.findOne("user"));
|
||||||
socket.emit("autoLogin");
|
socket.emit("autoLogin");
|
||||||
} else {
|
} else {
|
||||||
log.debug("auth", "need auth");
|
log.debug("auth", "need auth");
|
||||||
@ -253,9 +253,7 @@ export class DockgeServer {
|
|||||||
try {
|
try {
|
||||||
await Database.init(this);
|
await Database.init(this);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
log.error("server", "Failed to prepare your database: " + e.message);
|
||||||
log.error("server", "Failed to prepare your database: " + e.message);
|
|
||||||
}
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -293,7 +291,7 @@ export class DockgeServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run every 5 seconds
|
// Run every 5 seconds
|
||||||
Cron("*/2 * * * * *", {
|
const job = 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");
|
||||||
@ -378,9 +376,7 @@ export class DockgeServer {
|
|||||||
return process.env.TZ;
|
return process.env.TZ;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
log.warn("timezone", e.message + " in process.env.TZ");
|
||||||
log.warn("timezone", e.message + " in process.env.TZ");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const timezone = await Settings.get("serverTimezone");
|
const timezone = await Settings.get("serverTimezone");
|
||||||
@ -393,9 +389,7 @@ export class DockgeServer {
|
|||||||
return timezone;
|
return timezone;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
log.warn("timezone", e.message + " in settings");
|
||||||
log.warn("timezone", e.message + " in settings");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guess
|
// Guess
|
||||||
|
@ -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 : string, hash : string) {
|
export function verifyPassword(password, hash) {
|
||||||
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 : string, len : number) {
|
export function shake256(data, len) {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,8 @@
|
|||||||
// "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, RateLimiterOpts } from "limiter-es6-compat";
|
import { RateLimiter } 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;
|
||||||
@ -17,7 +11,7 @@ class KumaRateLimiter {
|
|||||||
/**
|
/**
|
||||||
* @param {object} config Rate limiter configuration object
|
* @param {object} config Rate limiter configuration object
|
||||||
*/
|
*/
|
||||||
constructor(config : KumaRateLimiterOpts) {
|
constructor(config) {
|
||||||
this.errorMessage = config.errorMessage;
|
this.errorMessage = config.errorMessage;
|
||||||
this.rateLimiter = new RateLimiter(config);
|
this.rateLimiter = new RateLimiter(config);
|
||||||
}
|
}
|
||||||
@ -30,11 +24,11 @@ class KumaRateLimiter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Should the request be passed through
|
* Should the request be passed through
|
||||||
* @param callback Callback function to call with decision
|
* @param {passCB} 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 : KumaRateLimiterCallback, num = 1) {
|
async pass(callback, num = 1) {
|
||||||
const remainingRequests = await this.removeTokens(num);
|
const remainingRequests = await this.removeTokens(num);
|
||||||
log.info("rate-limit", "remaining requests: " + remainingRequests);
|
log.info("rate-limit", "remaining requests: " + remainingRequests);
|
||||||
if (remainingRequests < 0) {
|
if (remainingRequests < 0) {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { DockgeServer } from "../dockge-server";
|
import { DockgeServer } from "../dockgeServer";
|
||||||
import { Router } from "../router";
|
import { Router } from "../router";
|
||||||
import express, { Express, Router as ExpressRouter } from "express";
|
import express, { Express, Router as ExpressRouter } from "express";
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
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 {
|
||||||
|
|
||||||
@ -16,19 +15,20 @@ export class Settings {
|
|||||||
* timestamp: 12345678
|
* timestamp: 12345678
|
||||||
* },
|
* },
|
||||||
* }
|
* }
|
||||||
|
* @type {{}}
|
||||||
*/
|
*/
|
||||||
static cacheList : LooseObject = {
|
static cacheList = {
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static cacheCleaner? : NodeJS.Timeout;
|
static cacheCleaner = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve value of setting based on key
|
* Retrieve value of setting based on key
|
||||||
* @param key Key of setting to retrieve
|
* @param {string} key Key of setting to retrieve
|
||||||
* @returns Value
|
* @returns {Promise<any>} Value
|
||||||
*/
|
*/
|
||||||
static async get(key : string) {
|
static async get(key) {
|
||||||
|
|
||||||
// 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 key Key of setting to set
|
* @param {string} key Key of setting to set
|
||||||
* @param value Value to set to
|
* @param {any} 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 : string, value : object | string | number | boolean, type : string | null = null) {
|
static async set(key, value, type = 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 type The type of setting
|
* @param {string} type The type of setting
|
||||||
* @returns Settings
|
* @returns {Promise<Bean>} Settings
|
||||||
*/
|
*/
|
||||||
static async getSettings(type : string) {
|
static async getSettings(type) {
|
||||||
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 : LooseObject = {};
|
const result = {};
|
||||||
|
|
||||||
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 type Type of settings to set
|
* @param {string} type Type of settings to set
|
||||||
* @param data Values of settings
|
* @param {object} data Values of settings
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
static async setSettings(type : string, data : LooseObject) {
|
static async setSettings(type, data) {
|
||||||
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 : string[]) {
|
static deleteCache(keyList) {
|
||||||
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 = undefined;
|
Settings.cacheCleaner = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
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, JWTDecoded } from "../util-server";
|
import { checkLogin, DockgeSocket, doubleCheckPassword } 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";
|
||||||
@ -42,12 +43,10 @@ export class MainSocketHandler extends SocketHandler {
|
|||||||
});
|
});
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
callback({
|
||||||
callback({
|
ok: false,
|
||||||
ok: false,
|
msg: e.message,
|
||||||
msg: e.message,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -58,7 +57,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) as JWTDecoded;
|
const decoded = jwt.verify(token, server.jwtSecret);
|
||||||
|
|
||||||
log.info("auth", "Username from JWT: " + decoded.username);
|
log.info("auth", "Username from JWT: " + decoded.username);
|
||||||
|
|
||||||
@ -92,13 +91,9 @@ 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,
|
||||||
@ -154,7 +149,6 @@ 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) {
|
||||||
@ -217,12 +211,10 @@ export class MainSocketHandler extends SocketHandler {
|
|||||||
});
|
});
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
callback({
|
||||||
callback({
|
ok: false,
|
||||||
ok: false,
|
msg: e.message,
|
||||||
msg: e.message,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -237,12 +229,10 @@ export class MainSocketHandler extends SocketHandler {
|
|||||||
});
|
});
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
callback({
|
||||||
callback({
|
ok: false,
|
||||||
ok: false,
|
msg: e.message,
|
||||||
msg: e.message,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -272,24 +262,22 @@ export class MainSocketHandler extends SocketHandler {
|
|||||||
server.sendInfo(socket);
|
server.sendInfo(socket);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
callback({
|
||||||
callback({
|
ok: false,
|
||||||
ok: false,
|
msg: e.message,
|
||||||
msg: e.message,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async login(username : string, password : string) : Promise<User | null> {
|
async login(username : string, password : string) {
|
||||||
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
|
||||||
|
@ -38,12 +38,10 @@ 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) {
|
||||||
if (e instanceof Error) {
|
errorCallback({
|
||||||
errorCallback({
|
ok: false,
|
||||||
ok: false,
|
msg: e.message,
|
||||||
msg: e.message,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -24,7 +24,6 @@ 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;
|
||||||
@ -35,15 +34,6 @@ 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 {
|
||||||
@ -60,7 +50,6 @@ export class Stack {
|
|||||||
status: this._status,
|
status: this._status,
|
||||||
tags: [],
|
tags: [],
|
||||||
isManagedByDockge: this.isManagedByDockge,
|
isManagedByDockge: this.isManagedByDockge,
|
||||||
composeFileName: this._composeFileName,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,9 +72,9 @@ export class Stack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
validate() {
|
validate() {
|
||||||
// Check name, allows [a-z][0-9] _ - only
|
// Check name, allows [a-z][A-Z][0-9] _ - only
|
||||||
if (!this.name.match(/^[a-z0-9_-]+$/)) {
|
if (!this.name.match(/^[a-zA-Z0-9_-]+$/)) {
|
||||||
throw new ValidationError("Stack name can only contain [a-z][0-9] _ - only");
|
throw new ValidationError("Stack name can only contain [a-z][A-Z][0-9] _ - only");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check YAML format
|
// Check YAML format
|
||||||
@ -95,7 +84,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, this._composeFileName), "utf-8");
|
this._composeYAML = fs.readFileSync(path.join(this.path, "compose.yaml"), "utf-8");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this._composeYAML = "";
|
this._composeYAML = "";
|
||||||
}
|
}
|
||||||
@ -146,7 +135,7 @@ export class Stack {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write or overwrite the compose.yaml
|
// Write or overwrite the compose.yaml
|
||||||
fs.writeFileSync(path.join(dir, this._composeFileName), this.composeYAML);
|
fs.writeFileSync(path.join(dir, "compose.yaml"), this.composeYAML);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deploy(socket? : DockgeSocket) : Promise<number> {
|
async deploy(socket? : DockgeSocket) : Promise<number> {
|
||||||
@ -160,7 +149,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" ], this.path);
|
let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "down", "--remove-orphans", "--rmi", "all" ], 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.");
|
||||||
}
|
}
|
||||||
@ -188,18 +177,11 @@ 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) {
|
||||||
if (e instanceof Error) {
|
log.warn("getStackList", `Failed to get stack ${filename}, error: ${e.message}`);
|
||||||
log.warn("getStackList", `Failed to get stack ${filename}, error: ${e.message}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -364,11 +346,7 @@ 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);
|
||||||
if (obj.Health === "") {
|
statusList.set(obj.Service, obj.State);
|
||||||
statusList.set(obj.Service, obj.State);
|
|
||||||
} else {
|
|
||||||
statusList.set(obj.Service, obj.Health);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,9 +54,7 @@ export class Terminal {
|
|||||||
try {
|
try {
|
||||||
this.ptyProcess?.resize(this.cols, this.rows);
|
this.ptyProcess?.resize(this.cols, this.rows);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
log.debug("Terminal", "Failed to resize terminal: " + e.message);
|
||||||
log.debug("Terminal", "Failed to resize terminal: " + e.message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,9 +67,7 @@ export class Terminal {
|
|||||||
try {
|
try {
|
||||||
this.ptyProcess?.resize(this.cols, this.rows);
|
this.ptyProcess?.resize(this.cols, this.rows);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
log.debug("Terminal", "Failed to resize terminal: " + e.message);
|
||||||
log.debug("Terminal", "Failed to resize terminal: " + e.message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,7 +85,7 @@ export class Terminal {
|
|||||||
|
|
||||||
// On Data
|
// On Data
|
||||||
this._ptyProcess.onData((data) => {
|
this._ptyProcess.onData((data) => {
|
||||||
this.buffer.pushItem(data);
|
this.buffer.push(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);
|
||||||
}
|
}
|
||||||
|
@ -12,11 +12,6 @@ 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();
|
||||||
|
|
||||||
@ -91,8 +86,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 = 58;
|
export const COMBINED_TERMINAL_COLS = 56;
|
||||||
export const COMBINED_TERMINAL_ROWS = 20;
|
export const COMBINED_TERMINAL_ROWS = 15;
|
||||||
|
|
||||||
export const ERROR_TYPE_VALIDATION = 1;
|
export const ERROR_TYPE_VALIDATION = 1;
|
||||||
|
|
||||||
|
@ -6,11 +6,6 @@ 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;
|
||||||
|
@ -4,14 +4,14 @@
|
|||||||
*/
|
*/
|
||||||
export class LimitQueue<T> extends Array<T> {
|
export class LimitQueue<T> extends Array<T> {
|
||||||
__limit;
|
__limit;
|
||||||
__onExceed? : (item : T | undefined) => void;
|
__onExceed = null;
|
||||||
|
|
||||||
constructor(limit: number) {
|
constructor(limit: number) {
|
||||||
super();
|
super();
|
||||||
this.__limit = limit;
|
this.__limit = limit;
|
||||||
}
|
}
|
||||||
|
|
||||||
pushItem(value : T) {
|
push(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();
|
||||||
|
@ -7,16 +7,11 @@ 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
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
#!/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);
|
|
@ -1,9 +0,0 @@
|
|||||||
// 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);
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,64 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
@ -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" :key="option" :value="option">{{ option }}</option>
|
<option v-for="option in options" :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)" />
|
||||||
|
@ -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" :key="port" :href="parsePort(port).url" target="_blank">
|
<a v-for="port in service.ports" :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>
|
||||||
@ -115,7 +115,7 @@
|
|||||||
{{ $tc("network", 2) }}
|
{{ $tc("network", 2) }}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div v-if="networkList.length === 0 && service.networks && service.networks.length > 0" class="text-warning mb-3">
|
<div v-if="networkList.length === 0 && 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,10 +179,8 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
bgStyle() {
|
bgStyle() {
|
||||||
if (this.status === "running" || this.status === "healthy") {
|
if (this.status === "running") {
|
||||||
return "bg-primary";
|
return "bg-primary";
|
||||||
} else if (this.status === "unhealthy") {
|
|
||||||
return "bg-danger";
|
|
||||||
} else {
|
} else {
|
||||||
return "bg-secondary";
|
return "bg-secondary";
|
||||||
}
|
}
|
||||||
|
@ -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">
|
<button class="btn btn-normal btn-sm me-2" type="button" @click="">
|
||||||
{{ $t("createExternalNetwork") }}
|
{{ $t("createExternalNetwork") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -77,8 +77,8 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.terminal = new Terminal({
|
this.terminal = new Terminal({
|
||||||
fontSize: 14,
|
fontSize: 16,
|
||||||
fontFamily: "'JetBrains Mono', monospace",
|
fontFamily: "monospace",
|
||||||
cursorBlink,
|
cursorBlink,
|
||||||
cols: this.cols,
|
cols: this.cols,
|
||||||
rows: this.rows,
|
rows: this.rows,
|
||||||
|
@ -19,6 +19,7 @@ export default {
|
|||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
uptime() {
|
uptime() {
|
||||||
|
return "0.00%";
|
||||||
return this.$t("notAvailableShort");
|
return this.$t("notAvailableShort");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -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() {
|
||||||
|
@ -13,7 +13,6 @@ 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";
|
||||||
@ -23,9 +22,6 @@ 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, {
|
||||||
|
@ -68,10 +68,9 @@
|
|||||||
<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>
|
<div class="mb-3">
|
||||||
<label for="name" class="form-label">{{ $t("stackName") }}</label>
|
<label for="name" class="form-label">{{ $t("stackName") }}</label>
|
||||||
<input id="name" v-model="stack.name" type="text" class="form-control" required @blur="stackNameToLowercase">
|
<input id="name" v-model="stack.name" type="text" class="form-control" required>
|
||||||
<div class="form-text">Lowercase only</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -118,7 +117,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-6">
|
<div class="col-lg-6">
|
||||||
<h4 class="mb-3">{{ stack.composeFileName }}</h4>
|
<h4 class="mb-3">compose.yaml</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}">
|
||||||
@ -583,10 +582,6 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
stackNameToLowercase() {
|
|
||||||
this.stack.name = this.stack?.name?.toLowerCase();
|
|
||||||
},
|
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@ -597,8 +592,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
@ -227,7 +227,5 @@ 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>
|
||||||
|
@ -680,10 +680,6 @@ 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
|
||||||
/*
|
/*
|
||||||
|
@ -10,7 +10,7 @@ import { POSITION } from "vue-toastification";
|
|||||||
*
|
*
|
||||||
* Generated by Trelent
|
* Generated by Trelent
|
||||||
*/
|
*/
|
||||||
function getTimezoneOffset(timeZone : string) {
|
function getTimezoneOffset(timeZone) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const tzString = now.toLocaleString("en-US", {
|
const tzString = now.toLocaleString("en-US", {
|
||||||
timeZone,
|
timeZone,
|
||||||
@ -124,6 +124,33 @@ export function hostNameRegexPattern(mqtt = false) {
|
|||||||
return `${ipRegexPattern}|${hostNameRegexPattern}`;
|
return `${ipRegexPattern}|${hostNameRegexPattern}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the tag color options
|
||||||
|
* Shared between components
|
||||||
|
* @param {any} self Component
|
||||||
|
* @returns {object[]} Colour options
|
||||||
|
*/
|
||||||
|
export function colorOptions(self) {
|
||||||
|
return [
|
||||||
|
{ name: self.$t("Gray"),
|
||||||
|
color: "#4B5563" },
|
||||||
|
{ name: self.$t("Red"),
|
||||||
|
color: "#DC2626" },
|
||||||
|
{ name: self.$t("Orange"),
|
||||||
|
color: "#D97706" },
|
||||||
|
{ name: self.$t("Green"),
|
||||||
|
color: "#059669" },
|
||||||
|
{ name: self.$t("Blue"),
|
||||||
|
color: "#2563EB" },
|
||||||
|
{ name: self.$t("Indigo"),
|
||||||
|
color: "#4F46E5" },
|
||||||
|
{ name: self.$t("Purple"),
|
||||||
|
color: "#7C3AED" },
|
||||||
|
{ name: self.$t("Pink"),
|
||||||
|
color: "#DB2777" },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads the toast timeout settings from storage.
|
* Loads the toast timeout settings from storage.
|
||||||
* @returns {object} The toast plugin options object.
|
* @returns {object} The toast plugin options object.
|
||||||
|
1
frontend/src/vite-env.d.ts
vendored
1
frontend/src/vite-env.d.ts
vendored
@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable */
|
|
||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
declare module "*.vue" {
|
declare module "*.vue" {
|
||||||
|
26
package.json
26
package.json
@ -1,31 +1,29 @@
|
|||||||
{
|
{
|
||||||
"name": "dockge",
|
"name": "dockge",
|
||||||
"version": "1.0.4",
|
"version": "1.0.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": "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": "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-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.11",
|
"@homebridge/node-pty-prebuilt-multiarch": "~0.11.10",
|
||||||
"@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.5",
|
"croner": "~7.0.4",
|
||||||
"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",
|
||||||
@ -34,8 +32,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.3",
|
"redbean-node": "0.3.2",
|
||||||
"socket.io": "~4.7.2",
|
"socket.io": "~4.7.2",
|
||||||
"socket.io-client": "~4.7.2",
|
"socket.io-client": "~4.7.2",
|
||||||
"timezones-list": "~3.0.2",
|
"timezones-list": "~3.0.2",
|
||||||
@ -45,19 +43,17 @@
|
|||||||
"yaml": "~2.3.4"
|
"yaml": "~2.3.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@fontsource/jetbrains-mono": "^5.0.17",
|
|
||||||
"@fortawesome/fontawesome-svg-core": "6.4.2",
|
"@fortawesome/fontawesome-svg-core": "6.4.2",
|
||||||
"@fortawesome/free-regular-svg-icons": "6.4.2",
|
"@fortawesome/free-regular-svg-icons": "6.4.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "6.4.2",
|
"@fortawesome/free-solid-svg-icons": "6.4.2",
|
||||||
"@fortawesome/vue-fontawesome": "3.0.3",
|
"@fortawesome/vue-fontawesome": "3.0.3",
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bootstrap": "~5.2.8",
|
||||||
"@types/bootstrap": "~5.2.9",
|
|
||||||
"@types/command-exists": "~1.2.3",
|
"@types/command-exists": "~1.2.3",
|
||||||
"@types/express": "~4.17.21",
|
"@types/express": "~4.17.21",
|
||||||
"@types/jsonwebtoken": "~9.0.5",
|
"@types/jsonwebtoken": "~9.0.4",
|
||||||
"@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.5.0",
|
"@vitejs/plugin-vue": "~4.3.4",
|
||||||
"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",
|
||||||
@ -68,7 +64,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": "~5.0.0",
|
"vite": "~4.5.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",
|
||||||
@ -77,7 +73,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
645
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -3,10 +3,6 @@
|
|||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler"
|
||||||
"skipLibCheck": true
|
}
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"backend/**/*"
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user