diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 1a292dc..0a6dee2 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -92,6 +92,9 @@ module.exports = { "one-var": [ "error", "never" ], "max-statements-per-line": [ "error", { "max": 1 }], "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/no-unused-vars": [ "warn", { + "args": "none" + }], "prefer-const" : "off", }, }; diff --git a/.github/DISCUSSION_TEMPLATE/ask-for-help.yml b/.github/DISCUSSION_TEMPLATE/ask-for-help.yml new file mode 100644 index 0000000..6d4191a --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/ask-for-help.yml @@ -0,0 +1,72 @@ +title: "❓ Ask for help" +labels: [help] +body: + - type: checkboxes + id: no-duplicate-issues + attributes: + label: "⚠️ Please verify that this bug has NOT been raised before." + description: "Search in the issues sections by clicking [HERE](https://github.com/louislam/dockge/discussions/categories/ask-for-help)" + options: + - label: "I checked and didn't find similar issue" + required: true + - type: checkboxes + attributes: + label: "🛡️ Security Policy" + description: Please review the security policy before reporting security related issues/bugs. + options: + - label: I agree to have read this project [Security Policy](https://github.com/louislam/dockge/security/policy) + required: true + - type: textarea + id: steps-to-reproduce + validations: + required: true + attributes: + label: "📝 Describe your problem" + description: "Please walk us through it step by step." + placeholder: "Describe what are you asking for..." + - type: textarea + id: error-msg + validations: + required: false + attributes: + label: "📝 Error Message(s) or Log" + - type: input + id: dockge-version + attributes: + label: "🐻 Dockge Version" + description: "Which version of Dockge are you running? Please do NOT provide the docker tag such as latest or 1" + placeholder: "Ex. 1.10.0" + validations: + required: true + - type: input + id: operating-system + attributes: + label: "💻 Operating System and Arch" + description: "Which OS is your server/device running on? (For Replit, please do not report this bug)" + placeholder: "Ex. Ubuntu 20.04 x86" + validations: + required: true + - type: input + id: browser-vendor + attributes: + label: "🌐 Browser" + description: "Which browser are you running on? (For Replit, please do not report this bug)" + placeholder: "Ex. Google Chrome 95.0.4638.69" + validations: + required: true + - type: input + id: docker-version + attributes: + label: "🐋 Docker Version" + description: "If running with Docker, which version are you running?" + placeholder: "Ex. Docker 20.10.9 / K8S / Podman" + validations: + required: false + - type: input + id: nodejs-version + attributes: + label: "🟩 NodeJS Version" + description: "If running with Node.js? which version are you running?" + placeholder: "Ex. 14.18.0" + validations: + required: false diff --git a/.github/DISCUSSION_TEMPLATE/feature-request.yml b/.github/DISCUSSION_TEMPLATE/feature-request.yml new file mode 100644 index 0000000..a23cf40 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/feature-request.yml @@ -0,0 +1,55 @@ +title: 🚀 Feature Request +labels: [feature-request] +body: + - type: checkboxes + id: no-duplicate-issues + attributes: + label: "⚠️ Please verify that this feature request has NOT been suggested before." + description: "Search in the issues sections by clicking [HERE](https://github.com/louislam/dockge/discussions/categories/feature-request)" + options: + - label: "I checked and didn't find similar feature request" + required: true + - type: dropdown + id: feature-area + attributes: + label: "🏷️ Feature Request Type" + description: "What kind of feature request is this?" + multiple: true + options: + - API + - UI Feature + - Other + validations: + required: true + - type: textarea + id: feature-description + validations: + required: true + attributes: + label: "🔖 Feature description" + description: "A clear and concise description of what the feature request is." + placeholder: "You should add ..." + - type: textarea + id: solution + validations: + required: true + attributes: + label: "✔️ Solution" + description: "A clear and concise description of what you want to happen." + placeholder: "In my use-case, ..." + - type: textarea + id: alternatives + validations: + required: false + attributes: + label: "❓ Alternatives" + description: "A clear and concise description of any alternative solutions or features you've considered." + placeholder: "I have considered ..." + - type: textarea + id: additional-context + validations: + required: false + attributes: + label: "📝 Additional Context" + description: "Add any other context or screenshots about the feature request here." + placeholder: "..." \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/ask-for-help.yaml b/.github/ISSUE_TEMPLATE/ask-for-help.yaml new file mode 100644 index 0000000..2a64539 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ask-for-help.yaml @@ -0,0 +1,14 @@ +name: "❓ Ask for help" +description: "Please go to the Discussions tab to submit a Help Request" +body: + - type: markdown + attributes: + value: | + Please go to https://github.com/louislam/dockge/discussions/new?category=ask-for-help + - type: checkboxes + id: no-duplicate-issues + attributes: + label: "Issues are for bug reports only" + options: + - label: "I understand" + required: true diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 0000000..a5acf3d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,99 @@ +name: "🐛 Bug Report" +description: "Submit a bug report to help us improve" +#title: "[Bug] " +labels: [bug] +body: + - type: checkboxes + id: no-duplicate-issues + attributes: + label: "⚠️ Please verify that this bug has NOT been reported before." + description: "Search in the issues sections by clicking [HERE](https://github.com/louislam/dockge/issues?q=)" + options: + - label: "I checked and didn't find similar issue" + required: true + - type: checkboxes + attributes: + label: "🛡️ Security Policy" + description: Please review the security policy before reporting security related issues/bugs. + options: + - label: I agree to have read this project [Security Policy](https://github.com/louislam/dockge/security/policy) + required: true + - type: textarea + id: description + validations: + required: false + attributes: + label: "Description" + description: "You could also upload screenshots" + - type: textarea + id: steps-to-reproduce + validations: + required: true + attributes: + label: "👟 Reproduction steps" + description: "How do you trigger this bug? Please walk us through it step by step." + placeholder: "..." + - type: textarea + id: expected-behavior + validations: + required: true + attributes: + label: "👀 Expected behavior" + description: "What did you think would happen?" + placeholder: "..." + - type: textarea + id: actual-behavior + validations: + required: true + attributes: + label: "😓 Actual Behavior" + description: "What actually happen?" + placeholder: "..." + - type: input + id: dockge-version + attributes: + label: "Dockge Version" + description: "Which version of Dockge are you running? Please do NOT provide the docker tag such as latest or 1" + placeholder: "Ex. 1.1.1" + validations: + required: true + - type: input + id: operating-system + attributes: + label: "💻 Operating System and Arch" + description: "Which OS is your server/device running on?" + placeholder: "Ex. Ubuntu 20.04 x64 " + validations: + required: true + - type: input + id: browser-vendor + attributes: + label: "🌐 Browser" + description: "Which browser are you running on?" + placeholder: "Ex. Google Chrome 95.0.4638.69" + validations: + required: true + - type: input + id: docker-version + attributes: + label: "🐋 Docker Version" + description: "If running with Docker, which version are you running?" + placeholder: "Ex. Docker 20.10.9 / K8S / Podman" + validations: + required: false + - type: input + id: nodejs-version + attributes: + label: "🟩 NodeJS Version" + description: "If running with Node.js? which version are you running?" + placeholder: "Ex. 14.18.0" + validations: + required: false + - type: textarea + id: logs + attributes: + label: "📝 Relevant log output" + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml new file mode 100644 index 0000000..1e0832a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -0,0 +1,14 @@ +name: 🚀 Feature Request +description: "Please go to the Discussions tab to submit a Feature Request" +body: + - type: markdown + attributes: + value: | + Please go to https://github.com/louislam/dockge/discussions/new?category=feature-request + - type: checkboxes + id: no-duplicate-issues + attributes: + label: "Issues are for bug reports only" + options: + - label: "I understand" + required: true diff --git a/.github/ISSUE_TEMPLATE/security.md b/.github/ISSUE_TEMPLATE/security.md new file mode 100644 index 0000000..ab03b45 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/security.md @@ -0,0 +1,19 @@ +--- + +name: "Security Issue" +about: "Just for alerting @louislam, do not provide any details here" +title: "Security Issue" +ref: "main" +labels: + +- security + +--- + +DO NOT PROVIDE ANY DETAILS HERE. Please privately report to https://github.com/louislam/dockge/security/advisories/new. + + +Why need this issue? It is because GitHub Advisory do not send a notification to @louislam, it is a workaround to do so. + +Your GitHub Advisory URL: + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..c820f96 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,34 @@ +⚠️⚠️⚠️ Since we do not accept all types of pull requests and do not want to waste your time. Please be sure that you have read pull request rules: +https://github.com/louislam/dockge/blob/master/CONTRIBUTING.md + +Tick the checkbox if you understand [x]: +- [ ] I have read and understand the pull request rules. + +# Description + +Fixes #(issue) + +## Type of change + +Please delete any options that are not relevant. + +- Bug fix (non-breaking change which fixes an issue) +- User interface (UI) +- New feature (non-breaking change which adds functionality) +- Breaking change (fix or feature that would cause existing functionality to not work as expected) +- Other +- This change requires a documentation update + +## Checklist + +- [ ] My code follows the style guidelines of this project +- [ ] I ran ESLint and other linters for modified files +- [ ] I have performed a self-review of my own code and tested it +- [ ] I have commented my code, particularly in hard-to-understand areas + (including JSDoc for methods) +- [ ] My changes generate no new warnings +- [ ] My code needed automated testing. I have added them (this is optional task) + +## Screenshots (if any) + +Please do not use any external image service. Instead, just paste in or drag and drop the image here, and it will be uploaded automatically. diff --git a/.github/config/exclude.txt b/.github/config/exclude.txt new file mode 100644 index 0000000..2532588 --- /dev/null +++ b/.github/config/exclude.txt @@ -0,0 +1 @@ +# This is a .gitignore style file for 'GrantBirki/json-yaml-validate' Action workflow diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fea3058..22a3875 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,12 +15,15 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - node: [18.x, 20.x] # Can be changed + node: [20.x] # Can be changed runs-on: ${{ matrix.os }} 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: @@ -48,5 +51,13 @@ jobs: - name: Install dependencies run: pnpm install + - name: Lint + run: pnpm run lint + + - name: Check Typescript + run: pnpm run check-ts + + - name: Build + run: pnpm run build:frontend # more things can be add later like tests etc.. - + diff --git a/.github/workflows/close-incorrect-issue.yml b/.github/workflows/close-incorrect-issue.yml new file mode 100644 index 0000000..ca7f85e --- /dev/null +++ b/.github/workflows/close-incorrect-issue.yml @@ -0,0 +1,42 @@ +name: Close Incorrect Issue + +on: + issues: + types: [opened] + +jobs: + close-incorrect-issue: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-latest] + node-version: [16] + + steps: + - uses: actions/checkout@v3 + + - 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: Close Incorrect Issue + run: node extra/close-incorrect-issue.js ${{ secrets.GITHUB_TOKEN }} ${{ github.event.issue.number }} ${{ github.event.issue.user.login }} diff --git a/.github/workflows/json-yaml-validate.yml b/.github/workflows/json-yaml-validate.yml new file mode 100644 index 0000000..365a1f1 --- /dev/null +++ b/.github/workflows/json-yaml-validate.yml @@ -0,0 +1,27 @@ +name: json-yaml-validate +on: + push: + branches: + - master + pull_request: + branches: + - master + - 2.0.X + workflow_dispatch: + +permissions: + contents: read + pull-requests: write # enable write permissions for pull request comments + +jobs: + json-yaml-validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: json-yaml-validate + id: json-yaml-validate + uses: GrantBirki/json-yaml-validate@v1.3.0 + with: + comment: "false" # enable comment mode + exclude_file: ".github/config/exclude.txt" # gitignore style file for exclusions diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..313a029 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,139 @@ +## Can I create a pull request for Dockge? + +Yes or no, it depends on what you will try to do. Since I don't want to waste your time, be sure to **create open a discussion, so we can have a discussion first**. Especially for a large pull request or you don't know if it will be merged or not. + +Here are some references: + +### ✅ Usually accepted: +- Bug fix +- Security fix +- Translation + +### ⚠️ Discussion required: +- Large pull requests +- New features + +### ❌ Won't be merged: +- Do not pass the auto-test +- Any breaking changes +- Duplicated pull requests +- Buggy +- UI/UX is not close to Dockge +- Modifications or deletions of existing logic without a valid reason. +- Adding functions that is completely out of scope +- Converting existing code into other programming languages +- Unnecessarily large code changes that are hard to review and cause conflicts with other PRs. + +The above cases may not cover all possible situations. + +I (@louislam) have the final say. If your pull request does not meet my expectations, I will reject it, no matter how much time you spend on it. Therefore, it is essential to have a discussion beforehand. + +I will assign your pull request to a [milestone](https://github.com/louislam/dockge/milestones), if I plan to review and merge it. + +Also, please don't rush or ask for an ETA, because I have to understand the pull request, make sure it is no breaking changes and stick to my vision of this project, especially for large pull requests. + +## Project Styles + +I personally do not like something that requires so many configurations before you can finally start the app. + +- Settings should be configurable in the frontend. Environment variables are discouraged, unless it is related to startup such as `DOCKGE_STACKS_DIR` +- Easy to use +- The web UI styling should be consistent and nice +- No native build dependency + +## Coding Styles + +- 4 spaces indentation +- Follow `.editorconfig` +- Follow ESLint +- Methods and functions should be documented with JSDoc + +## Name Conventions + +- Javascript/Typescript: camelCaseType +- SQLite: snake_case (Underscore) +- CSS/SCSS: kebab-case (Dash) + +## Tools + +- [`Node.js`](https://nodejs.org/) >= 20 +- [`pnpm`](https://pnpm.io/) +- [`git`](https://git-scm.com/) +- IDE that supports [`ESLint`](https://eslint.org/) and EditorConfig (I am using [`IntelliJ IDEA`](https://www.jetbrains.com/idea/)) +- A SQLite GUI tool (f.ex. [`SQLite Expert Personal`](https://www.sqliteexpert.com/download.html) or [`DBeaver Community`](https://dbeaver.io/download/)) + +## Install Dependencies for Development + +```bash +pnpm install +``` + +## Dev Server + +``` +pnpm run dev:frontend +pnpm run dev:backend +``` + +## Backend Dev Server + +It binds to `0.0.0.0:5001` by default. + +It is mainly a socket.io app + express.js. + +## Frontend Dev Server + +It binds to `0.0.0.0:5000` by default. The frontend dev server is used for development only. + +For production, it is not used. It will be compiled to `frontend-dist` directory instead. + +You can use Vue.js devtools Chrome extension for debugging. + +### Build the frontend + +```bash +pnpm run build +``` + +## Database Migration + +TODO + +## Dependencies + +Both frontend and backend share the same package.json. However, the frontend dependencies are eventually not used in the production environment, because it is usually also baked into dist files. So: + +- Frontend dependencies = "devDependencies" + - Examples: vue, chart.js +- Backend dependencies = "dependencies" + - Examples: socket.io, sqlite3 +- Development dependencies = "devDependencies" + - Examples: eslint, sass + +### Update Dependencies + +Should only be done by the maintainer. + +```bash +pnpm update +```` + +It should update the patch release version only. + +Patch release = the third digit ([Semantic Versioning](https://semver.org/)) + +If for security / bug / other reasons, a library must be updated, breaking changes need to be checked by the person proposing the change. + +## Translations + +Please add **all** the strings which are translatable to `src/lang/en.json` (If translation keys are omitted, they can not be translated). + +**Don't include any other languages in your initial Pull-Request** (even if this is your mother tongue), to avoid merge-conflicts between weblate and `master`. +The translations can then (after merging a PR into `master`) be translated by awesome people donating their language skills. + +If you want to help by translating Uptime Kuma into your language, please visit the [instructions on how to translate using weblate](https://github.com/louislam/uptime-kuma/blob/master/src/lang/README.md). + +## Spelling & Grammar + +Feel free to correct the grammar in the documentation or code. +My mother language is not English and my grammar is not that great. diff --git a/README.md b/README.md index 4d7b08b..d783c52 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ 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) +![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/latest?label=docker%20image%20ver.) ![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) @@ -22,10 +22,10 @@ View Video: https://youtu.be/AWAlOQeNpgU?t=48 - Reactive - Everything is just responsive. Progress (Pull/Up/Down) and terminal output are in real-time - 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 one too - 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 + - Dockge won't kidnap your compose files, they are stored on your drive as usual. You can interact with them using normal `docker compose` commands @@ -37,7 +37,7 @@ Requirements: - [Docker CE](https://docs.docker.com/engine/install/) 20+ is recommended / Podman - (Docker only) [Docker Compose Plugin](https://docs.docker.com/compose/install/linux/) - (Podman only) podman-docker (Debian: `apt install podman-docker`) -- OS: +- OS: - As long as you can run Docker CE / Podman, it should be fine, but: - Debian/Raspbian Buster or lower is not supported, please upgrade to Bullseye or higher - Arch: armv7, arm64, amd64 (a.k.a x86_64) @@ -55,11 +55,11 @@ cd /opt/dockge # Download the compose.yaml curl https://raw.githubusercontent.com/louislam/dockge/master/compose.yaml --output compose.yaml -# Start Server +# Start the Server docker compose up -d # 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 @@ -75,7 +75,7 @@ services: image: louislam/dockge:1 restart: unless-stopped ports: - # Host Port:Container Port + # Host Port : Container Port - 5001:5001 volumes: - /var/run/docker.sock:/var/run/docker.sock @@ -86,8 +86,8 @@ services: # 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) + # ✔️✔️✔️✔️ CORRECT: - /my-stacks:/my-stacks (Both paths match) + # ❌❌❌❌ WRONG: - /docker:/my-stacks (Both paths do not match) - /opt/stacks:/opt/stacks environment: # Tell Dockge where is your stacks directory @@ -117,9 +117,9 @@ docker compose up -d ## 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. -- Try to develop with ES Module + TypeScript (Originally, I planned to use Deno or Bun.js, but they do not support for arm64, so I stepped back to Node.js) +- Try to develop with ES Module + TypeScript (Originally, I planned to use Deno or Bun.js, but they don't have support for arm64, so I stepped back to Node.js) -If you love this project, please consider giving this project a ⭐. +If you love this project, please consider giving it a ⭐. ## 🗣️ @@ -130,17 +130,21 @@ https://github.com/louislam/dockge/issues ### Ask for Help / Discussions https://github.com/louislam/dockge/discussions +## Translation + +If you want to translate Dockge into your language, please read [Translation Guide](https://github.com/louislam/dockge/blob/master/frontend/src/lang/README.md) + ## FAQ #### "Dockge"? "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 came from Twitch emotes like `sadge`, `bedge` or `wokege`. They all end in `-ge`. #### 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 to try to use the docker `compose.yaml` for everything. If you want to manage a single container, you can just use Portainer or Docker CLI. #### Can I manage existing stacks? @@ -164,6 +168,4 @@ Yes, you can. However, you need to move your compose file into the stacks direct # Others -Dockge is built on top of [Compose V2](https://docs.docker.com/compose/migrate/). `compose.yaml` is also known as `docker-compose.yml`. - - +Dockge is built on top of [Compose V2](https://docs.docker.com/compose/migrate/). `compose.yaml` also known as `docker-compose.yml`. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..a0a632a --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,12 @@ +# Security Policy + +## Reporting a Vulnerability + +1. Please report security issues to https://github.com/louislam/dockge/security/advisories/new. +1. Please also create an empty security issue to alert me, as GitHub Advisories do not send a notification, I probably will miss it without this. https://github.com/louislam/dockge/issues/new?assignees=&labels=help&template=security.md + +Do not use the public issue tracker or discuss it in public as it will cause more damage. + +## Do you accept other 3rd-party bug bounty platforms? + +At this moment, I DO NOT accept other bug bounty platforms, because I am not familiar with these platforms and someone has tried to send a phishing link to me by doing this already. To minimize my own risk, please report through GitHub Advisories only. I will ignore all 3rd-party bug bounty platforms emails. diff --git a/backend/database.ts b/backend/database.ts index d508af4..a63bc18 100644 --- a/backend/database.ts +++ b/backend/database.ts @@ -5,6 +5,7 @@ import fs from "fs"; import path from "path"; import knex from "knex"; +// @ts-ignore import Dialect from "knex/lib/dialects/sqlite3/index.js"; import sqlite from "@louislam/sqlite3"; @@ -12,6 +13,11 @@ import { sleep } from "./util-common"; interface DBConfig { type?: "sqlite" | "mysql"; + hostname?: string; + port?: string; + database?: string; + username?: string; + password?: string; } export class Database { @@ -19,7 +25,7 @@ export class Database { * SQLite file path (Default: ./data/dockge.db) * @type {string} */ - static sqlitePath; + static sqlitePath : string; static noReject = true; @@ -51,7 +57,7 @@ export class Database { * @typedef {string|undefined} envString * @returns {{type: "sqlite"} | {type:envString, hostname:envString, port:envString, database:envString, username:envString, password:envString}} Database config */ - static readDBConfig() { + static readDBConfig() : DBConfig { const dbConfigString = fs.readFileSync(path.join(this.server.config.dataDir, "db-config.json")).toString("utf-8"); const dbConfig = JSON.parse(dbConfigString); @@ -67,10 +73,10 @@ export class Database { /** * @typedef {string|undefined} envString - * @param {{type: "sqlite"} | {type:envString, hostname:envString, port:envString, database:envString, username:envString, password:envString}} dbConfig the database configuration that should be written + * @param dbConfig the database configuration that should be written * @returns {void} */ - static writeDBConfig(dbConfig) { + static writeDBConfig(dbConfig : DBConfig) { fs.writeFileSync(path.join(this.server.config.dataDir, "db-config.json"), JSON.stringify(dbConfig, null, 4)); } @@ -80,14 +86,17 @@ export class Database { * @param {boolean} noLog Should logs not be output? * @returns {Promise} */ - static async connect(autoloadModels = true, noLog = false) { + static async connect(autoloadModels = true) { const acquireConnectionTimeout = 120 * 1000; - let dbConfig; + let dbConfig : DBConfig; try { dbConfig = this.readDBConfig(); Database.dbConfig = dbConfig; } catch (err) { - log.warn("db", err.message); + if (err instanceof Error) { + log.warn("db", err.message); + } + dbConfig = { type: "sqlite", }; @@ -176,13 +185,15 @@ export class Database { directory: Database.knexMigrationsPath, }); } catch (e) { - // Allow missing patch files for downgrade or testing pr. - if (e.message.includes("the following files are missing:")) { - log.warn("db", e.message); - log.warn("db", "Database migration failed, you may be downgrading Dockge."); - } else { - log.error("db", "Database migration failed"); - throw e; + if (e instanceof Error) { + // Allow missing patch files for downgrade or testing pr. + if (e.message.includes("the following files are missing:")) { + log.warn("db", e.message); + log.warn("db", "Database migration failed, you may be downgrading Dockge."); + } else { + log.error("db", "Database migration failed"); + throw e; + } } } } diff --git a/backend/dockge-server.ts b/backend/dockge-server.ts index 75fbdd5..a15e215 100644 --- a/backend/dockge-server.ts +++ b/backend/dockge-server.ts @@ -61,7 +61,7 @@ export class DockgeServer { */ needSetup = false; - jwtSecret? : string; + jwtSecret : string = ""; stacksDir : string = ""; @@ -130,7 +130,7 @@ export class DockgeServer { this.config.sslKey = args.sslKey || process.env.DOCKGE_SSL_KEY || undefined; this.config.sslCert = args.sslCert || process.env.DOCKGE_SSL_CERT || undefined; this.config.sslKeyPassphrase = args.sslKeyPassphrase || process.env.DOCKGE_SSL_KEY_PASSPHRASE || undefined; - this.config.port = args.port || parseInt(process.env.DOCKGE_PORT) || 5001; + this.config.port = args.port || Number(process.env.DOCKGE_PORT) || 5001; this.config.hostname = args.hostname || process.env.DOCKGE_HOSTNAME || undefined; this.config.dataDir = args.dataDir || process.env.DOCKGE_DATA_DIR || "./data/"; this.config.stacksDir = args.stacksDir || process.env.DOCKGE_STACKS_DIR || defaultStacksDir; @@ -219,7 +219,7 @@ export class DockgeServer { log.debug("auth", "check auto login"); if (await Settings.get("disableAuth")) { log.info("auth", "Disabled Auth: auto login to admin"); - this.afterLogin(socket as DockgeSocket, await R.findOne("user")); + this.afterLogin(socket as DockgeSocket, await R.findOne("user") as User); socket.emit("autoLogin"); } else { log.debug("auth", "need auth"); @@ -259,7 +259,9 @@ export class DockgeServer { try { await Database.init(this); } 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); } @@ -289,7 +291,7 @@ export class DockgeServer { } // Listen - this.httpServer.listen(5001, this.config.hostname, () => { + this.httpServer.listen(this.config.port, this.config.hostname, () => { if (this.config.hostname) { log.info( "server", `Listening on ${this.config.hostname}:${this.config.port}`); } else { @@ -297,7 +299,7 @@ export class DockgeServer { } // Run every 5 seconds - const job = Cron("*/2 * * * * *", { + Cron("*/2 * * * * *", { protect: true, // Enabled over-run protection. }, () => { log.debug("server", "Cron job running"); @@ -382,7 +384,9 @@ export class DockgeServer { return process.env.TZ; } } 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"); @@ -395,7 +399,9 @@ export class DockgeServer { return timezone; } } catch (e) { - log.warn("timezone", e.message + " in settings"); + if (e instanceof Error) { + log.warn("timezone", e.message + " in settings"); + } } // Guess diff --git a/backend/log.ts b/backend/log.ts index 37f2d4a..3cfac64 100644 --- a/backend/log.ts +++ b/backend/log.ts @@ -103,6 +103,10 @@ class Logger { * @param level Log level. One of INFO, WARN, ERROR, DEBUG or can be customized. */ log(module: string, msg: unknown, level: string) { + if (level === "DEBUG" && !isDev) { + return; + } + if (this.hideLog[level] && this.hideLog[level].includes(module.toLowerCase())) { return; } diff --git a/backend/password-hash.ts b/backend/password-hash.ts index fb8d42d..3a17440 100644 --- a/backend/password-hash.ts +++ b/backend/password-hash.ts @@ -17,7 +17,7 @@ export function generatePasswordHash(password : string) { * @param {string} hash Hash to verify against * @returns {boolean} Does the password match the hash? */ -export function verifyPassword(password, hash) { +export function verifyPassword(password : string, hash : string) { return bcrypt.compareSync(password, hash); } @@ -37,7 +37,7 @@ export const SHAKE256_LENGTH = 16; * @param {number} len Output length of the hash * @returns {string} The hashed data in hex format */ -export function shake256(data, len) { +export function shake256(data : string, len : number) { if (!data) { return ""; } diff --git a/backend/rate-limiter.ts b/backend/rate-limiter.ts index 3c43abe..9bfa8d2 100644 --- a/backend/rate-limiter.ts +++ b/backend/rate-limiter.ts @@ -1,8 +1,14 @@ // "limit" is bugged in Typescript, use "limiter-es6-compat" instead // See https://github.com/jhurliman/node-rate-limiter/issues/80 -import { RateLimiter } from "limiter-es6-compat"; +import { RateLimiter, RateLimiterOpts } from "limiter-es6-compat"; import { log } from "./log"; +export interface KumaRateLimiterOpts extends RateLimiterOpts { + errorMessage : string; +} + +export type KumaRateLimiterCallback = (err : object) => void; + class KumaRateLimiter { errorMessage : string; @@ -11,7 +17,7 @@ class KumaRateLimiter { /** * @param {object} config Rate limiter configuration object */ - constructor(config) { + constructor(config : KumaRateLimiterOpts) { this.errorMessage = config.errorMessage; this.rateLimiter = new RateLimiter(config); } @@ -24,11 +30,11 @@ class KumaRateLimiter { /** * Should the request be passed through - * @param {passCB} callback Callback function to call with decision + * @param callback Callback function to call with decision * @param {number} num Number of tokens to remove * @returns {Promise} Should the request be allowed? */ - async pass(callback, num = 1) { + async pass(callback : KumaRateLimiterCallback, num = 1) { const remainingRequests = await this.removeTokens(num); log.info("rate-limit", "remaining requests: " + remainingRequests); if (remainingRequests < 0) { diff --git a/backend/routers/main-router.ts b/backend/routers/main-router.ts index 8d791db..f882f94 100644 --- a/backend/routers/main-router.ts +++ b/backend/routers/main-router.ts @@ -1,4 +1,4 @@ -import { DockgeServer } from "../dockgeServer"; +import { DockgeServer } from "../dockge-server"; import { Router } from "../router"; import express, { Express, Router as ExpressRouter } from "express"; diff --git a/backend/settings.ts b/backend/settings.ts index fed94a7..c1703dc 100644 --- a/backend/settings.ts +++ b/backend/settings.ts @@ -1,5 +1,6 @@ import { R } from "redbean-node"; import { log } from "./log"; +import { LooseObject } from "./util-common"; export class Settings { @@ -15,20 +16,19 @@ export class Settings { * timestamp: 12345678 * }, * } - * @type {{}} */ - static cacheList = { + static cacheList : LooseObject = { }; - static cacheCleaner = null; + static cacheCleaner? : NodeJS.Timeout; /** * Retrieve value of setting based on key - * @param {string} key Key of setting to retrieve - * @returns {Promise} Value + * @param key Key of setting to retrieve + * @returns Value */ - static async get(key) { + static async get(key : string) { // Start cache clear if not started yet if (!Settings.cacheCleaner) { @@ -72,12 +72,12 @@ export class Settings { /** * Sets the specified setting to specified value - * @param {string} key Key of setting to set - * @param {any} value Value to set to + * @param key Key of setting to set + * @param value Value to set to * @param {?string} type Type of setting * @returns {Promise} */ - static async set(key, value, type = null) { + static async set(key : string, value : object | string | number | boolean, type : string | null = null) { let bean = await R.findOne("setting", " `key` = ? ", [ key, @@ -95,15 +95,15 @@ export class Settings { /** * Get settings based on type - * @param {string} type The type of setting - * @returns {Promise} Settings + * @param type The type of setting + * @returns Settings */ - static async getSettings(type) { + static async getSettings(type : string) { const list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [ type, ]); - const result = {}; + const result : LooseObject = {}; for (const row of list) { try { @@ -118,11 +118,11 @@ export class Settings { /** * Set settings based on type - * @param {string} type Type of settings to set - * @param {object} data Values of settings + * @param type Type of settings to set + * @param data Values of settings * @returns {Promise} */ - static async setSettings(type, data) { + static async setSettings(type : string, data : LooseObject) { const keyList = Object.keys(data); const promiseList = []; @@ -154,7 +154,7 @@ export class Settings { * @param {string[]} keyList Keys to remove * @returns {void} */ - static deleteCache(keyList) { + static deleteCache(keyList : string[]) { for (const key of keyList) { delete Settings.cacheList[key]; } @@ -167,7 +167,7 @@ export class Settings { static stopCacheCleaner() { if (Settings.cacheCleaner) { clearInterval(Settings.cacheCleaner); - Settings.cacheCleaner = null; + Settings.cacheCleaner = undefined; } } } diff --git a/backend/socket-handlers/docker-socket-handler.ts b/backend/socket-handlers/docker-socket-handler.ts index e945bef..3683114 100644 --- a/backend/socket-handlers/docker-socket-handler.ts +++ b/backend/socket-handlers/docker-socket-handler.ts @@ -187,6 +187,27 @@ export class DockerSocketHandler extends SocketHandler { } }); + // down stack + socket.on("downStack", 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); + await stack.down(socket); + callback({ + ok: true, + msg: "Downed" + }); + server.sendStackList(); + } catch (e) { + callbackError(e, callback); + } + }); + // Services status socket.on("serviceStatusList", async (stackName : unknown, callback) => { try { @@ -196,7 +217,7 @@ export class DockerSocketHandler extends SocketHandler { throw new ValidationError("Stack name must be a string"); } - const stack = Stack.getStack(server, stackName); + const stack = Stack.getStack(server, stackName, true); const serviceStatusList = Object.fromEntries(await stack.getServiceStatusList()); callback({ ok: true, diff --git a/backend/socket-handlers/main-socket-handler.ts b/backend/socket-handlers/main-socket-handler.ts index 194d093..bfdb45d 100644 --- a/backend/socket-handlers/main-socket-handler.ts +++ b/backend/socket-handlers/main-socket-handler.ts @@ -1,12 +1,11 @@ import { SocketHandler } from "../socket-handler.js"; -import { Socket } from "socket.io"; import { DockgeServer } from "../dockge-server"; import { log } from "../log"; import { R } from "redbean-node"; import { loginRateLimiter, twoFaRateLimiter } from "../rate-limiter"; import { generatePasswordHash, needRehashPassword, shake256, SHAKE256_LENGTH, verifyPassword } from "../password-hash"; import { User } from "../models/user"; -import { checkLogin, DockgeSocket, doubleCheckPassword } from "../util-server"; +import { checkLogin, DockgeSocket, doubleCheckPassword, JWTDecoded } from "../util-server"; import { passwordStrength } from "check-password-strength"; import jwt from "jsonwebtoken"; import { Settings } from "../settings"; @@ -43,10 +42,12 @@ export class MainSocketHandler extends SocketHandler { }); } catch (e) { - callback({ - ok: false, - msg: e.message, - }); + if (e instanceof Error) { + callback({ + ok: false, + msg: e.message, + }); + } } }); @@ -57,7 +58,7 @@ export class MainSocketHandler extends SocketHandler { log.info("auth", `Login by token. IP=${clientIP}`); try { - const decoded = jwt.verify(token, server.jwtSecret); + const decoded = jwt.verify(token, server.jwtSecret) as JWTDecoded; log.info("auth", "Username from JWT: " + decoded.username); @@ -91,9 +92,13 @@ export class MainSocketHandler extends SocketHandler { }); } } catch (error) { + if (!(error instanceof Error)) { + console.error("Unknown error:", error); + return; + } log.error("auth", `Invalid token. IP=${clientIP}`); if (error.message) { - log.error("auth", error.message, `IP=${clientIP}`); + log.error("auth", error.message + ` IP=${clientIP}`); } callback({ ok: false, @@ -149,6 +154,7 @@ export class MainSocketHandler extends SocketHandler { } if (data.token) { + // @ts-ignore const verify = notp.totp.verify(data.token, user.twofa_secret, twoFAVerifyOptions); if (user.twofa_last_token !== data.token && verify) { @@ -211,10 +217,12 @@ export class MainSocketHandler extends SocketHandler { }); } catch (e) { - callback({ - ok: false, - msg: e.message, - }); + if (e instanceof Error) { + callback({ + ok: false, + msg: e.message, + }); + } } }); @@ -229,10 +237,12 @@ export class MainSocketHandler extends SocketHandler { }); } catch (e) { - callback({ - ok: false, - msg: e.message, - }); + if (e instanceof Error) { + callback({ + ok: false, + msg: e.message, + }); + } } }); @@ -262,22 +272,24 @@ export class MainSocketHandler extends SocketHandler { server.sendInfo(socket); } catch (e) { - callback({ - ok: false, - msg: e.message, - }); + if (e instanceof Error) { + callback({ + ok: false, + msg: e.message, + }); + } } }); } - async login(username : string, password : string) { + async login(username : string, password : string) : Promise { if (typeof username !== "string" || typeof password !== "string") { return null; } const user = await R.findOne("user", " username = ? AND active = 1 ", [ username, - ]); + ]) as User; if (user && verifyPassword(password, user.password)) { // Upgrade the hash to bcrypt diff --git a/backend/socket-handlers/terminal-socket-handler.ts b/backend/socket-handlers/terminal-socket-handler.ts index f647dfb..0a0485b 100644 --- a/backend/socket-handlers/terminal-socket-handler.ts +++ b/backend/socket-handlers/terminal-socket-handler.ts @@ -38,10 +38,12 @@ export class TerminalSocketHandler extends SocketHandler { throw new Error("Terminal not found or it is not a Interactive Terminal."); } } catch (e) { - errorCallback({ - ok: false, - msg: e.message, - }); + if (e instanceof Error) { + errorCallback({ + ok: false, + msg: e.message, + }); + } } }); diff --git a/backend/stack.ts b/backend/stack.ts index 1a2366b..bf7657c 100644 --- a/backend/stack.ts +++ b/backend/stack.ts @@ -24,16 +24,28 @@ export class Stack { protected _status: number = UNKNOWN; protected _composeYAML?: string; protected _configFilePath?: string; + protected _composeFileName: string = "compose.yaml"; protected server: DockgeServer; protected combinedTerminal? : Terminal; protected static managedStackList: Map = new Map(); - constructor(server : DockgeServer, name : string, composeYAML? : string) { + constructor(server : DockgeServer, name : string, composeYAML? : string, skipFSOperations = false) { this.name = name; this.server = server; this._composeYAML = composeYAML; + + if (!skipFSOperations) { + // Check if compose file name is different from compose.yaml + const supportedFileNames = [ "compose.yaml", "compose.yml", "docker-compose.yml", "docker-compose.yaml" ]; + for (const filename of supportedFileNames) { + if (fs.existsSync(path.join(this.path, filename))) { + this._composeFileName = filename; + break; + } + } + } } toJSON() : object { @@ -50,6 +62,7 @@ export class Stack { status: this._status, tags: [], isManagedByDockge: this.isManagedByDockge, + composeFileName: this._composeFileName, }; } @@ -84,7 +97,7 @@ export class Stack { get composeYAML() : string { if (this._composeYAML === undefined) { try { - this._composeYAML = fs.readFileSync(path.join(this.path, "compose.yaml"), "utf-8"); + this._composeYAML = fs.readFileSync(path.join(this.path, this._composeFileName), "utf-8"); } catch (e) { this._composeYAML = ""; } @@ -135,7 +148,7 @@ export class Stack { } // Write or overwrite the compose.yaml - fs.writeFileSync(path.join(dir, "compose.yaml"), this.composeYAML); + fs.writeFileSync(path.join(dir, this._composeFileName), this.composeYAML); } async deploy(socket? : DockgeSocket) : Promise { @@ -163,10 +176,22 @@ export class Stack { return exitCode; } + updateStatus() { + let statusList = Stack.getStatusList(); + let status = statusList.get(this.name); + + if (status) { + this._status = status; + } else { + this._status = UNKNOWN; + } + } + static getStackList(server : DockgeServer, useCacheForManaged = false) : Map { let stacksDir = server.stacksDir; let stackList : Map; + // Use cached stack list? if (useCacheForManaged && this.managedStackList.size > 0) { stackList = this.managedStackList; } else { @@ -186,7 +211,9 @@ export class Stack { stack._status = CREATED_FILE; stackList.set(filename, stack); } 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}`); + } } } @@ -194,22 +221,19 @@ export class Stack { this.managedStackList = new Map(stackList); } - // Also get the list from `docker compose ls --all --format json` + // Get status from docker compose ls let res = childProcess.execSync("docker compose ls --all --format json"); let composeList = JSON.parse(res.toString()); for (let composeStack of composeList) { - - // Skip the dockge stack - // TODO: Could be self managed? - if (composeStack.Name === "dockge") { - continue; - } - let stack = stackList.get(composeStack.Name); // This stack probably is not managed by Dockge, but we still want to show it if (!stack) { + // Skip the dockge stack if it is not managed by Dockge + if (composeStack.Name === "dockge") { + continue; + } stack = new Stack(server, composeStack.Name); stackList.set(composeStack.Name, stack); } @@ -240,37 +264,51 @@ export class Stack { /** * Convert the status string from `docker compose ls` to the status number + * Input Example: "exited(1), running(1)" * @param status */ static statusConvert(status : string) : number { if (status.startsWith("created")) { return CREATED_STACK; - } else if (status.startsWith("running")) { - return RUNNING; - } else if (status.startsWith("exited")) { + } else if (status.includes("exited")) { + // If one of the service is exited, we consider the stack is exited return EXITED; + } else if (status.startsWith("running")) { + // If there is no exited services, there should be only running services + return RUNNING; } else { return UNKNOWN; } } - static getStack(server: DockgeServer, stackName: string) : Stack { + static getStack(server: DockgeServer, stackName: string, skipFSOperations = false) : Stack { let dir = path.join(server.stacksDir, stackName); - if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) { - // Maybe it is a stack managed by docker compose directly - let stackList = this.getStackList(server); - let stack = stackList.get(stackName); + if (!skipFSOperations) { + if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) { + // Maybe it is a stack managed by docker compose directly + let stackList = this.getStackList(server, true); + let stack = stackList.get(stackName); - if (stack) { - return stack; - } else { - // Really not found - throw new ValidationError("Stack not found"); + if (stack) { + return stack; + } else { + // Really not found + throw new ValidationError("Stack not found"); + } } + } else { + log.debug("getStack", "Skip FS operations"); + } + + let stack : Stack; + + if (!skipFSOperations) { + stack = new Stack(server, stackName); + } else { + stack = new Stack(server, stackName, undefined, true); } - let stack = new Stack(server, stackName); stack._status = UNKNOWN; stack._configFilePath = path.resolve(dir); return stack; @@ -303,12 +341,29 @@ export class Stack { return exitCode; } + async down(socket: DockgeSocket) : Promise { + const terminalName = getComposeTerminalName(this.name); + let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "down" ], this.path); + if (exitCode !== 0) { + throw new Error("Failed to down, please check the terminal output for more information."); + } + return exitCode; + } + async update(socket: DockgeSocket) { const terminalName = getComposeTerminalName(this.name); let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "pull" ], this.path); if (exitCode !== 0) { throw new Error("Failed to pull, please check the terminal output for more information."); } + + // If the stack is not running, we don't need to restart it + this.updateStatus(); + log.debug("update", "Status: " + this.status); + if (this.status !== RUNNING) { + return exitCode; + } + exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "up", "-d", "--remove-orphans" ], this.path); if (exitCode !== 0) { throw new Error("Failed to restart, please check the terminal output for more information."); @@ -342,16 +397,20 @@ export class Stack { async getServiceStatusList() { let statusList = new Map(); - let res = childProcess.execSync("docker compose ps --format json", { + let res = childProcess.spawnSync("docker", [ "compose", "ps", "--format", "json" ], { cwd: this.path, }); - let lines = res.toString().split("\n"); + let lines = res.stdout.toString().split("\n"); for (let line of lines) { try { 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) { } } diff --git a/backend/terminal.ts b/backend/terminal.ts index 7aa7369..3146e33 100644 --- a/backend/terminal.ts +++ b/backend/terminal.ts @@ -46,70 +46,6 @@ export class Terminal { this.cwd = cwd; Terminal.terminalMap.set(this.name, this); - } - - get rows() { - return this._rows; - } - - set rows(rows : number) { - this._rows = rows; - try { - this.ptyProcess?.resize(this.cols, this.rows); - } catch (e) { - log.debug("Terminal", "Failed to resize terminal: " + e.message); - } - } - - get cols() { - return this._cols; - } - - set cols(cols : number) { - this._cols = cols; - try { - this.ptyProcess?.resize(this.cols, this.rows); - } catch (e) { - log.debug("Terminal", "Failed to resize terminal: " + e.message); - } - } - - public start() { - if (this._ptyProcess) { - return; - } - - this._ptyProcess = pty.spawn(this.file, this.args, { - name: this.name, - cwd: this.cwd, - cols: TERMINAL_COLS, - rows: this.rows, - }); - - // On Data - this._ptyProcess.onData((data) => { - this.buffer.push(data); - if (this.server.io) { - this.server.io.to(this.name).emit("terminalWrite", this.name, data); - } - }); - - // On Exit - this._ptyProcess.onExit((res) => { - this.server.io.to(this.name).emit("terminalExit", this.name, res.exitCode); - - // Remove room - this.server.io.in(this.name).socketsLeave(this.name); - - Terminal.terminalMap.delete(this.name); - log.debug("Terminal", "Terminal " + this.name + " exited with code " + res.exitCode); - - clearInterval(this.keepAliveInterval); - - if (this.callback) { - this.callback(res.exitCode); - } - }); if (this.enableKeepAlive) { log.debug("Terminal", "Keep alive enabled for terminal " + this.name); @@ -131,6 +67,90 @@ export class Terminal { } } + get rows() { + return this._rows; + } + + set rows(rows : number) { + this._rows = rows; + try { + this.ptyProcess?.resize(this.cols, this.rows); + } catch (e) { + if (e instanceof Error) { + log.debug("Terminal", "Failed to resize terminal: " + e.message); + } + } + } + + get cols() { + return this._cols; + } + + set cols(cols : number) { + this._cols = cols; + try { + this.ptyProcess?.resize(this.cols, this.rows); + } catch (e) { + if (e instanceof Error) { + log.debug("Terminal", "Failed to resize terminal: " + e.message); + } + } + } + + public start() { + if (this._ptyProcess) { + return; + } + + try { + this._ptyProcess = pty.spawn(this.file, this.args, { + name: this.name, + cwd: this.cwd, + cols: TERMINAL_COLS, + rows: this.rows, + }); + + // On Data + this._ptyProcess.onData((data) => { + this.buffer.pushItem(data); + if (this.server.io) { + this.server.io.to(this.name).emit("terminalWrite", this.name, data); + } + }); + + // On Exit + this._ptyProcess.onExit(this.exit); + } catch (error) { + if (error instanceof Error) { + log.error("Terminal", "Failed to start terminal: " + error.message); + const exitCode = Number(error.message.split(" ").pop()); + this.exit({ + exitCode, + }); + } + } + } + + /** + * Exit event handler + * @param res + */ + protected exit = (res : {exitCode: number, signal?: number | undefined}) => { + this.server.io.to(this.name).emit("terminalExit", this.name, res.exitCode); + + // Remove room + this.server.io.in(this.name).socketsLeave(this.name); + + Terminal.terminalMap.delete(this.name); + log.debug("Terminal", "Terminal " + this.name + " exited with code " + res.exitCode); + + clearInterval(this.keepAliveInterval); + + if (this.callback) { + this.callback(res.exitCode); + } + }; + public onExit(callback : (exitCode : number) => void) { this.callback = callback; } diff --git a/backend/util-common.ts b/backend/util-common.ts index ad963d7..a254b8d 100644 --- a/backend/util-common.ts +++ b/backend/util-common.ts @@ -12,6 +12,11 @@ dayjs.extend(utc); dayjs.extend(timezone); dayjs.extend(relativeTime); +export interface LooseObject { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any +} + let randomBytes : (numBytes: number) => Uint8Array; initRandomBytes(); diff --git a/backend/util-server.ts b/backend/util-server.ts index 241782c..04d34db 100644 --- a/backend/util-server.ts +++ b/backend/util-server.ts @@ -6,6 +6,11 @@ import { ERROR_TYPE_VALIDATION } from "./util-common"; import { R } from "redbean-node"; import { verifyPassword } from "./password-hash"; +export interface JWTDecoded { + username : string; + h? : string; +} + export interface DockgeSocket extends Socket { userID: number; consoleTerminal? : Terminal; diff --git a/backend/utils/limit-queue.ts b/backend/utils/limit-queue.ts index 8a1a95d..2913051 100644 --- a/backend/utils/limit-queue.ts +++ b/backend/utils/limit-queue.ts @@ -4,14 +4,14 @@ */ export class LimitQueue extends Array { __limit; - __onExceed = null; + __onExceed? : (item : T | undefined) => void; constructor(limit: number) { super(); this.__limit = limit; } - push(value : T) { + pushItem(value : T) { super.push(value); if (this.length > this.__limit) { const item = this.shift(); diff --git a/extra/close-incorrect-issue.js b/extra/close-incorrect-issue.js new file mode 100644 index 0000000..7f3c47c --- /dev/null +++ b/extra/close-incorrect-issue.js @@ -0,0 +1,57 @@ +import github from "@actions/github"; + +(async () => { + try { + const token = process.argv[2]; + const issueNumber = process.argv[3]; + const username = process.argv[4]; + + const client = github.getOctokit(token).rest; + + const issue = { + owner: "louislam", + repo: "dockge", + number: issueNumber, + }; + + const labels = ( + await client.issues.listLabelsOnIssue({ + owner: issue.owner, + repo: issue.repo, + issue_number: issue.number + }) + ).data.map(({ name }) => name); + + if (labels.length === 0) { + console.log("Bad format here"); + + await client.issues.addLabels({ + owner: issue.owner, + repo: issue.repo, + issue_number: issue.number, + labels: [ "invalid-format" ] + }); + + // Add the issue closing comment + await client.issues.createComment({ + owner: issue.owner, + repo: issue.repo, + issue_number: issue.number, + body: `@${username}: Hello! :wave:\n\nThis issue is being automatically closed because it does not follow the issue template. Please DO NOT open a blank issue.` + }); + + // Close the issue + await client.issues.update({ + owner: issue.owner, + repo: issue.repo, + issue_number: issue.number, + state: "closed" + }); + } else { + console.log("Pass!"); + } + } catch (e) { + console.log(e); + } + +})(); diff --git a/extra/reformat-changelog.ts b/extra/reformat-changelog.ts new file mode 100644 index 0000000..cba6605 --- /dev/null +++ b/extra/reformat-changelog.ts @@ -0,0 +1,42 @@ +// Generate on GitHub +const input = ` +* Add Korean translation by @Alanimdeo in https://github.com/louislam/dockge/pull/86 +`; + +const template = ` +### 🆕 New Features + +### Improvements + +### 🐞 Bug Fixes + +### 🦎 Translation Contributions + +### Others +- Other small changes, code refactoring and comment/doc updates in this repo: +`; + +const lines = input.split("\n").filter((line) => line.trim() !== ""); + +for (const line of lines) { + // Split the last " by " + const usernamePullRequesURL = line.split(" by ").pop(); + + if (!usernamePullRequesURL) { + console.log("Unable to parse", line); + continue; + } + + const [ username, pullRequestURL ] = usernamePullRequesURL.split(" in "); + const pullRequestID = "#" + pullRequestURL.split("/").pop(); + let message = line.split(" by ").shift(); + + if (!message) { + console.log("Unable to parse", line); + continue; + } + + message = message.split("* ").pop(); + console.log("-", pullRequestID, message, `(Thanks ${username})`); +} +console.log(template); diff --git a/frontend/components.d.ts b/frontend/components.d.ts index bafec4c..708dd4e 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -11,6 +11,8 @@ declare module 'vue' { Appearance: typeof import('./src/components/settings/Appearance.vue')['default'] ArrayInput: typeof import('./src/components/ArrayInput.vue')['default'] ArraySelect: typeof import('./src/components/ArraySelect.vue')['default'] + BDropdown: typeof import('bootstrap-vue-next')['BDropdown'] + BDropdownItem: typeof import('bootstrap-vue-next')['BDropdownItem'] BModal: typeof import('bootstrap-vue-next')['BModal'] Confirm: typeof import('./src/components/Confirm.vue')['default'] Container: typeof import('./src/components/Container.vue')['default'] diff --git a/frontend/src/components/ArraySelect.vue b/frontend/src/components/ArraySelect.vue index 563bcbd..ebf505c 100644 --- a/frontend/src/components/ArraySelect.vue +++ b/frontend/src/components/ArraySelect.vue @@ -5,7 +5,7 @@
  • diff --git a/frontend/src/components/Container.vue b/frontend/src/components/Container.vue index 3eb96ff..604e068 100644 --- a/frontend/src/components/Container.vue +++ b/frontend/src/components/Container.vue @@ -9,7 +9,7 @@ @@ -27,7 +27,7 @@