From 9b9234434e1f81380698efc6b7ee00a0b3ca2d28 Mon Sep 17 00:00:00 2001 From: Louis Lam Date: Fri, 10 Nov 2023 21:52:38 +0800 Subject: [PATCH] wip --- README.md | 48 +++++++++++++++--- backend/database.ts | 2 +- backend/dockge-server.ts | 28 ++++++++++- backend/models/user.ts | 4 +- .../socket-handlers/docker-socket-handler.ts | 20 ++++++++ .../socket-handlers/main-socket-handler.ts | 50 +++++++++++++------ backend/stack.ts | 22 ++++++++ compose.yaml | 8 ++- frontend/src/components/Container.vue | 14 +++++- frontend/src/components/Login.vue | 36 +------------ frontend/src/components/settings/Security.vue | 5 +- frontend/src/lang/en.json | 3 +- frontend/src/mixins/socket.ts | 34 +++++++++++++ frontend/src/pages/Compose.vue | 30 +++++++++++ package.json | 2 +- pnpm-lock.yaml | 8 +-- 16 files changed, 241 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index 5bb67c3..a0f14ab 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # Dockge -A fancy, easy-to-use and reactive docker `compose.yaml` stack manager. +A fancy, easy-to-use and reactive docker `compose.yaml` stack oriented manager. @@ -21,23 +21,51 @@ A fancy, easy-to-use and reactive docker `compose.yaml` stack manager. ## 🔧 How to Install -1. Create a directory `dockge` -2. Create or download [`compose.yaml`](https://raw.githubusercontent.com/louislam/dockge/master/compose.yaml) and put it inside `dockge`: +### Basic + +Default stacks directory is `/opt/stacks`. + +``` +# Create a directory that stores your stacks +mkdir -p /opt/stacks + +# Create a directory that stores dockge's compose.yaml +mkdir -p /opt/dockge +cd /opt/dockge + +# Download the compose.yaml +wget https://raw.githubusercontent.com/louislam/dockge/master/compose.yaml + +# Start Server +docker-compose up -d +``` + +### Advanced + +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 version: "3.8" services: dockge: - image: louislam/dockge:nightly + image: louislam/dockge:1 + restart: unless-stopped ports: - 5001:5001 volumes: - - ./data:/app/data - /var/run/docker.sock:/var/run/docker.sock -``` -3. `docker-compose up -d` + - ./data:/app/data + + # Your stacks directory in the host + # (The paths inside container must be the same as the host) + - /my-stacks:/my-stacks + environment: + # Tell Dockge where is your stacks directory + - DOCKGE_STACKS_DIR=/my-stacks +``` -Dockge is now running on http://localhost:5001 ## Motivations @@ -57,6 +85,10 @@ The naming idea was coming from Twitch emotes like `sadge`, `bedge` or `wokege`. If you are not comfortable with the pronunciation, you can call it `Dockage` +#### Can I manage a single container without `compose.yaml`? + +The main objective of Dockge is that try to use docker `compose.yaml` for everything. If you want to manage a single container, you can just use Portainer or Docker CLI. + ## More Ideas? - Stats diff --git a/backend/database.ts b/backend/database.ts index 75f3c63..d508af4 100644 --- a/backend/database.ts +++ b/backend/database.ts @@ -134,7 +134,7 @@ export class Database { R.freeze(true); if (autoloadModels) { - await R.autoloadModels("./server/model"); + R.autoloadModels("./backend/models", "ts"); } if (dbConfig.type === "sqlite") { diff --git a/backend/dockge-server.ts b/backend/dockge-server.ts index 6a163ab..cf65d13 100644 --- a/backend/dockge-server.ts +++ b/backend/dockge-server.ts @@ -28,6 +28,7 @@ import { TerminalSocketHandler } from "./socket-handlers/terminal-socket-handler import { Stack } from "./stack"; import { Cron } from "croner"; import gracefulShutdown from "http-graceful-shutdown"; +import User from "./models/user"; export class DockgeServer { app : Express; @@ -194,7 +195,7 @@ export class DockgeServer { cors, }); - this.io.on("connection", (socket: Socket) => { + this.io.on("connection", async (socket: Socket) => { log.info("server", "Socket connected!"); this.sendInfo(socket, true); @@ -208,6 +209,20 @@ export class DockgeServer { for (const socketHandler of this.socketHandlerList) { socketHandler.create(socket as DockgeSocket, this); } + + // *************************** + // Better do anything after added all socket handlers here + // *************************** + + log.debug("auth", "check auto login"); + if (await Settings.get("disableAuth")) { + log.info("auth", "Disabled Auth: auto login to admin"); + this.afterLogin(socket as DockgeSocket, await R.findOne("user")); + socket.emit("autoLogin"); + } else { + log.debug("auth", "need auth"); + } + }); this.io.on("disconnect", () => { @@ -216,8 +231,17 @@ export class DockgeServer { } - prepareServer() { + async afterLogin(socket : DockgeSocket, user : User) { + socket.userID = user.id; + socket.join(user.id.toString()); + this.sendInfo(socket); + + try { + this.sendStackList(); + } catch (e) { + log.error("server", e); + } } /** diff --git a/backend/models/user.ts b/backend/models/user.ts index 720cdf7..1a91c34 100644 --- a/backend/models/user.ts +++ b/backend/models/user.ts @@ -20,7 +20,7 @@ export class User extends BeanModel { /** * Reset this users password - * @param {string} newPassword Users new password + * @param {string} newPassword * @returns {Promise} */ async resetPassword(newPassword : string) { @@ -42,3 +42,5 @@ export class User extends BeanModel { } } + +export default User; diff --git a/backend/socket-handlers/docker-socket-handler.ts b/backend/socket-handlers/docker-socket-handler.ts index 4df03cd..557f5f0 100644 --- a/backend/socket-handlers/docker-socket-handler.ts +++ b/backend/socket-handlers/docker-socket-handler.ts @@ -183,6 +183,26 @@ export class DockerSocketHandler extends SocketHandler { callbackError(e, callback); } }); + + // Services status + socket.on("serviceStatusList", async (stackName : unknown, callback) => { + try { + checkLogin(socket); + + if (typeof(stackName) !== "string") { + throw new ValidationError("Stack name must be a string"); + } + + const stack = Stack.getStack(server, stackName); + const serviceStatusList = Object.fromEntries(await stack.getServiceStatusList()); + callback({ + ok: true, + serviceStatusList, + }); + } catch (e) { + callbackError(e, callback); + } + }); } saveStack(socket : DockgeSocket, server : DockgeServer, name : unknown, composeYAML : unknown, isAdd : unknown) : Stack { diff --git a/backend/socket-handlers/main-socket-handler.ts b/backend/socket-handlers/main-socket-handler.ts index 601b2a2..194d093 100644 --- a/backend/socket-handlers/main-socket-handler.ts +++ b/backend/socket-handlers/main-socket-handler.ts @@ -72,7 +72,7 @@ export class MainSocketHandler extends SocketHandler { } log.debug("auth", "afterLogin"); - await this.afterLogin(server, socket, user); + await server.afterLogin(socket, user); log.debug("auth", "afterLogin ok"); log.info("auth", `Successfully logged in user ${decoded.username}. IP=${clientIP}`); @@ -129,7 +129,7 @@ export class MainSocketHandler extends SocketHandler { if (user) { if (user.twofa_status === 0) { - this.afterLogin(server, socket, user); + server.afterLogin(socket, user); log.info("auth", `Successfully logged in user ${data.username}. IP=${clientIP}`); @@ -152,7 +152,7 @@ export class MainSocketHandler extends SocketHandler { const verify = notp.totp.verify(data.token, user.twofa_secret, twoFAVerifyOptions); if (user.twofa_last_token !== data.token && verify) { - this.afterLogin(server, socket, user); + server.afterLogin(socket, user); await R.exec("UPDATE `user` SET twofa_last_token = ? WHERE id = ? ", [ data.token, @@ -189,6 +189,35 @@ export class MainSocketHandler extends SocketHandler { }); + // Change Password + socket.on("changePassword", async (password, callback) => { + try { + checkLogin(socket); + + if (! password.newPassword) { + throw new Error("Invalid new password"); + } + + if (passwordStrength(password.newPassword).value === "Too weak") { + throw new Error("Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length."); + } + + let user = await doubleCheckPassword(socket, password.currentPassword); + await user.resetPassword(password.newPassword); + + callback({ + ok: true, + msg: "Password has been updated successfully.", + }); + + } catch (e) { + callback({ + ok: false, + msg: e.message, + }); + } + }); + socket.on("getSettings", async (callback) => { try { checkLogin(socket); @@ -221,6 +250,8 @@ export class MainSocketHandler extends SocketHandler { await doubleCheckPassword(socket, currentPassword); } + console.log(data); + await Settings.setSettings("general", data); callback({ @@ -239,19 +270,6 @@ export class MainSocketHandler extends SocketHandler { }); } - async afterLogin(server: DockgeServer, socket : DockgeSocket, user : User) { - socket.userID = user.id; - socket.join(user.id.toString()); - - server.sendInfo(socket); - - try { - server.sendStackList(socket); - } catch (e) { - log.error("server", e); - } - } - async login(username : string, password : string) { if (typeof username !== "string" || typeof password !== "string") { return null; diff --git a/backend/stack.ts b/backend/stack.ts index 13c8e79..8afb0e1 100644 --- a/backend/stack.ts +++ b/backend/stack.ts @@ -333,4 +333,26 @@ export class Stack { terminal.join(socket); terminal.start(); } + + async getServiceStatusList() { + let statusList = new Map(); + + let res = childProcess.execSync("docker compose ps --format json", { + cwd: this.path, + }); + + let lines = res.toString().split("\n"); + + console.log(lines); + + for (let line of lines) { + try { + let obj = JSON.parse(line); + statusList.set(obj.Service, obj.State); + } catch (e) { + } + } + + return statusList; + } } diff --git a/compose.yaml b/compose.yaml index c3cfcc4..018a6cb 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,12 +1,18 @@ version: "3.8" services: dockge: - image: louislam/dockge:nightly + image: louislam/dockge:1 + restart: unless-stopped ports: + # Host Port : Container Port - 5001:5001 volumes: + # Docker Socket - /var/run/docker.sock:/var/run/docker.sock + # Dockge Config - ./data:/app/data + # Your stacks directory in the host (The paths inside container must be the same as the host) - /opt/stacks:/opt/stacks environment: + # Tell Dockge where is your stacks directory - DOCKGE_STACKS_DIR=/opt/stacks diff --git a/frontend/src/components/Container.vue b/frontend/src/components/Container.vue index ce1b936..ab530fb 100644 --- a/frontend/src/components/Container.vue +++ b/frontend/src/components/Container.vue @@ -7,7 +7,7 @@ {{ imageName }}:{{ imageTag }}
- Running + {{ status }}
@@ -146,6 +146,10 @@ export default defineComponent({ type: Boolean, default: false, }, + status: { + type: String, + default: "N/A", + } }, emits: [ ], @@ -156,6 +160,14 @@ export default defineComponent({ }, computed: { + bgStyle() { + if (this.status === "running") { + return "bg-primary"; + } else { + return "bg-secondary"; + } + }, + terminalRouteLink() { return { name: "containerTerminal", diff --git a/frontend/src/components/Login.vue b/frontend/src/components/Login.vue index ad90bad..9667d7a 100644 --- a/frontend/src/components/Login.vue +++ b/frontend/src/components/Login.vue @@ -71,7 +71,7 @@ export default { submit() { this.processing = true; - this.login(this.username, this.password, this.token, (res) => { + this.$root.login(this.username, this.password, this.token, (res) => { this.processing = false; if (res.tokenRequired) { @@ -82,40 +82,6 @@ export default { }); }, - /** - * Send request to log user in - * @param {string} username Username to log in with - * @param {string} password Password to log in with - * @param {string} token User token - * @param {loginCB} callback Callback to call with result - * @returns {void} - */ - login(username, password, token, callback) { - - this.$root.getSocket().emit("login", { - username, - password, - token, - }, (res) => { - if (res.tokenRequired) { - callback(res); - } - - if (res.ok) { - this.$root.storage().token = res.token; - this.$root.socketIO.token = res.token; - this.$root.loggedIn = true; - this.$root.username = this.$root.getJWTPayload()?.username; - - this.$root.afterLogin(); - - // Trigger Chrome Save Password - history.pushState({}, ""); - } - - callback(res); - }); - } }, }; diff --git a/frontend/src/components/settings/Security.vue b/frontend/src/components/settings/Security.vue index 2fe4d44..0ece48a 100644 --- a/frontend/src/components/settings/Security.vue +++ b/frontend/src/components/settings/Security.vue @@ -64,7 +64,8 @@ -
+ +
{{ $t("Two Factor Authentication") }}
@@ -182,7 +183,7 @@ export default { this.saveSettings(() => { this.password.currentPassword = ""; this.$root.username = null; - this.$root.socket.token = "autoLogin"; + this.$root.socketIO.token = "autoLogin"; }, this.password.currentPassword); }, diff --git a/frontend/src/lang/en.json b/frontend/src/lang/en.json index 3aa0ce2..91ff47d 100644 --- a/frontend/src/lang/en.json +++ b/frontend/src/lang/en.json @@ -43,5 +43,6 @@ "addContainer": "Add Container", "addNetwork": "Add Network", "disableauth.message1": "Are you sure want to disable authentication?", - "disableauth.message2": "It is designed for scenarios where you intend to implement third-party authentication in front of Uptime Kuma such as Cloudflare Access, Authelia or other authentication mechanisms." + "disableauth.message2": "It is designed for scenarios where you intend to implement third-party authentication in front of Uptime Kuma such as Cloudflare Access, Authelia or other authentication mechanisms.", + "passwordNotMatchMsg": "The repeat password does not match." } diff --git a/frontend/src/mixins/socket.ts b/frontend/src/mixins/socket.ts index 21d65bb..42e4698 100644 --- a/frontend/src/mixins/socket.ts +++ b/frontend/src/mixins/socket.ts @@ -220,6 +220,40 @@ export default defineComponent({ return undefined; }, + /** + * Send request to log user in + * @param {string} username Username to log in with + * @param {string} password Password to log in with + * @param {string} token User token + * @param {loginCB} callback Callback to call with result + * @returns {void} + */ + login(username : string, password : string, token : string, callback) { + this.getSocket().emit("login", { + username, + password, + token, + }, (res) => { + if (res.tokenRequired) { + callback(res); + } + + if (res.ok) { + this.storage().token = res.token; + this.socketIO.token = res.token; + this.loggedIn = true; + this.username = this.getJWTPayload()?.username; + + this.afterLogin(); + + // Trigger Chrome Save Password + history.pushState({}, ""); + } + + callback(res); + }); + }, + /** * Log in using a token * @param {string} token Token to log in with diff --git a/frontend/src/pages/Compose.vue b/frontend/src/pages/Compose.vue index c7877d1..3d87034 100644 --- a/frontend/src/pages/Compose.vue +++ b/frontend/src/pages/Compose.vue @@ -97,6 +97,7 @@ :name="name" :is-edit-mode="isEditMode" :first="name === Object.keys(jsonConfig.services)[0]" + :status="serviceStatusList[name]" />
@@ -201,6 +202,8 @@ services: let yamlErrorTimeout = null; +let serviceStatusTimeout = null; + export default { components: { NetworkInput, @@ -228,10 +231,12 @@ export default { stack: { }, + serviceStatusList: {}, isEditMode: false, submitted: false, showDeleteDialog: false, newContainerName: "", + stopServiceStatusTimeout: false, }; }, computed: { @@ -331,8 +336,33 @@ export default { this.stack.name = this.$route.params.stackName; this.loadStack(); } + + this.requestServiceStatus(); + }, + unmounted() { + this.stopServiceStatusTimeout = true; + clearTimeout(serviceStatusTimeout); }, methods: { + + startServiceStatusTimeout() { + clearTimeout(serviceStatusTimeout); + serviceStatusTimeout = setTimeout(async () => { + this.requestServiceStatus(); + }, 2000); + }, + + requestServiceStatus() { + this.$root.getSocket().emit("serviceStatusList", this.stack.name, (res) => { + if (res.ok) { + this.serviceStatusList = res.serviceStatusList; + } + if (!this.stopServiceStatusTimeout) { + this.startServiceStatusTimeout(); + } + }); + }, + exitConfirm(next) { if (this.isEditMode) { if (confirm("You are currently editing a stack. Are you sure you want to leave?")) { diff --git a/package.json b/package.json index a12b2d4..f9d1936 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "knex": "~2.5.1", "limiter-es6-compat": "~2.1.2", "mysql2": "^3.6.3", - "redbean-node": "0.3.1", + "redbean-node": "0.3.2", "socket.io": "~4.7.2", "socket.io-client": "~4.7.2", "timezones-list": "~3.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 849a631..6651088 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,8 +57,8 @@ dependencies: specifier: ^3.6.3 version: 3.6.3 redbean-node: - specifier: 0.3.1 - version: 0.3.1(mysql2@3.6.3) + specifier: 0.3.2 + version: 0.3.2(mysql2@3.6.3) socket.io: specifier: ~4.7.2 version: 4.7.2 @@ -3491,8 +3491,8 @@ packages: resolve: 1.22.8 dev: false - /redbean-node@0.3.1(mysql2@3.6.3): - resolution: {integrity: sha512-rz71vF7UtJQ14ttZ9I0QuaJ9TOwBCnZb+qHUBiU05f2fLaiQC79liisL3xgkHI8uE9et6HAkG8Z8VPkZbhgxKw==} + /redbean-node@0.3.2(mysql2@3.6.3): + resolution: {integrity: sha512-39VMxPWPpPicRlU4FSJJnJuUMoxw5/4envFthHtKnLe+3qWTBje3RMrJTFZcQGLruWQ/s2LgeYzdd+d0O+p+uQ==} dependencies: '@types/node': 20.3.3 await-lock: 2.2.2