diff --git a/.github/ISSUE_TEMPLATE/--bug-report.yaml b/.github/ISSUE_TEMPLATE/--bug-report.yaml new file mode 100644 index 000000000..53853d157 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/--bug-report.yaml @@ -0,0 +1,47 @@ +name: 🐞 Bug report +description: Create a bug report to help us improve coolify +title: "[Bug]: " +labels: [Bug] +assignees: +- andrasbacsai +- vasani-arpit +body: +- type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! Please fill the form in English +- type: checkboxes + attributes: + label: Is there an existing issue for this? + options: + - label: I have searched the existing issues + required: true +- type: textarea + attributes: + label: Description + description: A concise description of what you're experiencing and what you expect. + placeholder: | + When I do , happens and I see the error message attached below: + ```...``` + What I expect is + validations: + required: true +- type: textarea + attributes: + label: Steps To Reproduce + description: Add steps to reproduce this behaviour, include console / network logs & videos + placeholder: | + 1. Go to '...' + 2. Click on '....' + 3. Scroll down to '....' + 4. See error + validations: + required: true +- type: input + id: version + attributes: + label: Version + description: "The version of your coolify Instance" + placeholder: "2.5.2" + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/--feature-request.yaml b/.github/ISSUE_TEMPLATE/--feature-request.yaml new file mode 100644 index 000000000..9e217ff23 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/--feature-request.yaml @@ -0,0 +1,31 @@ +name: 🛠️ Feature request +description: Suggest an idea to improve coolify +title: '[Feature]: ' +labels: [Enhancement] +assignees: + - andrasbacsai + - vasani-arpit +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to request a feature for coolify! Please also add your request here to get feedback from the community: https://feedback.coolify.io/! + - type: checkboxes + attributes: + label: Is there an existing issue for this? + description: Please search to see if an issue related to this feature request already exists. + options: + - label: I have searched the existing issues + required: true + - type: textarea + attributes: + label: Summary + description: One paragraph description of the feature. + validations: + required: true + - type: textarea + attributes: + label: Why should this be worked on? + description: A concise description of the problems or use cases for this feature request. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/--task.yaml b/.github/ISSUE_TEMPLATE/--task.yaml new file mode 100644 index 000000000..a1ff080c5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/--task.yaml @@ -0,0 +1,20 @@ +name: 📝 Task +description: Create a task for the team to work on +title: "[Task]: " +labels: [Task] +body: +- type: checkboxes + attributes: + label: Is there an existing issue for this? + description: Please search to see if an issue related to this already exists. + options: + - label: I have searched the existing issues + required: true +- type: textarea + attributes: + label: SubTasks + placeholder: | + - Sub Task 1 + - Sub Task 2 + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..49618c8e7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: 🤔 Questions and Help + url: https://discord.com/invite/6rDM4fkymF + about: Reach out to us on discord or our github discussions page. diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml new file mode 100644 index 000000000..b0b7028d6 --- /dev/null +++ b/.github/workflows/github-actions.yml @@ -0,0 +1,39 @@ +name: release-coolify + +on: + release: + types: published + +jobs: + make-it-coolifyed: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v2 + - + name: Set up QEMU + uses: docker/setup-qemu-action@v1 + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - + name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Get current package version + uses: martinbeentjes/npm-get-version-action@v1.2.3 + id: package-version + - + name: Build and push + uses: docker/build-push-action@v2 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: coollabsio/coolify:latest,coollabsio/coolify:${{steps.package-version.outputs.current-version}} + cache-from: type=registry,ref=coollabsio/coolify:buildcache + cache-to: type=registry,ref=coollabsio/coolify:buildcache,mode=max diff --git a/README.md b/README.md index f342fd74a..da200223a 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,10 @@ https://demo.coolify.io/ (If it is unresponsive, that means someone overloaded the server. 🙃) +## Feedback + +If you have a new service / build pack you would like to add, raise an idea [here](https://feedback.coolify.io/) to get feedback from the community! + ## How to install Installation is automated with the following command: @@ -52,18 +56,21 @@ These are the predefined build packs, but with the Docker build pack, you can ho - NuxtJS - NextJS - React/Preact -- NextJS - Gatsby - Svelte - PHP +- Laravel - Rust - Docker +- Python +- Deno ### Databases One-click database is ready to be used internally or shared over the internet: - MongoDB +- MariaDB - MySQL - PostgreSQL - CouchDB @@ -73,9 +80,9 @@ One-click database is ready to be used internally or shared over the internet: You can host cool open-source services as well: -- [WordPress](https://wordpress.org) +- [WordPress](https://docs.coollabs.io/coolify/services/wordpress) - [Ghost](https://ghost.org) -- [Plausible Analytics](https://plausible.io) +- [Plausible Analytics](https://docs.coollabs.io/coolify/services/plausible-analytics) - [NocoDB](https://nocodb.com) - [VSCode Server](https://github.com/cdr/code-server) - [MinIO](https://min.io) @@ -85,6 +92,8 @@ You can host cool open-source services as well: - [Uptime Kuma](https://github.com/louislam/uptime-kuma) - [MeiliSearch](https://github.com/meilisearch/meilisearch) - [Umami](https://github.com/mikecao/umami) +- [Fider](https://fider.io) +- [Hasura](https://hasura.io) ## Migration from v1 diff --git a/data/traefik/docker-compose-tcp.yaml b/data/traefik/docker-compose-tcp.yaml new file mode 100644 index 000000000..110630d2e --- /dev/null +++ b/data/traefik/docker-compose-tcp.yaml @@ -0,0 +1,23 @@ +version: '3.5' + +services: + ${ID}: + container_name: proxy-for-${PORT} + image: traefik:v2.6 + command: + - --api.insecure=true + - --entrypoints.web.address=:${PORT} + - --providers.docker=false + - --providers.docker.exposedbydefault=false + - --providers.http.endpoint=http://host.docker.internal:3000/traefik.json?id=${ID} + - --providers.http.pollTimeout=5s + - --log.level=error + ports: + - '${PORT}:${PORT}' + networks: + - ${NETWORK} + +networks: + net: + external: false + name: ${NETWORK} diff --git a/docker-compose-traefik.yaml b/docker-compose-traefik.yaml new file mode 100644 index 000000000..09fd32525 --- /dev/null +++ b/docker-compose-traefik.yaml @@ -0,0 +1,29 @@ +version: '3.8' + +services: + proxy: + image: traefik:v2.6 + command: + - --api.insecure=true + - --entrypoints.web.address=:80 + - --entrypoints.websecure.address=:443 + - --providers.docker=false + - --providers.docker.exposedbydefault=false + - --providers.http.endpoint=http://host.docker.internal:3000/traefik.json + - --providers.http.pollTimeout=5s + - --log.level=error + ports: + - '80:80' + - '443:443' + - '8080:8080' + volumes: + - /var/run/docker.sock:/var/run/docker.sock + extra_hosts: + - 'host.docker.internal:host-gateway' + networks: + - coolify-infra + +networks: + coolify-infra: + attachable: true + name: coolify-infra diff --git a/docker-compose.yaml b/docker-compose.yaml index 6f3cd69f0..39866b5e2 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -39,3 +39,5 @@ volumes: name: coolify-ssl-certs coolify-letsencrypt: name: coolify-letsencrypt + coolify-traefik-letsencrypt: + name: coolify-traefik-letsencrypt diff --git a/package.json b/package.json index 7c28949bd..1f766bd18 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "coolify", "description": "An open-source & self-hostable Heroku / Netlify alternative.", - "version": "2.6.1", + "version": "2.9.0", "license": "AGPL-3.0", "scripts": { "dev": "docker-compose -f docker-compose-dev.yaml up -d && cross-env NODE_ENV=development & svelte-kit dev --host 0.0.0.0", @@ -30,60 +30,63 @@ }, "devDependencies": { "@sveltejs/adapter-node": "1.0.0-next.73", - "@sveltejs/kit": "1.0.0-next.316", - "@types/js-cookie": "3.0.1", + "@sveltejs/adapter-static": "1.0.0-next.31", + "@sveltejs/kit": "1.0.0-next.334", + "@types/js-cookie": "3.0.2", "@types/js-yaml": "4.0.5", - "@types/node": "17.0.25", - "@types/node-forge": "1.0.1", + "@types/node": "17.0.34", + "@types/node-forge": "1.0.2", "@typescript-eslint/eslint-plugin": "4.31.1", "@typescript-eslint/parser": "4.31.1", "@zerodevx/svelte-toast": "0.7.1", - "autoprefixer": "10.4.4", + "autoprefixer": "10.4.7", "cross-env": "7.0.3", "cross-var": "1.1.0", "eslint": "7.32.0", "eslint-config-prettier": "8.5.0", "eslint-plugin-svelte3": "3.4.1", "husky": "7.0.4", - "lint-staged": "12.4.0", - "postcss": "8.4.12", + "lint-staged": "12.4.1", + "postcss": "8.4.13", "prettier": "2.6.2", "prettier-plugin-svelte": "2.7.0", - "prettier-plugin-tailwindcss": "0.1.10", + "prettier-plugin-tailwindcss": "0.1.11", "prisma": "3.11.1", - "svelte": "3.47.0", - "svelte-check": "2.7.0", + "svelte": "3.48.0", + "svelte-check": "2.7.1", "svelte-preprocess": "4.10.6", "svelte-select": "4.4.7", - "sveltekit-i18n": "2.1.2", + "sveltekit-i18n": "2.2.1", "tailwindcss": "3.0.24", "ts-node": "10.7.0", - "tslib": "2.3.1", - "typescript": "4.6.3" + "tslib": "2.4.0", + "typescript": "4.6.4" }, "type": "module", "dependencies": { "@iarna/toml": "2.2.5", "@prisma/client": "3.11.1", - "@sentry/node": "6.19.6", + "@sentry/node": "6.19.7", "bcryptjs": "2.4.3", - "bullmq": "1.80.4", + "bullmq": "1.82.2", "compare-versions": "4.1.3", "cookie": "0.5.0", "cuid": "2.1.8", - "dayjs": "1.11.1", + "dayjs": "1.11.2", "dockerode": "3.3.1", "dotenv-extended": "2.9.0", "generate-password": "1.7.0", "get-port": "6.1.2", - "got": "12.0.3", + "got": "12.0.4", + "is-ip": "4.0.0", "js-cookie": "3.0.1", "js-yaml": "4.1.0", "jsonwebtoken": "8.5.1", "mustache": "4.2.0", "node-forge": "1.3.1", + "node-os-utils": "1.3.6", "p-limit": "4.0.0", - "svelte-kit-cookie-session": "2.1.3", + "svelte-kit-cookie-session": "2.1.4", "tailwindcss-scrollbar": "0.1.0", "unique-names-generator": "4.7.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8af565eeb..b626b4fbd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,27 +1,28 @@ -lockfileVersion: 5.3 +lockfileVersion: 5.4 specifiers: '@iarna/toml': 2.2.5 '@prisma/client': 3.11.1 - '@sentry/node': 6.19.6 + '@sentry/node': 6.19.7 '@sveltejs/adapter-node': 1.0.0-next.73 - '@sveltejs/kit': 1.0.0-next.316 - '@types/js-cookie': 3.0.1 + '@sveltejs/adapter-static': 1.0.0-next.31 + '@sveltejs/kit': 1.0.0-next.334 + '@types/js-cookie': 3.0.2 '@types/js-yaml': 4.0.5 - '@types/node': 17.0.25 - '@types/node-forge': 1.0.1 + '@types/node': 17.0.34 + '@types/node-forge': 1.0.2 '@typescript-eslint/eslint-plugin': 4.31.1 '@typescript-eslint/parser': 4.31.1 '@zerodevx/svelte-toast': 0.7.1 - autoprefixer: 10.4.4 + autoprefixer: 10.4.7 bcryptjs: 2.4.3 - bullmq: 1.80.4 + bullmq: 1.82.2 compare-versions: 4.1.3 cookie: 0.5.0 cross-env: 7.0.3 cross-var: 1.1.0 cuid: 2.1.8 - dayjs: 1.11.1 + dayjs: 1.11.2 dockerode: 3.3.1 dotenv-extended: 2.9.0 eslint: 7.32.0 @@ -29,90 +30,95 @@ specifiers: eslint-plugin-svelte3: 3.4.1 generate-password: 1.7.0 get-port: 6.1.2 - got: 12.0.3 + got: 12.0.4 husky: 7.0.4 + is-ip: 4.0.0 js-cookie: 3.0.1 js-yaml: 4.1.0 jsonwebtoken: 8.5.1 - lint-staged: 12.4.0 + lint-staged: 12.4.1 mustache: 4.2.0 node-forge: 1.3.1 + node-os-utils: 1.3.6 p-limit: 4.0.0 - postcss: 8.4.12 + postcss: 8.4.13 prettier: 2.6.2 prettier-plugin-svelte: 2.7.0 - prettier-plugin-tailwindcss: 0.1.10 + prettier-plugin-tailwindcss: 0.1.11 prisma: 3.11.1 - svelte: 3.47.0 - svelte-check: 2.7.0 - svelte-kit-cookie-session: 2.1.3 + svelte: 3.48.0 + svelte-check: 2.7.1 + svelte-kit-cookie-session: 2.1.4 svelte-preprocess: 4.10.6 svelte-select: 4.4.7 - sveltekit-i18n: 2.1.2 + sveltekit-i18n: 2.2.1 tailwindcss: 3.0.24 tailwindcss-scrollbar: 0.1.0 ts-node: 10.7.0 - tslib: 2.3.1 - typescript: 4.6.3 + tslib: 2.4.0 + typescript: 4.6.4 unique-names-generator: 4.7.1 dependencies: '@iarna/toml': 2.2.5 '@prisma/client': 3.11.1_prisma@3.11.1 - '@sentry/node': 6.19.6 + '@sentry/node': 6.19.7 bcryptjs: 2.4.3 - bullmq: 1.80.4 + bullmq: 1.82.2 compare-versions: 4.1.3 cookie: 0.5.0 cuid: 2.1.8 - dayjs: 1.11.1 + dayjs: 1.11.2 dockerode: 3.3.1 dotenv-extended: 2.9.0 generate-password: 1.7.0 get-port: 6.1.2 - got: 12.0.3 + got: 12.0.4 + is-ip: 4.0.0 js-cookie: 3.0.1 js-yaml: 4.1.0 jsonwebtoken: 8.5.1 mustache: 4.2.0 node-forge: 1.3.1 + node-os-utils: 1.3.6 p-limit: 4.0.0 - svelte-kit-cookie-session: 2.1.3 + svelte-kit-cookie-session: 2.1.4 tailwindcss-scrollbar: 0.1.0_tailwindcss@3.0.24 unique-names-generator: 4.7.1 devDependencies: '@sveltejs/adapter-node': 1.0.0-next.73 - '@sveltejs/kit': 1.0.0-next.316_svelte@3.47.0 - '@types/js-cookie': 3.0.1 + '@sveltejs/adapter-static': 1.0.0-next.31 + '@sveltejs/kit': 1.0.0-next.334_svelte@3.48.0 + '@types/js-cookie': 3.0.2 '@types/js-yaml': 4.0.5 - '@types/node': 17.0.25 - '@types/node-forge': 1.0.1 - '@typescript-eslint/eslint-plugin': 4.31.1_8ede7edd7694646e12d33c52460f622c - '@typescript-eslint/parser': 4.31.1_eslint@7.32.0+typescript@4.6.3 + '@types/node': 17.0.34 + '@types/node-forge': 1.0.2 + '@typescript-eslint/eslint-plugin': 4.31.1_lii63oz3usekbu5ehvrcuwn5jy + '@typescript-eslint/parser': 4.31.1_e4zyhrvfnqudwdx5bevnvkluy4 '@zerodevx/svelte-toast': 0.7.1 - autoprefixer: 10.4.4_postcss@8.4.12 + autoprefixer: 10.4.7_postcss@8.4.13 cross-env: 7.0.3 cross-var: 1.1.0 eslint: 7.32.0 eslint-config-prettier: 8.5.0_eslint@7.32.0 - eslint-plugin-svelte3: 3.4.1_eslint@7.32.0+svelte@3.47.0 + eslint-plugin-svelte3: 3.4.1_6wevxxng4y4ff26nzlndg2wnpa husky: 7.0.4 - lint-staged: 12.4.0 - postcss: 8.4.12 + lint-staged: 12.4.1 + postcss: 8.4.13 prettier: 2.6.2 - prettier-plugin-svelte: 2.7.0_prettier@2.6.2+svelte@3.47.0 - prettier-plugin-tailwindcss: 0.1.10_prettier@2.6.2 + prettier-plugin-svelte: 2.7.0_kkjbqzpydplecjtkxrgomroeru + prettier-plugin-tailwindcss: 0.1.11_prettier@2.6.2 prisma: 3.11.1 - svelte: 3.47.0 - svelte-check: 2.7.0_postcss@8.4.12+svelte@3.47.0 - svelte-preprocess: 4.10.6_41810887ae6c6d59323116f47e33fa38 + svelte: 3.48.0 + svelte-check: 2.7.1_f2ke6qjyzu5axsjd6yk3u4tn7a + svelte-preprocess: 4.10.6_nq4dx2skq5drra53vttuo4lltu svelte-select: 4.4.7 - sveltekit-i18n: 2.1.2_svelte@3.47.0 + sveltekit-i18n: 2.2.1_svelte@3.48.0 tailwindcss: 3.0.24_ts-node@10.7.0 - ts-node: 10.7.0_de7c86b0cde507c63a0402da5b982bd3 - tslib: 2.3.1 - typescript: 4.6.3 + ts-node: 10.7.0_3smuweqyuzdazdnyhhezld6mfa + tslib: 2.4.0 + typescript: 4.6.4 packages: /@babel/code-frame/7.12.11: @@ -210,6 +216,31 @@ packages: } dev: false + /@jridgewell/resolve-uri/3.0.7: + resolution: + { + integrity: sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA== + } + engines: { node: '>=6.0.0' } + dev: true + + /@jridgewell/sourcemap-codec/1.4.13: + resolution: + { + integrity: sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w== + } + dev: true + + /@jridgewell/trace-mapping/0.3.13: + resolution: + { + integrity: sha512-o1xbKhp9qnIAoHJSWd6KlCZfqslL4valSF81H8ImioOAxluWYWOpWkpyktY2vnt4tbrX9XYaxovq6cgowaJp2w== + } + dependencies: + '@jridgewell/resolve-uri': 3.0.7 + '@jridgewell/sourcemap-codec': 1.4.13 + dev: true + /@nodelib/fs.scandir/2.1.5: resolution: { @@ -283,55 +314,55 @@ packages: picomatch: 2.3.0 dev: true - /@sentry/core/6.19.6: + /@sentry/core/6.19.7: resolution: { - integrity: sha512-biEotGRr44/vBCOegkTfC9rwqaqRKIpFljKGyYU6/NtzMRooktqOhjmjmItNCMRknArdeaQwA8lk2jcZDXX3Og== + integrity: sha512-tOfZ/umqB2AcHPGbIrsFLcvApdTm9ggpi/kQZFkej7kMphjT+SGBiQfYtjyg9jcRW+ilAR4JXC9BGKsdEQ+8Vw== } engines: { node: '>=6' } dependencies: - '@sentry/hub': 6.19.6 - '@sentry/minimal': 6.19.6 - '@sentry/types': 6.19.6 - '@sentry/utils': 6.19.6 + '@sentry/hub': 6.19.7 + '@sentry/minimal': 6.19.7 + '@sentry/types': 6.19.7 + '@sentry/utils': 6.19.7 tslib: 1.14.1 dev: false - /@sentry/hub/6.19.6: + /@sentry/hub/6.19.7: resolution: { - integrity: sha512-PuEOBZxvx3bjxcXmWWZfWXG+orojQiWzv9LQXjIgroVMKM/GG4QtZbnWl1hOckUj7WtKNl4hEGO2g/6PyCV/vA== + integrity: sha512-y3OtbYFAqKHCWezF0EGGr5lcyI2KbaXW2Ik7Xp8Mu9TxbSTuwTe4rTntwg8ngPjUQU3SUHzgjqVB8qjiGqFXCA== } engines: { node: '>=6' } dependencies: - '@sentry/types': 6.19.6 - '@sentry/utils': 6.19.6 + '@sentry/types': 6.19.7 + '@sentry/utils': 6.19.7 tslib: 1.14.1 dev: false - /@sentry/minimal/6.19.6: + /@sentry/minimal/6.19.7: resolution: { - integrity: sha512-T1NKcv+HTlmd8EbzUgnGPl4ySQGHWMCyZ8a8kXVMZOPDzphN3fVIzkYzWmSftCWp0rpabXPt9aRF2mfBKU+mAQ== + integrity: sha512-wcYmSJOdvk6VAPx8IcmZgN08XTXRwRtB1aOLZm+MVHjIZIhHoBGZJYTVQS/BWjldsamj2cX3YGbGXNunaCfYJQ== } engines: { node: '>=6' } dependencies: - '@sentry/hub': 6.19.6 - '@sentry/types': 6.19.6 + '@sentry/hub': 6.19.7 + '@sentry/types': 6.19.7 tslib: 1.14.1 dev: false - /@sentry/node/6.19.6: + /@sentry/node/6.19.7: resolution: { - integrity: sha512-kHQMfsy40ZxxdS9zMPmXCOOLWOJbQj6/aVSHt/L1QthYcgkAi7NJQNXnQIPWQDe8eP3DfNIWM7dc446coqjXrQ== + integrity: sha512-gtmRC4dAXKODMpHXKfrkfvyBL3cI8y64vEi3fDD046uqYcrWdgoQsffuBbxMAizc6Ez1ia+f0Flue6p15Qaltg== } engines: { node: '>=6' } dependencies: - '@sentry/core': 6.19.6 - '@sentry/hub': 6.19.6 - '@sentry/types': 6.19.6 - '@sentry/utils': 6.19.6 + '@sentry/core': 6.19.7 + '@sentry/hub': 6.19.7 + '@sentry/types': 6.19.7 + '@sentry/utils': 6.19.7 cookie: 0.4.2 https-proxy-agent: 5.0.0 lru_map: 0.3.3 @@ -340,22 +371,22 @@ packages: - supports-color dev: false - /@sentry/types/6.19.6: + /@sentry/types/6.19.7: resolution: { - integrity: sha512-QH34LMJidEUPZK78l+Frt3AaVFJhEmIi05Zf8WHd9/iTt+OqvCHBgq49DDr1FWFqyYWm/QgW/3bIoikFpfsXyQ== + integrity: sha512-jH84pDYE+hHIbVnab3Hr+ZXr1v8QABfhx39KknxqKWr2l0oEItzepV0URvbEhB446lk/S/59230dlUUIBGsXbg== } engines: { node: '>=6' } dev: false - /@sentry/utils/6.19.6: + /@sentry/utils/6.19.7: resolution: { - integrity: sha512-fAMWcsguL0632eWrROp/vhPgI7sBj/JROWVPzpabwVkm9z3m1rQm6iLFn4qfkZL8Ozy6NVZPXOQ7EXmeU24byg== + integrity: sha512-z95ECmE3i9pbWoXQrD/7PgkBAzJYR+iXtPuTkpBjDKs86O3mT+PXOT3BAn79w2wkn7/i3vOGD2xVr1uiMl26dA== } engines: { node: '>=6' } dependencies: - '@sentry/types': 6.19.6 + '@sentry/types': 6.19.7 tslib: 1.14.1 dev: false @@ -376,19 +407,29 @@ packages: tiny-glob: 0.2.9 dev: true - /@sveltejs/kit/1.0.0-next.316_svelte@3.47.0: + /@sveltejs/adapter-static/1.0.0-next.31: resolution: { - integrity: sha512-oLjWOWzjriJD2t210r7ALuH/8ZADrJGsOODzRCRSJvRBCt0Q7VKVLqwKbM/RlZzD1k8Af2uRodQT11kP98hAIA== + integrity: sha512-d9RNA/de5ljb+gN8mKA3YfmfJoTbYFdH96NYDD8u4Lu9O/ZnseUxOAcAmD4/LKbLXOY/oYhRpt029xT2owyI3Q== } - engines: { node: '>=14.13' } + dependencies: + tiny-glob: 0.2.9 + dev: true + + /@sveltejs/kit/1.0.0-next.334_svelte@3.48.0: + resolution: + { + integrity: sha512-HPMF1oYBfyOG6wfU0Y6F4SID8jphue9yF+PXJqVTDBL5Z2WBG2ogum6MavE8aWhq+g2H6w5y0jNT8+8DO2KTCA== + } + engines: { node: '>=16' } hasBin: true peerDependencies: svelte: ^3.44.0 dependencies: - '@sveltejs/vite-plugin-svelte': 1.0.0-next.33_svelte@3.47.0+vite@2.9.1 + '@sveltejs/vite-plugin-svelte': 1.0.0-next.33_svelte@3.48.0+vite@2.9.1 + chokidar: 3.5.3 sade: 1.7.4 - svelte: 3.47.0 + svelte: 3.48.0 vite: 2.9.1 transitivePeerDependencies: - diff-match-patch @@ -398,7 +439,7 @@ packages: - supports-color dev: true - /@sveltejs/vite-plugin-svelte/1.0.0-next.33_svelte@3.47.0+vite@2.9.1: + /@sveltejs/vite-plugin-svelte/1.0.0-next.33_svelte@3.48.0+vite@2.9.1: resolution: { integrity: sha512-aj0h2+ZixgT+yoJFIs8dRRw/Cj9tgNu3+hY4CJikpa04mfhR61wXqJFfi2ZEFMUvFda5nCxKYIChFkc6wq5fJA== @@ -417,22 +458,22 @@ packages: kleur: 4.1.4 magic-string: 0.25.7 require-relative: 0.8.7 - svelte: 3.47.0 - svelte-hmr: 0.14.9_svelte@3.47.0 + svelte: 3.48.0 + svelte-hmr: 0.14.9_svelte@3.48.0 vite: 2.9.1 transitivePeerDependencies: - supports-color dev: true - /@sveltekit-i18n/base/1.1.1_svelte@3.47.0: + /@sveltekit-i18n/base/1.2.1_svelte@3.48.0: resolution: { - integrity: sha512-J/sMU0OwS3dCLOuilHMBqu8vZHuuXiNV9vFJx8Nb4/b5BlR/KCZ4bCXI8wZR02GHeCOYKZxWus07CM1scxa/jw== + integrity: sha512-F8gqG2+KAOeT0o2wYlUrW3TRCX7zaD7rBy/1CEVNw0irfw9TgFf/ODmhubkHHT3+6Zk+SMz8RNgeuffBfAMbJw== } peerDependencies: svelte: ^3.x dependencies: - svelte: 3.47.0 + svelte: 3.48.0 optionalDependencies: '@sveltekit-i18n/parser-default': 1.0.3 dev: true @@ -490,7 +531,7 @@ packages: dependencies: '@types/http-cache-semantics': 4.0.1 '@types/keyv': 3.1.3 - '@types/node': 17.0.25 + '@types/node': 17.0.34 '@types/responselike': 1.0.0 dev: false @@ -501,10 +542,10 @@ packages: } dev: false - /@types/js-cookie/3.0.1: + /@types/js-cookie/3.0.2: resolution: { - integrity: sha512-7wg/8gfHltklehP+oyJnZrz9XBuX5ZPP4zB6UsI84utdlkRYLnOm2HfpLXazTwZA+fpGn0ir8tGNgVnMEleBGQ== + integrity: sha512-6+0ekgfusHftJNYpihfkMu8BWdeHs9EOJuGcSofErjstGPfPGEu9yTu4t460lTzzAMl2cM5zngQJqPMHbbnvYA== } dev: true @@ -528,22 +569,22 @@ packages: integrity: sha512-FXCJgyyN3ivVgRoml4h94G/p3kY+u/B86La+QptcqJaWtBWtmc6TtkNfS40n9bIvyLteHh7zXOtgbobORKPbDg== } dependencies: - '@types/node': 17.0.25 + '@types/node': 17.0.34 dev: false - /@types/node-forge/1.0.1: + /@types/node-forge/1.0.2: resolution: { - integrity: sha512-96ELNKv9tQJ19afdBUiM5iDw7OYEc53iUc51gAPR2aGaqRsO1DBROjqgZRjZa1tkPj7TnEOR0EnyAX6iryGkzA== + integrity: sha512-J1OkeZGaW0y9Y7xD49Ja8O82B9l5nZDeoYuGWqIOYPAf9LR+xF23k9ILdzv8dH+2H033fx3D5oiA0GlmicI+sg== } dependencies: - '@types/node': 17.0.25 + '@types/node': 17.0.34 dev: true - /@types/node/17.0.25: + /@types/node/17.0.34: resolution: { - integrity: sha512-wANk6fBrUwdpY4isjWrKTufkrXdu1D2YHCot2fD/DfWxF5sMrVSA+KN7ydckvaTCh0HiqX9IVl0L5/ZoXg5M7w== + integrity: sha512-XImEz7XwTvDBtzlTnm8YvMqGW/ErMWBsKZ+hMTvnDIjGCKxwK5Xpc+c/oQjOauwq8M4OS11hEkpjX8rrI/eEgA== } /@types/pug/2.0.5: @@ -559,7 +600,7 @@ packages: integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== } dependencies: - '@types/node': 17.0.25 + '@types/node': 17.0.34 dev: false /@types/sass/1.16.1: @@ -568,10 +609,10 @@ packages: integrity: sha512-iZUcRrGuz/Tbg3loODpW7vrQJkUtpY2fFSf4ELqqkApcS2TkZ1msk7ie8iZPB86lDOP8QOTTmuvWjc5S0R9OjQ== } dependencies: - '@types/node': 17.0.25 + '@types/node': 17.0.34 dev: true - /@typescript-eslint/eslint-plugin/4.31.1_8ede7edd7694646e12d33c52460f622c: + /@typescript-eslint/eslint-plugin/4.31.1_lii63oz3usekbu5ehvrcuwn5jy: resolution: { integrity: sha512-UDqhWmd5i0TvPLmbK5xY3UZB0zEGseF+DHPghZ37Sb83Qd3p8ujhvAtkU4OF46Ka5Pm5kWvFIx0cCTBFKo0alA== @@ -585,21 +626,21 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/experimental-utils': 4.31.1_eslint@7.32.0+typescript@4.6.3 - '@typescript-eslint/parser': 4.31.1_eslint@7.32.0+typescript@4.6.3 + '@typescript-eslint/experimental-utils': 4.31.1_e4zyhrvfnqudwdx5bevnvkluy4 + '@typescript-eslint/parser': 4.31.1_e4zyhrvfnqudwdx5bevnvkluy4 '@typescript-eslint/scope-manager': 4.31.1 debug: 4.3.3 eslint: 7.32.0 functional-red-black-tree: 1.0.1 regexpp: 3.2.0 semver: 7.3.5 - tsutils: 3.21.0_typescript@4.6.3 - typescript: 4.6.3 + tsutils: 3.21.0_typescript@4.6.4 + typescript: 4.6.4 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/experimental-utils/4.31.1_eslint@7.32.0+typescript@4.6.3: + /@typescript-eslint/experimental-utils/4.31.1_e4zyhrvfnqudwdx5bevnvkluy4: resolution: { integrity: sha512-NtoPsqmcSsWty0mcL5nTZXMf7Ei0Xr2MT8jWjXMVgRK0/1qeQ2jZzLFUh4QtyJ4+/lPUyMw5cSfeeME+Zrtp9Q== @@ -611,7 +652,7 @@ packages: '@types/json-schema': 7.0.9 '@typescript-eslint/scope-manager': 4.31.1 '@typescript-eslint/types': 4.31.1 - '@typescript-eslint/typescript-estree': 4.31.1_typescript@4.6.3 + '@typescript-eslint/typescript-estree': 4.31.1_typescript@4.6.4 eslint: 7.32.0 eslint-scope: 5.1.1 eslint-utils: 3.0.0_eslint@7.32.0 @@ -620,7 +661,7 @@ packages: - typescript dev: true - /@typescript-eslint/parser/4.31.1_eslint@7.32.0+typescript@4.6.3: + /@typescript-eslint/parser/4.31.1_e4zyhrvfnqudwdx5bevnvkluy4: resolution: { integrity: sha512-dnVZDB6FhpIby6yVbHkwTKkn2ypjVIfAR9nh+kYsA/ZL0JlTsd22BiDjouotisY3Irmd3OW1qlk9EI5R8GrvRQ== @@ -635,10 +676,10 @@ packages: dependencies: '@typescript-eslint/scope-manager': 4.31.1 '@typescript-eslint/types': 4.31.1 - '@typescript-eslint/typescript-estree': 4.31.1_typescript@4.6.3 + '@typescript-eslint/typescript-estree': 4.31.1_typescript@4.6.4 debug: 4.3.3 eslint: 7.32.0 - typescript: 4.6.3 + typescript: 4.6.4 transitivePeerDependencies: - supports-color dev: true @@ -662,7 +703,7 @@ packages: engines: { node: ^8.10.0 || ^10.13.0 || >=11.10.1 } dev: true - /@typescript-eslint/typescript-estree/4.31.1_typescript@4.6.3: + /@typescript-eslint/typescript-estree/4.31.1_typescript@4.6.4: resolution: { integrity: sha512-EGHkbsUvjFrvRnusk6yFGqrqMBTue5E5ROnS5puj3laGQPasVUgwhrxfcgkdHNFECHAewpvELE1Gjv0XO3mdWg== @@ -680,8 +721,8 @@ packages: globby: 11.0.4 is-glob: 4.0.3 semver: 7.3.5 - tsutils: 3.21.0_typescript@4.6.3 - typescript: 4.6.3 + tsutils: 3.21.0_typescript@4.6.4 + typescript: 4.6.4 transitivePeerDependencies: - supports-color dev: true @@ -954,22 +995,22 @@ packages: typpy: 2.3.11 dev: false - /autoprefixer/10.4.4_postcss@8.4.12: + /autoprefixer/10.4.7_postcss@8.4.13: resolution: { - integrity: sha512-Tm8JxsB286VweiZ5F0anmbyGiNI3v3wGv3mz9W+cxEDYB/6jbnj6GM9H9mK3wIL8ftgl+C07Lcwb8PG5PCCPzA== + integrity: sha512-ypHju4Y2Oav95SipEcCcI5J7CGPuvz8oat7sUtYj3ClK44bldfvtvcxK6IEK++7rqB7YchDGzweZIBG+SD0ZAA== } engines: { node: ^10 || ^12 || >=14 } hasBin: true peerDependencies: postcss: ^8.1.0 dependencies: - browserslist: 4.20.2 - caniuse-lite: 1.0.30001320 + browserslist: 4.20.3 + caniuse-lite: 1.0.30001338 fraction.js: 4.2.0 normalize-range: 0.1.2 picocolors: 1.0.0 - postcss: 8.4.12 + postcss: 8.4.13 postcss-value-parser: 4.2.0 dev: true @@ -1651,18 +1692,18 @@ packages: fill-range: 7.0.1 dev: true - /browserslist/4.20.2: + /browserslist/4.20.3: resolution: { - integrity: sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA== + integrity: sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg== } engines: { node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7 } hasBin: true dependencies: - caniuse-lite: 1.0.30001320 - electron-to-chromium: 1.4.93 + caniuse-lite: 1.0.30001338 + electron-to-chromium: 1.4.137 escalade: 3.1.1 - node-releases: 2.0.2 + node-releases: 2.0.4 picocolors: 1.0.0 dev: true @@ -1684,10 +1725,10 @@ packages: ieee754: 1.2.1 dev: false - /bullmq/1.80.4: + /bullmq/1.82.2: resolution: { - integrity: sha512-j3PyjU16gqmb3Md9QjMInJdbMvxIlSJx7mojtoP06LV9MfhzW75DkDrpSuJlF0H+0+u6MViV4hhaGTxky5OJWw== + integrity: sha512-pDmMl6HmL/7B41ldBK4lnmGUcobkI/n/a0T3d/volMWC0ULxsaZ6R6fDePk23LwH9Fxu4o9Ny+zurCL3vG7lbg== } dependencies: cron-parser: 4.2.1 @@ -1751,10 +1792,10 @@ packages: engines: { node: '>=6' } dev: false - /caniuse-lite/1.0.30001320: + /caniuse-lite/1.0.30001338: resolution: { - integrity: sha512-MWPzG54AGdo3nWx7zHZTefseM5Y1ccM7hlQKHRqJkPozUaw3hNbBTMmLn16GG2FUzjR13Cr3NPfhIieX5PzXDA== + integrity: sha512-1gLHWyfVoRDsHieO+CaeYe7jSo/MT7D7lhaXUiwwbuR5BwQxORs0f1tAwUSQr3YbxRXJvxHM/PA5FfPQRnsPeQ== } dev: true @@ -2046,10 +2087,10 @@ packages: } dev: false - /dayjs/1.11.1: + /dayjs/1.11.2: resolution: { - integrity: sha512-ER7EjqVAMkRRsxNCC5YqJ9d9VQYuWdGt7aiH2qA5R5wt8ZmWaP2dLUSIK6y/kVzLMlmh1Tvu5xUf4M/wdGJ5KA== + integrity: sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw== } dev: false @@ -2265,10 +2306,10 @@ packages: safe-buffer: 5.2.1 dev: false - /electron-to-chromium/1.4.93: + /electron-to-chromium/1.4.137: resolution: { - integrity: sha512-ywq9Pc5Gwwpv7NG767CtoU8xF3aAUQJjH9//Wy3MBCg4w5JSLbJUq2L8IsCdzPMjvSgxuue9WcVaTOyyxCL0aQ== + integrity: sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA== } dev: true @@ -2613,7 +2654,7 @@ packages: eslint: 7.32.0 dev: true - /eslint-plugin-svelte3/3.4.1_eslint@7.32.0+svelte@3.47.0: + /eslint-plugin-svelte3/3.4.1_6wevxxng4y4ff26nzlndg2wnpa: resolution: { integrity: sha512-7p59WG8qV8L6wLdl4d/c3mdjkgVglQCdv5XOTk/iNPBKXuuV+Q0eFP5Wa6iJd/G2M1qR3BkLPEzaANOqKAZczw== @@ -2624,7 +2665,7 @@ packages: svelte: ^3.2.0 dependencies: eslint: 7.32.0 - svelte: 3.47.0 + svelte: 3.48.0 dev: true /eslint-scope/5.1.1: @@ -3078,10 +3119,10 @@ packages: } dev: true - /got/12.0.3: + /got/12.0.4: resolution: { - integrity: sha512-hmdcXi/S0gcAtDg4P8j/rM7+j3o1Aq6bXhjxkDhRY2ipe7PHpvx/14DgTY2czHOLaGeU8VRvRecidwfu9qdFug== + integrity: sha512-2Eyz4iU/ktq7wtMFXxzK7g5p35uNYLLdiZarZ5/Yn3IJlNEpBd5+dCgcAyxN8/8guZLszffwe3wVyw+DEVrpBg== } engines: { node: '>=14.16' } dependencies: @@ -3283,6 +3324,14 @@ packages: - supports-color dev: false + /ip-regex/5.0.0: + resolution: + { + integrity: sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw== + } + engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } + dev: false + /is-binary-path/2.1.0: resolution: { @@ -3341,6 +3390,16 @@ packages: is-extglob: 2.1.1 dev: true + /is-ip/4.0.0: + resolution: + { + integrity: sha512-4B4XA2HEIm/PY+OSpeMBXr8pGWBYbXuHgjMAqrwbLO3CPTCAd9ArEJzBUKGZtk9viY6+aSfadGnWyjY3ydYZkw== + } + engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } + dependencies: + ip-regex: 5.0.0 + dev: false + /is-number/7.0.0: resolution: { @@ -3524,10 +3583,10 @@ packages: engines: { node: '>=10' } dev: true - /lint-staged/12.4.0: + /lint-staged/12.4.1: resolution: { - integrity: sha512-3X7MR0h9b7qf4iXf/1n7RlVAx+EzpAZXoCEMhVSpaBlgKDfH2ewf+QUm7BddFyq29v4dgPP+8+uYpWuSWx035A== + integrity: sha512-PTXgzpflrQ+pODQTG116QNB+Q6uUTDg5B5HqGvNhoQSGt8Qy+MA/6zSnR8n38+sxP5TapzeQGTvoKni0KRS8Vg== } engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } hasBin: true @@ -3864,10 +3923,10 @@ packages: dev: false optional: true - /nanoid/3.3.1: + /nanoid/3.3.4: resolution: { - integrity: sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== + integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== } engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } hasBin: true @@ -3894,10 +3953,17 @@ packages: dev: false optional: true - /node-releases/2.0.2: + /node-os-utils/1.3.6: resolution: { - integrity: sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg== + integrity: sha512-WympE9ELtdOzNak/rAuuIV5DwvX/PTJtN0LjyWeGyTTR2Kt0sY56ldLoGbVBnfM1dz46VeO3sHcNZI5BZ+EB+w== + } + dev: false + + /node-releases/2.0.4: + resolution: + { + integrity: sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ== } dev: true @@ -4090,7 +4156,7 @@ packages: hasBin: true dev: true - /postcss-js/4.0.0_postcss@8.4.12: + /postcss-js/4.0.0_postcss@8.4.13: resolution: { integrity: sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ== @@ -4100,10 +4166,10 @@ packages: postcss: ^8.3.3 dependencies: camelcase-css: 2.0.1 - postcss: 8.4.12 + postcss: 8.4.13 dev: true - /postcss-load-config/3.1.4_postcss@8.4.12+ts-node@10.7.0: + /postcss-load-config/3.1.4_4jqnslpwnj4ifyjfqbkuebd4fy: resolution: { integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg== @@ -4119,12 +4185,12 @@ packages: optional: true dependencies: lilconfig: 2.0.5 - postcss: 8.4.12 - ts-node: 10.7.0_de7c86b0cde507c63a0402da5b982bd3 + postcss: 8.4.13 + ts-node: 10.7.0_3smuweqyuzdazdnyhhezld6mfa yaml: 1.10.2 dev: true - /postcss-nested/5.0.6_postcss@8.4.12: + /postcss-nested/5.0.6_postcss@8.4.13: resolution: { integrity: sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA== @@ -4133,7 +4199,7 @@ packages: peerDependencies: postcss: ^8.2.14 dependencies: - postcss: 8.4.12 + postcss: 8.4.13 postcss-selector-parser: 6.0.10 dev: true @@ -4155,14 +4221,14 @@ packages: } dev: true - /postcss/8.4.12: + /postcss/8.4.13: resolution: { - integrity: sha512-lg6eITwYe9v6Hr5CncVbK70SoioNQIq81nsaG86ev5hAidQvmOeETBqs7jm43K2F5/Ley3ytDtriImV6TpNiSg== + integrity: sha512-jtL6eTBrza5MPzy8oJLFuUscHDXTV5KcLlqAWHl5q5WYRfnNRGSmOZmOZ1T6Gy7A99mOZfqungmZMpMmCVJ8ZA== } engines: { node: ^10 || ^12 || >=14 } dependencies: - nanoid: 3.3.1 + nanoid: 3.3.4 picocolors: 1.0.0 source-map-js: 1.0.2 dev: true @@ -4175,7 +4241,7 @@ packages: engines: { node: '>= 0.8.0' } dev: true - /prettier-plugin-svelte/2.7.0_prettier@2.6.2+svelte@3.47.0: + /prettier-plugin-svelte/2.7.0_kkjbqzpydplecjtkxrgomroeru: resolution: { integrity: sha512-fQhhZICprZot2IqEyoiUYLTRdumULGRvw0o4dzl5jt0jfzVWdGqeYW27QTWAeXhoupEZJULmNoH3ueJwUWFLIA== @@ -4185,13 +4251,13 @@ packages: svelte: ^3.2.0 dependencies: prettier: 2.6.2 - svelte: 3.47.0 + svelte: 3.48.0 dev: true - /prettier-plugin-tailwindcss/0.1.10_prettier@2.6.2: + /prettier-plugin-tailwindcss/0.1.11_prettier@2.6.2: resolution: { - integrity: sha512-ooDGNuXUjgCXfShliVYQ6+0iXqUFXn+zdNInPe0WZN9qINt9srbLGFGY5jeVL4MXtY20/4S8JaBcd8l6N6NfCQ== + integrity: sha512-a28+1jvpIZQdZ/W97wOXb6VqI762MKE/TxMMuibMEHhyYsSxQA8Ek30KObd5kJI2HF1ldtSYprFayXJXi3pz8Q== } engines: { node: '>=12.17.0' } peerDependencies: @@ -4493,7 +4559,7 @@ packages: integrity: sha512-PwDt186XaL3QN5qXj/H9DGyHhP3/RYYgZZwqBv9Tv8rsAaiwFH1IsJJlcgD37J7UW5a6O67qX0KWKS3/pu0m4w== } dependencies: - tslib: 2.3.1 + tslib: 2.4.0 dev: true /sade/1.7.4: @@ -4678,14 +4744,6 @@ packages: engines: { node: '>=0.10.0' } dev: true - /source-map/0.7.3: - resolution: - { - integrity: sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== - } - engines: { node: '>= 8' } - dev: true - /sourcemap-codec/1.4.8: resolution: { @@ -4858,24 +4916,24 @@ packages: engines: { node: '>= 0.4' } dev: true - /svelte-check/2.7.0_postcss@8.4.12+svelte@3.47.0: + /svelte-check/2.7.1_f2ke6qjyzu5axsjd6yk3u4tn7a: resolution: { - integrity: sha512-GrvG24j0+i8AOm0k0KyJ6Dqc+TAR2yzB7rtS4nljHStunVxCTr/1KYlv4EsOeoqtHLzeWMOd5D2O6nDdP/yw4A== + integrity: sha512-vHVu2+SQ6ibt77iTQaq2oiOjBgGL48qqcg0ZdEOsP5pPOjgeyR9QbnaEdzdBs9nsVYBc/42haKtzb2uFqS8GVw== } hasBin: true peerDependencies: svelte: ^3.24.0 dependencies: + '@jridgewell/trace-mapping': 0.3.13 chokidar: 3.5.3 fast-glob: 3.2.11 import-fresh: 3.3.0 picocolors: 1.0.0 sade: 1.7.4 - source-map: 0.7.3 - svelte: 3.47.0 - svelte-preprocess: 4.10.6_41810887ae6c6d59323116f47e33fa38 - typescript: 4.6.3 + svelte: 3.48.0 + svelte-preprocess: 4.10.6_nq4dx2skq5drra53vttuo4lltu + typescript: 4.6.4 transitivePeerDependencies: - '@babel/core' - coffeescript @@ -4889,7 +4947,7 @@ packages: - sugarss dev: true - /svelte-hmr/0.14.9_svelte@3.47.0: + /svelte-hmr/0.14.9_svelte@3.48.0: resolution: { integrity: sha512-bKE9+4qb4sAnA+TKHiYurUl970rjA0XmlP9TEP7K/ncyWz3m81kA4HOgmlZK/7irGK7gzZlaPDI3cmf8fp/+tg== @@ -4897,17 +4955,17 @@ packages: peerDependencies: svelte: '>=3.19.0' dependencies: - svelte: 3.47.0 + svelte: 3.48.0 dev: true - /svelte-kit-cookie-session/2.1.3: + /svelte-kit-cookie-session/2.1.4: resolution: { - integrity: sha512-7Xk3CNbpLAi1KodlsV5W5jULQ2NxQunaXtAYqAuzIEXIq2EwC4oDa25kdmHjNe33epV0t4r0WwxZOuSdJPsapg== + integrity: sha512-z/ckxHWguYyy66UqfId4Lu+A77ft/3mV5oozbRTI9bnQY0tyJfns/SM0ikdkj7OV5GiI8kq7GSOGvajWwyGeZw== } dev: false - /svelte-preprocess/4.10.6_41810887ae6c6d59323116f47e33fa38: + /svelte-preprocess/4.10.6_nq4dx2skq5drra53vttuo4lltu: resolution: { integrity: sha512-I2SV1w/AveMvgIQlUF/ZOO3PYVnhxfcpNyGt8pxpUVhPfyfL/CZBkkw/KPfuFix5FJ9TnnNYMhACK3DtSaYVVQ== @@ -4955,11 +5013,11 @@ packages: '@types/sass': 1.16.1 detect-indent: 6.1.0 magic-string: 0.25.7 - postcss: 8.4.12 + postcss: 8.4.13 sorcery: 0.10.0 strip-indent: 3.0.0 - svelte: 3.47.0 - typescript: 4.6.3 + svelte: 3.48.0 + typescript: 4.6.4 dev: true /svelte-select/4.4.7: @@ -4969,25 +5027,25 @@ packages: } dev: true - /svelte/3.47.0: + /svelte/3.48.0: resolution: { - integrity: sha512-4JaJp3HEoTCGARRWZQIZDUanhYv0iyoHikklVHVLH9xFE9db22g4TDv7CPeNA8HD1JgjXI1vlhR1JZvvhaTu2Q== + integrity: sha512-fN2YRm/bGumvjUpu6yI3BpvZnpIm9I6A7HR4oUNYd7ggYyIwSA/BX7DJ+UXXffLp6XNcUijyLvttbPVCYa/3xQ== } engines: { node: '>= 8' } dev: true - /sveltekit-i18n/2.1.2_svelte@3.47.0: + /sveltekit-i18n/2.2.1_svelte@3.48.0: resolution: { - integrity: sha512-s5YxcbNd2EWNZaZR1A4Drt8s53E4fpUkN4XIWd3VRpw1pihZVWssqmBW1qkjQ6AB0kiu1Qwule+vt1HkbQOjrg== + integrity: sha512-1CyaRN6dBvp467JjBdji+nJf+7pZ3myFu+2YaCuGSAt09Cvt5ndfRbzy+aAd5WJdk6Lu/hnPEE7ZZFauTbDRNw== } peerDependencies: svelte: ^3.x dependencies: - '@sveltekit-i18n/base': 1.1.1_svelte@3.47.0 + '@sveltekit-i18n/base': 1.2.1_svelte@3.48.0 '@sveltekit-i18n/parser-default': 1.0.3 - svelte: 3.47.0 + svelte: 3.48.0 dev: true /table/6.7.2: @@ -5037,10 +5095,10 @@ packages: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.0.0 - postcss: 8.4.12 - postcss-js: 4.0.0_postcss@8.4.12 - postcss-load-config: 3.1.4_postcss@8.4.12+ts-node@10.7.0 - postcss-nested: 5.0.6_postcss@8.4.12 + postcss: 8.4.13 + postcss-js: 4.0.0_postcss@8.4.13 + postcss-load-config: 3.1.4_4jqnslpwnj4ifyjfqbkuebd4fy + postcss-nested: 5.0.6_postcss@8.4.13 postcss-selector-parser: 6.0.10 postcss-value-parser: 4.2.0 quick-lru: 5.1.1 @@ -5113,7 +5171,7 @@ packages: engines: { node: '>=0.10.0' } dev: true - /ts-node/10.7.0_de7c86b0cde507c63a0402da5b982bd3: + /ts-node/10.7.0_3smuweqyuzdazdnyhhezld6mfa: resolution: { integrity: sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A== @@ -5135,14 +5193,14 @@ packages: '@tsconfig/node12': 1.0.9 '@tsconfig/node14': 1.0.1 '@tsconfig/node16': 1.0.2 - '@types/node': 17.0.25 + '@types/node': 17.0.34 acorn: 8.5.0 acorn-walk: 8.2.0 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 4.6.3 + typescript: 4.6.4 v8-compile-cache-lib: 3.0.0 yn: 3.1.1 dev: true @@ -5153,14 +5211,14 @@ packages: integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== } - /tslib/2.3.1: + /tslib/2.4.0: resolution: { - integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== + integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== } dev: true - /tsutils/3.21.0_typescript@4.6.3: + /tsutils/3.21.0_typescript@4.6.4: resolution: { integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== @@ -5170,7 +5228,7 @@ packages: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' dependencies: tslib: 1.14.1 - typescript: 4.6.3 + typescript: 4.6.4 dev: true /tweetnacl/0.14.5: @@ -5203,10 +5261,10 @@ packages: engines: { node: '>=10' } dev: true - /typescript/4.6.3: + /typescript/4.6.4: resolution: { - integrity: sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw== + integrity: sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg== } engines: { node: '>=4.2.0' } hasBin: true @@ -5283,7 +5341,7 @@ packages: optional: true dependencies: esbuild: 0.14.34 - postcss: 8.4.12 + postcss: 8.4.13 resolve: 1.22.0 rollup: 2.61.1 optionalDependencies: diff --git a/prisma/migrations/20220408070805_added_expose_port/migration.sql b/prisma/migrations/20220408070805_added_expose_port/migration.sql new file mode 100644 index 000000000..a23afd64a --- /dev/null +++ b/prisma/migrations/20220408070805_added_expose_port/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Application" ADD COLUMN "exposePort" INTEGER; diff --git a/prisma/migrations/20220430124553_expose_port_for_services/migration.sql b/prisma/migrations/20220430124553_expose_port_for_services/migration.sql new file mode 100644 index 000000000..fdbab5713 --- /dev/null +++ b/prisma/migrations/20220430124553_expose_port_for_services/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Service" ADD COLUMN "exposePort" INTEGER; diff --git a/prisma/migrations/20220509130501_custom_plausible_script/migration.sql b/prisma/migrations/20220509130501_custom_plausible_script/migration.sql new file mode 100644 index 000000000..6c8c28ff4 --- /dev/null +++ b/prisma/migrations/20220509130501_custom_plausible_script/migration.sql @@ -0,0 +1,24 @@ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_PlausibleAnalytics" ( + "id" TEXT NOT NULL PRIMARY KEY, + "email" TEXT, + "username" TEXT, + "password" TEXT NOT NULL, + "postgresqlUser" TEXT NOT NULL, + "postgresqlPassword" TEXT NOT NULL, + "postgresqlDatabase" TEXT NOT NULL, + "postgresqlPublicPort" INTEGER, + "secretKeyBase" TEXT, + "scriptName" TEXT NOT NULL DEFAULT 'plausible.js', + "serviceId" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "PlausibleAnalytics_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_PlausibleAnalytics" ("createdAt", "email", "id", "password", "postgresqlDatabase", "postgresqlPassword", "postgresqlPublicPort", "postgresqlUser", "secretKeyBase", "serviceId", "updatedAt", "username") SELECT "createdAt", "email", "id", "password", "postgresqlDatabase", "postgresqlPassword", "postgresqlPublicPort", "postgresqlUser", "secretKeyBase", "serviceId", "updatedAt", "username" FROM "PlausibleAnalytics"; +DROP TABLE "PlausibleAnalytics"; +ALTER TABLE "new_PlausibleAnalytics" RENAME TO "PlausibleAnalytics"; +CREATE UNIQUE INDEX "PlausibleAnalytics_serviceId_key" ON "PlausibleAnalytics"("serviceId"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/prisma/migrations/20220510081125_custom_wordpress_db/migration.sql b/prisma/migrations/20220510081125_custom_wordpress_db/migration.sql new file mode 100644 index 000000000..5a1cd301c --- /dev/null +++ b/prisma/migrations/20220510081125_custom_wordpress_db/migration.sql @@ -0,0 +1,32 @@ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Wordpress" ( + "id" TEXT NOT NULL PRIMARY KEY, + "extraConfig" TEXT, + "tablePrefix" TEXT, + "ownMysql" BOOLEAN NOT NULL DEFAULT false, + "mysqlHost" TEXT, + "mysqlPort" INTEGER, + "mysqlUser" TEXT NOT NULL, + "mysqlPassword" TEXT NOT NULL, + "mysqlRootUser" TEXT NOT NULL, + "mysqlRootUserPassword" TEXT NOT NULL, + "mysqlDatabase" TEXT, + "mysqlPublicPort" INTEGER, + "ftpEnabled" BOOLEAN NOT NULL DEFAULT false, + "ftpUser" TEXT, + "ftpPassword" TEXT, + "ftpPublicPort" INTEGER, + "ftpHostKey" TEXT, + "ftpHostKeyPrivate" TEXT, + "serviceId" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "Wordpress_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_Wordpress" ("createdAt", "extraConfig", "ftpEnabled", "ftpHostKey", "ftpHostKeyPrivate", "ftpPassword", "ftpPublicPort", "ftpUser", "id", "mysqlDatabase", "mysqlPassword", "mysqlPublicPort", "mysqlRootUser", "mysqlRootUserPassword", "mysqlUser", "serviceId", "tablePrefix", "updatedAt") SELECT "createdAt", "extraConfig", "ftpEnabled", "ftpHostKey", "ftpHostKeyPrivate", "ftpPassword", "ftpPublicPort", "ftpUser", "id", "mysqlDatabase", "mysqlPassword", "mysqlPublicPort", "mysqlRootUser", "mysqlRootUserPassword", "mysqlUser", "serviceId", "tablePrefix", "updatedAt" FROM "Wordpress"; +DROP TABLE "Wordpress"; +ALTER TABLE "new_Wordpress" RENAME TO "Wordpress"; +CREATE UNIQUE INDEX "Wordpress_serviceId_key" ON "Wordpress"("serviceId"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/prisma/migrations/20220517081328_traefik/migration.sql b/prisma/migrations/20220517081328_traefik/migration.sql new file mode 100644 index 000000000..a83281fa4 --- /dev/null +++ b/prisma/migrations/20220517081328_traefik/migration.sql @@ -0,0 +1,24 @@ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Setting" ( + "id" TEXT NOT NULL PRIMARY KEY, + "fqdn" TEXT, + "isRegistrationEnabled" BOOLEAN NOT NULL DEFAULT false, + "dualCerts" BOOLEAN NOT NULL DEFAULT false, + "minPort" INTEGER NOT NULL DEFAULT 9000, + "maxPort" INTEGER NOT NULL DEFAULT 9100, + "proxyPassword" TEXT NOT NULL, + "proxyUser" TEXT NOT NULL, + "proxyHash" TEXT, + "isAutoUpdateEnabled" BOOLEAN NOT NULL DEFAULT false, + "isDNSCheckEnabled" BOOLEAN NOT NULL DEFAULT true, + "isTraefikUsed" BOOLEAN NOT NULL DEFAULT true, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); +INSERT INTO "new_Setting" ("createdAt", "dualCerts", "fqdn", "id", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "maxPort", "minPort", "proxyHash", "proxyPassword", "proxyUser", "updatedAt") SELECT "createdAt", "dualCerts", "fqdn", "id", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "maxPort", "minPort", "proxyHash", "proxyPassword", "proxyUser", "updatedAt" FROM "Setting"; +DROP TABLE "Setting"; +ALTER TABLE "new_Setting" RENAME TO "Setting"; +CREATE UNIQUE INDEX "Setting_fqdn_key" ON "Setting"("fqdn"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/prisma/migrations/20220519095648_minio_apifqdn/migration.sql b/prisma/migrations/20220519095648_minio_apifqdn/migration.sql new file mode 100644 index 000000000..a44712864 --- /dev/null +++ b/prisma/migrations/20220519095648_minio_apifqdn/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Minio" ADD COLUMN "apiFqdn" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 58e4fd8a2..7cef6adda 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -20,6 +20,7 @@ model Setting { proxyHash String? isAutoUpdateEnabled Boolean @default(false) isDNSCheckEnabled Boolean @default(true) + isTraefikUsed Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } @@ -84,6 +85,7 @@ model Application { buildPack String? projectId Int? port Int? + exposePort Int? installCommand String? buildCommand String? startCommand String? @@ -289,6 +291,7 @@ model Service { id String @id @default(cuid()) name String fqdn String? + exposePort Int? dualCerts Boolean @default(false) type String? version String? @@ -320,6 +323,7 @@ model PlausibleAnalytics { postgresqlDatabase String postgresqlPublicPort Int? secretKeyBase String? + scriptName String @default("plausible.js") serviceId String @unique service Service @relation(fields: [serviceId], references: [id]) createdAt DateTime @default(now()) @@ -331,6 +335,7 @@ model Minio { rootUser String rootUserPassword String publicPort Int? + apiFqdn String? serviceId String @unique service Service @relation(fields: [serviceId], references: [id]) createdAt DateTime @default(now()) @@ -350,6 +355,9 @@ model Wordpress { id String @id @default(cuid()) extraConfig String? tablePrefix String? + ownMysql Boolean @default(false) + mysqlHost String? + mysqlPort Int? mysqlUser String mysqlPassword String mysqlRootUser String diff --git a/src/app.html b/src/app.html index a0336e87d..ab9a75726 100644 --- a/src/app.html +++ b/src/app.html @@ -3,7 +3,6 @@ - Coolify %svelte.head% diff --git a/src/hooks.ts b/src/hooks.ts index 50624b22f..639bab353 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -114,5 +114,5 @@ export const getSession: GetSession = function ({ locals }) { }; export async function handleError({ error, event }) { - if (!dev) sentry.captureException(error, event); + // if (!dev) sentry.captureException(error, event); } diff --git a/src/lib/buildPacks/common.ts b/src/lib/buildPacks/common.ts index 6ac518136..c6f1f75f8 100644 --- a/src/lib/buildPacks/common.ts +++ b/src/lib/buildPacks/common.ts @@ -8,14 +8,16 @@ import { staticDeployments } from '$lib/components/common'; const staticApps = ['static', 'react', 'vuejs', 'svelte', 'gatsby', 'astro', 'eleventy']; const nodeBased = [ 'react', + 'preact', 'vuejs', 'svelte', 'gatsby', - 'php', 'astro', 'eleventy', 'node', - 'nestjs' + 'nestjs', + 'nuxtjs', + 'nextjs' ]; export function makeLabelForStandaloneApplication({ @@ -403,7 +405,72 @@ export function setDefaultBaseImage(buildPack) { label: 'webdevops/php-nginx:7.1-alpine' } ]; - + const pythonVersions = [ + { + value: 'python:3.10-alpine', + label: 'python:3.10-alpine' + }, + { + value: 'python:3.10-buster', + label: 'python:3.10-buster' + }, + { + value: 'python:3.10-bullseye', + label: 'python:3.10-bullseye' + }, + { + value: 'python:3.10-slim-bullseye', + label: 'python:3.10-slim-bullseye' + }, + { + value: 'python:3.9-alpine', + label: 'python:3.9-alpine' + }, + { + value: 'python:3.9-buster', + label: 'python:3.9-buster' + }, + { + value: 'python:3.9-bullseye', + label: 'python:3.9-bullseye' + }, + { + value: 'python:3.9-slim-bullseye', + label: 'python:3.9-slim-bullseye' + }, + { + value: 'python:3.8-alpine', + label: 'python:3.8-alpine' + }, + { + value: 'python:3.8-buster', + label: 'python:3.8-buster' + }, + { + value: 'python:3.8-bullseye', + label: 'python:3.8-bullseye' + }, + { + value: 'python:3.8-slim-bullseye', + label: 'python:3.8-slim-bullseye' + }, + { + value: 'python:3.7-alpine', + label: 'python:3.7-alpine' + }, + { + value: 'python:3.7-buster', + label: 'python:3.7-buster' + }, + { + value: 'python:3.7-bullseye', + label: 'python:3.7-bullseye' + }, + { + value: 'python:3.7-slim-bullseye', + label: 'python:3.7-slim-bullseye' + } + ]; let payload = { baseImage: null, baseBuildImage: null, @@ -423,7 +490,8 @@ export function setDefaultBaseImage(buildPack) { payload.baseBuildImages = nodeVersions; } if (buildPack === 'python') { - payload.baseImage = 'python:3-alpine'; + payload.baseImage = 'python:3.10-alpine'; + payload.baseImages = pythonVersions; } if (buildPack === 'rust') { payload.baseImage = 'rust:latest'; diff --git a/src/lib/buildPacks/gatsby.ts b/src/lib/buildPacks/gatsby.ts index cdf95f1dd..a10f84c43 100644 --- a/src/lib/buildPacks/gatsby.ts +++ b/src/lib/buildPacks/gatsby.ts @@ -2,7 +2,7 @@ import { buildCacheImageWithNode, buildImage } from '$lib/docker'; import { promises as fs } from 'fs'; const createDockerfile = async (data, imageforBuild): Promise => { - const { applicationId, tag, workdir, publishDirectory, baseImage, buildId } = data; + const { applicationId, tag, workdir, publishDirectory, baseImage, buildId, port } = data; const Dockerfile: Array = []; Dockerfile.push(`FROM ${imageforBuild}`); @@ -12,7 +12,7 @@ const createDockerfile = async (data, imageforBuild): Promise => { if (baseImage.includes('nginx')) { Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); } - Dockerfile.push(`EXPOSE 80`); + Dockerfile.push(`EXPOSE ${port}`); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); }; diff --git a/src/lib/buildPacks/laravel.ts b/src/lib/buildPacks/laravel.ts index a83363dc0..4912a772f 100644 --- a/src/lib/buildPacks/laravel.ts +++ b/src/lib/buildPacks/laravel.ts @@ -2,7 +2,7 @@ import { buildCacheImageForLaravel, buildImage } from '$lib/docker'; import { promises as fs } from 'fs'; const createDockerfile = async (data, image): Promise => { - const { workdir, applicationId, tag, buildId } = data; + const { workdir, applicationId, tag, buildId, port } = data; const Dockerfile: Array = []; Dockerfile.push(`FROM ${image}`); @@ -24,7 +24,7 @@ const createDockerfile = async (data, image): Promise => { `COPY --chown=application:application --from=${applicationId}:${tag}-cache /app/mix-manifest.json /app/public/mix-manifest.json` ); Dockerfile.push(`COPY --chown=application:application . ./`); - Dockerfile.push(`EXPOSE 80`); + Dockerfile.push(`EXPOSE ${port}`); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); }; diff --git a/src/lib/buildPacks/php.ts b/src/lib/buildPacks/php.ts index b3c9651ce..d04e86712 100644 --- a/src/lib/buildPacks/php.ts +++ b/src/lib/buildPacks/php.ts @@ -2,7 +2,7 @@ import { buildImage } from '$lib/docker'; import { promises as fs } from 'fs'; const createDockerfile = async (data, image, htaccessFound): Promise => { - const { workdir, baseDirectory, buildId } = data; + const { workdir, baseDirectory, buildId, port } = data; const Dockerfile: Array = []; let composerFound = false; try { @@ -22,7 +22,7 @@ const createDockerfile = async (data, image, htaccessFound): Promise => { } Dockerfile.push(`COPY /entrypoint.sh /opt/docker/provision/entrypoint.d/30-entrypoint.sh`); - Dockerfile.push(`EXPOSE 80`); + Dockerfile.push(`EXPOSE ${port}`); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); }; diff --git a/src/lib/buildPacks/react.ts b/src/lib/buildPacks/react.ts index 3b55bfa23..5217e8f96 100644 --- a/src/lib/buildPacks/react.ts +++ b/src/lib/buildPacks/react.ts @@ -2,7 +2,7 @@ import { buildCacheImageWithNode, buildImage } from '$lib/docker'; import { promises as fs } from 'fs'; const createDockerfile = async (data, image): Promise => { - const { applicationId, tag, workdir, publishDirectory, baseImage, buildId } = data; + const { applicationId, tag, workdir, publishDirectory, baseImage, buildId, port } = data; const Dockerfile: Array = []; Dockerfile.push(`FROM ${image}`); @@ -12,7 +12,7 @@ const createDockerfile = async (data, image): Promise => { if (baseImage.includes('nginx')) { Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); } - Dockerfile.push(`EXPOSE 80`); + Dockerfile.push(`EXPOSE ${port}`); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); }; diff --git a/src/lib/buildPacks/static.ts b/src/lib/buildPacks/static.ts index 8f3c2c7d4..79647ae93 100644 --- a/src/lib/buildPacks/static.ts +++ b/src/lib/buildPacks/static.ts @@ -12,7 +12,8 @@ const createDockerfile = async (data, image): Promise => { secrets, pullmergeRequestId, baseImage, - buildId + buildId, + port } = data; const Dockerfile: Array = []; @@ -42,7 +43,7 @@ const createDockerfile = async (data, image): Promise => { if (baseImage.includes('nginx')) { Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); } - Dockerfile.push(`EXPOSE 80`); + Dockerfile.push(`EXPOSE ${port}`); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); }; diff --git a/src/lib/buildPacks/svelte.ts b/src/lib/buildPacks/svelte.ts index 5604e7ed6..bede0e806 100644 --- a/src/lib/buildPacks/svelte.ts +++ b/src/lib/buildPacks/svelte.ts @@ -2,7 +2,7 @@ import { buildCacheImageWithNode, buildImage } from '$lib/docker'; import { promises as fs } from 'fs'; const createDockerfile = async (data, image): Promise => { - const { applicationId, tag, workdir, publishDirectory, baseImage, buildId } = data; + const { applicationId, tag, workdir, publishDirectory, baseImage, buildId, port } = data; const Dockerfile: Array = []; Dockerfile.push(`FROM ${image}`); @@ -12,7 +12,7 @@ const createDockerfile = async (data, image): Promise => { if (baseImage.includes('nginx')) { Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); } - Dockerfile.push(`EXPOSE 80`); + Dockerfile.push(`EXPOSE ${port}`); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); }; diff --git a/src/lib/buildPacks/vuejs.ts b/src/lib/buildPacks/vuejs.ts index 5604e7ed6..bede0e806 100644 --- a/src/lib/buildPacks/vuejs.ts +++ b/src/lib/buildPacks/vuejs.ts @@ -2,7 +2,7 @@ import { buildCacheImageWithNode, buildImage } from '$lib/docker'; import { promises as fs } from 'fs'; const createDockerfile = async (data, image): Promise => { - const { applicationId, tag, workdir, publishDirectory, baseImage, buildId } = data; + const { applicationId, tag, workdir, publishDirectory, baseImage, buildId, port } = data; const Dockerfile: Array = []; Dockerfile.push(`FROM ${image}`); @@ -12,7 +12,7 @@ const createDockerfile = async (data, image): Promise => { if (baseImage.includes('nginx')) { Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); } - Dockerfile.push(`EXPOSE 80`); + Dockerfile.push(`EXPOSE ${port}`); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); }; diff --git a/src/lib/common.ts b/src/lib/common.ts index acbe6c88c..cae9350ce 100644 --- a/src/lib/common.ts +++ b/src/lib/common.ts @@ -4,6 +4,8 @@ import { dev } from '$app/env'; import * as Sentry from '@sentry/node'; import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-generator'; import type { Config } from 'unique-names-generator'; +import { promises as dns } from 'dns'; +import { isIP } from 'is-ip'; import * as db from '$lib/database'; import { buildLogQueue } from './queues'; @@ -14,24 +16,25 @@ import Cookie from 'cookie'; import os from 'os'; import type { RequestEvent } from '@sveltejs/kit/types/internal'; import type { Job } from 'bullmq'; +import { t } from './translations'; try { if (!dev) { - Sentry.init({ - dsn: process.env['COOLIFY_SENTRY_DSN'], - tracesSampleRate: 0, - environment: 'production', - debug: true, - release: currentVersion, - initialScope: { - tags: { - appId: process.env['COOLIFY_APP_ID'], - 'os.arch': getOsArch(), - 'os.platform': os.platform(), - 'os.release': os.release() - } - } - }); + // Sentry.init({ + // dsn: process.env['COOLIFY_SENTRY_DSN'], + // tracesSampleRate: 0, + // environment: 'production', + // debug: true, + // release: currentVersion, + // initialScope: { + // tags: { + // appId: process.env['COOLIFY_APP_ID'], + // 'os.arch': getOsArch(), + // 'os.platform': os.platform(), + // 'os.release': os.release() + // } + // } + // }); } } catch (err) { console.log('Could not initialize Sentry, no worries.'); @@ -93,12 +96,16 @@ export const getUserDetails = async ( const userId = event?.locals?.session?.data?.userId || null; let permission = 'read'; if (teamId && userId) { - const data = await db.prisma.permission.findFirst({ - where: { teamId, userId }, - select: { permission: true }, - rejectOnNotFound: true - }); - if (data.permission) permission = data.permission; + try { + const data = await db.prisma.permission.findFirst({ + where: { teamId, userId }, + select: { permission: true }, + rejectOnNotFound: true + }); + if (data.permission) permission = data.permission; + } catch (error) { + console.log(error); + } } const payload = { @@ -179,3 +186,97 @@ export function getDomain(domain: string): string { export function getOsArch() { return os.arch(); } + +export async function isDNSValid(event: any, domain: string): Promise { + let resolves = []; + try { + if (isIP(event.url.hostname)) { + resolves = [event.url.hostname]; + } else { + resolves = await dns.resolve4(event.url.hostname); + } + } catch (error) { + throw { + message: t.get('application.dns_not_set_error', { domain }) + }; + } + + try { + let ipDomainFound = false; + dns.setServers(['1.1.1.1', '8.8.8.8']); + const dnsResolve = await dns.resolve4(domain); + if (dnsResolve.length > 0) { + for (const ip of dnsResolve) { + if (resolves.includes(ip)) { + ipDomainFound = true; + } + } + } + if (!ipDomainFound) throw false; + } catch (error) { + throw { + message: t.get('application.domain_not_valid') + }; + } +} + +export async function checkDomainsIsValidInDNS({ event, fqdn, dualCerts }): Promise { + const domain = getDomain(fqdn); + const domainDualCert = domain.includes('www.') ? domain.replace('www.', '') : `www.${domain}`; + dns.setServers(['1.1.1.1', '8.8.8.8']); + let resolves = []; + try { + if (isIP(event.url.hostname)) { + resolves = [event.url.hostname]; + } else { + resolves = await dns.resolve4(event.url.hostname); + } + } catch (error) { + throw { + message: t.get('application.dns_not_set_error', { domain }) + }; + } + + if (dualCerts) { + try { + const ipDomain = await dns.resolve4(domain); + const ipDomainDualCert = await dns.resolve4(domainDualCert); + + let ipDomainFound = false; + let ipDomainDualCertFound = false; + + for (const ip of ipDomain) { + if (resolves.includes(ip)) { + ipDomainFound = true; + } + } + for (const ip of ipDomainDualCert) { + if (resolves.includes(ip)) { + ipDomainDualCertFound = true; + } + } + if (ipDomainFound && ipDomainDualCertFound) return { status: 200 }; + throw false; + } catch (error) { + throw { + message: t.get('application.dns_not_set_error', { domain }) + }; + } + } else { + try { + const ipDomain = await dns.resolve4(domain); + let ipDomainFound = false; + for (const ip of ipDomain) { + if (resolves.includes(ip)) { + ipDomainFound = true; + } + } + if (ipDomainFound) return { status: 200 }; + throw false; + } catch (error) { + throw { + message: t.get('application.dns_not_set_error', { domain }) + }; + } + } +} diff --git a/src/lib/components/DatabaseLinks.svelte b/src/lib/components/DatabaseLinks.svelte index 9ef11a238..5927c405c 100644 --- a/src/lib/components/DatabaseLinks.svelte +++ b/src/lib/components/DatabaseLinks.svelte @@ -3,6 +3,7 @@ import Clickhouse from './svg/databases/Clickhouse.svelte'; import CouchDb from './svg/databases/CouchDB.svelte'; import MongoDb from './svg/databases/MongoDB.svelte'; + import MariaDb from './svg/databases/MariaDB.svelte'; import MySql from './svg/databases/MySQL.svelte'; import PostgreSql from './svg/databases/PostgreSQL.svelte'; import Redis from './svg/databases/Redis.svelte'; @@ -17,6 +18,8 @@ {:else if database.type === 'mysql'} + {:else if database.type === 'mariadb'} + {:else if database.type === 'postgresql'} {:else if database.type === 'redis'} diff --git a/src/lib/components/PageLoader.svelte b/src/lib/components/PageLoader.svelte new file mode 100644 index 000000000..edbbcd80c --- /dev/null +++ b/src/lib/components/PageLoader.svelte @@ -0,0 +1,35 @@ + + +
+
+
+ + diff --git a/src/lib/components/common.ts b/src/lib/components/common.ts index d6ff2f8ea..20b66a9cd 100644 --- a/src/lib/components/common.ts +++ b/src/lib/components/common.ts @@ -52,6 +52,12 @@ export const supportedDatabaseTypesAndVersions = [ versions: ['5.0', '4.4', '4.2'] }, { name: 'mysql', fancyName: 'MySQL', baseImage: 'bitnami/mysql', versions: ['8.0', '5.7'] }, + { + name: 'mariadb', + fancyName: 'MariaDB', + baseImage: 'bitnami/mariadb', + versions: ['10.7', '10.6', '10.5', '10.4', '10.3', '10.2'] + }, { name: 'postgresql', fancyName: 'PostgreSQL', @@ -213,5 +219,25 @@ export const supportedServiceTypesAndVersions = [ ports: { main: 3000 } + // }, + // { + // name: 'appwrite', + // fancyName: 'AppWrite', + // baseImage: 'appwrite/appwrite', + // images: ['appwrite/influxdb', 'appwrite/telegraf', 'mariadb:10.7', 'redis:6.0-alpine3.12'], + // versions: ['latest', '0.13.0'], + // recommendedVersion: '0.13.0', + // ports: { + // main: 3000 + // } + // } } ]; + +export const getServiceMainPort = (service: string) => { + const serviceType = supportedServiceTypesAndVersions.find((s) => s.name === service); + if (serviceType) { + return serviceType.ports.main; + } + return null; +}; diff --git a/src/lib/components/svg/databases/MariaDB.svelte b/src/lib/components/svg/databases/MariaDB.svelte new file mode 100644 index 000000000..1de83ef15 --- /dev/null +++ b/src/lib/components/svg/databases/MariaDB.svelte @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/src/lib/components/svg/databases/MongoDB.svelte b/src/lib/components/svg/databases/MongoDB.svelte index b99ffe4da..fbb261aa2 100644 --- a/src/lib/components/svg/databases/MongoDB.svelte +++ b/src/lib/components/svg/databases/MongoDB.svelte @@ -3,31 +3,88 @@ + diff --git a/src/lib/components/svg/services/PlausibleAnalytics.svelte b/src/lib/components/svg/services/PlausibleAnalytics.svelte index 9ae5774fc..c02a9e6d9 100644 --- a/src/lib/components/svg/services/PlausibleAnalytics.svelte +++ b/src/lib/components/svg/services/PlausibleAnalytics.svelte @@ -4,6 +4,6 @@ plausible logo diff --git a/src/lib/components/templates.ts b/src/lib/components/templates.ts index 5166baabc..aa1ed9528 100644 --- a/src/lib/components/templates.ts +++ b/src/lib/components/templates.ts @@ -128,9 +128,7 @@ export function findBuildPack(pack, packageManager = 'npm') { if (pack === 'astro') { return { ...metaData, - installCommand: `yarn install`, - buildCommand: `yarn build`, - startCommand: null, + ...defaultBuildAndDeploy(packageManager), publishDirectory: `dist`, port: 80 }; @@ -138,9 +136,7 @@ export function findBuildPack(pack, packageManager = 'npm') { if (pack === 'eleventy') { return { ...metaData, - installCommand: `yarn install`, - buildCommand: `yarn build`, - startCommand: null, + ...defaultBuildAndDeploy(packageManager), publishDirectory: `_site`, port: 80 }; diff --git a/src/lib/database/applications.ts b/src/lib/database/applications.ts index d64537b69..87d6f90d8 100644 --- a/src/lib/database/applications.ts +++ b/src/lib/database/applications.ts @@ -138,7 +138,18 @@ export async function getApplicationWebhook({ return s; }); } - return { ...application }; + const { baseImage, baseBuildImage, baseBuildImages, baseImages } = setDefaultBaseImage( + application.buildPack + ); + + // Set default build images + if (!application.baseImage) { + application.baseImage = baseImage; + } + if (!application.baseBuildImage) { + application.baseBuildImage = baseBuildImage; + } + return { ...application, baseBuildImages, baseImages }; } catch (e) { throw { status: 404, body: { message: e.message } }; } @@ -267,6 +278,7 @@ export async function configureApplication({ name, fqdn, port, + exposePort, installCommand, buildCommand, startCommand, @@ -286,6 +298,7 @@ export async function configureApplication({ name: string; fqdn: string; port: number; + exposePort: number; installCommand: string; buildCommand: string; startCommand: string; @@ -307,6 +320,7 @@ export async function configureApplication({ buildPack, fqdn, port, + exposePort, installCommand, buildCommand, startCommand, diff --git a/src/lib/database/checks.ts b/src/lib/database/checks.ts index bbe773ae5..2c1c25cd8 100644 --- a/src/lib/database/checks.ts +++ b/src/lib/database/checks.ts @@ -51,10 +51,12 @@ export async function isSecretExists({ export async function isDomainConfigured({ id, - fqdn + fqdn, + checkOwn = false }: { id: string; fqdn: string; + checkOwn?: boolean; }): Promise { const domain = getDomain(fqdn); const nakedDomain = domain.replace('www.', ''); @@ -72,12 +74,15 @@ export async function isDomainConfigured({ where: { OR: [ { fqdn: { endsWith: `//${nakedDomain}` } }, - { fqdn: { endsWith: `//www.${nakedDomain}` } } + { fqdn: { endsWith: `//www.${nakedDomain}` } }, + { minio: { apiFqdn: { endsWith: `//${nakedDomain}` } } }, + { minio: { apiFqdn: { endsWith: `//www.${nakedDomain}` } } } ], - id: { not: id } + id: { not: checkOwn ? undefined : id } }, select: { fqdn: true } }); + const coolifyFqdn = await prisma.setting.findFirst({ where: { OR: [ diff --git a/src/lib/database/common.ts b/src/lib/database/common.ts index 314f77b08..c26796ea4 100644 --- a/src/lib/database/common.ts +++ b/src/lib/database/common.ts @@ -28,7 +28,7 @@ if (!dev) { } export const prisma = new PrismaClient({ - errorFormat: 'pretty', + errorFormat: 'minimal', rejectOnNotFound: false }); @@ -58,7 +58,7 @@ export function ErrorHandler(e: { truncatedError.message = 'git clone failed'; } if (!e.message?.includes('Coolify Proxy is not running')) { - sentry.captureException(truncatedError); + // sentry.captureException(truncatedError); } const payload = { status: truncatedError.status || 500, @@ -149,6 +149,19 @@ export function generateDatabaseConfiguration(database: Database & { settings: D MONGODB_ROOT_PASSWORD: string; }; } + | { + volume: string; + image: string; + ulimits: Record; + privatePort: number; + environmentVariables: { + MARIADB_ROOT_USER: string; + MARIADB_ROOT_PASSWORD: string; + MARIADB_USER: string; + MARIADB_PASSWORD: string; + MARIADB_DATABASE: string; + }; + } | { volume: string; image: string; @@ -207,6 +220,20 @@ export function generateDatabaseConfiguration(database: Database & { settings: D volume: `${id}-${type}-data:/bitnami/mysql/data`, ulimits: {} }; + } else if (type === 'mariadb') { + return { + privatePort: 3306, + environmentVariables: { + MARIADB_ROOT_USER: rootUser, + MARIADB_ROOT_PASSWORD: rootUserPassword, + MARIADB_USER: dbUser, + MARIADB_PASSWORD: dbUserPassword, + MARIADB_DATABASE: defaultDatabase + }, + image: `${baseImage}:${version}`, + volume: `${id}-${type}-data:/bitnami/mariadb`, + ulimits: {} + }; } else if (type === 'mongodb') { return { privatePort: 27017, @@ -278,6 +305,12 @@ export async function getFreePort() { select: { mysqlPublicPort: true } }) ).map((a) => a.mysqlPublicPort); - const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed]; + const minioUsed = await ( + await prisma.minio.findMany({ + where: { publicPort: { not: null } }, + select: { publicPort: true } + }) + ).map((a) => a.publicPort); + const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed, ...minioUsed]; return await getPort({ port: portNumbers(minPort, maxPort), exclude: usedPorts }); } diff --git a/src/lib/database/databases.ts b/src/lib/database/databases.ts index 388047ac3..edc15c729 100644 --- a/src/lib/database/databases.ts +++ b/src/lib/database/databases.ts @@ -184,6 +184,10 @@ export async function updatePasswordInDb(database, user, newPassword, isRoot) { await asyncExecShell( `DOCKER_HOST=${host} docker exec ${id} mysql -u ${rootUser} -p${rootUserPassword} -e \"ALTER USER '${user}'@'%' IDENTIFIED WITH caching_sha2_password BY '${newPassword}';\"` ); + } else if (type === 'mariadb') { + await asyncExecShell( + `DOCKER_HOST=${host} docker exec ${id} mysql -u ${rootUser} -p${rootUserPassword} -e \"SET PASSWORD FOR '${user}'@'%' = PASSWORD('${newPassword}');\"` + ); } else if (type === 'postgresql') { if (isRoot) { await asyncExecShell( diff --git a/src/lib/database/destinations.ts b/src/lib/database/destinations.ts index 2f6cdb488..f378874b0 100644 --- a/src/lib/database/destinations.ts +++ b/src/lib/database/destinations.ts @@ -1,6 +1,6 @@ import { asyncExecShell, getEngine } from '$lib/common'; import { dockerInstance } from '$lib/docker'; -import { startCoolifyProxy } from '$lib/haproxy'; +import { startCoolifyProxy, startTraefikProxy } from '$lib/haproxy'; import { getDatabaseImage } from '.'; import { prisma } from './common'; import type { DestinationDocker, Service, Application, Prisma } from '@prisma/client'; @@ -125,7 +125,14 @@ export async function newLocalDestination({ } await prisma.destinationDocker.updateMany({ where: { engine }, data: { isCoolifyProxyUsed } }); } - if (isCoolifyProxyUsed) await startCoolifyProxy(engine); + if (isCoolifyProxyUsed) { + const settings = await prisma.setting.findFirst(); + if (settings?.isTraefikUsed) { + await startTraefikProxy(engine); + } else { + await startCoolifyProxy(engine); + } + } return destination.id; } export async function removeDestination({ id }: Pick): Promise { @@ -133,12 +140,14 @@ export async function removeDestination({ id }: Pick): if (destination.isCoolifyProxyUsed) { const host = getEngine(destination.engine); const { network } = destination; + const settings = await prisma.setting.findFirst(); + const containerName = settings.isTraefikUsed ? 'coolify-proxy' : 'coolify-haproxy'; const { stdout: found } = await asyncExecShell( - `DOCKER_HOST=${host} docker ps -a --filter network=${network} --filter name=coolify-haproxy --format '{{.}}'` + `DOCKER_HOST=${host} docker ps -a --filter network=${network} --filter name=${containerName} --format '{{.}}'` ); if (found) { await asyncExecShell( - `DOCKER_HOST="${host}" docker network disconnect ${network} coolify-haproxy` + `DOCKER_HOST="${host}" docker network disconnect ${network} ${containerName}` ); await asyncExecShell(`DOCKER_HOST="${host}" docker network rm ${network}`); } diff --git a/src/lib/database/services.ts b/src/lib/database/services.ts index df760cf76..453153c0c 100644 --- a/src/lib/database/services.ts +++ b/src/lib/database/services.ts @@ -327,35 +327,62 @@ export async function updatePlausibleAnalyticsService({ id, fqdn, email, + exposePort, username, - name + name, + scriptName }: { id: string; fqdn: string; + exposePort?: number; name: string; email: string; username: string; + scriptName: string; }): Promise { - await prisma.plausibleAnalytics.update({ where: { serviceId: id }, data: { email, username } }); - await prisma.service.update({ where: { id }, data: { name, fqdn } }); + await prisma.plausibleAnalytics.update({ + where: { serviceId: id }, + data: { email, username, scriptName } + }); + await prisma.service.update({ where: { id }, data: { name, fqdn, exposePort } }); } export async function updateService({ id, fqdn, + exposePort, name }: { id: string; fqdn: string; + exposePort?: number; name: string; }): Promise { - return await prisma.service.update({ where: { id }, data: { fqdn, name } }); + return await prisma.service.update({ where: { id }, data: { fqdn, name, exposePort } }); +} +export async function updateMinioService({ + id, + fqdn, + apiFqdn, + exposePort, + name +}: { + id: string; + fqdn: string; + apiFqdn: string; + exposePort?: number; + name: string; +}): Promise { + return await prisma.service.update({ + where: { id }, + data: { fqdn, name, exposePort, minio: { update: { apiFqdn } } } + }); } - export async function updateFiderService({ id, fqdn, name, + exposePort, emailNoreply, emailMailgunApiKey, emailMailgunDomain, @@ -368,6 +395,7 @@ export async function updateFiderService({ }: { id: string; fqdn: string; + exposePort?: number; name: string; emailNoreply: string; emailMailgunApiKey: string; @@ -384,6 +412,7 @@ export async function updateFiderService({ data: { fqdn, name, + exposePort, fider: { update: { emailNoreply, @@ -405,22 +434,49 @@ export async function updateWordpress({ id, fqdn, name, + exposePort, + ownMysql, mysqlDatabase, - extraConfig + extraConfig, + mysqlHost, + mysqlPort, + mysqlUser, + mysqlPassword }: { id: string; fqdn: string; name: string; + exposePort?: number; + ownMysql: boolean; mysqlDatabase: string; extraConfig: string; + mysqlHost?: string; + mysqlPort?: number; + mysqlUser?: string; + mysqlPassword?: string; }): Promise { + mysqlPassword = encrypt(mysqlPassword); return await prisma.service.update({ where: { id }, - data: { fqdn, name, wordpress: { update: { mysqlDatabase, extraConfig } } } + data: { + fqdn, + name, + exposePort, + wordpress: { + update: { + mysqlDatabase, + extraConfig, + mysqlHost, + mysqlUser, + mysqlPassword, + mysqlPort + } + } + } }); } -export async function updateMinioService({ +export async function updateMinioServicePort({ id, publicPort }: { @@ -434,16 +490,18 @@ export async function updateGhostService({ id, fqdn, name, + exposePort, mariadbDatabase }: { id: string; fqdn: string; name: string; + exposePort?: number; mariadbDatabase: string; }): Promise { return await prisma.service.update({ where: { id }, - data: { fqdn, name, ghost: { update: { mariadbDatabase } } } + data: { fqdn, name, exposePort, ghost: { update: { mariadbDatabase } } } }); } diff --git a/src/lib/haproxy/configuration.ts b/src/lib/haproxy/configuration.ts index 745e8f4af..eb6b7ef00 100644 --- a/src/lib/haproxy/configuration.ts +++ b/src/lib/haproxy/configuration.ts @@ -55,6 +55,9 @@ frontend http http-request redirect location {{{redirectValue}}} code ${ dev ? 302 : 301 } if { req.hdr(host) -i {{redirectTo}} } + {{#scriptName}} + http-request set-path /js/plausible.js if { hdr(host) -i {{domain}} } { path_beg -i /js/{{scriptName}} } + {{/scriptName}} {{/services}} {{#coolify}} @@ -218,7 +221,15 @@ export async function configureHAProxy(): Promise { const services = await listServicesWithIncludes(); for (const service of services) { - const { fqdn, id, type, destinationDocker, destinationDockerId, updatedAt } = service; + const { + fqdn, + id, + type, + destinationDocker, + destinationDockerId, + updatedAt, + plausibleAnalytics + } = service; if (destinationDockerId) { const { engine } = destinationDocker; const found = supportedServiceTypesAndVersions.find((a) => a.name === type); @@ -232,6 +243,12 @@ export async function configureHAProxy(): Promise { const isWWW = fqdn.includes('www.'); const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`; if (isRunning) { + // Plausible Analytics custom script + let scriptName = false; + if (type === 'plausibleanalytics' && plausibleAnalytics.scriptName !== 'plausible.js') { + scriptName = plausibleAnalytics.scriptName; + } + data.services.push({ id, port, @@ -241,7 +258,8 @@ export async function configureHAProxy(): Promise { isHttps, redirectValue, redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain, - updatedAt: updatedAt.getTime() + updatedAt: updatedAt.getTime(), + scriptName }); } } diff --git a/src/lib/haproxy/index.ts b/src/lib/haproxy/index.ts index 5b713e343..b9a8e9581 100644 --- a/src/lib/haproxy/index.ts +++ b/src/lib/haproxy/index.ts @@ -3,12 +3,22 @@ import { asyncExecShell, getEngine } from '$lib/common'; import got, { type Got, type Response } from 'got'; import * as db from '$lib/database'; import type { DestinationDocker } from '@prisma/client'; - +import fs from 'fs/promises'; +import yaml from 'js-yaml'; const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555'; export const defaultProxyImage = `coolify-haproxy-alpine:latest`; export const defaultProxyImageTcp = `coolify-haproxy-tcp-alpine:latest`; export const defaultProxyImageHttp = `coolify-haproxy-http-alpine:latest`; +export const defaultTraefikImage = `traefik:v2.6`; + +const mainTraefikEndpoint = dev + ? 'http://host.docker.internal:3000/webhooks/traefik/main.json' + : 'http://coolify:3000/webhooks/traefik/main.json'; + +const otherTraefikEndpoint = dev + ? 'http://host.docker.internal:3000/webhooks/traefik/other.json' + : 'http://coolify:3000/webhooks/traefik/other.json'; export async function haproxyInstance(): Promise { const { proxyPassword } = await db.listSettings(); @@ -98,13 +108,21 @@ export async function checkHAProxy(haproxy?: Got): Promise { } export async function stopTcpHttpProxy( + id: string, destinationDocker: DestinationDocker, - publicPort: number + publicPort: number, + forceName: string = null ): Promise<{ stdout: string; stderr: string } | Error> { const { engine } = destinationDocker; const host = getEngine(engine); - const containerName = `haproxy-for-${publicPort}`; + const settings = await db.listSettings(); + let containerName = `${id}-${publicPort}`; + if (!settings.isTraefikUsed) { + containerName = `haproxy-for-${publicPort}`; + } + if (forceName) containerName = forceName; const found = await checkContainer(engine, containerName); + try { if (found) { return await asyncExecShell( @@ -115,12 +133,76 @@ export async function stopTcpHttpProxy( return error; } } -export async function startTcpProxy( +export async function startTraefikTCPProxy( destinationDocker: DestinationDocker, id: string, publicPort: number, privatePort: number, - volume?: string + type?: string +): Promise<{ stdout: string; stderr: string } | Error> { + const { network, engine } = destinationDocker; + const host = getEngine(engine); + const containerName = `${id}-${publicPort}`; + const found = await checkContainer(engine, containerName, true); + let dependentId = id; + if (type === 'wordpressftp') dependentId = `${id}-ftp`; + const foundDependentContainer = await checkContainer(engine, dependentId, true); + try { + if (foundDependentContainer && !found) { + const { stdout: Config } = await asyncExecShell( + `DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'` + ); + const ip = JSON.parse(Config)[0].Gateway; + const tcpProxy = { + version: '3.5', + services: { + [`${id}-${publicPort}`]: { + container_name: containerName, + image: 'traefik:v2.6', + command: [ + `--entrypoints.tcp.address=:${publicPort}`, + `--providers.http.endpoint=${otherTraefikEndpoint}?id=${id}&privatePort=${privatePort}&publicPort=${publicPort}&type=tcp`, + '--providers.http.pollTimeout=2s', + '--log.level=error' + ], + ports: [`${publicPort}:${publicPort}`], + extra_hosts: ['host.docker.internal:host-gateway', `host.docker.internal:${ip}`], + volumes: ['/var/run/docker.sock:/var/run/docker.sock'], + networks: ['coolify-infra', network] + } + }, + networks: { + [network]: { + external: false, + name: network + }, + 'coolify-infra': { + external: false, + name: 'coolify-infra' + } + } + }; + await fs.writeFile(`/tmp/docker-compose-${id}.yaml`, yaml.dump(tcpProxy)); + await asyncExecShell( + `DOCKER_HOST=${host} docker compose -f /tmp/docker-compose-${id}.yaml up -d` + ); + await fs.rm(`/tmp/docker-compose-${id}.yaml`); + } + if (!foundDependentContainer && found) { + return await asyncExecShell( + `DOCKER_HOST=${host} docker stop -t 0 ${containerName} && docker rm ${containerName}` + ); + } + } catch (error) { + console.log(error); + return error; + } +} +export async function startTcpProxy( + destinationDocker: DestinationDocker, + id: string, + publicPort: number, + privatePort: number ): Promise<{ stdout: string; stderr: string } | Error> { const { network, engine } = destinationDocker; const host = getEngine(engine); @@ -128,7 +210,6 @@ export async function startTcpProxy( const containerName = `haproxy-for-${publicPort}`; const found = await checkContainer(engine, containerName, true); const foundDependentContainer = await checkContainer(engine, id, true); - try { if (foundDependentContainer && !found) { const { stdout: Config } = await asyncExecShell( @@ -136,9 +217,7 @@ export async function startTcpProxy( ); const ip = JSON.parse(Config)[0].Gateway; return await asyncExecShell( - `DOCKER_HOST=${host} docker run --restart always -e PORT=${publicPort} -e APP=${id} -e PRIVATE_PORT=${privatePort} --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' --network ${network} -p ${publicPort}:${publicPort} --name ${containerName} ${ - volume ? `-v ${volume}` : '' - } -d coollabsio/${defaultProxyImageTcp}` + `DOCKER_HOST=${host} docker run --restart always -e PORT=${publicPort} -e APP=${id} -e PRIVATE_PORT=${privatePort} --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' --network ${network} -p ${publicPort}:${publicPort} --name ${containerName} -d coollabsio/${defaultProxyImageTcp}` ); } if (!foundDependentContainer && found) { @@ -151,6 +230,75 @@ export async function startTcpProxy( } } +export async function startTraefikHTTPProxy( + destinationDocker: DestinationDocker, + id: string, + publicPort: number, + privatePort: number +): Promise<{ stdout: string; stderr: string } | Error> { + const { network, engine } = destinationDocker; + const host = getEngine(engine); + + const containerName = `${id}-${publicPort}`; + const found = await checkContainer(engine, containerName, true); + const foundDependentContainer = await checkContainer(engine, id, true); + + try { + if (foundDependentContainer && !found) { + const { stdout: Config } = await asyncExecShell( + `DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'` + ); + const ip = JSON.parse(Config)[0].Gateway; + const tcpProxy = { + version: '3.5', + services: { + [`${id}-${publicPort}`]: { + container_name: containerName, + image: 'traefik:v2.6', + command: [ + `--entrypoints.http.address=:${publicPort}`, + `--providers.http.endpoint=${otherTraefikEndpoint}?id=${id}&privatePort=${privatePort}&publicPort=${publicPort}&type=http`, + '--providers.http.pollTimeout=2s', + '--certificatesresolvers.letsencrypt.acme.httpchallenge=true', + '--certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json', + '--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http', + '--log.level=error' + ], + ports: [`${publicPort}:${publicPort}`], + extra_hosts: ['host.docker.internal:host-gateway', `host.docker.internal:${ip}`], + networks: ['coolify-infra', network], + volumes: ['coolify-traefik-letsencrypt:/etc/traefik/acme'] + } + }, + networks: { + [network]: { + external: false, + name: network + }, + 'coolify-infra': { + external: false, + name: 'coolify-infra' + } + }, + volumes: { + 'coolify-traefik-letsencrypt': {} + } + }; + await fs.writeFile(`/tmp/docker-compose-${id}.yaml`, yaml.dump(tcpProxy)); + await asyncExecShell( + `DOCKER_HOST=${host} docker compose -f /tmp/docker-compose-${id}.yaml up -d` + ); + await fs.rm(`/tmp/docker-compose-${id}.yaml`); + } + if (!foundDependentContainer && found) { + return await asyncExecShell( + `DOCKER_HOST=${host} docker stop -t 0 ${containerName} && docker rm ${containerName}` + ); + } + } catch (error) { + return error; + } +} export async function startHttpProxy( destinationDocker: DestinationDocker, id: string, @@ -197,10 +345,50 @@ export async function startCoolifyProxy(engine: string): Promise { `DOCKER_HOST="${host}" docker run -e HAPROXY_USERNAME=${proxyUser} -e HAPROXY_PASSWORD=${proxyPassword} --restart always --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' -v coolify-ssl-certs:/usr/local/etc/haproxy/ssl --network coolify-infra -p "80:80" -p "443:443" -p "8404:8404" -p "5555:5555" -p "5000:5000" --name coolify-haproxy -d coollabsio/${defaultProxyImage}` ); await db.prisma.setting.update({ where: { id }, data: { proxyHash: null } }); + await db.setDestinationSettings({ engine, isCoolifyProxyUsed: true }); } await configureNetworkCoolifyProxy(engine); } +export async function startTraefikProxy(engine: string): Promise { + const host = getEngine(engine); + const found = await checkContainer(engine, 'coolify-proxy', true); + const { id, proxyPassword, proxyUser } = await db.listSettings(); + if (!found) { + const { stdout: Config } = await asyncExecShell( + `DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'` + ); + const ip = JSON.parse(Config)[0].Gateway; + await asyncExecShell( + `DOCKER_HOST="${host}" docker run --restart always \ + --add-host 'host.docker.internal:host-gateway' \ + --add-host 'host.docker.internal:${ip}' \ + -v coolify-traefik-letsencrypt:/etc/traefik/acme \ + -v /var/run/docker.sock:/var/run/docker.sock \ + --network coolify-infra \ + -p "80:80" \ + -p "443:443" \ + -p "8080:8080" \ + --name coolify-proxy \ + -d ${defaultTraefikImage} \ + --api.insecure=true \ + --entrypoints.web.address=:80 \ + --entrypoints.websecure.address=:443 \ + --providers.docker=true \ + --providers.docker.exposedbydefault=false \ + --providers.http.endpoint=${mainTraefikEndpoint} \ + --providers.http.pollTimeout=5s \ + --certificatesresolvers.letsencrypt.acme.httpchallenge=true \ + --certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json \ + --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web \ + --log.level=error` + ); + await db.prisma.setting.update({ where: { id }, data: { proxyHash: null } }); + await db.setDestinationSettings({ engine, isCoolifyProxyUsed: true }); + } + await configureNetworkTraefikProxy(engine); +} + export async function isContainerExited(engine: string, containerName: string): Promise { let isExited = false; const host = getEngine(engine); @@ -245,6 +433,21 @@ export async function checkContainer( return containerFound; } +export async function getContainerUsage(engine: string, container: string): Promise { + const host = getEngine(engine); + try { + const { stdout } = await asyncExecShell( + `DOCKER_HOST="${host}" docker container stats ${container} --no-stream --no-trunc --format "{{json .}}"` + ); + return JSON.parse(stdout); + } catch (err) { + return { + MemUsage: 0, + CPUPerc: 0, + NetIO: 0 + }; + } +} export async function stopCoolifyProxy( engine: string ): Promise<{ stdout: string; stderr: string } | Error> { @@ -263,6 +466,24 @@ export async function stopCoolifyProxy( return error; } } +export async function stopTraefikProxy( + engine: string +): Promise<{ stdout: string; stderr: string } | Error> { + const host = getEngine(engine); + const found = await checkContainer(engine, 'coolify-proxy'); + await db.setDestinationSettings({ engine, isCoolifyProxyUsed: false }); + const { id } = await db.prisma.setting.findFirst({}); + await db.prisma.setting.update({ where: { id }, data: { proxyHash: null } }); + try { + if (found) { + await asyncExecShell( + `DOCKER_HOST="${host}" docker stop -t 0 coolify-proxy && docker rm coolify-proxy` + ); + } + } catch (error) { + return error; + } +} export async function configureNetworkCoolifyProxy(engine: string): Promise { const host = getEngine(engine); @@ -279,3 +500,19 @@ export async function configureNetworkCoolifyProxy(engine: string): Promise { + const host = getEngine(engine); + const destinations = await db.prisma.destinationDocker.findMany({ where: { engine } }); + const { stdout: networks } = await asyncExecShell( + `DOCKER_HOST="${host}" docker ps -a --filter name=coolify-proxy --format '{{json .Networks}}'` + ); + const configuredNetworks = networks.replace(/"/g, '').replace('\n', '').split(','); + for (const destination of destinations) { + if (!configuredNetworks.includes(destination.network)) { + await asyncExecShell( + `DOCKER_HOST="${host}" docker network connect ${destination.network} coolify-proxy` + ); + } + } +} diff --git a/src/lib/letsencrypt/index.ts b/src/lib/letsencrypt/index.ts index f443625ae..0f2a2b1d6 100644 --- a/src/lib/letsencrypt/index.ts +++ b/src/lib/letsencrypt/index.ts @@ -290,3 +290,30 @@ export async function generateSSLCerts(): Promise { } } } + +export async function renewSSLCerts(): Promise { + if (!dev) { + const host = 'unix:///var/run/docker.sock'; + await asyncExecShell(`docker pull alpine:latest`); + const certbotImage = + process.arch === 'x64' ? 'certbot/certbot' : 'certbot/certbot:arm64v8-latest'; + + const { stdout: certificates } = await asyncExecShell( + `DOCKER_HOST=${host} docker run --rm -v "coolify-letsencrypt:/etc/letsencrypt" -v "coolify-ssl-certs:/app/ssl" alpine:latest sh -c "ls -1 /etc/letsencrypt/live/ | grep -v README"` + ); + + for (const certificate of certificates.trim().split('\n')) { + try { + await asyncExecShell( + `DOCKER_HOST=${host} docker run --rm --name certbot-renewal -p 9080:9080 -v "coolify-letsencrypt:/etc/letsencrypt" ${certbotImage} --cert-name ${certificate} --logs-dir /etc/letsencrypt/logs renew --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port 9080` + ); + await asyncExecShell( + `DOCKER_HOST=${host} docker run --rm -v "coolify-letsencrypt:/etc/letsencrypt" -v "coolify-ssl-certs:/app/ssl" alpine:latest sh -c "test -d /etc/letsencrypt/live/${certificate}/ && cat /etc/letsencrypt/live/${certificate}/fullchain.pem /etc/letsencrypt/live/${certificate}/privkey.pem > /app/ssl/${certificate}.pem"` + ); + } catch (error) { + console.log(error); + } + } + await reloadHaproxy('unix:///var/run/docker.sock'); + } +} diff --git a/src/lib/locales/en.json b/src/lib/locales/en.json index a7adfcef0..c455523af 100644 --- a/src/lib/locales/en.json +++ b/src/lib/locales/en.json @@ -178,13 +178,15 @@ "delete_application": "Delete application", "permission_denied_delete_application": "You do not have permission to delete this application", "domain_already_in_use": "Domain {{domain}} is already used.", - "dns_not_set_error": "DNS not set or propogated for {{domain}}.

Please check your DNS settings.", + "dns_not_set_error": "DNS not set correctly or propogated for {{domain}}.

Please check your DNS settings.", + "domain_required": "Domain is required.", "settings_saved": "Settings saved.", "dns_not_set_partial_error": "DNS not set", + "domain_not_valid": "Could not resolve domain or it's not pointing to the server IP address.

Please check your DNS configuration and try again.", "git_source": "Git Source", "git_repository": "Git Repository", "build_pack": "Build Pack", - "base_image": "Deplyoment Image", + "base_image": "Deployment Image", "base_image_explainer": "Image that will be used for the deployment.", "base_build_image": "Build Image", "base_build_image_explainer": "Image that will be used during the build process.", @@ -204,6 +206,7 @@ "enable_automatic_deployment": "Enable Automatic Deployment", "enable_auto_deploy_webhooks": "Enable automatic deployment through webhooks.", "enable_mr_pr_previews": "Enable MR/PR Previews", + "expose_a_port": "Expose a port", "enable_preview_deploy_mr_pr_requests": "Enable preview deployments from pull or merge requests.", "debug_logs": "Debug Logs", "enable_debug_log_during_build": "Enable debug logs during build phase.
Sensitive information could be visible and saved in logs.", @@ -311,7 +314,7 @@ "credential_stat_explainer": "Credentials for stats page.", "auto_update_enabled": "Auto update enabled?", "auto_update_enabled_explainer": "Enable automatic updates for Coolify. It will be done automatically behind the scenes, if there is no build process running.", - "generate_www_non_www_ssl": "It will generate certificates for both www and non-www.
You need to have both DNS entries set in advance.

Service needs to be restarted.", + "generate_www_non_www_ssl": "It will generate certificates for both www and non-www.
You need to have both DNS entries set in advance.", "is_dns_check_enabled": "DNS check enabled?", "is_dns_check_enabled_explainer": "You can disable DNS check before creating SSL certificates.

Turning it off is useful when Coolify is behind a reverse proxy or tunnel." }, diff --git a/src/lib/locales/fr.json b/src/lib/locales/fr.json index 36f10dfb4..242ac6fce 100644 --- a/src/lib/locales/fr.json +++ b/src/lib/locales/fr.json @@ -61,6 +61,7 @@ "enable_debug_log_during_build": "Activez les journaux de débogage pendant la phase de build.
Les informations sensibles peuvent être visibles et enregistrées dans les journaux.", "enable_mr_pr_previews": "Activer les aperçus MR/PR", "enable_preview_deploy_mr_pr_requests": "Activez les déploiements de prévisualisation à partir de demandes d'extraction ou de fusion.", + "expose_a_port": "Exposer un port", "features": "Caractéristiques", "git_repository": "Dépôt Git", "git_source": "Source Git", diff --git a/src/lib/queues/builder.ts b/src/lib/queues/builder.ts index 3a74320ef..c69ffb678 100644 --- a/src/lib/queues/builder.ts +++ b/src/lib/queues/builder.ts @@ -48,6 +48,7 @@ export default async function (job: Job): Promise): Promise): Promise): Promise): Promise): Promise { } catch (error) { console.log(error); } - console.log(`Is LowDiskSpace detected? ${lowDiskSpace}`); if (lowDiskSpace) { // Cleanup old coolify images try { diff --git a/src/lib/queues/index.ts b/src/lib/queues/index.ts index 60097680d..b35a058d7 100644 --- a/src/lib/queues/index.ts +++ b/src/lib/queues/index.ts @@ -116,8 +116,8 @@ const cron = async (): Promise => { await queue.proxyTcpHttp.add('proxyTcpHttp', {}, { repeat: { every: 10000 } }); await queue.ssl.add('ssl', {}, { repeat: { every: dev ? 10000 : 60000 } }); if (!dev) await queue.cleanup.add('cleanup', {}, { repeat: { every: 300000 } }); - await queue.sslRenew.add('sslRenew', {}, { repeat: { every: 1800000 } }); - await queue.autoUpdater.add('autoUpdater', {}, { repeat: { every: 60000 } }); + if (!dev) await queue.sslRenew.add('sslRenew', {}, { repeat: { every: 1800000 } }); + if (!dev) await queue.autoUpdater.add('autoUpdater', {}, { repeat: { every: 60000 } }); }; cron().catch((error) => { console.log('cron failed to start'); diff --git a/src/lib/queues/proxy.ts b/src/lib/queues/proxy.ts index db8742e74..245fb3b02 100644 --- a/src/lib/queues/proxy.ts +++ b/src/lib/queues/proxy.ts @@ -1,4 +1,4 @@ -import { ErrorHandler } from '$lib/database'; +import { ErrorHandler, prisma } from '$lib/database'; import { configureHAProxy } from '$lib/haproxy/configuration'; export default async function (): Promise { try { - return await configureHAProxy(); + const settings = await prisma.setting.findFirst(); + if (!settings.isTraefikUsed) { + return await configureHAProxy(); + } } catch (error) { return ErrorHandler(error.response?.body || error); } diff --git a/src/lib/queues/proxyTcpHttp.ts b/src/lib/queues/proxyTcpHttp.ts index c268d2aa0..9ce239b66 100644 --- a/src/lib/queues/proxyTcpHttp.ts +++ b/src/lib/queues/proxyTcpHttp.ts @@ -1,5 +1,16 @@ import { ErrorHandler, generateDatabaseConfiguration, prisma } from '$lib/database'; -import { startCoolifyProxy, startHttpProxy, startTcpProxy } from '$lib/haproxy'; +import { + checkContainer, + startCoolifyProxy, + startHttpProxy, + startTcpProxy, + startTraefikHTTPProxy, + startTraefikProxy, + startTraefikTCPProxy, + stopCoolifyProxy, + stopTcpHttpProxy, + stopTraefikProxy +} from '$lib/haproxy'; export default async function (): Promise { try { // Coolify Proxy + const engine = '/var/run/docker.sock'; + const settings = await prisma.setting.findFirst(); const localDocker = await prisma.destinationDocker.findFirst({ - where: { engine: '/var/run/docker.sock' } + where: { engine, network: 'coolify' } }); if (localDocker && localDocker.isCoolifyProxyUsed) { - await startCoolifyProxy('/var/run/docker.sock'); + if (settings.isTraefikUsed) { + const found = await checkContainer(engine, 'coolify-haproxy'); + if (found) await stopCoolifyProxy(engine); + await startTraefikProxy(engine); + } else { + const found = await checkContainer(engine, 'coolify-proxy'); + if (found) await stopTraefikProxy(engine); + await startCoolifyProxy(engine); + } } + // TCP Proxies const databasesWithPublicPort = await prisma.database.findMany({ where: { publicPort: { not: null } }, @@ -21,8 +43,16 @@ export default async function (): Promise { try { - return await generateSSLCerts(); + const settings = await prisma.setting.findFirst(); + if (!settings.isTraefikUsed) { + return await generateSSLCerts(); + } } catch (error) { console.log(error); throw error; diff --git a/src/lib/queues/sslrenewal.ts b/src/lib/queues/sslrenewal.ts index 4af5bae64..0c14b2dc9 100644 --- a/src/lib/queues/sslrenewal.ts +++ b/src/lib/queues/sslrenewal.ts @@ -1,9 +1,14 @@ -import { asyncExecShell } from '$lib/common'; -import { reloadHaproxy } from '$lib/haproxy'; +import { renewSSLCerts } from '$lib/letsencrypt'; +import { prisma } from '$lib/database'; export default async function (): Promise { - await asyncExecShell( - `docker run --rm --name certbot-renewal -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs renew` - ); - await reloadHaproxy('unix:///var/run/docker.sock'); + try { + const settings = await prisma.setting.findFirst(); + if (!settings.isTraefikUsed) { + return await renewSSLCerts(); + } + } catch (error) { + console.log(error); + throw error; + } } diff --git a/src/lib/realtime.ts b/src/lib/realtime.ts new file mode 100644 index 000000000..419bb154b --- /dev/null +++ b/src/lib/realtime.ts @@ -0,0 +1,3 @@ +// import ioClient from 'socket.io-client'; +// const socket = ioClient('http://localhost:3000'); +// export const io = socket; diff --git a/src/lib/store.ts b/src/lib/store.ts index 684922bfb..9366d70b2 100644 --- a/src/lib/store.ts +++ b/src/lib/store.ts @@ -12,3 +12,14 @@ export const features: Readable<{ latestVersion: string; beta: boolean }> = read beta: browser && window.localStorage.getItem('beta') === 'true', latestVersion: browser && window.localStorage.getItem('latestVersion') }); + +export const isTraefikUsed: Writable = writable(false); + +export const status: Writable = writable({ + application: { + isRunning: false, + isExited: false, + loading: false, + initialLoading: true + } +}); diff --git a/src/lib/types/builderJob.ts b/src/lib/types/builderJob.ts index 145f7cc46..968e2c897 100644 --- a/src/lib/types/builderJob.ts +++ b/src/lib/types/builderJob.ts @@ -12,6 +12,7 @@ export type BuilderJob = { buildPack: BuildPackName; projectId: number; port: number; + exposePort?: number; installCommand: string; buildCommand?: string; startCommand?: string; diff --git a/src/lib/types/composeFile.ts b/src/lib/types/composeFile.ts index 33dfbdd43..724a53b69 100644 --- a/src/lib/types/composeFile.ts +++ b/src/lib/types/composeFile.ts @@ -18,6 +18,7 @@ export type ComposeFileService = { restart: ComposeFileRestartOption; depends_on?: string[]; command?: string; + ports?: string[]; build?: { context: string; dockerfile: string; diff --git a/src/routes/_Trend.svelte b/src/routes/_Trend.svelte new file mode 100644 index 000000000..723ba2a39 --- /dev/null +++ b/src/routes/_Trend.svelte @@ -0,0 +1,39 @@ + + +{#if trend === 'up'} + + + + + + +{:else if trend === 'down'} + + + + + + + +{/if} diff --git a/src/routes/__layout.svelte b/src/routes/__layout.svelte index 5952073e0..492673a28 100644 --- a/src/routes/__layout.svelte +++ b/src/routes/__layout.svelte @@ -34,23 +34,30 @@ @@ -153,16 +162,16 @@ {#if loading} {:else} - {#if isExited} + {#if $status.application.isExited} {/if} - {#if isRunning} + {#if $status.application.initialLoading} + + {:else if $status.application.isRunning}
-
- -
- +
+
- - + {/if} {#if application.buildCommand || application.buildPack === 'rust' || application.buildPack === 'laravel'}
!isRunning && changeSettings('dualCerts')} + on:click={() => !$status.application.isRunning && changeSettings('dualCerts')} />
{#if application.buildPack === 'python'} @@ -451,9 +573,24 @@ /> {/if} - - {#if !notNodeDeployments.includes(application.buildPack)} + {#if application.buildPack !== 'docker'}
+ + +
Useful if you would like to use your own reverse proxy or tunnel and also in development mode. Otherwise leave empty.'} + /> +
+ {/if} + {#if !notNodeDeployments.includes(application.buildPack)} +
@@ -491,7 +628,7 @@
{/if} {#if application.buildPack === 'docker'} -
+
diff --git a/src/routes/applications/[id]/logs/_Loading.svelte b/src/routes/applications/[id]/logs/_Loading.svelte index 73fbe709e..7add70c19 100644 --- a/src/routes/applications/[id]/logs/_Loading.svelte +++ b/src/routes/applications/[id]/logs/_Loading.svelte @@ -1,4 +1,8 @@ -
+ + +
diff --git a/src/routes/applications/[id]/logs/build/_BuildLog.svelte b/src/routes/applications/[id]/logs/build/_BuildLog.svelte index 378e58c2c..b95e295f1 100644 --- a/src/routes/applications/[id]/logs/build/_BuildLog.svelte +++ b/src/routes/applications/[id]/logs/build/_BuildLog.svelte @@ -129,23 +129,28 @@ {#if currentStatus === 'running'} {/if}
diff --git a/src/routes/applications/[id]/previews/index.json.ts b/src/routes/applications/[id]/previews/index.json.ts index ab10a06b8..d92690671 100644 --- a/src/routes/applications/[id]/previews/index.json.ts +++ b/src/routes/applications/[id]/previews/index.json.ts @@ -17,7 +17,7 @@ export const get: RequestHandler = async (event) => { const destinationDocker = await db.getDestinationByApplicationId({ id, teamId }); const docker = dockerInstance({ destinationDocker }); const listContainers = await docker.engine.listContainers({ - filters: { network: [destinationDocker.network] } + filters: { network: [destinationDocker.network], name: [id] } }); const containers = listContainers.filter((container) => { return ( @@ -30,11 +30,7 @@ export const get: RequestHandler = async (event) => { JSON.parse(Buffer.from(container.Labels['coolify.configuration'], 'base64').toString()) ) .filter((container) => { - return ( - container.type !== 'manual' && - container.type !== 'webhook_commit' && - container.applicationId === id - ); + return container.pullmergeRequestId && container.applicationId === id; }); return { body: { diff --git a/src/routes/applications/[id]/previews/index.svelte b/src/routes/applications/[id]/previews/index.svelte index 152aa00d2..f343c17df 100644 --- a/src/routes/applications/[id]/previews/index.svelte +++ b/src/routes/applications/[id]/previews/index.svelte @@ -31,6 +31,7 @@ import { errorNotification } from '$lib/form'; import { toast } from '@zerodevx/svelte-toast'; import { t } from '$lib/translations'; + import { goto } from '$app/navigation'; const { id } = $page.params; async function refreshSecrets() { @@ -39,11 +40,18 @@ } async function redeploy(container) { try { - await post(`/applications/${id}/deploy.json`, { + const { buildId } = await post(`/applications/${id}/deploy.json`, { pullmergeRequestId: container.pullmergeRequestId, branch: container.branch }); toast.push('Application redeployed queued.'); + if ($page.url.pathname.startsWith(`/applications/${id}/logs/build`)) { + return window.location.assign(`/applications/${id}/logs/build?buildId=${buildId}`); + } else { + return await goto(`/applications/${id}/logs/build?buildId=${buildId}`, { + replaceState: true + }); + } } catch ({ error }) { return errorNotification(error); } diff --git a/src/routes/applications/[id]/status.json.ts b/src/routes/applications/[id]/status.json.ts new file mode 100644 index 000000000..27c5df3d7 --- /dev/null +++ b/src/routes/applications/[id]/status.json.ts @@ -0,0 +1,36 @@ +import { asyncExecShell, getUserDetails } from '$lib/common'; +import * as db from '$lib/database'; +import { ErrorHandler } from '$lib/database'; +import { checkContainer, getContainerUsage, isContainerExited } from '$lib/haproxy'; +import type { RequestHandler } from '@sveltejs/kit'; +import { setDefaultConfiguration } from '$lib/buildPacks/common'; + +export const get: RequestHandler = async (event) => { + const { teamId, status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + + const { id } = event.params; + + let isRunning = false; + let isExited = false; + try { + const application = await db.getApplication({ id, teamId }); + if (application.destinationDockerId) { + [isRunning, isExited] = await Promise.all([ + checkContainer(application.destinationDocker.engine, id), + isContainerExited(application.destinationDocker.engine, id) + ]); + } + return { + status: 200, + body: { + isRunning, + isExited + }, + headers: {} + }; + } catch (error) { + console.log(error); + return ErrorHandler(error); + } +}; diff --git a/src/routes/applications/[id]/usage.json.ts b/src/routes/applications/[id]/usage.json.ts new file mode 100644 index 000000000..ab161d81e --- /dev/null +++ b/src/routes/applications/[id]/usage.json.ts @@ -0,0 +1,30 @@ +import { getUserDetails } from '$lib/common'; +import * as db from '$lib/database'; +import { ErrorHandler } from '$lib/database'; +import { getContainerUsage } from '$lib/haproxy'; +import type { RequestHandler } from '@sveltejs/kit'; + +export const get: RequestHandler = async (event) => { + const { teamId, status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + + const { id } = event.params; + + let usage = {}; + try { + const application = await db.getApplication({ id, teamId }); + if (application.destinationDockerId) { + [usage] = await Promise.all([getContainerUsage(application.destinationDocker.engine, id)]); + } + return { + status: 200, + body: { + usage + }, + headers: {} + }; + } catch (error) { + console.log(error); + return ErrorHandler(error); + } +}; diff --git a/src/routes/applications/index.svelte b/src/routes/applications/index.svelte index 07e4564c5..1eed20ad9 100644 --- a/src/routes/applications/index.svelte +++ b/src/routes/applications/index.svelte @@ -60,7 +60,7 @@
{/if}
-
+
{#if !applications || ownApplications.length === 0}
{$t('application.no_applications_found')}
diff --git a/src/routes/dashboard.json.ts b/src/routes/dashboard.json.ts index ff01dbfd8..1b9528861 100644 --- a/src/routes/dashboard.json.ts +++ b/src/routes/dashboard.json.ts @@ -2,45 +2,69 @@ import { getUserDetails } from '$lib/common'; import * as db from '$lib/database'; import { ErrorHandler } from '$lib/database'; import type { RequestHandler } from '@sveltejs/kit'; +import os from 'node:os'; +import osu from 'node-os-utils'; export const get: RequestHandler = async (event) => { const { userId, teamId, status, body } = await getUserDetails(event); if (status === 401) return { status, body }; - - try { - const applicationsCount = await db.prisma.application.count({ - where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } } - }); - const sourcesCount = await db.prisma.gitSource.count({ - where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } } - }); - const destinationsCount = await db.prisma.destinationDocker.count({ - where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } } - }); - const teamsCount = await db.prisma.permission.count({ where: { userId } }); - const databasesCount = await db.prisma.database.count({ - where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } } - }); - const servicesCount = await db.prisma.service.count({ - where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } } - }); - const teams = await db.prisma.permission.findMany({ - where: { userId }, - include: { team: { include: { _count: { select: { users: true } } } } } - }); - return { - body: { - teams, - applicationsCount, - sourcesCount, - destinationsCount, - teamsCount, - databasesCount, - servicesCount - } - }; - } catch (error) { - return ErrorHandler(error); + const usage = event.url.searchParams.get('usage'); + if (usage) { + try { + return { + status: 200, + body: { + uptime: os.uptime(), + memory: await osu.mem.info(), + cpu: { + load: os.loadavg(), + usage: await osu.cpu.usage(), + count: os.cpus().length + }, + disk: await osu.drive.info() + } + }; + } catch (error) { + return ErrorHandler(error); + } + } else { + try { + const applicationsCount = await db.prisma.application.count({ + where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } } + }); + const sourcesCount = await db.prisma.gitSource.count({ + where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } } + }); + const destinationsCount = await db.prisma.destinationDocker.count({ + where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } } + }); + const teamsCount = await db.prisma.permission.count({ where: { userId } }); + const databasesCount = await db.prisma.database.count({ + where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } } + }); + const servicesCount = await db.prisma.service.count({ + where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } } + }); + const teams = await db.prisma.permission.findMany({ + where: { userId }, + include: { team: { include: { _count: { select: { users: true } } } } } + }); + const settings = await db.prisma.setting.findFirst(); + return { + body: { + teams, + applicationsCount, + sourcesCount, + destinationsCount, + teamsCount, + databasesCount, + servicesCount, + settings + } + }; + } catch (error) { + return ErrorHandler(error); + } } }; diff --git a/src/routes/databases/[id]/_Databases/_Databases.svelte b/src/routes/databases/[id]/_Databases/_Databases.svelte index 1034a3543..b40cfb345 100644 --- a/src/routes/databases/[id]/_Databases/_Databases.svelte +++ b/src/routes/databases/[id]/_Databases/_Databases.svelte @@ -11,6 +11,7 @@ import MySql from './_MySQL.svelte'; import MongoDb from './_MongoDB.svelte'; + import MariaDb from './_MariaDB.svelte'; import PostgreSql from './_PostgreSQL.svelte'; import Redis from './_Redis.svelte'; import CouchDb from './_CouchDb.svelte'; @@ -190,6 +191,8 @@ {:else if database.type === 'mongodb'} + {:else if database.type === 'mariadb'} + {:else if database.type === 'redis'} {:else if database.type === 'couchdb'} diff --git a/src/routes/databases/[id]/_Databases/_MariaDB.svelte b/src/routes/databases/[id]/_Databases/_MariaDB.svelte new file mode 100644 index 000000000..3bb135421 --- /dev/null +++ b/src/routes/databases/[id]/_Databases/_MariaDB.svelte @@ -0,0 +1,79 @@ + + +
+
MariaDB
+
+
+
+ + +
+
+ + +
+
+ + + +
+
+ + +
+
+ + + +
+
diff --git a/src/routes/databases/[id]/configuration/type.svelte b/src/routes/databases/[id]/configuration/type.svelte index a73163058..fadba1365 100644 --- a/src/routes/databases/[id]/configuration/type.svelte +++ b/src/routes/databases/[id]/configuration/type.svelte @@ -37,6 +37,7 @@ import Clickhouse from '$lib/components/svg/databases/Clickhouse.svelte'; import CouchDB from '$lib/components/svg/databases/CouchDB.svelte'; import MongoDB from '$lib/components/svg/databases/MongoDB.svelte'; + import MariaDB from '$lib/components/svg/databases/MariaDB.svelte'; import MySQL from '$lib/components/svg/databases/MySQL.svelte'; import PostgreSQL from '$lib/components/svg/databases/PostgreSQL.svelte'; import Redis from '$lib/components/svg/databases/Redis.svelte'; @@ -68,6 +69,8 @@ {:else if type.name === 'mongodb'} + {:else if type.name === 'mariadb'} + {:else if type.name === 'mysql'} {:else if type.name === 'postgresql'} diff --git a/src/routes/databases/[id]/index.svelte b/src/routes/databases/[id]/index.svelte index 3ead53529..c9eb7a4ff 100644 --- a/src/routes/databases/[id]/index.svelte +++ b/src/routes/databases/[id]/index.svelte @@ -33,10 +33,40 @@
@@ -49,4 +79,31 @@
+
+
Database Usage
+
+
+
+
Used Memory / Memory Limit
+
+ {usage?.MemUsage} +
+
+ +
+
Used CPU
+
+ {usage?.CPUPerc} +
+
+ +
+
Network IO
+
+ {usage?.NetIO} +
+
+
+
+
diff --git a/src/routes/databases/[id]/settings.json.ts b/src/routes/databases/[id]/settings.json.ts index 1f09946c6..e4263ce9a 100644 --- a/src/routes/databases/[id]/settings.json.ts +++ b/src/routes/databases/[id]/settings.json.ts @@ -1,7 +1,7 @@ import { getUserDetails } from '$lib/common'; import * as db from '$lib/database'; import { generateDatabaseConfiguration, ErrorHandler, getFreePort } from '$lib/database'; -import { startTcpProxy, stopTcpHttpProxy } from '$lib/haproxy'; +import { startTcpProxy, startTraefikTCPProxy, stopTcpHttpProxy } from '$lib/haproxy'; import type { RequestHandler } from '@sveltejs/kit'; export const post: RequestHandler = async (event) => { @@ -13,6 +13,7 @@ export const post: RequestHandler = async (event) => { const publicPort = await getFreePort(); try { + const settings = await db.listSettings(); await db.setDatabase({ id, isPublic, appendOnly }); const database = await db.getDatabase({ id, teamId }); const { destinationDockerId, destinationDocker, publicPort: oldPublicPort } = database; @@ -21,7 +22,11 @@ export const post: RequestHandler = async (event) => { if (destinationDockerId) { if (isPublic) { await db.prisma.database.update({ where: { id }, data: { publicPort } }); - await startTcpProxy(destinationDocker, id, publicPort, privatePort); + if (settings.isTraefikUsed) { + await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort); + } else { + await startTcpProxy(destinationDocker, id, publicPort, privatePort); + } } else { await db.prisma.database.update({ where: { id }, data: { publicPort: null } }); await stopTcpHttpProxy(destinationDocker, oldPublicPort); diff --git a/src/routes/databases/[id]/usage.json.ts b/src/routes/databases/[id]/usage.json.ts new file mode 100644 index 000000000..9ec987310 --- /dev/null +++ b/src/routes/databases/[id]/usage.json.ts @@ -0,0 +1,30 @@ +import { getUserDetails } from '$lib/common'; +import * as db from '$lib/database'; +import { ErrorHandler } from '$lib/database'; +import { getContainerUsage } from '$lib/haproxy'; +import type { RequestHandler } from '@sveltejs/kit'; + +export const get: RequestHandler = async (event) => { + const { teamId, status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + + const { id } = event.params; + + let usage = {}; + try { + const database = await db.getDatabase({ id, teamId }); + if (database.destinationDockerId) { + [usage] = await Promise.all([getContainerUsage(database.destinationDocker.engine, id)]); + } + return { + status: 200, + body: { + usage + }, + headers: {} + }; + } catch (error) { + console.log(error); + return ErrorHandler(error); + } +}; diff --git a/src/routes/databases/index.svelte b/src/routes/databases/index.svelte index 6717ac88a..7939910ae 100644 --- a/src/routes/databases/index.svelte +++ b/src/routes/databases/index.svelte @@ -3,6 +3,7 @@ import Clickhouse from '$lib/components/svg/databases/Clickhouse.svelte'; import CouchDB from '$lib/components/svg/databases/CouchDB.svelte'; import MongoDB from '$lib/components/svg/databases/MongoDB.svelte'; + import MariaDB from '$lib/components/svg/databases/MariaDB.svelte'; import MySQL from '$lib/components/svg/databases/MySQL.svelte'; import PostgreSQL from '$lib/components/svg/databases/PostgreSQL.svelte'; import Redis from '$lib/components/svg/databases/Redis.svelte'; @@ -46,7 +47,7 @@
-
+
{#if !databases || ownDatabases.length === 0}
{$t('database.no_databases_found')}
@@ -66,6 +67,8 @@ {:else if database.type === 'mysql'} + {:else if database.type === 'mariadb'} + {:else if database.type === 'postgresql'} {:else if database.type === 'redis'} @@ -98,6 +101,8 @@ {:else if database.type === 'mongodb'} + {:else if database.type === 'mariadb'} + {:else if database.type === 'mysql'} {:else if database.type === 'postgresql'} diff --git a/src/routes/destinations/[id]/index.json.ts b/src/routes/destinations/[id]/index.json.ts index aa9be6aea..8c4c67cee 100644 --- a/src/routes/destinations/[id]/index.json.ts +++ b/src/routes/destinations/[id]/index.json.ts @@ -26,8 +26,12 @@ export const get: RequestHandler = async (event) => { // // await saveSshKey(destination); // payload.state = await checkContainer(engine, 'coolify-haproxy'); } else { + let containerName = 'coolify-proxy'; + if (!settings.isTraefikUsed) { + containerName = 'coolify-haproxy'; + } payload.state = - destination?.engine && (await checkContainer(destination.engine, 'coolify-haproxy')); + destination?.engine && (await checkContainer(destination.engine, containerName)); } return { status: 200, diff --git a/src/routes/destinations/[id]/index.svelte b/src/routes/destinations/[id]/index.svelte index 1effefdd7..dabc3c88f 100644 --- a/src/routes/destinations/[id]/index.svelte +++ b/src/routes/destinations/[id]/index.svelte @@ -39,10 +39,13 @@ import { t } from '$lib/translations'; -
-
{$t('application.destination')}
- > - {destination.name} +
+
+
+ Configuration +
+ {destination.name} +
diff --git a/src/routes/destinations/[id]/restart.json.ts b/src/routes/destinations/[id]/restart.json.ts index 9d2da459a..c2cf88fa4 100644 --- a/src/routes/destinations/[id]/restart.json.ts +++ b/src/routes/destinations/[id]/restart.json.ts @@ -1,7 +1,12 @@ import { getUserDetails } from '$lib/common'; import { ErrorHandler } from '$lib/database'; import * as db from '$lib/database'; -import { startCoolifyProxy, stopCoolifyProxy } from '$lib/haproxy'; +import { + startCoolifyProxy, + startTraefikProxy, + stopCoolifyProxy, + stopTraefikProxy +} from '$lib/haproxy'; import type { RequestHandler } from '@sveltejs/kit'; export const post: RequestHandler = async (event) => { @@ -11,9 +16,16 @@ export const post: RequestHandler = async (event) => { const { engine } = await event.request.json(); try { - await stopCoolifyProxy(engine); - await startCoolifyProxy(engine); + const settings = await db.prisma.setting.findFirst({}); + if (settings?.isTraefikUsed) { + await stopTraefikProxy(engine); + await startTraefikProxy(engine); + } else { + await stopCoolifyProxy(engine); + await startCoolifyProxy(engine); + } await db.setDestinationSettings({ engine, isCoolifyProxyUsed: true }); + return { status: 200 }; diff --git a/src/routes/destinations/[id]/start.json.ts b/src/routes/destinations/[id]/start.json.ts index f5272418a..865187542 100644 --- a/src/routes/destinations/[id]/start.json.ts +++ b/src/routes/destinations/[id]/start.json.ts @@ -1,6 +1,12 @@ import { getUserDetails } from '$lib/common'; import { ErrorHandler } from '$lib/database'; -import { startCoolifyProxy, stopCoolifyProxy } from '$lib/haproxy'; +import * as db from '$lib/database'; +import { + startCoolifyProxy, + startTraefikProxy, + stopCoolifyProxy, + stopTraefikProxy +} from '$lib/haproxy'; import type { RequestHandler } from '@sveltejs/kit'; export const post: RequestHandler = async (event) => { @@ -8,14 +14,24 @@ export const post: RequestHandler = async (event) => { if (status === 401) return { status, body }; const { engine } = await event.request.json(); + const settings = await db.prisma.setting.findFirst({}); try { - await startCoolifyProxy(engine); + if (settings?.isTraefikUsed) { + await startTraefikProxy(engine); + } else { + await startCoolifyProxy(engine); + } + return { status: 200 }; } catch (error) { - await stopCoolifyProxy(engine); + if (settings?.isTraefikUsed) { + await stopTraefikProxy(engine); + } else { + await stopCoolifyProxy(engine); + } return ErrorHandler(error); } }; diff --git a/src/routes/destinations/[id]/stop.json.ts b/src/routes/destinations/[id]/stop.json.ts index 495c7612e..a1e01d218 100644 --- a/src/routes/destinations/[id]/stop.json.ts +++ b/src/routes/destinations/[id]/stop.json.ts @@ -1,6 +1,7 @@ import { getUserDetails } from '$lib/common'; import { ErrorHandler } from '$lib/database'; -import { stopCoolifyProxy } from '$lib/haproxy'; +import * as db from '$lib/database'; +import { stopCoolifyProxy, stopTraefikProxy } from '$lib/haproxy'; import type { RequestHandler } from '@sveltejs/kit'; export const post: RequestHandler = async (event) => { @@ -9,7 +10,13 @@ export const post: RequestHandler = async (event) => { const { engine } = await event.request.json(); try { - await stopCoolifyProxy(engine); + const settings = await db.prisma.setting.findFirst({}); + if (settings?.isTraefikUsed) { + await stopTraefikProxy(engine); + } else { + await stopCoolifyProxy(engine); + } + return { status: 200 }; diff --git a/src/routes/destinations/index.svelte b/src/routes/destinations/index.svelte index 2254cfe5e..1313cb1b6 100644 --- a/src/routes/destinations/index.svelte +++ b/src/routes/destinations/index.svelte @@ -58,7 +58,7 @@ {/if}
-
+
{#if !destinations || ownDestinations.length === 0}
{$t('destination.no_destination_found')}
diff --git a/src/routes/index.svelte b/src/routes/index.svelte index 1eaabbd5c..858a0387b 100644 --- a/src/routes/index.svelte +++ b/src/routes/index.svelte @@ -21,6 +21,10 @@
{$t('index.dashboard')}
-
-
-
-
- +
+
Used Memory
+
+ {(usage?.memory.usedMemMb).toFixed(0)}MB +
+
+ +
+
Free Memory
+
+ {usage?.memory.freeMemPercentage}% + {#if !memoryWarning} + + {/if} +
+
+ +
+
+
Total CPUs
+
+ {usage?.cpu.count} +
+
+
+
CPU Usage
+
+ {usage?.cpu.usage}% + {#if !cpuWarning} + + {/if} +
+
+
+
Load Average (5/10/30mins)
+
+ {usage?.cpu.load.join('/')} +
+
+
+
+
+
Total Disk
+
+ {usage?.disk.totalGb}GB +
+
+
+
Used Disk
+
+ {usage?.disk.usedGb}GB +
+
+
+
Free Disk
+
+ {usage?.disk.freePercentage}% + {#if !diskWarning} + + {/if} +
+
+
+
Resources
+ {/if} +
+ +
{$t('index.applications')}
+
+ {applicationsCount} +
+
+ +
{$t('index.destinations')}
+
+ {destinationsCount} +
+
+ + +
{$t('index.git_sources')}
+
+ {sourcesCount} +
+
+
+
+ +
{$t('index.databases')}
+
+ {databasesCount} +
+
+ + +
{$t('index.services')}
+
+ {servicesCount} +
+
+ + +
{$t('index.teams')}
+
+ {teamsCount} +
+
+
diff --git a/src/routes/services/[id]/_Services/_MinIO.svelte b/src/routes/services/[id]/_Services/_MinIO.svelte index 8610b0025..10d36a252 100644 --- a/src/routes/services/[id]/_Services/_MinIO.svelte +++ b/src/routes/services/[id]/_Services/_MinIO.svelte @@ -30,14 +30,16 @@ value={service.minio.rootUserPassword} />
-
- - -
+{#if !service.minio.apiFqdn} +
+ + +
+{/if} diff --git a/src/routes/services/[id]/_Services/_PlausibleAnalytics.svelte b/src/routes/services/[id]/_Services/_PlausibleAnalytics.svelte index 26b11c6f7..c348f123e 100644 --- a/src/routes/services/[id]/_Services/_PlausibleAnalytics.svelte +++ b/src/routes/services/[id]/_Services/_PlausibleAnalytics.svelte @@ -1,13 +1,32 @@
Plausible Analytics
+
+ + + +
- diff --git a/src/routes/services/[id]/_Services/_Services.svelte b/src/routes/services/[id]/_Services/_Services.svelte index a2a2effac..d1d638bb4 100644 --- a/src/routes/services/[id]/_Services/_Services.svelte +++ b/src/routes/services/[id]/_Services/_Services.svelte @@ -1,4 +1,6 @@
@@ -86,6 +101,14 @@
+ {#if service.type === 'minio' && !service.minio.apiFqdn && isRunning} +
+ IMPORTANT! There was a small modification with + Minio in the latest version of Coolify. Now you can separate the Console URL from the API URL, + so you could use both through SSL. But this proccess cannot be done automatically, so you have + to stop your Minio instance, configure the new domain and start it back. Sorry for any inconvenience. +
+ {/if}
@@ -131,25 +154,62 @@ {/if}
-
-
- - -
+ {#if service.type === 'minio'} +
+
+ +
+ + +
+
+
+ + +
+ + +
+ {:else} +
+
+ + +
+ + +
+ {/if} - -
!isRunning && changeSettings('dualCerts')} />
+
+ + +
Useful if you would like to use your own reverse proxy or tunnel and also in development mode. Otherwise leave empty.'} + /> +
+ {#if service.type === 'plausibleanalytics'} - + {:else if service.type === 'minio'} {:else if service.type === 'vscodeserver'} diff --git a/src/routes/services/[id]/_Services/_Wordpress.svelte b/src/routes/services/[id]/_Services/_Wordpress.svelte index 601843534..13b222fc5 100644 --- a/src/routes/services/[id]/_Services/_Wordpress.svelte +++ b/src/routes/services/[id]/_Services/_Wordpress.svelte @@ -18,6 +18,7 @@ let ftpUser = service.wordpress.ftpUser; let ftpPassword = service.wordpress.ftpPassword; let ftpLoading = false; + let ownMysql = service.wordpress.ownMysql; function generateUrl(publicPort) { return browser @@ -40,7 +41,7 @@ publicPort, ftpUser: user, ftpPassword: password - } = await post(`/services/${id}/wordpress/settings.json`, { + } = await post(`/services/${id}/wordpress/ftp.json`, { ftpEnabled }); ftpUrl = generateUrl(publicPort); @@ -52,6 +53,18 @@ } finally { ftpLoading = false; } + } else { + try { + if (name === 'ownMysql') { + ownMysql = !ownMysql; + } + await post(`/services/${id}/wordpress/settings.json`, { + ownMysql + }); + service.wordpress.ownMysql = ownMysql; + } catch ({ error }) { + return errorNotification(error); + } } } @@ -106,52 +119,96 @@ define('SUBDOMAIN_INSTALL', false);`
MySQL
+
+ !isRunning && changeSettings('ownMysql')} + title="Use your own MySQL server" + description="Enables the use of your own MySQL server. If you don't have one, you can use the one provided by Coolify." + /> +
+{#if service.wordpress.ownMysql} +
+ + +
+
+ + +
+{/if}
-
- - -
-
- - -
+{#if !service.wordpress.ownMysql} +
+ + +
+
+ + +
+{/if}
- +
diff --git a/src/routes/services/[id]/appwrite-wip/index.json.ts b/src/routes/services/[id]/appwrite-wip/index.json.ts new file mode 100644 index 000000000..e269e8fe7 --- /dev/null +++ b/src/routes/services/[id]/appwrite-wip/index.json.ts @@ -0,0 +1,21 @@ +import { getUserDetails } from '$lib/common'; +import * as db from '$lib/database'; +import { ErrorHandler } from '$lib/database'; +import type { RequestHandler } from '@sveltejs/kit'; + +export const post: RequestHandler = async (event) => { + const { status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + const { id } = event.params; + + let { name, fqdn, exposePort } = await event.request.json(); + if (fqdn) fqdn = fqdn.toLowerCase(); + if (exposePort) exposePort = Number(exposePort); + + try { + await db.updateService({ id, fqdn, name, exposePort }); + return { status: 201 }; + } catch (error) { + return ErrorHandler(error); + } +}; diff --git a/src/routes/services/[id]/appwrite-wip/start.json.ts b/src/routes/services/[id]/appwrite-wip/start.json.ts new file mode 100644 index 000000000..70748a0a6 --- /dev/null +++ b/src/routes/services/[id]/appwrite-wip/start.json.ts @@ -0,0 +1,519 @@ +import { asyncExecShell, createDirectories, getEngine, getUserDetails } from '$lib/common'; +import * as db from '$lib/database'; +import { promises as fs } from 'fs'; +import yaml from 'js-yaml'; +import type { RequestHandler } from '@sveltejs/kit'; +import { ErrorHandler, getServiceImage } from '$lib/database'; +import { makeLabelForServices } from '$lib/buildPacks/common'; +import type { ComposeFile } from '$lib/types/composeFile'; +import { getServiceMainPort } from '$lib/components/common'; + +export const post: RequestHandler = async (event) => { + const { teamId, status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + + const { id } = event.params; + + try { + const service = await db.getService({ id, teamId }); + const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } = + service; + const network = destinationDockerId && destinationDocker.network; + const host = getEngine(destinationDocker.engine); + const port = getServiceMainPort('n8n'); + + const { workdir } = await createDirectories({ repository: type, buildId: id }); + const image = getServiceImage(type); + + if (serviceSecret.length > 0) { + serviceSecret.forEach((secret) => { + variables[secret.name] = secret.value; + }); + } + + const variables = { + _APP_ENV: 'production', + _APP_VERSION: '', + _APP_LOCALE: '', + _APP_OPTIONS_ABUSE: '', + _APP_OPTIONS_FORCE_HTTPS: '', + _APP_OPENSSL_KEY_V1: '', + _APP_DOMAIN: '', + _APP_DOMAIN_TARGET: '', + _APP_CONSOLE_WHITELIST_ROOT: '', + _APP_CONSOLE_WHITELIST_EMAILS: '', + _APP_CONSOLE_WHITELIST_IPS: '', + _APP_SYSTEM_EMAIL_NAME: '', + _APP_SYSTEM_EMAIL_ADDRESS: '', + _APP_SYSTEM_RESPONSE_FORMAT: '', + _APP_SYSTEM_SECURITY_EMAIL_ADDRESS: '', + _APP_USAGE_STATS: '', + _APP_LOGGING_PROVIDER: '', + _APP_LOGGING_CONFIG: '', + _APP_USAGE_AGGREGATION_INTERVAL: '', + _APP_WORKER_PER_CORE: '', + _APP_REDIS_HOST: '', + _APP_REDIS_PORT: '', + _APP_REDIS_USER: '', + _APP_REDIS_PASS: '', + _APP_DB_HOST: '', + _APP_DB_PORT: '', + _APP_DB_SCHEMA: '', + _APP_DB_USER: '', + _APP_DB_PASS: '', + _APP_DB_ROOT_PASS: '', + _APP_INFLUXDB_HOST: '', + _APP_INFLUXDB_PORT: '', + _APP_STATSD_HOST: '', + _APP_STATSD_PORT: '', + _APP_SMTP_HOST: '', + _APP_SMTP_PORT: '', + _APP_SMTP_SECURE: '', + _APP_SMTP_USERNAME: '', + _APP_SMTP_PASSWORD: '', + _APP_STORAGE_LIMIT: '', + _APP_STORAGE_ANTIVIRUS: '', + _APP_STORAGE_ANTIVIRUS_HOST: '', + _APP_STORAGE_ANTIVIRUS_PORT: '', + _APP_STORAGE_DEVICE: '', + _APP_STORAGE_S3_ACCESS_KEY: '', + _APP_STORAGE_S3_SECRET: '', + _APP_STORAGE_S3_REGION: '', + _APP_STORAGE_S3_BUCKET: '', + _APP_STORAGE_DO_SPACES_ACCESS_KEY: '', + _APP_STORAGE_DO_SPACES_SECRET: '', + _APP_STORAGE_DO_SPACES_REGION: '', + _APP_STORAGE_DO_SPACES_BUCKET: '', + _APP_FUNCTIONS_SIZE_LIMIT: '', + _APP_FUNCTIONS_TIMEOUT: '', + _APP_FUNCTIONS_BUILD_TIMEOUT: '', + _APP_FUNCTIONS_CONTAINERS: '', + _APP_FUNCTIONS_CPUS: '', + _APP_FUNCTIONS_MEMORY: '', + _APP_FUNCTIONS_MEMORY_SWAP: '', + _APP_FUNCTIONS_RUNTIMES: '', + _APP_EXECUTOR_SECRET: '', + _APP_EXECUTOR_RUNTIME_NETWORK: '', + _APP_FUNCTIONS_ENVS: '', + _APP_FUNCTIONS_INACTIVE_THRESHOLD: '', + DOCKERHUB_PULL_USERNAME: '', + DOCKERHUB_PULL_PASSWORD: '', + DOCKERHUB_PULL_EMAIL: '', + _APP_MAINTENANCE_INTERVAL: '', + _APP_MAINTENANCE_RETENTION_EXECUTION: '', + _APP_MAINTENANCE_RETENTION_ABUSE: '', + _APP_MAINTENANCE_RETENTION_AUDIT: '' + }; + const config = { + appwrite: { + image: `${image}:${version}`, + volumes: [ + `${id}-appwrite-uploads:/storage/uploads`, + `${id}-appwrite-cache:/storage/cache`, + `${id}-appwrite-config:/storage/config`, + `${id}-appwrite-certificates:/storage/certificates`, + `${id}-appwrite-functions:/storage/functions` + ], + environmentVariables: { + _APP_ENV: variables._APP_ENV, + _APP_WORKER_PER_CORE: variables._APP_WORKER_PER_CORE, + _APP_LOCALE: variables._APP_LOCALE, + _APP_CONSOLE_WHITELIST_ROOT: variables._APP_CONSOLE_WHITELIST_ROOT, + _APP_CONSOLE_WHITELIST_EMAILS: variables._APP_CONSOLE_WHITELIST_EMAILS, + _APP_CONSOLE_WHITELIST_IPS: variables._APP_CONSOLE_WHITELIST_IPS, + _APP_SYSTEM_EMAIL_NAME: variables._APP_SYSTEM_EMAIL_NAME, + _APP_SYSTEM_EMAIL_ADDRESS: variables._APP_SYSTEM_EMAIL_ADDRESS, + _APP_SYSTEM_SECURITY_EMAIL_ADDRESS: variables._APP_SYSTEM_SECURITY_EMAIL_ADDRESS, + _APP_SYSTEM_RESPONSE_FORMAT: variables._APP_SYSTEM_RESPONSE_FORMAT, + _APP_OPTIONS_ABUSE: variables._APP_OPTIONS_ABUSE, + _APP_OPTIONS_FORCE_HTTPS: variables._APP_OPTIONS_FORCE_HTTPS, + _APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1, + _APP_DOMAIN: variables._APP_DOMAIN, + _APP_DOMAIN_TARGET: variables._APP_DOMAIN_TARGET, + _APP_REDIS_HOST: variables._APP_REDIS_HOST, + _APP_REDIS_PORT: variables._APP_REDIS_PORT, + _APP_REDIS_USER: variables._APP_REDIS_USER, + _APP_REDIS_PASS: variables._APP_REDIS_PASS, + _APP_DB_HOST: variables._APP_DB_HOST, + _APP_DB_PORT: variables._APP_DB_PORT, + _APP_DB_SCHEMA: variables._APP_DB_SCHEMA, + _APP_DB_USER: variables._APP_DB_USER, + _APP_DB_PASS: variables._APP_DB_PASS, + _APP_SMTP_HOST: variables._APP_SMTP_HOST, + _APP_SMTP_PORT: variables._APP_SMTP_PORT, + _APP_SMTP_SECURE: variables._APP_SMTP_SECURE, + _APP_SMTP_USERNAME: variables._APP_SMTP_USERNAME, + _APP_SMTP_PASSWORD: variables._APP_SMTP_PASSWORD, + _APP_USAGE_STATS: variables._APP_USAGE_STATS, + _APP_INFLUXDB_HOST: variables._APP_INFLUXDB_HOST, + _APP_INFLUXDB_PORT: variables._APP_INFLUXDB_PORT, + _APP_STORAGE_LIMIT: variables._APP_STORAGE_LIMIT, + _APP_STORAGE_ANTIVIRUS: variables._APP_STORAGE_ANTIVIRUS, + _APP_STORAGE_ANTIVIRUS_HOST: variables._APP_STORAGE_ANTIVIRUS_HOST, + _APP_STORAGE_ANTIVIRUS_PORT: variables._APP_STORAGE_ANTIVIRUS_PORT, + _APP_STORAGE_DEVICE: variables._APP_STORAGE_DEVICE, + _APP_STORAGE_S3_ACCESS_KEY: variables._APP_STORAGE_S3_ACCESS_KEY, + _APP_STORAGE_S3_SECRET: variables._APP_STORAGE_S3_SECRET, + _APP_STORAGE_S3_REGION: variables._APP_STORAGE_S3_REGION, + _APP_STORAGE_S3_BUCKET: variables._APP_STORAGE_S3_BUCKET, + _APP_STORAGE_DO_SPACES_ACCESS_KEY: variables._APP_STORAGE_DO_SPACES_ACCESS_KEY, + _APP_STORAGE_DO_SPACES_SECRET: variables._APP_STORAGE_DO_SPACES_SECRET, + _APP_STORAGE_DO_SPACES_REGION: variables._APP_STORAGE_DO_SPACES_REGION, + _APP_STORAGE_DO_SPACES_BUCKET: variables._APP_STORAGE_DO_SPACES_BUCKET, + _APP_FUNCTIONS_SIZE_LIMIT: variables._APP_FUNCTIONS_SIZE_LIMIT, + _APP_FUNCTIONS_TIMEOUT: variables._APP_FUNCTIONS_TIMEOUT, + _APP_FUNCTIONS_BUILD_TIMEOUT: variables._APP_FUNCTIONS_BUILD_TIMEOUT, + _APP_FUNCTIONS_CONTAINERS: variables._APP_FUNCTIONS_CONTAINERS, + _APP_FUNCTIONS_CPUS: variables._APP_FUNCTIONS_CPUS, + _APP_FUNCTIONS_MEMORY: variables._APP_FUNCTIONS_MEMORY, + _APP_FUNCTIONS_MEMORY_SWAP: variables._APP_FUNCTIONS_MEMORY_SWAP, + _APP_EXECUTOR_SECRET: variables._APP_EXECUTOR_SECRET, + _APP_FUNCTIONS_RUNTIMES: variables._APP_FUNCTIONS_RUNTIMES, + _APP_LOGGING_PROVIDER: variables._APP_LOGGING_PROVIDER, + _APP_LOGGING_CONFIG: variables._APP_LOGGING_CONFIG, + _APP_STATSD_HOST: variables._APP_STATSD_HOST, + _APP_STATSD_PORT: variables._APP_STATSD_PORT, + _APP_MAINTENANCE_INTERVAL: variables._APP_MAINTENANCE_INTERVAL, + _APP_MAINTENANCE_RETENTION_EXECUTION: variables._APP_MAINTENANCE_RETENTION_EXECUTION, + _APP_MAINTENANCE_RETENTION_ABUSE: variables._APP_MAINTENANCE_RETENTION_ABUSE, + _APP_MAINTENANCE_RETENTION_AUDIT: variables._APP_MAINTENANCE_RETENTION_AUDIT + } + }, + appwriteRealtime: { + image: `${image}:${version}`, + environmentVariables: { + _APP_ENV: variables._APP_ENV, + _APP_WORKER_PER_CORE: variables._APP_WORKER_PER_CORE, + _APP_OPTIONS_ABUSE: variables._APP_OPTIONS_ABUSE, + _APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1, + _APP_REDIS_HOST: variables._APP_REDIS_HOST, + _APP_REDIS_PORT: variables._APP_REDIS_PORT, + _APP_DB_HOST: variables._APP_DB_HOST, + _APP_DB_PORT: variables._APP_DB_PORT, + _APP_DB_SCHEMA: variables._APP_DB_SCHEMA, + _APP_DB_USER: variables._APP_DB_USER, + _APP_DB_PASS: variables._APP_DB_PASS, + _APP_USAGE_STATS: variables._APP_USAGE_STATS, + _APP_LOGGING_PROVIDER: variables._APP_LOGGING_PROVIDER, + _APP_LOGGING_CONFIG: variables._APP_LOGGING_CONFIG + } + }, + appwriteExecutor: { + image: `${image}:${version}`, + volumes: [ + `${id}-appwrite-functions:/storage/functions`, + `/tmp:/tmp`, + '/var/run/docker.sock:/var/run/docker.sock' + ], + environmentVariables: { + DOCKERHUB_PULL_USERNAME: variables.DOCKERHUB_PULL_USERNAME, + DOCKERHUB_PULL_PASSWORD: variables.DOCKERHUB_PULL_PASSWORD, + _APP_LOGGING_PROVIDER: variables._APP_LOGGING_PROVIDER, + _APP_LOGGING_CONFIG: variables._APP_LOGGING_CONFIG, + _APP_VERSION: variables._APP_VERSION, + _APP_ENV: variables._APP_ENV, + _APP_STORAGE_DEVICE: variables._APP_STORAGE_DEVICE, + _APP_STORAGE_S3_ACCESS_KEY: variables._APP_STORAGE_S3_ACCESS_KEY, + _APP_STORAGE_S3_SECRET: variables._APP_STORAGE_S3_SECRET, + _APP_STORAGE_S3_REGION: variables._APP_STORAGE_S3_REGION, + _APP_STORAGE_S3_BUCKET: variables._APP_STORAGE_S3_BUCKET, + _APP_STORAGE_DO_SPACES_ACCESS_KEY: variables._APP_STORAGE_DO_SPACES_ACCESS_KEY, + _APP_STORAGE_DO_SPACES_SECRET: variables._APP_STORAGE_DO_SPACES_SECRET, + _APP_STORAGE_DO_SPACES_REGION: variables._APP_STORAGE_DO_SPACES_REGION, + _APP_STORAGE_DO_SPACES_BUCKET: variables._APP_STORAGE_DO_SPACES_BUCKET, + _APP_FUNCTIONS_CPUS: variables._APP_FUNCTIONS_CPUS, + _APP_FUNCTIONS_MEMORY: variables._APP_FUNCTIONS_MEMORY, + _APP_FUNCTIONS_MEMORY_SWAP: variables._APP_FUNCTIONS_MEMORY_SWAP, + _APP_FUNCTIONS_TIMEOUT: variables._APP_FUNCTIONS_TIMEOUT, + _APP_EXECUTOR_SECRET: variables._APP_EXECUTOR_SECRET, + _APP_FUNCTIONS_RUNTIMES: variables._APP_FUNCTIONS_RUNTIMES, + _APP_FUNCTIONS_INACTIVE_THRESHOLD: variables._APP_FUNCTIONS_INACTIVE_THRESHOLD, + _APP_EXECUTOR_RUNTIME_NETWORK: variables._APP_EXECUTOR_RUNTIME_NETWORK + } + }, + appwriteWorkerDatabase: { + image: `${image}:${version}`, + environmentVariables: { + _APP_ENV: variables._APP_ENV, + _APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1, + _APP_REDIS_HOST: variables._APP_REDIS_HOST, + _APP_REDIS_PORT: variables._APP_REDIS_PORT, + _APP_REDIS_USER: variables._APP_REDIS_USER, + _APP_REDIS_PASS: variables._APP_REDIS_PASS, + _APP_DB_HOST: variables._APP_DB_HOST, + _APP_DB_PORT: variables._APP_DB_PORT, + _APP_DB_SCHEMA: variables._APP_DB_SCHEMA, + _APP_DB_USER: variables._APP_DB_USER, + _APP_DB_PASS: variables._APP_DB_PASS, + _APP_LOGGING_PROVIDER: variables._APP_LOGGING_PROVIDER, + _APP_LOGGING_CONFIG: variables._APP_LOGGING_CONFIG + } + }, + appwriteWorkerBuilds: { + image: `${image}:${version}`, + environmentVariables: { + _APP_ENV: variables._APP_ENV, + _APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1, + _APP_EXECUTOR_SECRET: variables._APP_EXECUTOR_SECRET, + _APP_REDIS_HOST: variables._APP_REDIS_HOST, + _APP_REDIS_PORT: variables._APP_REDIS_PORT, + _APP_REDIS_USER: variables._APP_REDIS_USER, + _APP_REDIS_PASS: variables._APP_REDIS_PASS, + _APP_DB_HOST: variables._APP_DB_HOST, + _APP_DB_PORT: variables._APP_DB_PORT, + _APP_DB_SCHEMA: variables._APP_DB_SCHEMA, + _APP_DB_USER: variables._APP_DB_USER, + _APP_DB_PASS: variables._APP_DB_PASS, + _APP_LOGGING_PROVIDER: variables._APP_LOGGING_PROVIDER, + _APP_LOGGING_CONFIG: variables._APP_LOGGING_CONFIG + } + }, + appwriteWorkerAudits: { + image: `${image}:${version}`, + environmentVariables: { + _APP_ENV: variables._APP_ENV, + _APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1, + _APP_REDIS_HOST: variables._APP_REDIS_HOST, + _APP_REDIS_PORT: variables._APP_REDIS_PORT, + _APP_REDIS_USER: variables._APP_REDIS_USER, + _APP_REDIS_PASS: variables._APP_REDIS_PASS, + _APP_DB_HOST: variables._APP_DB_HOST, + _APP_DB_PORT: variables._APP_DB_PORT, + _APP_DB_SCHEMA: variables._APP_DB_SCHEMA, + _APP_DB_USER: variables._APP_DB_USER, + _APP_DB_PASS: variables._APP_DB_PASS, + _APP_LOGGING_PROVIDER: variables._APP_LOGGING_PROVIDER, + _APP_LOGGING_CONFIG: variables._APP_LOGGING_CONFIG + } + }, + appwriteWorkerWebhooks: { + image: `${image}:${version}`, + environmentVariables: { + _APP_ENV: variables._APP_ENV, + _APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1, + _APP_SYSTEM_SECURITY_EMAIL_ADDRESS: variables._APP_SYSTEM_SECURITY_EMAIL_ADDRESS, + _APP_REDIS_HOST: variables._APP_REDIS_HOST, + _APP_REDIS_PORT: variables._APP_REDIS_PORT, + _APP_REDIS_USER: variables._APP_REDIS_USER, + _APP_REDIS_PASS: variables._APP_REDIS_PASS, + _APP_LOGGING_PROVIDER: variables._APP_LOGGING_PROVIDER, + _APP_LOGGING_CONFIG: variables._APP_LOGGING_CONFIG + } + }, + appwriteWorkerDeletes: { + image: `${image}:${version}`, + volumes: [ + `${id}-appwrite-uploads:/storage/uploads`, + `${id}-appwrite-cache:/storage/cache`, + `${id}-appwrite-certificates:/storage/certificates` + ], + environmentVariables: { + _APP_ENV: variables._APP_ENV, + _APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1, + _APP_REDIS_HOST: variables._APP_REDIS_HOST, + _APP_REDIS_PORT: variables._APP_REDIS_PORT, + _APP_REDIS_USER: variables._APP_REDIS_USER, + _APP_REDIS_PASS: variables._APP_REDIS_PASS, + _APP_DB_HOST: variables._APP_DB_HOST, + _APP_DB_PORT: variables._APP_DB_PORT, + _APP_DB_SCHEMA: variables._APP_DB_SCHEMA, + _APP_DB_USER: variables._APP_DB_USER, + _APP_DB_PASS: variables._APP_DB_PASS, + _APP_STORAGE_DEVICE: variables._APP_STORAGE_DEVICE, + _APP_STORAGE_S3_ACCESS_KEY: variables._APP_STORAGE_S3_ACCESS_KEY, + _APP_STORAGE_S3_SECRET: variables._APP_STORAGE_S3_SECRET, + _APP_STORAGE_S3_REGION: variables._APP_STORAGE_S3_REGION, + _APP_STORAGE_S3_BUCKET: variables._APP_STORAGE_S3_BUCKET, + _APP_STORAGE_DO_SPACES_ACCESS_KEY: variables._APP_STORAGE_DO_SPACES_ACCESS_KEY, + _APP_STORAGE_DO_SPACES_SECRET: variables._APP_STORAGE_DO_SPACES_SECRET, + _APP_STORAGE_DO_SPACES_REGION: variables._APP_STORAGE_DO_SPACES_REGION, + _APP_STORAGE_DO_SPACES_BUCKET: variables._APP_STORAGE_DO_SPACES_BUCKET, + _APP_LOGGING_PROVIDER: variables._APP_LOGGING_PROVIDER, + _APP_LOGGING_CONFIG: variables._APP_LOGGING_CONFIG + } + }, + appwriteWorkerCertificates: { + image: `${image}:${version}`, + volumes: [ + `${id}-appwrite-config:/storage/config`, + `${id}-appwrite-certificates:/storage/certificates` + ], + environmentVariables: { + _APP_ENV: variables._APP_ENV, + _APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1, + _APP_SYSTEM_SECURITY_EMAIL_ADDRESS: variables._APP_SYSTEM_SECURITY_EMAIL_ADDRESS, + _APP_REDIS_HOST: variables._APP_REDIS_HOST, + _APP_REDIS_PORT: variables._APP_REDIS_PORT, + _APP_REDIS_USER: variables._APP_REDIS_USER, + _APP_REDIS_PASS: variables._APP_REDIS_PASS, + _APP_DOMAIN_TARGET: variables._APP_DOMAIN_TARGET, + _APP_DB_HOST: variables._APP_DB_HOST, + _APP_DB_PORT: variables._APP_DB_PORT, + _APP_DB_SCHEMA: variables._APP_DB_SCHEMA, + _APP_DB_USER: variables._APP_DB_USER, + _APP_DB_PASS: variables._APP_DB_PASS, + _APP_LOGGING_PROVIDER: variables._APP_LOGGING_PROVIDER, + _APP_LOGGING_CONFIG: variables._APP_LOGGING_CONFIG + } + }, + appwriteWorkerFunctions: { + image: `${image}:${version}`, + envvironmentVariables: { + _APP_ENV: variables._APP_ENV, + _APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1, + _APP_REDIS_HOST: variables._APP_REDIS_HOST, + _APP_REDIS_PORT: variables._APP_REDIS_PORT, + _APP_REDIS_USER: variables._APP_REDIS_USER, + _APP_REDIS_PASS: variables._APP_REDIS_PASS, + _APP_DB_HOST: variables._APP_DB_HOST, + _APP_DB_PORT: variables._APP_DB_PORT, + _APP_DB_SCHEMA: variables._APP_DB_SCHEMA, + _APP_DB_USER: variables._APP_DB_USER, + _APP_DB_PASS: variables._APP_DB_PASS, + _APP_FUNCTIONS_TIMEOUT: variables._APP_FUNCTIONS_TIMEOUT, + _APP_EXECUTOR_SECRET: variables._APP_EXECUTOR_SECRET, + _APP_USAGE_STATS: variables._APP_USAGE_STATS, + DOCKERHUB_PULL_USERNAME: variables.DOCKERHUB_PULL_USERNAME, + DOCKERHUB_PULL_PASSWORD: variables.DOCKERHUB_PULL_PASSWORD + } + }, + appwriteWorkerMails: { + image: `${image}:${version}`, + environmentVariables: { + _APP_ENV: variables._APP_ENV, + _APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1, + _APP_SYSTEM_EMAIL_NAME: variables._APP_SYSTEM_EMAIL_NAME, + _APP_SYSTEM_EMAIL_ADDRESS: variables._APP_SYSTEM_EMAIL_ADDRESS, + _APP_REDIS_HOST: variables._APP_REDIS_HOST, + _APP_REDIS_PORT: variables._APP_REDIS_PORT, + _APP_REDIS_USER: variables._APP_REDIS_USER, + _APP_REDIS_PASS: variables._APP_REDIS_PASS, + _APP_SMTP_HOST: variables._APP_SMTP_HOST, + _APP_SMTP_PORT: variables._APP_SMTP_PORT, + _APP_SMTP_SECURE: variables._APP_SMTP_SECURE, + _APP_SMTP_USERNAME: variables._APP_SMTP_USERNAME, + _APP_SMTP_PASSWORD: variables._APP_SMTP_PASSWORD, + _APP_LOGGING_PROVIDER: variables._APP_LOGGING_PROVIDER, + _APP_LOGGING_CONFIG: variables._APP_LOGGING_CONFIG + } + }, + appwriteMaintenance: { + image: `${image}:${version}`, + environmentVariables: { + _APP_ENV: variables._APP_ENV, + _APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1, + _APP_REDIS_HOST: variables._APP_REDIS_HOST, + _APP_REDIS_PORT: variables._APP_REDIS_PORT, + _APP_REDIS_USER: variables._APP_REDIS_USER, + _APP_REDIS_PASS: variables._APP_REDIS_PASS, + _APP_MAINTENANCE_INTERVAL: variables._APP_MAINTENANCE_INTERVAL, + _APP_MAINTENANCE_RETENTION_EXECUTION: variables._APP_MAINTENANCE_RETENTION_EXECUTION, + _APP_MAINTENANCE_RETENTION_ABUSE: variables._APP_MAINTENANCE_RETENTION_ABUSE, + _APP_MAINTENANCE_RETENTION_AUDIT: variables._APP_MAINTENANCE_RETENTION_AUDIT + } + }, + appwriteUsage: { + image: `${image}:${version}`, + environmentVariables: { + _APP_ENV: variables._APP_ENV, + _APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1, + _APP_DB_HOST: variables._APP_DB_HOST, + _APP_DB_PORT: variables._APP_DB_PORT, + _APP_DB_SCHEMA: variables._APP_DB_SCHEMA, + _APP_DB_USER: variables._APP_DB_USER, + _APP_DB_PASS: variables._APP_DB_PASS, + _APP_INFLUXDB_HOST: variables._APP_INFLUXDB_HOST, + _APP_INFLUXDB_PORT: variables._APP_INFLUXDB_PORT, + _APP_USAGE_AGGREGATION_INTERVAL: variables._APP_USAGE_AGGREGATION_INTERVAL, + _APP_REDIS_HOST: variables._APP_REDIS_HOST, + _APP_REDIS_PORT: variables._APP_REDIS_PORT, + _APP_REDIS_USER: variables._APP_REDIS_USER, + _APP_REDIS_PASS: variables._APP_REDIS_PASS + } + }, + appwriteSchedule: { + image: `${image}:${version}`, + environmentVariables: { + _APP_ENV: variables._APP_ENV, + _APP_REDIS_HOST: variables._APP_REDIS_HOST, + _APP_REDIS_PORT: variables._APP_REDIS_PORT, + _APP_REDIS_USER: variables._APP_REDIS_USER, + _APP_REDIS_PASS: variables._APP_REDIS_PASS + } + }, + mariadb: { + image: 'mariadb:10.7', + volumes: [`${id}-appwrite-mariadb:/var/lib/mysql`], + environmentVariables: { + MYSQL_ROOT_PASSWORD: variables._APP_DB_ROOT_PASS, + MYSQL_DATABASE: variables._APP_DB_SCHEMA, + MYSQL_USER: variables._APP_DB_USER, + MYSQL_PASSWORD: variables._APP_DB_PASS + } + }, + redis: { + image: 'redis:6.0-alpine3.12', + volumes: [`${id}-appwrite-redis:/data`] + }, + influxdb: { + image: 'appwrite/influxdb:1.0.0', + volumes: [`${id}-appwrite-influxdb:/var/lib/influxdb`] + }, + telegraf: { + image: 'appwrite/telegraf:1.0.0', + environmentVariables: { + _APP_INFLUXDB_HOST: variables._APP_INFLUXDB_HOST, + _APP_INFLUXDB_PORT: variables._APP_INFLUXDB_PORT + } + } + }; + + const composeFile: ComposeFile = { + version: '3.8', + services: { + [id]: { + container_name: id, + image: config.image, + networks: [network], + volumes: [...config.appwrite.volumes], + environment: config.environmentVariables, + restart: 'always', + labels: makeLabelForServices('appwrite'), + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + deploy: { + restart_policy: { + condition: 'on-failure', + delay: '5s', + max_attempts: 3, + window: '120s' + } + } + } + }, + networks: { + [network]: { + external: true + } + }, + volumes: { + [config.volume.split(':')[0]]: { + name: config.volume.split(':')[0] + } + } + }; + const composeFileDestination = `${workdir}/docker-compose.yaml`; + await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); + + try { + await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`); + await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); + return { + status: 200 + }; + } catch (error) { + return ErrorHandler(error); + } + } catch (error) { + return ErrorHandler(error); + } +}; diff --git a/src/routes/services/[id]/appwrite-wip/stop.json.ts b/src/routes/services/[id]/appwrite-wip/stop.json.ts new file mode 100644 index 000000000..c604e1cc3 --- /dev/null +++ b/src/routes/services/[id]/appwrite-wip/stop.json.ts @@ -0,0 +1,35 @@ +import { getUserDetails, removeDestinationDocker } from '$lib/common'; +import * as db from '$lib/database'; +import { ErrorHandler } from '$lib/database'; +import { checkContainer } from '$lib/haproxy'; +import type { RequestHandler } from '@sveltejs/kit'; + +export const post: RequestHandler = async (event) => { + const { teamId, status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + + const { id } = event.params; + + try { + const service = await db.getService({ id, teamId }); + const { destinationDockerId, destinationDocker, fqdn } = service; + if (destinationDockerId) { + const engine = destinationDocker.engine; + + try { + const found = await checkContainer(engine, id); + if (found) { + await removeDestinationDocker({ id, engine }); + } + } catch (error) { + console.error(error); + } + } + + return { + status: 200 + }; + } catch (error) { + return ErrorHandler(error); + } +}; diff --git a/src/routes/services/[id]/check.json.ts b/src/routes/services/[id]/check.json.ts index 9a9193524..4b8557c66 100644 --- a/src/routes/services/[id]/check.json.ts +++ b/src/routes/services/[id]/check.json.ts @@ -1,19 +1,56 @@ import { asyncExecShell, getDomain, getEngine, getUserDetails } from '$lib/common'; import * as db from '$lib/database'; import { ErrorHandler } from '$lib/database'; +import { t } from '$lib/translations'; import type { RequestHandler } from '@sveltejs/kit'; +import getPort from 'get-port'; export const post: RequestHandler = async (event) => { const { status, body } = await getUserDetails(event); if (status === 401) return { status, body }; const { id } = event.params; - let { fqdn } = await event.request.json(); + let { fqdn, exposePort, otherFqdns } = await event.request.json(); if (fqdn) fqdn = fqdn.toLowerCase(); + if (otherFqdns) otherFqdns = otherFqdns.map((fqdn) => fqdn.toLowerCase()); + if (exposePort) exposePort = Number(exposePort); try { - const found = await db.isDomainConfigured({ id, fqdn }); + let found = await db.isDomainConfigured({ id, fqdn }); + if (found) { + throw { + message: t.get('application.domain_already_in_use', { + domain: getDomain(fqdn).replace('www.', '') + }) + }; + } + if (otherFqdns) { + for (const ofqdn of otherFqdns) { + const domain = getDomain(ofqdn); + const nakedDomain = domain.replace('www.', ''); + found = await db.isDomainConfigured({ id, fqdn: ofqdn, checkOwn: true }); + if (found) { + throw { + message: t.get('application.domain_already_in_use', { + domain: nakedDomain + }) + }; + } + } + } + if (exposePort) { + exposePort = Number(exposePort); + + if (exposePort < 1024 || exposePort > 65535) { + throw { message: `Exposed Port needs to be between 1024 and 65535.` }; + } + + const publicPort = await getPort({ port: exposePort }); + if (publicPort !== exposePort) { + throw { message: `Port ${exposePort} is already in use.` }; + } + } return { status: found ? 500 : 200, body: { diff --git a/src/routes/services/[id]/fider/start.json.ts b/src/routes/services/[id]/fider/start.json.ts index 812497620..51e45ec3f 100644 --- a/src/routes/services/[id]/fider/start.json.ts +++ b/src/routes/services/[id]/fider/start.json.ts @@ -13,6 +13,7 @@ import { ErrorHandler, getServiceImage } from '$lib/database'; import { makeLabelForServices } from '$lib/buildPacks/common'; import type { ComposeFile } from '$lib/types/composeFile'; import type { Service, DestinationDocker, Prisma } from '@prisma/client'; +import { getServiceMainPort } from '$lib/components/common'; export const post: RequestHandler = async (event) => { const { teamId, status, body } = await getUserDetails(event); @@ -30,6 +31,7 @@ export const post: RequestHandler = async (event) => { destinationDockerId, destinationDocker, serviceSecret, + exposePort, fider: { postgresqlUser, postgresqlPassword, @@ -48,6 +50,7 @@ export const post: RequestHandler = async (event) => { } = service; const network = destinationDockerId && destinationDocker.network; const host = getEngine(destinationDocker.engine); + const port = getServiceMainPort('fider'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); @@ -97,6 +100,7 @@ export const post: RequestHandler = async (event) => { volumes: [], restart: 'always', labels: makeLabelForServices('fider'), + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), deploy: { restart_policy: { condition: 'on-failure', diff --git a/src/routes/services/[id]/ghost/index.json.ts b/src/routes/services/[id]/ghost/index.json.ts index 81ca8ade1..1b1225f59 100644 --- a/src/routes/services/[id]/ghost/index.json.ts +++ b/src/routes/services/[id]/ghost/index.json.ts @@ -11,11 +11,12 @@ export const post: RequestHandler = async (event) => { let { name, fqdn, + exposePort, ghost: { mariadbDatabase } } = await event.request.json(); if (fqdn) fqdn = fqdn.toLowerCase(); try { - await db.updateGhostService({ id, fqdn, name, mariadbDatabase }); + await db.updateGhostService({ id, fqdn, name, exposePort, mariadbDatabase }); return { status: 201 }; } catch (error) { return ErrorHandler(error); diff --git a/src/routes/services/[id]/ghost/start.json.ts b/src/routes/services/[id]/ghost/start.json.ts index 45c6936fe..67e9364b9 100644 --- a/src/routes/services/[id]/ghost/start.json.ts +++ b/src/routes/services/[id]/ghost/start.json.ts @@ -12,6 +12,7 @@ import type { RequestHandler } from '@sveltejs/kit'; import { ErrorHandler, getServiceImage } from '$lib/database'; import { makeLabelForServices } from '$lib/buildPacks/common'; import type { ComposeFile } from '$lib/types/composeFile'; +import { getServiceMainPort } from '$lib/components/common'; export const post: RequestHandler = async (event) => { const { teamId, status, body } = await getUserDetails(event); @@ -19,6 +20,8 @@ export const post: RequestHandler = async (event) => { const { id } = event.params; + const port = getServiceMainPort('ghost'); + try { const service = await db.getService({ id, teamId }); const { @@ -27,6 +30,7 @@ export const post: RequestHandler = async (event) => { destinationDockerId, destinationDocker, serviceSecret, + exposePort, fqdn, ghost: { defaultEmail, @@ -89,6 +93,7 @@ export const post: RequestHandler = async (event) => { volumes: [config.ghost.volume], environment: config.ghost.environmentVariables, restart: 'always', + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('ghost'), depends_on: [`${id}-mariadb`], deploy: { diff --git a/src/routes/services/[id]/hasura/start.json.ts b/src/routes/services/[id]/hasura/start.json.ts index 325d6e33c..8a8cc3f04 100644 --- a/src/routes/services/[id]/hasura/start.json.ts +++ b/src/routes/services/[id]/hasura/start.json.ts @@ -7,6 +7,7 @@ import { ErrorHandler, getServiceImage } from '$lib/database'; import { makeLabelForServices } from '$lib/buildPacks/common'; import type { ComposeFile } from '$lib/types/composeFile'; import type { Service, DestinationDocker, Prisma } from '@prisma/client'; +import { getServiceMainPort } from '$lib/components/common'; export const post: RequestHandler = async (event) => { const { teamId, status, body } = await getUserDetails(event); @@ -23,10 +24,12 @@ export const post: RequestHandler = async (event) => { destinationDockerId, destinationDocker, serviceSecret, + exposePort, hasura: { postgresqlUser, postgresqlPassword, postgresqlDatabase } } = service; const network = destinationDockerId && destinationDocker.network; const host = getEngine(destinationDocker.engine); + const port = getServiceMainPort('hasura'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); @@ -65,6 +68,7 @@ export const post: RequestHandler = async (event) => { volumes: [], restart: 'always', labels: makeLabelForServices('hasura'), + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), deploy: { restart_policy: { condition: 'on-failure', diff --git a/src/routes/services/[id]/index.svelte b/src/routes/services/[id]/index.svelte index a16591266..4d231a8fb 100644 --- a/src/routes/services/[id]/index.svelte +++ b/src/routes/services/[id]/index.svelte @@ -30,19 +30,43 @@
@@ -52,6 +76,7 @@
{service.name}
+ {#if service.fqdn}
+
+
Service Usage
+
+
+
+
Used Memory / Memory Limit
+
+ {usage?.MemUsage} +
+
+
+
Used CPU
+
+ {usage?.CPUPerc} +
+
+ +
+
Network IO
+
+ {usage?.NetIO} +
+
+
+
+
diff --git a/src/routes/services/[id]/languagetool/index.json.ts b/src/routes/services/[id]/languagetool/index.json.ts index d717502c5..dcba0b6f8 100644 --- a/src/routes/services/[id]/languagetool/index.json.ts +++ b/src/routes/services/[id]/languagetool/index.json.ts @@ -9,13 +9,15 @@ export const post: RequestHandler = async (event) => { const { id } = event.params; - let { name, fqdn } = await event.request.json(); + let { name, fqdn, exposePort } = await event.request.json(); if (fqdn) fqdn = fqdn.toLowerCase(); + if (exposePort) exposePort = Number(exposePort); try { - await db.updateService({ id, fqdn, name }); + await db.updateService({ id, fqdn, name, exposePort }); return { status: 201 }; } catch (error) { + console.log(error); return ErrorHandler(error); } }; diff --git a/src/routes/services/[id]/languagetool/start.json.ts b/src/routes/services/[id]/languagetool/start.json.ts index e11263161..0818ac6f7 100644 --- a/src/routes/services/[id]/languagetool/start.json.ts +++ b/src/routes/services/[id]/languagetool/start.json.ts @@ -6,6 +6,7 @@ import type { RequestHandler } from '@sveltejs/kit'; import { ErrorHandler, getServiceImage } from '$lib/database'; import { makeLabelForServices } from '$lib/buildPacks/common'; import type { ComposeFile } from '$lib/types/composeFile'; +import { getServiceMainPort } from '$lib/components/common'; export const post: RequestHandler = async (event) => { const { teamId, status, body } = await getUserDetails(event); @@ -15,9 +16,11 @@ export const post: RequestHandler = async (event) => { try { const service = await db.getService({ id, teamId }); - const { type, version, destinationDockerId, destinationDocker, serviceSecret } = service; + const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } = + service; const network = destinationDockerId && destinationDocker.network; const host = getEngine(destinationDocker.engine); + const port = getServiceMainPort('languagetool'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); @@ -42,6 +45,7 @@ export const post: RequestHandler = async (event) => { networks: [network], environment: config.environmentVariables, restart: 'always', + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), volumes: [config.volume], labels: makeLabelForServices('languagetool'), deploy: { diff --git a/src/routes/services/[id]/meilisearch/index.json.ts b/src/routes/services/[id]/meilisearch/index.json.ts index d717502c5..ff98ede6d 100644 --- a/src/routes/services/[id]/meilisearch/index.json.ts +++ b/src/routes/services/[id]/meilisearch/index.json.ts @@ -9,11 +9,12 @@ export const post: RequestHandler = async (event) => { const { id } = event.params; - let { name, fqdn } = await event.request.json(); + let { name, fqdn, exposePort } = await event.request.json(); if (fqdn) fqdn = fqdn.toLowerCase(); + if (exposePort) exposePort = Number(exposePort); try { - await db.updateService({ id, fqdn, name }); + await db.updateService({ id, fqdn, name, exposePort }); return { status: 201 }; } catch (error) { return ErrorHandler(error); diff --git a/src/routes/services/[id]/meilisearch/start.json.ts b/src/routes/services/[id]/meilisearch/start.json.ts index 1f11054a8..b019d69b9 100644 --- a/src/routes/services/[id]/meilisearch/start.json.ts +++ b/src/routes/services/[id]/meilisearch/start.json.ts @@ -6,6 +6,7 @@ import type { RequestHandler } from '@sveltejs/kit'; import { ErrorHandler, getServiceImage } from '$lib/database'; import { makeLabelForServices } from '$lib/buildPacks/common'; import type { ComposeFile } from '$lib/types/composeFile'; +import { getServiceMainPort } from '$lib/components/common'; export const post: RequestHandler = async (event) => { const { teamId, status, body } = await getUserDetails(event); @@ -18,9 +19,11 @@ export const post: RequestHandler = async (event) => { const { meiliSearch: { masterKey } } = service; - const { type, version, destinationDockerId, destinationDocker, serviceSecret } = service; + const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } = + service; const network = destinationDockerId && destinationDocker.network; const host = getEngine(destinationDocker.engine); + const port = getServiceMainPort('meilisearch'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); @@ -47,6 +50,7 @@ export const post: RequestHandler = async (event) => { networks: [network], environment: config.environmentVariables, restart: 'always', + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), volumes: [config.volume], labels: makeLabelForServices('meilisearch'), deploy: { diff --git a/src/routes/services/[id]/minio/index.json.ts b/src/routes/services/[id]/minio/index.json.ts index d717502c5..32056218d 100644 --- a/src/routes/services/[id]/minio/index.json.ts +++ b/src/routes/services/[id]/minio/index.json.ts @@ -9,11 +9,17 @@ export const post: RequestHandler = async (event) => { const { id } = event.params; - let { name, fqdn } = await event.request.json(); + let { + name, + fqdn, + exposePort, + minio: { apiFqdn } + } = await event.request.json(); if (fqdn) fqdn = fqdn.toLowerCase(); - + if (exposePort) exposePort = Number(exposePort); + if (apiFqdn) apiFqdn = apiFqdn.toLowerCase(); try { - await db.updateService({ id, fqdn, name }); + await db.updateMinioService({ id, fqdn, apiFqdn, name, exposePort }); return { status: 201 }; } catch (error) { return ErrorHandler(error); diff --git a/src/routes/services/[id]/minio/start.json.ts b/src/routes/services/[id]/minio/start.json.ts index c744eae80..9dd75bdd9 100644 --- a/src/routes/services/[id]/minio/start.json.ts +++ b/src/routes/services/[id]/minio/start.json.ts @@ -3,10 +3,10 @@ import * as db from '$lib/database'; import { promises as fs } from 'fs'; import yaml from 'js-yaml'; import type { RequestHandler } from '@sveltejs/kit'; -import { startHttpProxy } from '$lib/haproxy'; import { ErrorHandler, getFreePort, getServiceImage } from '$lib/database'; import { makeLabelForServices } from '$lib/buildPacks/common'; import type { ComposeFile } from '$lib/types/composeFile'; +import { getServiceMainPort } from '$lib/components/common'; export const post: RequestHandler = async (event) => { const { teamId, status, body } = await getUserDetails(event); @@ -22,17 +22,18 @@ export const post: RequestHandler = async (event) => { fqdn, destinationDockerId, destinationDocker, + exposePort, minio: { rootUser, rootUserPassword }, serviceSecret } = service; const network = destinationDockerId && destinationDocker.network; const host = getEngine(destinationDocker.engine); + const port = getServiceMainPort('minio'); const publicPort = await getFreePort(); const consolePort = 9001; - const apiPort = 9000; const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); @@ -62,6 +63,7 @@ export const post: RequestHandler = async (event) => { networks: [network], volumes: [config.volume], restart: 'always', + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('minio'), deploy: { restart_policy: { @@ -90,8 +92,7 @@ export const post: RequestHandler = async (event) => { try { await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); - await db.updateMinioService({ id, publicPort }); - await startHttpProxy(destinationDocker, id, publicPort, apiPort); + await db.updateMinioServicePort({ id, publicPort }); return { status: 200 }; diff --git a/src/routes/services/[id]/minio/stop.json.ts b/src/routes/services/[id]/minio/stop.json.ts index 3e2340237..4ae3c106b 100644 --- a/src/routes/services/[id]/minio/stop.json.ts +++ b/src/routes/services/[id]/minio/stop.json.ts @@ -12,12 +12,7 @@ export const post: RequestHandler = async (event) => { try { const service = await db.getService({ id, teamId }); - const { - destinationDockerId, - destinationDocker, - fqdn, - minio: { publicPort } - } = service; + const { destinationDockerId, destinationDocker } = service; await db.updateMinioService({ id, publicPort: null }); if (destinationDockerId) { const engine = destinationDocker.engine; @@ -30,11 +25,6 @@ export const post: RequestHandler = async (event) => { } catch (error) { console.error(error); } - try { - await stopTcpHttpProxy(destinationDocker, publicPort); - } catch (error) { - console.log(error); - } } return { diff --git a/src/routes/services/[id]/n8n/index.json.ts b/src/routes/services/[id]/n8n/index.json.ts index 5ec3fa69a..e269e8fe7 100644 --- a/src/routes/services/[id]/n8n/index.json.ts +++ b/src/routes/services/[id]/n8n/index.json.ts @@ -8,11 +8,12 @@ export const post: RequestHandler = async (event) => { if (status === 401) return { status, body }; const { id } = event.params; - let { name, fqdn } = await event.request.json(); + let { name, fqdn, exposePort } = await event.request.json(); if (fqdn) fqdn = fqdn.toLowerCase(); + if (exposePort) exposePort = Number(exposePort); try { - await db.updateService({ id, fqdn, name }); + await db.updateService({ id, fqdn, name, exposePort }); return { status: 201 }; } catch (error) { return ErrorHandler(error); diff --git a/src/routes/services/[id]/n8n/start.json.ts b/src/routes/services/[id]/n8n/start.json.ts index 7386a4fd9..073fede9b 100644 --- a/src/routes/services/[id]/n8n/start.json.ts +++ b/src/routes/services/[id]/n8n/start.json.ts @@ -6,6 +6,7 @@ import type { RequestHandler } from '@sveltejs/kit'; import { ErrorHandler, getServiceImage } from '$lib/database'; import { makeLabelForServices } from '$lib/buildPacks/common'; import type { ComposeFile } from '$lib/types/composeFile'; +import { getServiceMainPort } from '$lib/components/common'; export const post: RequestHandler = async (event) => { const { teamId, status, body } = await getUserDetails(event); @@ -15,9 +16,11 @@ export const post: RequestHandler = async (event) => { try { const service = await db.getService({ id, teamId }); - const { type, version, destinationDockerId, destinationDocker, serviceSecret } = service; + const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } = + service; const network = destinationDockerId && destinationDocker.network; const host = getEngine(destinationDocker.engine); + const port = getServiceMainPort('n8n'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); @@ -45,6 +48,7 @@ export const post: RequestHandler = async (event) => { environment: config.environmentVariables, restart: 'always', labels: makeLabelForServices('n8n'), + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), deploy: { restart_policy: { condition: 'on-failure', diff --git a/src/routes/services/[id]/nocodb/index.json.ts b/src/routes/services/[id]/nocodb/index.json.ts index 5ec3fa69a..e269e8fe7 100644 --- a/src/routes/services/[id]/nocodb/index.json.ts +++ b/src/routes/services/[id]/nocodb/index.json.ts @@ -8,11 +8,12 @@ export const post: RequestHandler = async (event) => { if (status === 401) return { status, body }; const { id } = event.params; - let { name, fqdn } = await event.request.json(); + let { name, fqdn, exposePort } = await event.request.json(); if (fqdn) fqdn = fqdn.toLowerCase(); + if (exposePort) exposePort = Number(exposePort); try { - await db.updateService({ id, fqdn, name }); + await db.updateService({ id, fqdn, name, exposePort }); return { status: 201 }; } catch (error) { return ErrorHandler(error); diff --git a/src/routes/services/[id]/nocodb/start.json.ts b/src/routes/services/[id]/nocodb/start.json.ts index 4933a5347..bf3a702df 100644 --- a/src/routes/services/[id]/nocodb/start.json.ts +++ b/src/routes/services/[id]/nocodb/start.json.ts @@ -6,6 +6,7 @@ import type { RequestHandler } from '@sveltejs/kit'; import { ErrorHandler, getServiceImage } from '$lib/database'; import { makeLabelForServices } from '$lib/buildPacks/common'; import type { ComposeFile } from '$lib/types/composeFile'; +import { getServiceMainPort } from '$lib/components/common'; export const post: RequestHandler = async (event) => { const { teamId, status, body } = await getUserDetails(event); @@ -15,9 +16,11 @@ export const post: RequestHandler = async (event) => { try { const service = await db.getService({ id, teamId }); - const { type, version, destinationDockerId, destinationDocker, serviceSecret } = service; + const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } = + service; const network = destinationDockerId && destinationDocker.network; const host = getEngine(destinationDocker.engine); + const port = getServiceMainPort('nocodb'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); @@ -40,6 +43,7 @@ export const post: RequestHandler = async (event) => { networks: [network], environment: config.environmentVariables, restart: 'always', + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('nocodb'), deploy: { restart_policy: { diff --git a/src/routes/services/[id]/plausibleanalytics/index.json.ts b/src/routes/services/[id]/plausibleanalytics/index.json.ts index 519b582a2..aeaf655ce 100644 --- a/src/routes/services/[id]/plausibleanalytics/index.json.ts +++ b/src/routes/services/[id]/plausibleanalytics/index.json.ts @@ -11,14 +11,29 @@ export const post: RequestHandler = async (event) => { let { name, fqdn, - plausibleAnalytics: { email, username } + exposePort, + plausibleAnalytics: { email, username, scriptName } } = await event.request.json(); if (fqdn) fqdn = fqdn.toLowerCase(); if (email) email = email.toLowerCase(); - + if (exposePort) exposePort = Number(exposePort); + if (scriptName) { + scriptName = scriptName.toLowerCase(); + if (scriptName.startsWith('/')) { + scriptName = scriptName.replaceAll(/\//gi, ''); + } + } try { - await db.updatePlausibleAnalyticsService({ id, fqdn, name, email, username }); + await db.updatePlausibleAnalyticsService({ + id, + fqdn, + name, + email, + username, + exposePort, + scriptName + }); return { status: 201 }; } catch (error) { return ErrorHandler(error); diff --git a/src/routes/services/[id]/plausibleanalytics/start.json.ts b/src/routes/services/[id]/plausibleanalytics/start.json.ts index 8b61e1ff8..e98e7093a 100644 --- a/src/routes/services/[id]/plausibleanalytics/start.json.ts +++ b/src/routes/services/[id]/plausibleanalytics/start.json.ts @@ -6,6 +6,7 @@ import type { RequestHandler } from '@sveltejs/kit'; import { ErrorHandler, getServiceImage } from '$lib/database'; import { makeLabelForServices } from '$lib/buildPacks/common'; import type { ComposeFile } from '$lib/types/composeFile'; +import { getServiceMainPort } from '$lib/components/common'; export const post: RequestHandler = async (event) => { const { teamId, status, body } = await getUserDetails(event); @@ -22,6 +23,7 @@ export const post: RequestHandler = async (event) => { destinationDockerId, destinationDocker, serviceSecret, + exposePort, plausibleAnalytics: { id: plausibleDbId, username, @@ -78,6 +80,7 @@ export const post: RequestHandler = async (event) => { } const network = destinationDockerId && destinationDocker.network; const host = getEngine(destinationDocker.engine); + const port = getServiceMainPort('plausibleanalytics'); const { workdir } = await createDirectories({ repository: type, buildId: id }); @@ -132,6 +135,7 @@ COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`; networks: [network], environment: config.plausibleAnalytics.environmentVariables, restart: 'always', + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), depends_on: [`${id}-postgresql`, `${id}-clickhouse`], labels: makeLabelForServices('plausibleAnalytics'), deploy: { diff --git a/src/routes/services/[id]/umami/index.json.ts b/src/routes/services/[id]/umami/index.json.ts index d717502c5..ff98ede6d 100644 --- a/src/routes/services/[id]/umami/index.json.ts +++ b/src/routes/services/[id]/umami/index.json.ts @@ -9,11 +9,12 @@ export const post: RequestHandler = async (event) => { const { id } = event.params; - let { name, fqdn } = await event.request.json(); + let { name, fqdn, exposePort } = await event.request.json(); if (fqdn) fqdn = fqdn.toLowerCase(); + if (exposePort) exposePort = Number(exposePort); try { - await db.updateService({ id, fqdn, name }); + await db.updateService({ id, fqdn, name, exposePort }); return { status: 201 }; } catch (error) { return ErrorHandler(error); diff --git a/src/routes/services/[id]/umami/start.json.ts b/src/routes/services/[id]/umami/start.json.ts index 6a51e5cde..7ba5547ed 100644 --- a/src/routes/services/[id]/umami/start.json.ts +++ b/src/routes/services/[id]/umami/start.json.ts @@ -8,6 +8,7 @@ import { makeLabelForServices } from '$lib/buildPacks/common'; import type { ComposeFile } from '$lib/types/composeFile'; import type { Service, DestinationDocker, Prisma } from '@prisma/client'; import bcrypt from 'bcryptjs'; +import { getServiceMainPort } from '$lib/components/common'; export const post: RequestHandler = async (event) => { const { teamId, status, body } = await getUserDetails(event); @@ -24,6 +25,7 @@ export const post: RequestHandler = async (event) => { destinationDockerId, destinationDocker, serviceSecret, + exposePort, umami: { umamiAdminPassword, postgresqlUser, @@ -34,6 +36,7 @@ export const post: RequestHandler = async (event) => { } = service; const network = destinationDockerId && destinationDocker.network; const host = getEngine(destinationDocker.engine); + const port = getServiceMainPort('umami'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); @@ -156,6 +159,7 @@ export const post: RequestHandler = async (event) => { networks: [network], volumes: [], restart: 'always', + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('umami'), deploy: { restart_policy: { diff --git a/src/routes/services/[id]/uptimekuma/index.json.ts b/src/routes/services/[id]/uptimekuma/index.json.ts index 5ec3fa69a..e269e8fe7 100644 --- a/src/routes/services/[id]/uptimekuma/index.json.ts +++ b/src/routes/services/[id]/uptimekuma/index.json.ts @@ -8,11 +8,12 @@ export const post: RequestHandler = async (event) => { if (status === 401) return { status, body }; const { id } = event.params; - let { name, fqdn } = await event.request.json(); + let { name, fqdn, exposePort } = await event.request.json(); if (fqdn) fqdn = fqdn.toLowerCase(); + if (exposePort) exposePort = Number(exposePort); try { - await db.updateService({ id, fqdn, name }); + await db.updateService({ id, fqdn, name, exposePort }); return { status: 201 }; } catch (error) { return ErrorHandler(error); diff --git a/src/routes/services/[id]/uptimekuma/start.json.ts b/src/routes/services/[id]/uptimekuma/start.json.ts index 6327db0ec..9032ce469 100644 --- a/src/routes/services/[id]/uptimekuma/start.json.ts +++ b/src/routes/services/[id]/uptimekuma/start.json.ts @@ -6,6 +6,7 @@ import type { RequestHandler } from '@sveltejs/kit'; import { ErrorHandler, getServiceImage } from '$lib/database'; import { makeLabelForServices } from '$lib/buildPacks/common'; import type { ComposeFile } from '$lib/types/composeFile'; +import { getServiceMainPort } from '$lib/components/common'; export const post: RequestHandler = async (event) => { const { teamId, status, body } = await getUserDetails(event); @@ -15,9 +16,11 @@ export const post: RequestHandler = async (event) => { try { const service = await db.getService({ id, teamId }); - const { type, version, destinationDockerId, destinationDocker, serviceSecret } = service; + const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } = + service; const network = destinationDockerId && destinationDocker.network; const host = getEngine(destinationDocker.engine); + const port = getServiceMainPort('uptimekuma'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); @@ -42,6 +45,7 @@ export const post: RequestHandler = async (event) => { volumes: [config.volume], environment: config.environmentVariables, restart: 'always', + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('uptimekuma'), deploy: { restart_policy: { diff --git a/src/routes/services/[id]/usage.json.ts b/src/routes/services/[id]/usage.json.ts new file mode 100644 index 000000000..24e27b1c4 --- /dev/null +++ b/src/routes/services/[id]/usage.json.ts @@ -0,0 +1,30 @@ +import { getUserDetails } from '$lib/common'; +import * as db from '$lib/database'; +import { ErrorHandler } from '$lib/database'; +import { getContainerUsage } from '$lib/haproxy'; +import type { RequestHandler } from '@sveltejs/kit'; + +export const get: RequestHandler = async (event) => { + const { teamId, status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + + const { id } = event.params; + + let usage = {}; + try { + const service = await db.getService({ id, teamId }); + if (service.destinationDockerId) { + [usage] = await Promise.all([getContainerUsage(service.destinationDocker.engine, id)]); + } + return { + status: 200, + body: { + usage + }, + headers: {} + }; + } catch (error) { + console.log(error); + return ErrorHandler(error); + } +}; diff --git a/src/routes/services/[id]/vaultwarden/index.json.ts b/src/routes/services/[id]/vaultwarden/index.json.ts index 5ec3fa69a..e269e8fe7 100644 --- a/src/routes/services/[id]/vaultwarden/index.json.ts +++ b/src/routes/services/[id]/vaultwarden/index.json.ts @@ -8,11 +8,12 @@ export const post: RequestHandler = async (event) => { if (status === 401) return { status, body }; const { id } = event.params; - let { name, fqdn } = await event.request.json(); + let { name, fqdn, exposePort } = await event.request.json(); if (fqdn) fqdn = fqdn.toLowerCase(); + if (exposePort) exposePort = Number(exposePort); try { - await db.updateService({ id, fqdn, name }); + await db.updateService({ id, fqdn, name, exposePort }); return { status: 201 }; } catch (error) { return ErrorHandler(error); diff --git a/src/routes/services/[id]/vaultwarden/start.json.ts b/src/routes/services/[id]/vaultwarden/start.json.ts index 511b040a3..41b790266 100644 --- a/src/routes/services/[id]/vaultwarden/start.json.ts +++ b/src/routes/services/[id]/vaultwarden/start.json.ts @@ -6,6 +6,7 @@ import type { RequestHandler } from '@sveltejs/kit'; import { getServiceImage, ErrorHandler } from '$lib/database'; import { makeLabelForServices } from '$lib/buildPacks/common'; import type { ComposeFile } from '$lib/types/composeFile'; +import { getServiceMainPort } from '$lib/components/common'; export const post: RequestHandler = async (event) => { const { teamId, status, body } = await getUserDetails(event); @@ -15,10 +16,12 @@ export const post: RequestHandler = async (event) => { try { const service = await db.getService({ id, teamId }); - const { type, version, destinationDockerId, destinationDocker, serviceSecret } = service; + const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } = + service; const network = destinationDockerId && destinationDocker.network; const host = getEngine(destinationDocker.engine); + const port = getServiceMainPort('vaultwarden'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); @@ -43,6 +46,7 @@ export const post: RequestHandler = async (event) => { networks: [network], volumes: [config.volume], restart: 'always', + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('vaultWarden'), deploy: { restart_policy: { diff --git a/src/routes/services/[id]/vscodeserver/index.json.ts b/src/routes/services/[id]/vscodeserver/index.json.ts index d717502c5..ff98ede6d 100644 --- a/src/routes/services/[id]/vscodeserver/index.json.ts +++ b/src/routes/services/[id]/vscodeserver/index.json.ts @@ -9,11 +9,12 @@ export const post: RequestHandler = async (event) => { const { id } = event.params; - let { name, fqdn } = await event.request.json(); + let { name, fqdn, exposePort } = await event.request.json(); if (fqdn) fqdn = fqdn.toLowerCase(); + if (exposePort) exposePort = Number(exposePort); try { - await db.updateService({ id, fqdn, name }); + await db.updateService({ id, fqdn, name, exposePort }); return { status: 201 }; } catch (error) { return ErrorHandler(error); diff --git a/src/routes/services/[id]/vscodeserver/start.json.ts b/src/routes/services/[id]/vscodeserver/start.json.ts index 4ca29c215..ba9807f12 100644 --- a/src/routes/services/[id]/vscodeserver/start.json.ts +++ b/src/routes/services/[id]/vscodeserver/start.json.ts @@ -6,6 +6,7 @@ import type { RequestHandler } from '@sveltejs/kit'; import { ErrorHandler, getServiceImage } from '$lib/database'; import { makeLabelForServices } from '$lib/buildPacks/common'; import type { ComposeFile } from '$lib/types/composeFile'; +import { getServiceMainPort } from '$lib/components/common'; export const post: RequestHandler = async (event) => { const { teamId, status, body } = await getUserDetails(event); @@ -22,11 +23,13 @@ export const post: RequestHandler = async (event) => { destinationDocker, serviceSecret, persistentStorage, + exposePort, vscodeserver: { password } } = service; const network = destinationDockerId && destinationDocker.network; const host = getEngine(destinationDocker.engine); + const port = getServiceMainPort('vscodeserver'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); @@ -75,6 +78,7 @@ export const post: RequestHandler = async (event) => { networks: [network], volumes: [config.volume, ...volumes], restart: 'always', + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('vscodeServer'), deploy: { restart_policy: { diff --git a/src/routes/services/[id]/wordpress/ftp.json.ts b/src/routes/services/[id]/wordpress/ftp.json.ts new file mode 100644 index 000000000..fab9cd7df --- /dev/null +++ b/src/routes/services/[id]/wordpress/ftp.json.ts @@ -0,0 +1,187 @@ +import { dev } from '$app/env'; +import { asyncExecShell, getEngine, getUserDetails } from '$lib/common'; +import { decrypt, encrypt } from '$lib/crypto'; +import * as db from '$lib/database'; +import { ErrorHandler, generatePassword, getFreePort } from '$lib/database'; +import { + checkContainer, + startTcpProxy, + startTraefikTCPProxy, + stopTcpHttpProxy +} from '$lib/haproxy'; +import type { ComposeFile } from '$lib/types/composeFile'; +import type { RequestHandler } from '@sveltejs/kit'; +import cuid from 'cuid'; +import fs from 'fs/promises'; +import yaml from 'js-yaml'; + +export const post: RequestHandler = async (event) => { + const { status, body, teamId } = await getUserDetails(event); + if (status === 401) return { status, body }; + + const { id } = event.params; + + const { ftpEnabled } = await event.request.json(); + const publicPort = await getFreePort(); + + let ftpUser = cuid(); + let ftpPassword = generatePassword(); + + const hostkeyDir = dev ? '/tmp/hostkeys' : '/app/ssl/hostkeys'; + try { + const data = await db.prisma.wordpress.update({ + where: { serviceId: id }, + data: { ftpEnabled }, + include: { service: { include: { destinationDocker: true } } } + }); + const { + service: { destinationDockerId, destinationDocker }, + ftpPublicPort, + ftpUser: user, + ftpPassword: savedPassword, + ftpHostKey, + ftpHostKeyPrivate + } = data; + const { network, engine } = destinationDocker; + const settings = await db.prisma.setting.findFirst(); + const host = getEngine(engine); + if (ftpEnabled) { + if (user) ftpUser = user; + if (savedPassword) ftpPassword = decrypt(savedPassword); + + const { stdout: password } = await asyncExecShell( + `echo ${ftpPassword} | openssl passwd -1 -stdin` + ); + if (destinationDockerId) { + try { + await fs.stat(hostkeyDir); + } catch (error) { + await asyncExecShell(`mkdir -p ${hostkeyDir}`); + } + if (!ftpHostKey) { + await asyncExecShell( + `ssh-keygen -t ed25519 -f ssh_host_ed25519_key -N "" -q -f ${hostkeyDir}/${id}.ed25519` + ); + const { stdout: ftpHostKey } = await asyncExecShell(`cat ${hostkeyDir}/${id}.ed25519`); + await db.prisma.wordpress.update({ + where: { serviceId: id }, + data: { ftpHostKey: encrypt(ftpHostKey) } + }); + } else { + await asyncExecShell(`echo "${decrypt(ftpHostKey)}" > ${hostkeyDir}/${id}.ed25519`); + } + if (!ftpHostKeyPrivate) { + await asyncExecShell(`ssh-keygen -t rsa -b 4096 -N "" -f ${hostkeyDir}/${id}.rsa`); + const { stdout: ftpHostKeyPrivate } = await asyncExecShell(`cat ${hostkeyDir}/${id}.rsa`); + await db.prisma.wordpress.update({ + where: { serviceId: id }, + data: { ftpHostKeyPrivate: encrypt(ftpHostKeyPrivate) } + }); + } else { + await asyncExecShell(`echo "${decrypt(ftpHostKeyPrivate)}" > ${hostkeyDir}/${id}.rsa`); + } + + await db.prisma.wordpress.update({ + where: { serviceId: id }, + data: { + ftpPublicPort: publicPort, + ftpUser: user ? undefined : ftpUser, + ftpPassword: savedPassword ? undefined : encrypt(ftpPassword) + } + }); + + try { + const isRunning = await checkContainer(engine, `${id}-ftp`); + if (isRunning) { + await asyncExecShell( + `DOCKER_HOST=${host} docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp` + ); + } + } catch (error) { + console.log(error); + // + } + const volumes = [ + `${id}-wordpress-data:/home/${ftpUser}`, + `${ + dev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys' + }/${id}.ed25519:/etc/ssh/ssh_host_ed25519_key`, + `${ + dev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys' + }/${id}.rsa:/etc/ssh/ssh_host_rsa_key`, + `${ + dev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys' + }/${id}.sh:/etc/sftp.d/chmod.sh` + ]; + + const compose: ComposeFile = { + version: '3.8', + services: { + [`${id}-ftp`]: { + image: `atmoz/sftp:alpine`, + command: `'${ftpUser}:${password.replace('\n', '').replace(/\$/g, '$$$')}:e:33'`, + extra_hosts: ['host.docker.internal:host-gateway'], + container_name: `${id}-ftp`, + volumes, + networks: [network], + depends_on: [], + restart: 'always' + } + }, + networks: { + [network]: { + external: true + } + }, + volumes: { + [`${id}-wordpress-data`]: { + external: true, + name: `${id}-wordpress-data` + } + } + }; + await fs.writeFile( + `${hostkeyDir}/${id}.sh`, + `#!/bin/bash\nchmod 600 /etc/ssh/ssh_host_ed25519_key /etc/ssh/ssh_host_rsa_key` + ); + await asyncExecShell(`chmod +x ${hostkeyDir}/${id}.sh`); + await fs.writeFile(`${hostkeyDir}/${id}-docker-compose.yml`, yaml.dump(compose)); + await asyncExecShell( + `DOCKER_HOST=${host} docker compose -f ${hostkeyDir}/${id}-docker-compose.yml up -d` + ); + } + return { + status: 201, + body: { + publicPort, + ftpUser, + ftpPassword + } + }; + } else { + await db.prisma.wordpress.update({ + where: { serviceId: id }, + data: { ftpPublicPort: null } + }); + try { + await asyncExecShell( + `DOCKER_HOST=${host} docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp` + ); + } catch (error) { + // + } + await stopTcpHttpProxy(id, destinationDocker, ftpPublicPort); + return { + status: 200, + body: {} + }; + } + } catch (error) { + console.log(error); + return ErrorHandler(error); + } finally { + await asyncExecShell( + `rm -f ${hostkeyDir}/${id}-docker-compose.yml ${hostkeyDir}/${id}.ed25519 ${hostkeyDir}/${id}.ed25519.pub ${hostkeyDir}/${id}.rsa ${hostkeyDir}/${id}.rsa.pub ${hostkeyDir}/${id}.sh` + ); + } +}; diff --git a/src/routes/services/[id]/wordpress/index.json.ts b/src/routes/services/[id]/wordpress/index.json.ts index 413bd1f75..b10bfeefc 100644 --- a/src/routes/services/[id]/wordpress/index.json.ts +++ b/src/routes/services/[id]/wordpress/index.json.ts @@ -11,12 +11,27 @@ export const post: RequestHandler = async (event) => { let { name, fqdn, - wordpress: { extraConfig, mysqlDatabase } + exposePort, + ownMysql, + wordpress: { extraConfig, mysqlDatabase, mysqlHost, mysqlPort, mysqlUser, mysqlPassword } } = await event.request.json(); if (fqdn) fqdn = fqdn.toLowerCase(); - + if (exposePort) exposePort = Number(exposePort); + if (mysqlPort) mysqlPort = Number(mysqlPort); try { - await db.updateWordpress({ id, fqdn, name, extraConfig, mysqlDatabase }); + await db.updateWordpress({ + id, + fqdn, + name, + extraConfig, + ownMysql, + mysqlDatabase, + exposePort, + mysqlHost, + mysqlPort, + mysqlUser, + mysqlPassword + }); return { status: 201 }; } catch (error) { return ErrorHandler(error); diff --git a/src/routes/services/[id]/wordpress/settings.json.ts b/src/routes/services/[id]/wordpress/settings.json.ts index 4f9c1f9b2..4848dd220 100644 --- a/src/routes/services/[id]/wordpress/settings.json.ts +++ b/src/routes/services/[id]/wordpress/settings.json.ts @@ -8,7 +8,6 @@ import type { ComposeFile } from '$lib/types/composeFile'; import type { RequestHandler } from '@sveltejs/kit'; import cuid from 'cuid'; import fs from 'fs/promises'; -import getPort, { portNumbers } from 'get-port'; import yaml from 'js-yaml'; export const post: RequestHandler = async (event) => { @@ -17,170 +16,17 @@ export const post: RequestHandler = async (event) => { const { id } = event.params; - const { ftpEnabled } = await event.request.json(); - const publicPort = await getFreePort(); - - let ftpUser = cuid(); - let ftpPassword = generatePassword(); - - const hostkeyDir = dev ? '/tmp/hostkeys' : '/app/ssl/hostkeys'; + const { ownMysql } = await event.request.json(); try { - const data = await db.prisma.wordpress.update({ + await db.prisma.wordpress.update({ where: { serviceId: id }, - data: { ftpEnabled }, - include: { service: { include: { destinationDocker: true } } } + data: { ownMysql } }); - const { - service: { destinationDockerId, destinationDocker }, - ftpPublicPort: oldPublicPort, - ftpUser: user, - ftpPassword: savedPassword, - ftpHostKey, - ftpHostKeyPrivate - } = data; - if (user) ftpUser = user; - if (savedPassword) ftpPassword = decrypt(savedPassword); - - const { stdout: password } = await asyncExecShell( - `echo ${ftpPassword} | openssl passwd -1 -stdin` - ); - if (destinationDockerId) { - try { - await fs.stat(hostkeyDir); - } catch (error) { - await asyncExecShell(`mkdir -p ${hostkeyDir}`); - } - if (!ftpHostKey) { - await asyncExecShell( - `ssh-keygen -t ed25519 -f ssh_host_ed25519_key -N "" -q -f ${hostkeyDir}/${id}.ed25519` - ); - const { stdout: ftpHostKey } = await asyncExecShell(`cat ${hostkeyDir}/${id}.ed25519`); - await db.prisma.wordpress.update({ - where: { serviceId: id }, - data: { ftpHostKey: encrypt(ftpHostKey) } - }); - } else { - await asyncExecShell(`echo "${decrypt(ftpHostKey)}" > ${hostkeyDir}/${id}.ed25519`); - } - if (!ftpHostKeyPrivate) { - await asyncExecShell(`ssh-keygen -t rsa -b 4096 -N "" -f ${hostkeyDir}/${id}.rsa`); - const { stdout: ftpHostKeyPrivate } = await asyncExecShell(`cat ${hostkeyDir}/${id}.rsa`); - await db.prisma.wordpress.update({ - where: { serviceId: id }, - data: { ftpHostKeyPrivate: encrypt(ftpHostKeyPrivate) } - }); - } else { - await asyncExecShell(`echo "${decrypt(ftpHostKeyPrivate)}" > ${hostkeyDir}/${id}.rsa`); - } - const { network, engine } = destinationDocker; - const host = getEngine(engine); - if (ftpEnabled) { - await db.prisma.wordpress.update({ - where: { serviceId: id }, - data: { - ftpPublicPort: publicPort, - ftpUser: user ? undefined : ftpUser, - ftpPassword: savedPassword ? undefined : encrypt(ftpPassword) - } - }); - - try { - const isRunning = await checkContainer(engine, `${id}-ftp`); - if (isRunning) { - await asyncExecShell( - `DOCKER_HOST=${host} docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp` - ); - } - } catch (error) { - console.log(error); - // - } - const volumes = [ - `${id}-wordpress-data:/home/${ftpUser}`, - `${ - dev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys' - }/${id}.ed25519:/etc/ssh/ssh_host_ed25519_key`, - `${ - dev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys' - }/${id}.rsa:/etc/ssh/ssh_host_rsa_key`, - `${ - dev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys' - }/${id}.sh:/etc/sftp.d/chmod.sh` - ]; - - const compose: ComposeFile = { - version: '3.8', - services: { - [`${id}-ftp`]: { - image: `atmoz/sftp:alpine`, - command: `'${ftpUser}:${password.replace('\n', '').replace(/\$/g, '$$$')}:e:33'`, - extra_hosts: ['host.docker.internal:host-gateway'], - container_name: `${id}-ftp`, - volumes, - networks: [network], - depends_on: [], - restart: 'always' - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: { - [`${id}-wordpress-data`]: { - external: true, - name: `${id}-wordpress-data` - } - } - }; - await fs.writeFile( - `${hostkeyDir}/${id}.sh`, - `#!/bin/bash\nchmod 600 /etc/ssh/ssh_host_ed25519_key /etc/ssh/ssh_host_rsa_key` - ); - await asyncExecShell(`chmod +x ${hostkeyDir}/${id}.sh`); - await fs.writeFile(`${hostkeyDir}/${id}-docker-compose.yml`, yaml.dump(compose)); - await asyncExecShell( - `DOCKER_HOST=${host} docker compose -f ${hostkeyDir}/${id}-docker-compose.yml up -d` - ); - - await startTcpProxy(destinationDocker, `${id}-ftp`, publicPort, 22); - } else { - await db.prisma.wordpress.update({ - where: { serviceId: id }, - data: { ftpPublicPort: null } - }); - try { - await asyncExecShell( - `DOCKER_HOST=${host} docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp` - ); - } catch (error) { - // - } - await stopTcpHttpProxy(destinationDocker, oldPublicPort); - } - } - if (ftpEnabled) { - return { - status: 201, - body: { - publicPort, - ftpUser, - ftpPassword - } - }; - } else { - return { - status: 200, - body: {} - }; - } + return { + status: 201 + }; } catch (error) { console.log(error); return ErrorHandler(error); - } finally { - await asyncExecShell( - `rm -f ${hostkeyDir}/${id}-docker-compose.yml ${hostkeyDir}/${id}.ed25519 ${hostkeyDir}/${id}.ed25519.pub ${hostkeyDir}/${id}.rsa ${hostkeyDir}/${id}.rsa.pub ${hostkeyDir}/${id}.sh` - ); } }; diff --git a/src/routes/services/[id]/wordpress/start.json.ts b/src/routes/services/[id]/wordpress/start.json.ts index 0572be971..8375c06d0 100644 --- a/src/routes/services/[id]/wordpress/start.json.ts +++ b/src/routes/services/[id]/wordpress/start.json.ts @@ -6,6 +6,7 @@ import type { RequestHandler } from '@sveltejs/kit'; import { ErrorHandler, getServiceImage } from '$lib/database'; import { makeLabelForServices } from '$lib/buildPacks/common'; import type { ComposeFile } from '$lib/types/composeFile'; +import { getServiceMainPort } from '$lib/components/common'; export const post: RequestHandler = async (event) => { const { teamId, status, body } = await getUserDetails(event); @@ -22,19 +23,24 @@ export const post: RequestHandler = async (event) => { destinationDockerId, serviceSecret, destinationDocker, + exposePort, wordpress: { mysqlDatabase, + mysqlHost, + mysqlPort, mysqlUser, mysqlPassword, extraConfig, mysqlRootUser, - mysqlRootUserPassword + mysqlRootUserPassword, + ownMysql } } = service; const network = destinationDockerId && destinationDocker.network; const host = getEngine(destinationDocker.engine); const image = getServiceImage(type); + const port = getServiceMainPort('wordpress'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const config = { @@ -42,7 +48,7 @@ export const post: RequestHandler = async (event) => { image: `${image}:${version}`, volume: `${id}-wordpress-data:/var/www/html`, environmentVariables: { - WORDPRESS_DB_HOST: `${id}-mysql`, + WORDPRESS_DB_HOST: ownMysql ? `${mysqlHost}:${mysqlPort}` : `${id}-mysql`, WORDPRESS_DB_USER: mysqlUser, WORDPRESS_DB_PASSWORD: mysqlPassword, WORDPRESS_DB_NAME: mysqlDatabase, @@ -66,7 +72,7 @@ export const post: RequestHandler = async (event) => { config.wordpress.environmentVariables[secret.name] = secret.value; }); } - const composeFile: ComposeFile = { + let composeFile: ComposeFile = { version: '3.8', services: { [id]: { @@ -76,7 +82,7 @@ export const post: RequestHandler = async (event) => { volumes: [config.wordpress.volume], networks: [network], restart: 'always', - depends_on: [`${id}-mysql`], + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('wordpress'), deploy: { restart_policy: { @@ -86,22 +92,6 @@ export const post: RequestHandler = async (event) => { window: '120s' } } - }, - [`${id}-mysql`]: { - container_name: `${id}-mysql`, - image: config.mysql.image, - volumes: [config.mysql.volume], - environment: config.mysql.environmentVariables, - networks: [network], - restart: 'always', - deploy: { - restart_policy: { - condition: 'on-failure', - delay: '5s', - max_attempts: 3, - window: '120s' - } - } } }, networks: { @@ -112,12 +102,32 @@ export const post: RequestHandler = async (event) => { volumes: { [config.wordpress.volume.split(':')[0]]: { name: config.wordpress.volume.split(':')[0] - }, - [config.mysql.volume.split(':')[0]]: { - name: config.mysql.volume.split(':')[0] } } }; + if (!ownMysql) { + composeFile.services[id].depends_on = [`${id}-mysql`]; + composeFile.services[`${id}-mysql`] = { + container_name: `${id}-mysql`, + image: config.mysql.image, + volumes: [config.mysql.volume], + environment: config.mysql.environmentVariables, + networks: [network], + restart: 'always', + deploy: { + restart_policy: { + condition: 'on-failure', + delay: '5s', + max_attempts: 3, + window: '120s' + } + } + }; + + composeFile.volumes[config.mysql.volume.split(':')[0]] = { + name: config.mysql.volume.split(':')[0] + }; + } const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); try { diff --git a/src/routes/services/index.svelte b/src/routes/services/index.svelte index bab3a4322..32c7e14ea 100644 --- a/src/routes/services/index.svelte +++ b/src/routes/services/index.svelte @@ -55,7 +55,7 @@
-
+
{#if !services || ownServices.length === 0}
{$t('service.no_service')}
diff --git a/src/routes/settings/check.json.ts b/src/routes/settings/check.json.ts index bec577001..88eaa0293 100644 --- a/src/routes/settings/check.json.ts +++ b/src/routes/settings/check.json.ts @@ -1,25 +1,54 @@ -import { asyncExecShell, getEngine, getUserDetails } from '$lib/common'; +import { dev } from '$app/env'; +import { checkDomainsIsValidInDNS, getDomain, getUserDetails, isDNSValid } from '$lib/common'; import * as db from '$lib/database'; import { ErrorHandler } from '$lib/database'; import { t } from '$lib/translations'; import type { RequestHandler } from '@sveltejs/kit'; +export const get: RequestHandler = async (event) => { + const { status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + const domain = event.url.searchParams.get('domain'); + if (!domain) { + return { + status: 500, + body: { + message: t.get('application.domain_required') + } + }; + } + try { + await isDNSValid(event, domain); + return { + status: 200 + }; + } catch (error) { + return ErrorHandler(error); + } +}; + export const post: RequestHandler = async (event) => { const { status, body } = await getUserDetails(event); if (status === 401) return { status, body }; const { id } = event.params; - let { fqdn } = await event.request.json(); + let { fqdn, forceSave, dualCerts, isDNSCheckEnabled } = await event.request.json(); if (fqdn) fqdn = fqdn.toLowerCase(); - try { const found = await db.isDomainConfigured({ id, fqdn }); + console.log(found); + if (found) { + throw { + message: t.get('application.domain_already_in_use', { + domain: getDomain(fqdn).replace('www.', '') + }) + }; + } + if (isDNSCheckEnabled && !forceSave) { + return await checkDomainsIsValidInDNS({ event, fqdn, dualCerts }); + } return { - status: found ? 500 : 200, - body: { - error: - found && t.get('application.domain_already_in_use', { domain: fqdn.replace('www.', '') }) - } + status: 200 }; } catch (error) { return ErrorHandler(error); diff --git a/src/routes/settings/index.json.ts b/src/routes/settings/index.json.ts index 5546d782f..e57b159c6 100644 --- a/src/routes/settings/index.json.ts +++ b/src/routes/settings/index.json.ts @@ -1,3 +1,4 @@ +import { dev } from '$app/env'; import { getUserDetails } from '$lib/common'; import * as db from '$lib/database'; import { listSettings, ErrorHandler } from '$lib/database'; diff --git a/src/routes/settings/index.svelte b/src/routes/settings/index.svelte index 6a9b6ba5a..6eeeb2bdf 100644 --- a/src/routes/settings/index.svelte +++ b/src/routes/settings/index.svelte @@ -31,27 +31,33 @@ import Setting from '$lib/components/Setting.svelte'; import Explainer from '$lib/components/Explainer.svelte'; import { errorNotification } from '$lib/form'; - import { del, post } from '$lib/api'; + import { del, get, post } from '$lib/api'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import { browser } from '$app/env'; import { getDomain } from '$lib/components/common'; import { toast } from '@zerodevx/svelte-toast'; import { t } from '$lib/translations'; - import { features } from '$lib/store'; + import { features, isTraefikUsed } from '$lib/store'; let isRegistrationEnabled = settings.isRegistrationEnabled; let dualCerts = settings.dualCerts; let isAutoUpdateEnabled = settings.isAutoUpdateEnabled; let isDNSCheckEnabled = settings.isDNSCheckEnabled; + $isTraefikUsed = settings.isTraefikUsed; let minPort = settings.minPort; let maxPort = settings.maxPort; + let forceSave = false; let fqdn = settings.fqdn; + let nonWWWDomain = fqdn && getDomain(fqdn).replace(/^www\./, ''); + let isNonWWWDomainOK = false; + let isWWWDomainOK = false; let isFqdnSet = !!settings.fqdn; let loading = { save: false, - remove: false + remove: false, + proxyMigration: false }; async function removeFqdn() { @@ -69,6 +75,7 @@ } async function changeSettings(name) { try { + resetView(); if (name === 'isRegistrationEnabled') { isRegistrationEnabled = !isRegistrationEnabled; } @@ -81,6 +88,7 @@ if (name === 'isDNSCheckEnabled') { isDNSCheckEnabled = !isDNSCheckEnabled; } + await post(`/settings.json`, { isRegistrationEnabled, dualCerts, @@ -95,8 +103,10 @@ async function handleSubmit() { try { loading.save = true; + nonWWWDomain = fqdn && getDomain(fqdn).replace(/^www\./, ''); + if (fqdn !== settings.fqdn) { - await post(`/settings/check.json`, { fqdn }); + await post(`/settings/check.json`, { fqdn, forceSave, dualCerts, isDNSCheckEnabled }); await post(`/settings.json`, { fqdn }); return window.location.reload(); } @@ -105,12 +115,64 @@ settings.minPort = minPort; settings.maxPort = maxPort; } + forceSave = false; } catch ({ error }) { + if (error?.startsWith($t('application.dns_not_set_partial_error'))) { + forceSave = true; + if (dualCerts) { + isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false); + isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true); + } else { + const isWWW = getDomain(settings.fqdn).includes('www.'); + if (isWWW) { + isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true); + } else { + isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false); + } + } + } return errorNotification(error); } finally { loading.save = false; } } + async function renewCerts() { + try { + toast.push('Renewing certificates...'); + return await post(`/settings/renew.json`, {}); + } catch ({ error }) { + return errorNotification(error); + } + } + async function isDNSValid(domain, isWWW) { + try { + await get(`/settings/check.json?domain=${domain}`); + toast.push('DNS configuration is valid.'); + isWWW ? (isWWWDomainOK = true) : (isNonWWWDomainOK = true); + return true; + } catch ({ error }) { + errorNotification(error); + isWWW ? (isWWWDomainOK = false) : (isNonWWWDomainOK = false); + return false; + } + } + function resetView() { + forceSave = false; + } + async function migrateProxy(to) { + if (loading.proxyMigration) return; + try { + loading.proxyMigration = true; + await post(`/update.json`, { type: to }); + const data = await get(`/settings.json`); + $isTraefikUsed = data.settings.isTraefikUsed; + return toast.push('Proxy migration started, it takes a few seconds.'); + } catch ({ error }) { + return errorNotification(error); + } finally { + loading.proxyMigration = false; + } + }
@@ -123,11 +185,18 @@
{$t('index.global_settings')}
{loading.save + ? $t('forms.saving') + : forceSave + ? $t('forms.confirm_continue') + : $t('forms.save')} + {#if isFqdnSet} +
@@ -152,11 +241,47 @@ bind:value={fqdn} readonly={!$session.isAdmin || isFqdnSet} disabled={!$session.isAdmin || isFqdnSet} + on:input={resetView} name="fqdn" id="fqdn" pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$" placeholder="{$t('forms.eg')}: https://coolify.io" /> + + {#if forceSave} +
+ {#if isNonWWWDomainOK} + + {:else} + + {/if} + {#if dualCerts} + {#if isWWWDomainOK} + + {:else} + + {/if} + {/if} +
+ {/if}
@@ -219,6 +344,19 @@ on:click={() => changeSettings('isAutoUpdateEnabled')} />
+
+
+
+ Renew SSL Certificates manually +
+ +
+
+ +
+
{/if}
diff --git a/src/routes/settings/renew.json.ts b/src/routes/settings/renew.json.ts new file mode 100644 index 000000000..3cbb8f4ad --- /dev/null +++ b/src/routes/settings/renew.json.ts @@ -0,0 +1,26 @@ +import { getUserDetails } from '$lib/common'; +import { ErrorHandler } from '$lib/database'; +import { renewSSLCerts } from '$lib/letsencrypt'; +import { t } from '$lib/translations'; +import type { RequestHandler } from '@sveltejs/kit'; + +export const post: RequestHandler = async (event) => { + const { teamId, status, body } = await getUserDetails(event); + if (teamId !== '0') + return { + status: 401, + body: { + message: t.get('setting.permission_denied') + } + }; + if (status === 401) return { status, body }; + + try { + renewSSLCerts(); + return { + status: 201 + }; + } catch (error) { + return ErrorHandler(error); + } +}; diff --git a/src/routes/sources/[id]/_Github.svelte b/src/routes/sources/[id]/_Github.svelte index b03fe8abd..6d91f2699 100644 --- a/src/routes/sources/[id]/_Github.svelte +++ b/src/routes/sources/[id]/_Github.svelte @@ -80,10 +80,13 @@
- {#if !source.githubAppId} + {#if !source.githubApp?.installationId}
General
+ {#if source.apiUrl && source.htmlUrl && source.name} + + {/if}
@@ -117,11 +120,6 @@ />
- {#if source.apiUrl && source.htmlUrl && source.name} -
- -
- {/if}
{:else if source.githubApp?.installationId}
diff --git a/src/routes/sources/[id]/index.svelte b/src/routes/sources/[id]/index.svelte index 7538dbbbd..ffdd7d6a1 100644 --- a/src/routes/sources/[id]/index.svelte +++ b/src/routes/sources/[id]/index.svelte @@ -68,14 +68,52 @@ } -
-
{$t('application.git_source')}
- > - {source.name} +
+
+
+ Configuration +
+ {source.name} +
+ {#if source?.type === 'gitlab'} + + + + {:else if source?.type === 'github'} + + + + {/if}
- {#if !source.gitlabAppId && !source.githubAppId} + {#if !source.gitlabAppId && (!source.githubAppId || !source.githubApp?.installationId)}
Select a provider
diff --git a/src/routes/sources/index.svelte b/src/routes/sources/index.svelte index 72489bdea..8d85a89db 100644 --- a/src/routes/sources/index.svelte +++ b/src/routes/sources/index.svelte @@ -62,7 +62,7 @@ {/if}
-
+
{#if !sources || ownSources.length === 0}
{$t('source.no_git_sources_found')}
@@ -74,11 +74,48 @@ {#each ownSources as source}
+
+ {#if source?.type === 'gitlab'} + + + + {:else if source?.type === 'github'} + + + + {/if} +
{source.name}
{#if $session.teamId === '0' && otherSources.length > 0}
{source.teams[0].name}
diff --git a/src/routes/update.json.ts b/src/routes/update.json.ts index 19845b90e..8d697f32e 100644 --- a/src/routes/update.json.ts +++ b/src/routes/update.json.ts @@ -6,6 +6,7 @@ import * as db from '$lib/database'; import type { RequestHandler } from '@sveltejs/kit'; import compare from 'compare-versions'; import got from 'got'; +import { checkContainer, startCoolifyProxy, startTraefikProxy } from '$lib/haproxy'; export const get: RequestHandler = async (request) => { try { @@ -34,14 +35,14 @@ export const get: RequestHandler = async (request) => { export const post: RequestHandler = async (event) => { const { type, latestVersion } = await event.request.json(); + const settings = await db.prisma.setting.findFirst(); if (type === 'update') { try { if (!dev) { - const { isAutoUpdateEnabled } = await db.prisma.setting.findFirst(); await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`); await asyncExecShell(`env | grep COOLIFY > .env`); await asyncExecShell( - `sed -i '/COOLIFY_AUTO_UPDATE=/c\COOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env` + `sed -i '/COOLIFY_AUTO_UPDATE=/c\COOLIFY_AUTO_UPDATE=${settings.isAutoUpdateEnabled}' .env` ); await asyncExecShell( `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-redis && docker rm coolify coolify-redis && docker compose up -d --force-recreate"` @@ -61,6 +62,44 @@ export const post: RequestHandler = async (event) => { } catch (error) { return ErrorHandler(error); } + } else if (type === 'traefik') { + try { + // const found = await checkContainer('/var/run/docker.sock', 'coolify-haproxy'); + // if (found) { + // await asyncExecShell(`docker stop -t 0 coolify-haproxy`); + // await asyncExecShell(`docker rm coolify-haproxy`); + // } + // await startTraefikProxy('/var/run/docker.sock'); + await db.prisma.setting.update({ + where: { id: settings.id }, + data: { isTraefikUsed: true } + }); + return { + status: 200, + body: {} + }; + } catch (error) { + return ErrorHandler(error); + } + } else if (type === 'haproxy') { + try { + // const found = await checkContainer('/var/run/docker.sock', 'coolify-proxy'); + // if (found) { + // await asyncExecShell(`docker stop -t 0 coolify-proxy`); + // await asyncExecShell(`docker rm coolify-proxy`); + // } + // await startCoolifyProxy('/var/run/docker.sock'); + await db.prisma.setting.update({ + where: { id: settings.id }, + data: { isTraefikUsed: false } + }); + return { + status: 200, + body: {} + }; + } catch (error) { + return ErrorHandler(error); + } } return { status: 500 diff --git a/src/routes/webhooks/github/events.ts b/src/routes/webhooks/github/events.ts index 74214a7ba..cc777a6c5 100644 --- a/src/routes/webhooks/github/events.ts +++ b/src/routes/webhooks/github/events.ts @@ -73,6 +73,7 @@ export const post: RequestHandler = async (event) => { JSON.stringify({ buildPack: applicationFound.buildPack, port: applicationFound.port, + exposePort: applicationFound.exposePort, installCommand: applicationFound.installCommand, buildCommand: applicationFound.buildCommand, startCommand: applicationFound.startCommand diff --git a/src/routes/webhooks/gitlab/events.ts b/src/routes/webhooks/gitlab/events.ts index d84646088..6d2cdb65c 100644 --- a/src/routes/webhooks/gitlab/events.ts +++ b/src/routes/webhooks/gitlab/events.ts @@ -46,6 +46,7 @@ export const post: RequestHandler = async (event) => { JSON.stringify({ buildPack: applicationFound.buildPack, port: applicationFound.port, + exposePort: applicationFound.exposePort, installCommand: applicationFound.installCommand, buildCommand: applicationFound.buildCommand, startCommand: applicationFound.startCommand diff --git a/src/routes/webhooks/traefik/main.json.ts b/src/routes/webhooks/traefik/main.json.ts new file mode 100644 index 000000000..467dcddf7 --- /dev/null +++ b/src/routes/webhooks/traefik/main.json.ts @@ -0,0 +1,360 @@ +import { dev } from '$app/env'; +import { asyncExecShell, getDomain, getEngine } from '$lib/common'; +import { supportedServiceTypesAndVersions } from '$lib/components/common'; +import * as db from '$lib/database'; +import { listServicesWithIncludes } from '$lib/database'; +import { checkContainer } from '$lib/haproxy'; +import type { RequestHandler } from '@sveltejs/kit'; + +function configureMiddleware( + { id, container, port, domain, nakedDomain, isHttps, isWWW, isDualCerts }, + traefik +) { + if (isHttps) { + traefik.http.routers[id] = { + entrypoints: ['web'], + rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`, + service: `${id}`, + middlewares: ['redirect-to-https'] + }; + + traefik.http.services[id] = { + loadbalancer: { + servers: [ + { + url: `http://${container}:${port}` + } + ] + } + }; + + if (isDualCerts) { + traefik.http.routers[`${id}-secure`] = { + entrypoints: ['websecure'], + rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`, + service: `${id}`, + tls: { + certresolver: 'letsencrypt' + }, + middlewares: [] + }; + } else { + if (isWWW) { + traefik.http.routers[`${id}-secure-www`] = { + entrypoints: ['websecure'], + rule: `Host(\`www.${nakedDomain}\`)`, + service: `${id}`, + tls: { + certresolver: 'letsencrypt' + }, + middlewares: [] + }; + traefik.http.routers[`${id}-secure`] = { + entrypoints: ['websecure'], + rule: `Host(\`${nakedDomain}\`)`, + service: `${id}`, + tls: { + domains: { + main: `${domain}` + } + }, + middlewares: ['redirect-to-www'] + }; + traefik.http.routers[`${id}`].middlewares.push('redirect-to-www'); + } else { + traefik.http.routers[`${id}-secure-www`] = { + entrypoints: ['websecure'], + rule: `Host(\`www.${nakedDomain}\`)`, + service: `${id}`, + tls: { + domains: { + main: `${domain}` + } + }, + middlewares: ['redirect-to-non-www'] + }; + traefik.http.routers[`${id}-secure`] = { + entrypoints: ['websecure'], + rule: `Host(\`${domain}\`)`, + service: `${id}`, + tls: { + certresolver: 'letsencrypt' + }, + middlewares: [] + }; + traefik.http.routers[`${id}`].middlewares.push('redirect-to-non-www'); + } + } + } else { + traefik.http.routers[id] = { + entrypoints: ['web'], + rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`, + service: `${id}`, + middlewares: [] + }; + + traefik.http.routers[`${id}-secure`] = { + entrypoints: ['websecure'], + rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`, + service: `${id}`, + tls: { + domains: { + main: `${nakedDomain}` + } + }, + middlewares: ['redirect-to-http'] + }; + + traefik.http.services[id] = { + loadbalancer: { + servers: [ + { + url: `http://${container}:${port}` + } + ] + } + }; + + if (!isDualCerts) { + if (isWWW) { + traefik.http.routers[`${id}`].middlewares.push('redirect-to-www'); + traefik.http.routers[`${id}-secure`].middlewares.push('redirect-to-www'); + } else { + traefik.http.routers[`${id}`].middlewares.push('redirect-to-non-www'); + traefik.http.routers[`${id}-secure`].middlewares.push('redirect-to-non-www'); + } + } + } +} +export const get: RequestHandler = async (event) => { + const traefik = { + http: { + routers: {}, + services: {}, + middlewares: { + 'redirect-to-https': { + redirectscheme: { + scheme: 'https' + } + }, + 'redirect-to-http': { + redirectscheme: { + scheme: 'http' + } + }, + 'redirect-to-non-www': { + redirectregex: { + regex: '^https?://www\\.(.+)', + replacement: 'http://${1}' + } + }, + 'redirect-to-www': { + redirectregex: { + regex: '^https?://(?:www\\.)?(.+)', + replacement: 'http://www.${1}' + } + } + } + } + }; + const applications = await db.prisma.application.findMany({ + include: { destinationDocker: true, settings: true } + }); + const data = { + applications: [], + services: [], + coolify: [] + }; + for (const application of applications) { + const { + fqdn, + id, + port, + destinationDocker, + destinationDockerId, + settings: { previews, dualCerts } + } = application; + if (destinationDockerId) { + const { engine, network } = destinationDocker; + const isRunning = await checkContainer(engine, id); + if (fqdn) { + const domain = getDomain(fqdn); + const nakedDomain = domain.replace(/^www\./, ''); + const isHttps = fqdn.startsWith('https://'); + const isWWW = fqdn.includes('www.'); + if (isRunning) { + data.applications.push({ + id, + container: id, + port: port || 3000, + domain, + nakedDomain, + isRunning, + isHttps, + isWWW, + isDualCerts: dualCerts + }); + } + if (previews) { + const host = getEngine(engine); + const { stdout } = await asyncExecShell( + `DOCKER_HOST=${host} docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"` + ); + const containers = stdout + .trim() + .split('\n') + .filter((a) => a) + .map((c) => c.replace(/"/g, '')); + if (containers.length > 0) { + for (const container of containers) { + const previewDomain = `${container.split('-')[1]}.${domain}`; + const nakedDomain = previewDomain.replace(/^www\./, ''); + data.applications.push({ + id: container, + container, + port: port || 3000, + domain: previewDomain, + isRunning, + nakedDomain, + isHttps, + isWWW, + isDualCerts: dualCerts + }); + } + } + } + } + } + } + const services = await listServicesWithIncludes(); + + for (const service of services) { + const { + fqdn, + id, + type, + destinationDocker, + destinationDockerId, + dualCerts, + plausibleAnalytics + } = service; + if (destinationDockerId) { + const { engine } = destinationDocker; + const found = supportedServiceTypesAndVersions.find((a) => a.name === type); + if (found) { + const port = found.ports.main; + const publicPort = service[type]?.publicPort; + const isRunning = await checkContainer(engine, id); + if (fqdn) { + const domain = getDomain(fqdn); + const nakedDomain = domain.replace(/^www\./, ''); + const isHttps = fqdn.startsWith('https://'); + const isWWW = fqdn.includes('www.'); + if (isRunning) { + // Plausible Analytics custom script + let scriptName = false; + if (type === 'plausibleanalytics' && plausibleAnalytics.scriptName !== 'plausible.js') { + scriptName = plausibleAnalytics.scriptName; + } + + let container = id; + let otherDomain = null; + let otherNakedDomain = null; + let otherIsHttps = null; + let otherIsWWW = null; + + if (type === 'minio' && service.minio.apiFqdn) { + otherDomain = getDomain(service.minio.apiFqdn); + otherNakedDomain = otherDomain.replace(/^www\./, ''); + otherIsHttps = service.minio.apiFqdn.startsWith('https://'); + otherIsWWW = service.minio.apiFqdn.includes('www.'); + } + data.services.push({ + id, + container, + type, + otherDomain, + otherNakedDomain, + otherIsHttps, + otherIsWWW, + port, + publicPort, + domain, + nakedDomain, + isRunning, + isHttps, + isWWW, + isDualCerts: dualCerts, + scriptName + }); + } + } + } + } + } + + const { fqdn, dualCerts } = await db.prisma.setting.findFirst(); + if (fqdn) { + const domain = getDomain(fqdn); + const nakedDomain = domain.replace(/^www\./, ''); + const isHttps = fqdn.startsWith('https://'); + const isWWW = fqdn.includes('www.'); + data.coolify.push({ + id: dev ? 'host.docker.internal' : 'coolify', + container: dev ? 'host.docker.internal' : 'coolify', + port: 3000, + domain, + nakedDomain, + isHttps, + isWWW, + isDualCerts: dualCerts + }); + } + for (const application of data.applications) { + configureMiddleware(application, traefik); + } + for (const service of data.services) { + const { id, scriptName } = service; + + configureMiddleware(service, traefik); + if (service.type === 'minio') { + service.id = id + '-minio'; + service.container = id; + service.domain = service.otherDomain; + service.nakedDomain = service.otherNakedDomain; + service.isHttps = service.otherIsHttps; + service.isWWW = service.otherIsWWW; + service.port = 9000; + configureMiddleware(service, traefik); + } + + if (scriptName) { + traefik.http.middlewares[`${id}-redir`] = { + replacepathregex: { + regex: `/js/${scriptName}`, + replacement: '/js/plausible.js' + } + }; + if (traefik.http.routers[id].middlewares.length > 0) { + traefik.http.routers[id].middlewares.push(`${id}-redir`); + } else { + traefik.http.routers[id].middlewares = [`${id}-redir`]; + } + } + } + for (const coolify of data.coolify) { + configureMiddleware(coolify, traefik); + } + if (Object.keys(traefik.http.routers).length === 0) { + traefik.http.routers = null; + } + if (Object.keys(traefik.http.services).length === 0) { + traefik.http.services = null; + } + return { + status: 200, + body: { + ...traefik + } + }; +}; diff --git a/src/routes/webhooks/traefik/other.json.ts b/src/routes/webhooks/traefik/other.json.ts new file mode 100644 index 000000000..a1c357878 --- /dev/null +++ b/src/routes/webhooks/traefik/other.json.ts @@ -0,0 +1,136 @@ +import { dev } from '$app/env'; +import { getDomain } from '$lib/common'; +import * as db from '$lib/database'; +import type { RequestHandler } from '@sveltejs/kit'; + +export const get: RequestHandler = async (event) => { + const id = event.url.searchParams.get('id'); + if (id) { + const privatePort = event.url.searchParams.get('privatePort'); + const publicPort = event.url.searchParams.get('publicPort'); + const type = event.url.searchParams.get('type'); + let traefik = {}; + if (publicPort && type && privatePort) { + if (type === 'tcp') { + traefik = { + [type]: { + routers: { + [id]: { + entrypoints: [type], + rule: `HostSNI(\`*\`)`, + service: id + } + }, + services: { + [id]: { + loadbalancer: { + servers: [{ address: `${id}:${privatePort}` }] + } + } + } + } + }; + } else if (type === 'http') { + const service = await db.prisma.service.findFirst({ + where: { id }, + include: { minio: true } + }); + if (service) { + if (service.type === 'minio') { + if (service?.minio?.apiFqdn) { + const { + minio: { apiFqdn } + } = service; + const domain = getDomain(apiFqdn); + const isHttps = apiFqdn.startsWith('https://'); + traefik = { + [type]: { + routers: { + [id]: { + entrypoints: [type], + rule: `Host(\`${domain}\`)`, + service: id + } + }, + services: { + [id]: { + loadbalancer: { + servers: [{ url: `http://${id}:${privatePort}` }] + } + } + } + } + }; + if (isHttps) { + if (dev) { + traefik[type].routers[id].tls = { + domains: { + main: `${domain}` + } + }; + } else { + traefik[type].routers[id].tls = { + certresolver: 'letsencrypt' + }; + } + } + } + } else { + if (service?.fqdn) { + const domain = getDomain(service.fqdn); + const isHttps = service.fqdn.startsWith('https://'); + traefik = { + [type]: { + routers: { + [id]: { + entrypoints: [type], + rule: `Host(\`${domain}:${privatePort}\`)`, + service: id + } + }, + services: { + [id]: { + loadbalancer: { + servers: [{ url: `http://${id}:${privatePort}` }] + } + } + } + } + }; + if (isHttps) { + if (dev) { + traefik[type].routers[id].tls = { + domains: { + main: `${domain}` + } + }; + } else { + traefik[type].routers[id].tls = { + certresolver: 'letsencrypt' + }; + } + } + } + } + } else { + return { + status: 500 + }; + } + } + } else { + return { + status: 500 + }; + } + return { + status: 200, + body: { + ...traefik + } + }; + } + return { + status: 500 + }; +}; diff --git a/src/tailwind.css b/src/tailwind.css index 529c91cb9..b8f1e647b 100644 --- a/src/tailwind.css +++ b/src/tailwind.css @@ -49,6 +49,9 @@ textarea { @apply h-12 w-96 rounded border-none bg-coolgray-200 p-2 px-0 text-xs tracking-tight outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 md:text-sm; } +#svelte .custom-select-wrapper .spinner { + @apply text-coollabs-100; +} #svelte .listContainer { @apply bg-coolgray-400 text-white scrollbar-w-2 scrollbar-thumb-green-500 scrollbar-track-coolgray-200; } @@ -66,26 +69,6 @@ textarea { select { @apply h-12 w-96 rounded bg-coolgray-200 p-2 text-xs font-bold tracking-tight text-white placeholder-stone-600 outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:text-stone-600 md:text-sm; } -.svelte-select { - --background: rgb(32 32 32); - --inputColor: white; - --multiItemPadding: 0; - --multiSelectPadding: 0 0.5rem 0 0.5rem; - --border: none; - --placeholderColor: rgb(87 83 78); - --listBackground: rgb(32 32 32); - --itemColor: white; - --itemHoverBG: rgb(107 22 237); - --multiItemBG: rgb(32 32 32); - --multiClearHoverBG: transparent; - --multiClearHoverFill: rgb(239 68 68); - --multiItemActiveBG: transparent; - --multiClearBG: transparent; - --clearSelectFocusColor: white; - --clearSelectHoverColor: rgb(239 68 68); - --multiItemBorderRadius: 0.25rem; - --listShadow: none; -} label { @apply inline-block w-64 text-xs tracking-tight md:text-sm; @@ -107,7 +90,7 @@ a { @apply mr-4 text-base tracking-tight md:text-2xl; } .nav-main { - @apply fixed top-0 left-0 min-h-screen w-16 min-w-[4rem] border-r border-stone-800 bg-coolgray-200; + @apply fixed top-0 left-0 min-h-screen w-16 min-w-[4rem] overflow-auto border-r border-stone-800 bg-coolgray-200 scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200 xl:overflow-visible; } .nav-side { @@ -356,7 +339,7 @@ a { } .box-selection { - @apply min-w-[16rem] max-w-[24rem] justify-center rounded-lg border-transparent bg-coolgray-200 p-6 shadow-lg transition duration-150 hover:scale-105 hover:border-transparent hover:bg-coolgray-400; + @apply min-w-[16rem] max-w-[24rem] justify-center rounded border-transparent bg-coolgray-200 p-6 hover:border-transparent hover:bg-coolgray-400; } ._toastBar { diff --git a/svelte.config.js b/svelte.config.js index 352c3e330..8fa7ba43e 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -1,6 +1,5 @@ import preprocess from 'svelte-preprocess'; import adapter from '@sveltejs/adapter-node'; - const config = { preprocess: preprocess(), kit: { @@ -10,6 +9,12 @@ const config = { }, floc: true, vite: { + proxy: { + '/api/v2': { + target: 'http://localhost:3001/api/v2', + changeOrigin: true + } + }, optimizeDeps: { exclude: ['svelte-kit-cookie-session'] },