diff --git a/.env.template b/.env.template index 0fa7427f3..3237d9b2c 100644 --- a/.env.template +++ b/.env.template @@ -2,5 +2,7 @@ COOLIFY_APP_ID= COOLIFY_SECRET_KEY=12341234123412341234123412341234 COOLIFY_DATABASE_URL=file:../db/dev.db COOLIFY_SENTRY_DSN= -COOLIFY_IS_ON="docker" -COOLIFY_WHITE_LABELED="false" \ No newline at end of file +COOLIFY_IS_ON=docker +COOLIFY_WHITE_LABELED=false +COOLIFY_WHITE_LABELED_ICON= +COOLIFY_AUTO_UPDATE=false \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..858d6ae2c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "i18n-ally.localesPaths": ["src/lib/locales"], + "i18n-ally.keystyle": "nested", + "i18n-ally.extract.ignoredByFiles": { + "src\\routes\\__layout.svelte": ["Coolify", "coolLabs logo"] + }, + "i18n-ally.sourceLanguage": "en", + "i18n-ally.enabledFrameworks": ["svelte"], + "i18n-ally.enabledParsers": ["js", "ts", "json"], + "i18n-ally.extract.autoDetect": true +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8a21ec10a..2be3c4ff5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,14 +1,23 @@ -# Welcome +# 👋 Welcome -First of all, thank you for considering to contribute to my project! It means a lot 💜. +First of all, thank you for considering contributing to my project! It means a lot 💜. -# Technical skills required +## 🙋 Want to help? -- Node.js / Javascript -- Svelte / SvelteKit -- Prisma.io +If you begin in GitHub contribution, you can find the [first contribution](https://github.com/firstcontributions/first-contributions) and follow this guide. -# Recommended Pull Request Guideline +Follow the [introduction](#introduction) to get started then start contributing! + +This is a little list of what you can do to help the project: + +- [🧑‍💻 Develop your own ideas](#developer-contribution) +- [🌐 Translate the project](#translation) + +## 👋 Introduction + +🔴 At the moment, Coolify **doesn't support Windows**. You must use Linux or MacOS. + +#### Recommended Pull Request Guideline - Fork the project - Clone your fork repo to local @@ -16,15 +25,17 @@ First of all, thank you for considering to contribute to my project! It means a - Push to your fork repo - Create a pull request: https://github.com/coollabsio/compare - Write a proper description -- Open the pull request to review +- Open the pull request to review against `next` branch + +--- # How to start after you set up your local fork? -This repository best with [pnpm](https://pnpm.io) due to the lock file. I recommend you should try and use `pnpm` as well, because it is cool and efficient! +Due to the lock file, this repository is best with [pnpm](https://pnpm.io). I recommend you try and use `pnpm` because it is cool and efficient! You need to have [Docker Engine](https://docs.docker.com/engine/install/) installed locally. -## Setup development environment +#### Setup a local development environment - Copy `.env.template` to `.env` and set the `COOLIFY_APP_ID` environment variable to something cool. - Install dependencies with `pnpm install`. @@ -33,12 +44,234 @@ You need to have [Docker Engine](https://docs.docker.com/engine/install/) instal - Seed the database with base entities with `pnpm db:seed` - You can start coding after starting `pnpm dev`. -## Database migrations +#### How to start after you set up your local fork? + +This repository works better with [pnpm](https://pnpm.io) due to the lock file. I recommend you to give it a try and use `pnpm` as well because it is cool and efficient! + +You need to have [Docker Engine](https://docs.docker.com/engine/install/) installed locally. + +## 🧑‍💻 Developer contribution + +### Technical skills required + +- **Languages**: Node.js / Javascript / Typescript +- **Framework JS/TS**: Svelte / SvelteKit +- **Database ORM**: Prisma.io +- **Docker Engine** + +### Database migrations During development, if you change the database layout, you need to run `pnpm db:push` to migrate the database and create types for Prisma. You also need to restart the development process. If the schema is finalized, you need to create a migration file with `pnpm db:migrate ` where `nameOfMigration` is given by you. Make it sense. :) -## Tricky parts +### Tricky parts -- BullMQ, the queue system Coolify is using, cannot be hot reloaded. So if you change anything in the files related to it, you need to restart the development process. I'm actively looking of a different queue/scheduler library. I'm open for discussion! +- BullMQ, the queue system Coolify uses, cannot be hot reloaded. So if you change anything in the files related to it, you need to restart the development process. I'm actively looking for a different queue/scheduler library. I'm open to discussion! + +--- + +# How to add new services + +You can add any open-source and self-hostable software (service/application) to Coolify if the following statements are true: + +- Self-hostable (obviously) +- Open-source +- Maintained (I do not want to add software full of bugs) + +## Backend + +There are 5 steps you should make on the backend side. + +1. Create Prisma / database schema for the new service. +2. Add supported versions of the service. +3. Update global functions. +4. Create API endpoints. +5. Define automatically generated variables. + +> I will use [Umami](https://umami.is/) as an example service. + +### Create Prisma / database schema for the new service. + +You only need to do this if you store passwords or any persistent configuration. Mostly it is required by all services, but there are some exceptions, like NocoDB. + +Update Prisma schema in [prisma/schema.prisma](prisma/schema.prisma). + +- Add new model with the new service name. +- Make a relationshup with `Service` model. +- In the `Service` model, the name of the new field should be with low-capital. +- If the service needs a database, define a `publicPort` field to be able to make it's database public, example field name in case of PostgreSQL: `postgresqlPublicPort`. It should be a optional field. + +If you are finished with the Prisma schema, you should update the database schema with `pnpm db:push` command. + +> You must restart the running development environment to be able to use the new model + +> If you use VSCode, you probably need to restart the `Typescript Language Server` to get the new types loaded in the running VSCode. + +### Add supported versions + +Supported versions are hardcoded into Coolify (for now). + +You need to update `supportedServiceTypesAndVersions` function at [src/lib/components/common.ts](src/lib/components/common.ts). Example JSON: + +```js + { + // Name used to identify the service internally + name: 'umami', + // Fancier name to show to the user + fancyName: 'Umami', + // Docker base image for the service + baseImage: 'ghcr.io/mikecao/umami', + // Optional: If there is any dependent image, you should list it here + images: [], + // Usable tags + versions: ['postgresql-latest'], + // Which tag is the recommended + recommendedVersion: 'postgresql-latest', + // Application's default port, Umami listens on 3000 + ports: { + main: 3000 + } + } +``` + +### Update global functions + +1. Add the new service to the `include` variable in [src/lib/database/services.ts](src/lib/database/services.ts), so it will be included in all places in the database queries where it is required. + +```js +const include: Prisma.ServiceInclude = { + destinationDocker: true, + persistentStorage: true, + serviceSecret: true, + minio: true, + plausibleAnalytics: true, + vscodeserver: true, + wordpress: true, + ghost: true, + meiliSearch: true, + umami: true // This line! +}; +``` + +2. Update the database update query with the new service type to `configureServiceType` function in [src/lib/database/services.ts](src/lib/database/services.ts). This function defines the automatically generated variables (passwords, users, etc.) and it's encryption process (if applicable). + +```js +[...] +else if (type === 'umami') { + const postgresqlUser = cuid(); + const postgresqlPassword = encrypt(generatePassword()); + const postgresqlDatabase = 'umami'; + const hashSalt = encrypt(generatePassword(64)); + await prisma.service.update({ + where: { id }, + data: { + type, + umami: { + create: { + postgresqlDatabase, + postgresqlPassword, + postgresqlUser, + hashSalt, + } + } + } + }); + } +``` + +3. Add decryption process for configurations and passwords to `getService` function in [src/lib/database/services.ts](src/lib/database/services.ts) + +```js +if (body.umami?.postgresqlPassword) + body.umami.postgresqlPassword = decrypt(body.umami.postgresqlPassword); + +if (body.umami?.hashSalt) body.umami.hashSalt = decrypt(body.umami.hashSalt); +``` + +4. Add service deletion query to `removeService` function in [src/lib/database/services.ts](src/lib/database/services.ts) + +### Create API endpoints. + +You need to add a new folder under [src/routes/services/[id]](src/routes/services/[id]) with the low-capital name of the service. You need 3 default files in that folder. + +#### `index.json.ts`: + +It has a POST endpoint that updates the service details in Coolify's database, such as name, url, other configurations, like passwords. It should look something like this: + +```js +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 } = await event.request.json(); + if (fqdn) fqdn = fqdn.toLowerCase(); + + try { + await db.updateService({ id, fqdn, name }); + return { status: 201 }; + } catch (error) { + return ErrorHandler(error); + } +}; +``` + +If it's necessary, you can create your own database update function, specifically for the new service. + +#### `start.json.ts` + +It has a POST endpoint that sets all the required secrets, persistent volumes, `docker-compose.yaml` file and sends a request to the specified docker engine. + +You could also define an `HTTP` or `TCP` proxy for every other port that should be proxied to your server. (See `startHttpProxy` and `startTcpProxy` functions in [src/lib/haproxy/index.ts](src/lib/haproxy/index.ts)) + +#### `stop.json.ts` + +It has a POST endpoint that stops the service and all dependent (TCP/HTTP proxies) containers. If publicPort is specified it also needs to cleanup it from the database. + +## Frontend + +1. You need to add a custom logo at [src/lib/components/svg/services/](src/lib/components/svg/services/) as a svelte component. + + SVG is recommended, but you can use PNG as well. It should have the `isAbsolute` variable with the suitable CSS classes, primarily for sizing and positioning. + +2. You need to include it the logo at + +- [src/routes/services/index.svelte](src/routes/services/index.svelte) with `isAbsolute` in two places, +- [src/lib/components/ServiceLinks.svelte](src/lib/components/ServiceLinks.svelte) with `isAbsolute` and a link to the docs/main site of the service +- [src/routes/services/[id]/configuration/type.svelte](src/routes/services/[id]/configuration/type.svelte) with `isAbsolute`. + +3. By default the URL and the name frontend forms are included in [src/routes/services/[id]/\_Services/\_Services.svelte](src/routes/services/[id]/_Services/_Services.svelte). + + If you need to show more details on the frontend, such as users/passwords, you need to add Svelte component to [src/routes/services/[id]/\_Services](src/routes/services/[id]/_Services) with an underscore. For example, see other files in that folder. + + You also need to add the new inputs to the `index.json.ts` file of the specific service, like for MinIO here: [src/routes/services/[id]/minio/index.json.ts](src/routes/services/[id]/minio/index.json.ts) + +## 🌐 Translate the project + +The project use [sveltekit-i18n](https://github.com/sveltekit-i18n/lib) to translate the project. +It follows the [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) to name languages. + +### Installation + +You must have gone throw all the [intro](#introduction) steps before you can start translating. + +It's only an advice, but I recommend you to use: + +- Visual Studio Code +- [i18n Ally for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=Lokalise.i18n-ally): ideal to see the progress of the translation. +- [Svelte for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode): to get the syntax color for the project + +### Adding a language + +If your language doesn't appear in the [locales folder list](src/lib/locales/), follow the step below: + +1. In `src/lib/locales/`, Copy paste `en.json` and rename it with your language (eg: `cz.json`). +2. In the [lang.json](src/lib/lang.json) file, add a line after the first bracket (`{`) with `"ISO of your language": "Language",` (eg: `"cz": "Czech",`). +3. Have fun translating! diff --git a/Dockerfile b/Dockerfile index df4614a13..87c4d54c9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,31 +1,42 @@ -FROM node:16.14.0-alpine -RUN apk add --no-cache g++ cmake make python3 -WORKDIR /app -COPY package*.json . -RUN yarn install -COPY . . -RUN yarn build - -FROM node:16.14.0-alpine +FROM node:16.14.2-alpine as install WORKDIR /app -LABEL coolify.managed true - -RUN apk add --no-cache git git-lfs openssh-client curl jq cmake sqlite openssl - +RUN apk add --no-cache curl RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@6 RUN pnpm add -g pnpm -RUN curl -fsSL "https://download.docker.com/linux/static/stable/x86_64/docker-20.10.9.tgz" | tar -xzvf - docker/docker -C . --strip-components 1 && mv docker /usr/bin/docker -RUN mkdir -p ~/.docker/cli-plugins/ -RUN curl -SL https://github.com/docker/compose/releases/download/v2.2.2/docker-compose-linux-x86_64 -o ~/.docker/cli-plugins/docker-compose -RUN chmod +x ~/.docker/cli-plugins/docker-compose +COPY package*.json . +RUN pnpm install + +FROM node:16.14.2-alpine +ARG TARGETPLATFORM + +WORKDIR /app + +ENV PRISMA_QUERY_ENGINE_BINARY=/app/prisma-engines/query-engine \ + PRISMA_MIGRATION_ENGINE_BINARY=/app/prisma-engines/migration-engine \ + PRISMA_INTROSPECTION_ENGINE_BINARY=/app/prisma-engines/introspection-engine \ + PRISMA_FMT_BINARY=/app/prisma-engines/prisma-fmt \ + PRISMA_CLI_QUERY_ENGINE_TYPE=binary \ + PRISMA_CLIENT_ENGINE_TYPE=binary + +COPY --from=coollabsio/prisma-engine:latest /prisma-engines/query-engine /prisma-engines/migration-engine /prisma-engines/introspection-engine /prisma-engines/prisma-fmt /app/prisma-engines/ + +COPY --from=install /app/node_modules ./node_modules +COPY . . + +RUN apk add --no-cache git git-lfs openssh-client curl jq cmake sqlite openssl +RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@6 +RUN pnpm add -g pnpm +RUN mkdir -p ~/.docker/cli-plugins/ +RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-20.10.9 -o /usr/bin/docker +RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-compose-linux-2.3.4 -o ~/.docker/cli-plugins/docker-compose +RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker + +RUN pnpm prisma generate +RUN pnpm build + -COPY --from=0 /app/docker-compose.yaml . -COPY --from=0 /app/build . -COPY --from=0 /app/package.json . -COPY --from=0 /app/node_modules ./node_modules -COPY --from=0 /app/prisma ./prisma EXPOSE 3000 CMD ["pnpm", "start"] \ No newline at end of file diff --git a/README.md b/README.md index 2cb2d5bf6..d214d42b7 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ If you would like no questions during installation ### Git Sources -You can use the following Git Sources to be auto-deployed to your Coolifyt instance! (Self hosted versions also supported.) +You can use the following Git Sources to be auto-deployed to your Coolifyt instance! (Self-hosted versions are also supported.) - Github - GitLab @@ -38,7 +38,7 @@ You can deploy your applications to the following destinations: ### Applications -These are the predefined build packs, but with the Docker build pack, you can host basically anything that is hostable with a single Dockerfile. +These are the predefined build packs, but with the Docker build pack, you can host anything that is hostable with a single Dockerfile. - Static sites - NodeJS diff --git a/data/fluentd/Dockerfile-dev b/data/fluentd/Dockerfile-dev new file mode 100644 index 000000000..bb5524df9 --- /dev/null +++ b/data/fluentd/Dockerfile-dev @@ -0,0 +1,6 @@ +FROM fluent/fluent-bit:1.9.0 +COPY fluentbit-dev.conf /tmp/fluentbit.conf +ENTRYPOINT ["/fluent-bit/bin/fluent-bit", "-c", "/tmp/fluentbit.conf"] +# USER root +# RUN ["gem", "install", "fluent-plugin-mongo"] +# USER fluent \ No newline at end of file diff --git a/data/fluentd/fluentbit-dev.conf b/data/fluentd/fluentbit-dev.conf new file mode 100644 index 000000000..8d06fd0af --- /dev/null +++ b/data/fluentd/fluentbit-dev.conf @@ -0,0 +1,24 @@ +[INPUT] + Name forward + Listen 0.0.0.0 + Port 24224 + Buffer_Chunk_Size 32KB + Buffer_Max_Size 64KB + +[OUTPUT] + Name influxdb + Match * + Host coolify-influxdb + Port 8086 + Bucket containerlogs + Org organization + HTTP_Token supertoken + Sequence_Tag _seq + Tag_Keys container_name +[OUTPUT] + Name http + Match * + Host host.docker.internal + Port 3000 + URI /logs.json + Format json \ No newline at end of file diff --git a/data/fluentd/fluentd-dev.conf b/data/fluentd/fluentd-dev.conf new file mode 100644 index 000000000..9ae4a06c0 --- /dev/null +++ b/data/fluentd/fluentd-dev.conf @@ -0,0 +1,28 @@ + + @type forward + port 24224 + bind 0.0.0.0 + + + + @type http + endpoint http://host.docker.internal:3000/logs.json + + flush_at_shutdown true + flush_mode immediate + flush_thread_count 8 + flush_thread_interval 1 + flush_thread_burst_interval 1 + retry_forever true + retry_type exponential_backoff + + + + + @type parser + key_name log + reserve_data true + + @type json + + \ No newline at end of file diff --git a/haproxy-http.Dockerfile b/data/haproxy-http.Dockerfile similarity index 100% rename from haproxy-http.Dockerfile rename to data/haproxy-http.Dockerfile diff --git a/haproxy-tcp.Dockerfile b/data/haproxy-tcp.Dockerfile similarity index 100% rename from haproxy-tcp.Dockerfile rename to data/haproxy-tcp.Dockerfile diff --git a/haproxy.Dockerfile b/data/haproxy.Dockerfile similarity index 100% rename from haproxy.Dockerfile rename to data/haproxy.Dockerfile diff --git a/data/haproxy/haproxy.cfg-http.template b/data/haproxy/haproxy.cfg-http.template index 72fdece7a..f819e9be7 100644 --- a/data/haproxy/haproxy.cfg-http.template +++ b/data/haproxy/haproxy.cfg-http.template @@ -4,10 +4,10 @@ global defaults mode http log global - timeout http-request 60s - timeout connect 10s - timeout client 60s - timeout server 60s + timeout http-request 120s + timeout connect 20s + timeout client 120s + timeout server 120s frontend "${APP}" mode http diff --git a/data/haproxy/haproxy.cfg.template b/data/haproxy/haproxy.cfg.template index c39cb7878..35cb8a74d 100644 --- a/data/haproxy/haproxy.cfg.template +++ b/data/haproxy/haproxy.cfg.template @@ -5,10 +5,10 @@ global defaults mode http log global - timeout http-request 60s - timeout connect 10s - timeout client 60s - timeout server 60s + timeout http-request 120s + timeout connect 20s + timeout client 120s + timeout server 120s userlist haproxy-dataplaneapi user admin insecure-password "${HAPROXY_PASSWORD}" diff --git a/data/prisma/build-prisma-engine.sh b/data/prisma/build-prisma-engine.sh new file mode 100644 index 000000000..4c0757d39 --- /dev/null +++ b/data/prisma/build-prisma-engine.sh @@ -0,0 +1 @@ +docker build --platform linux/amd64,linux/arm64 -t coollabsio/prisma-engine -f prisma-engine.Dockerfile --push . \ No newline at end of file diff --git a/data/prisma/prisma-engine.Dockerfile b/data/prisma/prisma-engine.Dockerfile new file mode 100644 index 000000000..76d1204fb --- /dev/null +++ b/data/prisma/prisma-engine.Dockerfile @@ -0,0 +1,10 @@ +FROM rust:1.58.1-alpine3.14 as prisma +WORKDIR /prisma +ENV RUSTFLAGS="-C target-feature=-crt-static" +RUN apk --no-cache add openssl direnv git musl-dev openssl-dev build-base perl protoc +RUN git clone --depth=1 --branch=3.12.x https://github.com/prisma/prisma-engines.git /prisma +RUN cargo build --release + +FROM alpine +WORKDIR /prisma-engines +COPY --from=prisma /prisma/target/release/query-engine /prisma/target/release/migration-engine /prisma/target/release/introspection-engine /prisma/target/release/prisma-fmt /prisma-engines/ diff --git a/docker-compose-dev.yaml b/docker-compose-dev.yaml index 41f287f2b..6c89e87d5 100644 --- a/docker-compose-dev.yaml +++ b/docker-compose-dev.yaml @@ -2,10 +2,8 @@ version: '3.8' services: redis: - image: 'bitnami/redis:6.2' + image: redis:6.2-alpine container_name: coolify-redis - environment: - - ALLOW_EMPTY_PASSWORD=yes networks: - coolify-infra ports: @@ -13,7 +11,24 @@ services: published: 6379 protocol: tcp mode: host - + # fluentbit: + # container_name: coolify-fluentbit + # build: + # context: ./data/fluentd + # dockerfile: Dockerfile-dev + # ports: + # - target: 24224 + # published: 24224 + # protocol: tcp + # mode: host + # - target: 24224 + # published: 24224 + # protocol: udp + # mode: host + # networks: + # - coolify-infra + # extra_hosts: + # - 'host.docker.internal:host-gateway' networks: coolify-infra: attachable: true diff --git a/docker-compose.yaml b/docker-compose.yaml index 145f2536a..6f3cd69f0 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -21,11 +21,9 @@ services: - coolify-infra depends_on: ['redis'] redis: - image: bitnami/redis:6.2 + image: redis:6.2-alpine restart: always container_name: coolify-redis - environment: - - ALLOW_EMPTY_PASSWORD=yes networks: - coolify-infra diff --git a/package.json b/package.json index 71e06130f..9f38331ad 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,14 @@ { "name": "coolify", "description": "An open-source & self-hostable Heroku / Netlify alternative.", - "version": "2.4.2", + "version": "2.5.2", "license": "AGPL-3.0", "scripts": { - "dev": "docker-compose -f docker-compose-dev.yaml up -d && cross-env NODE_ENV=development & svelte-kit dev", + "dev": "docker-compose -f docker-compose-dev.yaml up -d && cross-env NODE_ENV=development & svelte-kit dev --host 0.0.0.0", "dev:stop": "docker-compose -f docker-compose-dev.yaml down", "dev:logs": "docker-compose -f docker-compose-dev.yaml logs -f --tail 10", "studio": "npx prisma studio", - "start": "npx prisma migrate deploy && npx prisma generate && npx prisma db seed && node index.js", + "start": "npx prisma migrate deploy && npx prisma generate && npx prisma db seed && node build/index.js", "build": "svelte-kit build", "preview": "svelte-kit preview", "check": "svelte-check --tsconfig ./tsconfig.json", @@ -17,22 +17,23 @@ "db:push": "prisma db push && prisma generate", "db:seed": "prisma db seed", "db:migrate": "COOLIFY_DATABASE_URL=file:../db/migration.db prisma migrate dev --skip-seed --name", - "release:staging": "cross-var docker build -t coollabsio/coolify:$npm_package_version . && docker push coollabsio/coolify:$npm_package_version", - "release:pre": "cross-var docker build -t coollabsio/coolify:$npm_package_version -t coollabsio/coolify:latest .", - "release:coolify": "cross-var yarn release:pre && docker push coollabsio/coolify:$npm_package_version && docker push coollabsio/coolify:latest", - "release:haproxy": "docker build -f haproxy.Dockerfile -t coollabsio/coolify-haproxy-alpine:1.0.0 -t coollabsio/coolify-haproxy-alpine:latest . && docker image push --all-tags coollabsio/coolify-haproxy-alpine", - "release:haproxy:tcp": "docker build -f haproxy-tcp.Dockerfile -t coollabsio/coolify-haproxy-tcp-alpine:1.0.0 -t coollabsio/coolify-haproxy-tcp-alpine:latest . && docker image push --all-tags coollabsio/coolify-haproxy-tcp-alpine", - "release:haproxy:http": "docker build -f haproxy-http.Dockerfile -t coollabsio/coolify-haproxy-http-alpine:1.0.0 -t coollabsio/coolify-haproxy-http-alpine:latest . && docker image push --all-tags coollabsio/coolify-haproxy-http-alpine", + "release:production:all": "cross-var docker build --platform linux/amd64,linux/arm64 -t coollabsio/coolify:$npm_package_version -t coollabsio/coolify:latest --push .", + "release:production:amd": "cross-var docker build --platform linux/amd64 -t coollabsio/coolify:$npm_package_version -t coollabsio/coolify:latest --push .", + "release:production:arm": "cross-var docker build --platform linux/arm64 -t coollabsio/coolify:$npm_package_version -t coollabsio/coolify:latest --push .", + "release:staging:all": "cross-var docker build --platform linux/amd64,linux/arm64 -t coollabsio/coolify:$npm_package_version --push .", + "release:staging:amd": "cross-var docker build --platform linux/amd64 -t coollabsio/coolify:$npm_package_version --push .", + "release:staging:arm": "cross-var docker build --platform linux/arm64 -t coollabsio/coolify:$npm_package_version --push .", + "release:haproxy": "docker build --platform linux/amd64,linux/arm64 -t coollabsio/coolify-haproxy-alpine:latest -t coollabsio/coolify-haproxy-alpine:1.1.0 -f data/haproxy.Dockerfile --push .", + "release:haproxy:tcp": "docker build --platform linux/amd64,linux/arm64 -t coollabsio/coolify-haproxy-tcp-alpine:latest -t coollabsio/coolify-haproxy-tcp-alpine:1.1.0 -f data/haproxy-tcp.Dockerfile --push .", + "release:haproxy:http": "docker build --platform linux/amd64,linux/arm64 -t coollabsio/coolify-haproxy-http-alpine:latest -t coollabsio/coolify-haproxy-http-alpine:1.1.0 -f data/haproxy-http.Dockerfile --push .", "prepare": "husky install" }, "devDependencies": { "@sveltejs/adapter-node": "1.0.0-next.73", - "@sveltejs/kit": "1.0.0-next.303", - "@types/bcrypt": "5.0.0", - "@types/dockerode": "^3.3.8", + "@sveltejs/kit": "1.0.0-next.316", "@types/js-cookie": "3.0.1", "@types/js-yaml": "4.0.5", - "@types/node": "17.0.23", + "@types/node": "17.0.25", "@types/node-forge": "1.0.1", "@typescript-eslint/eslint-plugin": "4.31.1", "@typescript-eslint/parser": "4.31.1", @@ -44,17 +45,18 @@ "eslint-config-prettier": "8.5.0", "eslint-plugin-svelte3": "3.4.1", "husky": "7.0.4", - "lint-staged": "12.3.7", + "lint-staged": "12.4.0", "postcss": "8.4.12", - "prettier": "2.6.1", - "prettier-plugin-svelte": "2.6.0", - "prettier-plugin-tailwindcss": "0.1.8", + "prettier": "2.6.2", + "prettier-plugin-svelte": "2.7.0", + "prettier-plugin-tailwindcss": "0.1.10", "prisma": "3.11.1", - "svelte": "3.46.4", - "svelte-check": "2.4.6", - "svelte-preprocess": "4.10.4", + "svelte": "3.47.0", + "svelte-check": "2.7.0", + "svelte-preprocess": "4.10.6", "svelte-select": "4.4.7", - "tailwindcss": "3.0.23", + "tailwindcss": "3.0.24", + "sveltekit-i18n": "2.1.2", "ts-node": "10.7.0", "tslib": "2.3.1", "typescript": "4.6.3" @@ -63,26 +65,25 @@ "dependencies": { "@iarna/toml": "2.2.5", "@prisma/client": "3.11.1", - "@sentry/node": "6.19.2", - "bcrypt": "5.0.1", - "bullmq": "1.78.1", + "@sentry/node": "6.19.6", + "bcryptjs": "2.4.3", + "bullmq": "1.80.4", "compare-versions": "4.1.3", - "cookie": "0.4.2", - "cooltipz-css": "2.1.0", + "cookie": "0.5.0", "cuid": "2.1.8", - "dayjs": "1.11.0", + "dayjs": "1.11.1", "dockerode": "3.3.1", "dotenv-extended": "2.9.0", "generate-password": "1.7.0", "get-port": "6.1.2", - "got": "12.0.2", + "got": "12.0.3", "js-cookie": "3.0.1", "js-yaml": "4.1.0", "jsonwebtoken": "8.5.1", "mustache": "4.2.0", - "node-forge": "1.3.0", + "node-forge": "1.3.1", "p-limit": "4.0.0", - "svelte-kit-cookie-session": "2.1.2", + "svelte-kit-cookie-session": "2.1.3", "tailwindcss-scrollbar": "0.1.0", "unique-names-generator": "4.7.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b87591e81..c0e8a2bd9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3,28 +3,25 @@ lockfileVersion: 5.3 specifiers: '@iarna/toml': 2.2.5 '@prisma/client': 3.11.1 - '@sentry/node': 6.19.2 + '@sentry/node': 6.19.6 '@sveltejs/adapter-node': 1.0.0-next.73 - '@sveltejs/kit': 1.0.0-next.303 - '@types/bcrypt': 5.0.0 - '@types/dockerode': ^3.3.8 + '@sveltejs/kit': 1.0.0-next.316 '@types/js-cookie': 3.0.1 '@types/js-yaml': 4.0.5 - '@types/node': 17.0.23 + '@types/node': 17.0.25 '@types/node-forge': 1.0.1 '@typescript-eslint/eslint-plugin': 4.31.1 '@typescript-eslint/parser': 4.31.1 '@zerodevx/svelte-toast': 0.7.1 autoprefixer: 10.4.4 - bcrypt: 5.0.1 - bullmq: 1.78.1 + bcryptjs: 2.4.3 + bullmq: 1.80.4 compare-versions: 4.1.3 - cookie: 0.4.2 - cooltipz-css: 2.1.0 + cookie: 0.5.0 cross-env: 7.0.3 cross-var: 1.1.0 cuid: 2.1.8 - dayjs: 1.11.0 + dayjs: 1.11.1 dockerode: 3.3.1 dotenv-extended: 2.9.0 eslint: 7.32.0 @@ -32,26 +29,27 @@ specifiers: eslint-plugin-svelte3: 3.4.1 generate-password: 1.7.0 get-port: 6.1.2 - got: 12.0.2 + got: 12.0.3 husky: 7.0.4 js-cookie: 3.0.1 js-yaml: 4.1.0 jsonwebtoken: 8.5.1 - lint-staged: 12.3.7 + lint-staged: 12.4.0 mustache: 4.2.0 - node-forge: 1.3.0 + node-forge: 1.3.1 p-limit: 4.0.0 postcss: 8.4.12 - prettier: 2.6.1 - prettier-plugin-svelte: 2.6.0 - prettier-plugin-tailwindcss: 0.1.8 + prettier: 2.6.2 + prettier-plugin-svelte: 2.7.0 + prettier-plugin-tailwindcss: 0.1.10 prisma: 3.11.1 - svelte: 3.46.4 - svelte-check: 2.4.6 - svelte-kit-cookie-session: 2.1.2 - svelte-preprocess: 4.10.4 + svelte: 3.47.0 + svelte-check: 2.7.0 + svelte-kit-cookie-session: 2.1.3 + svelte-preprocess: 4.10.6 svelte-select: 4.4.7 - tailwindcss: 3.0.23 + tailwindcss: 3.0.24 + sveltekit-i18n: 2.1.2 tailwindcss-scrollbar: 0.1.0 ts-node: 10.7.0 tslib: 2.3.1 @@ -61,37 +59,34 @@ specifiers: dependencies: '@iarna/toml': 2.2.5 '@prisma/client': 3.11.1_prisma@3.11.1 - '@sentry/node': 6.19.2 - bcrypt: 5.0.1 - bullmq: 1.78.1 + '@sentry/node': 6.19.6 + bcryptjs: 2.4.3 + bullmq: 1.80.4 compare-versions: 4.1.3 - cookie: 0.4.2 - cooltipz-css: 2.1.0 + cookie: 0.5.0 cuid: 2.1.8 - dayjs: 1.11.0 + dayjs: 1.11.1 dockerode: 3.3.1 dotenv-extended: 2.9.0 generate-password: 1.7.0 get-port: 6.1.2 - got: 12.0.2 + got: 12.0.3 js-cookie: 3.0.1 js-yaml: 4.1.0 jsonwebtoken: 8.5.1 mustache: 4.2.0 - node-forge: 1.3.0 + node-forge: 1.3.1 p-limit: 4.0.0 - svelte-kit-cookie-session: 2.1.2 - tailwindcss-scrollbar: 0.1.0_tailwindcss@3.0.23 + svelte-kit-cookie-session: 2.1.3 + 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.303_svelte@3.46.4 - '@types/bcrypt': 5.0.0 - '@types/dockerode': 3.3.8 + '@sveltejs/kit': 1.0.0-next.316_svelte@3.47.0 '@types/js-cookie': 3.0.1 '@types/js-yaml': 4.0.5 - '@types/node': 17.0.23 + '@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 @@ -101,20 +96,21 @@ devDependencies: 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.46.4 + eslint-plugin-svelte3: 3.4.1_eslint@7.32.0+svelte@3.47.0 husky: 7.0.4 - lint-staged: 12.3.7 + lint-staged: 12.4.0 postcss: 8.4.12 - prettier: 2.6.1 - prettier-plugin-svelte: 2.6.0_prettier@2.6.1+svelte@3.46.4 - prettier-plugin-tailwindcss: 0.1.8_prettier@2.6.1 + 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 prisma: 3.11.1 - svelte: 3.46.4 - svelte-check: 2.4.6_postcss@8.4.12+svelte@3.46.4 - svelte-preprocess: 4.10.4_296873641a0ad9f42fe92172d27bcedd + svelte: 3.47.0 + svelte-check: 2.7.0_postcss@8.4.12+svelte@3.47.0 + svelte-preprocess: 4.10.6_41810887ae6c6d59323116f47e33fa38 svelte-select: 4.4.7 - tailwindcss: 3.0.23_b89136460714832cdda11d1e9d57d1ff - ts-node: 10.7.0_ee885bc7281b682b6adbed6ae09ee090 + tailwindcss: 3.0.24_ts-node@10.7.0 + ts-node: 10.7.0_de7c86b0cde507c63a0402da5b982bd3 + sveltekit-i18n: 2.1.2_svelte@3.46.4 tslib: 2.3.1 typescript: 4.6.3 @@ -214,26 +210,6 @@ packages: } dev: false - /@mapbox/node-pre-gyp/1.0.6: - resolution: - { - integrity: sha512-qK1ECws8UxuPqOA8F5LFD90vyVU33W7N3hGfgsOVfrJaRVc8McC3JClTDHpeSbL9CBrOHly/4GsNPAvIgNZE+g== - } - hasBin: true - dependencies: - detect-libc: 1.0.3 - https-proxy-agent: 5.0.0 - make-dir: 3.1.0 - node-fetch: 2.6.6 - nopt: 5.0.0 - npmlog: 5.0.1 - rimraf: 3.0.2 - semver: 7.3.5 - tar: 6.1.11 - transitivePeerDependencies: - - supports-color - dev: false - /@nodelib/fs.scandir/2.1.5: resolution: { @@ -307,55 +283,55 @@ packages: picomatch: 2.3.0 dev: true - /@sentry/core/6.19.2: + /@sentry/core/6.19.6: resolution: { - integrity: sha512-yu1R3ewBT4udmB4v7sc4biQZ0Z0rfB9+TzB5ZKoCftbe6kqXjFMMaFRYNUF9HicVldKAsBktgkWw3+yfqGkw/A== + integrity: sha512-biEotGRr44/vBCOegkTfC9rwqaqRKIpFljKGyYU6/NtzMRooktqOhjmjmItNCMRknArdeaQwA8lk2jcZDXX3Og== } engines: { node: '>=6' } dependencies: - '@sentry/hub': 6.19.2 - '@sentry/minimal': 6.19.2 - '@sentry/types': 6.19.2 - '@sentry/utils': 6.19.2 + '@sentry/hub': 6.19.6 + '@sentry/minimal': 6.19.6 + '@sentry/types': 6.19.6 + '@sentry/utils': 6.19.6 tslib: 1.14.1 dev: false - /@sentry/hub/6.19.2: + /@sentry/hub/6.19.6: resolution: { - integrity: sha512-W7KCgNBgdBIMagOxy5J5KQPe+maYxSqfE8a5ncQ3R8BcZDQEKnkW/1FplNbfRLZqA/tL/ndKb7pTPqVtzsbARw== + integrity: sha512-PuEOBZxvx3bjxcXmWWZfWXG+orojQiWzv9LQXjIgroVMKM/GG4QtZbnWl1hOckUj7WtKNl4hEGO2g/6PyCV/vA== } engines: { node: '>=6' } dependencies: - '@sentry/types': 6.19.2 - '@sentry/utils': 6.19.2 + '@sentry/types': 6.19.6 + '@sentry/utils': 6.19.6 tslib: 1.14.1 dev: false - /@sentry/minimal/6.19.2: + /@sentry/minimal/6.19.6: resolution: { - integrity: sha512-ClwxKm77iDHET7kpzv1JvzDx1er5DoNu+EUjst0kQzARIrXvu9xuZuE2/CnBWycQWqw8o3HoGoKz65uIhsUCzQ== + integrity: sha512-T1NKcv+HTlmd8EbzUgnGPl4ySQGHWMCyZ8a8kXVMZOPDzphN3fVIzkYzWmSftCWp0rpabXPt9aRF2mfBKU+mAQ== } engines: { node: '>=6' } dependencies: - '@sentry/hub': 6.19.2 - '@sentry/types': 6.19.2 + '@sentry/hub': 6.19.6 + '@sentry/types': 6.19.6 tslib: 1.14.1 dev: false - /@sentry/node/6.19.2: + /@sentry/node/6.19.6: resolution: { - integrity: sha512-Z1qREpTpYHxaeWjc1zMUk8ZTAp1WbxMiI2TVNc+a14DVT19Z2xNXb06MiRfeLgNc9lVGdmzR62dPmMBjVgPJYg== + integrity: sha512-kHQMfsy40ZxxdS9zMPmXCOOLWOJbQj6/aVSHt/L1QthYcgkAi7NJQNXnQIPWQDe8eP3DfNIWM7dc446coqjXrQ== } engines: { node: '>=6' } dependencies: - '@sentry/core': 6.19.2 - '@sentry/hub': 6.19.2 - '@sentry/types': 6.19.2 - '@sentry/utils': 6.19.2 + '@sentry/core': 6.19.6 + '@sentry/hub': 6.19.6 + '@sentry/types': 6.19.6 + '@sentry/utils': 6.19.6 cookie: 0.4.2 https-proxy-agent: 5.0.0 lru_map: 0.3.3 @@ -364,22 +340,22 @@ packages: - supports-color dev: false - /@sentry/types/6.19.2: + /@sentry/types/6.19.6: resolution: { - integrity: sha512-XO5qmVBdTs+7PdCz7fAwn1afWxSnRE2KLBFg5/vOdKosPSSHsSHUURSkxiEZc2QsR+JpRB4AeQ26AkIRX38qTg== + integrity: sha512-QH34LMJidEUPZK78l+Frt3AaVFJhEmIi05Zf8WHd9/iTt+OqvCHBgq49DDr1FWFqyYWm/QgW/3bIoikFpfsXyQ== } engines: { node: '>=6' } dev: false - /@sentry/utils/6.19.2: + /@sentry/utils/6.19.6: resolution: { - integrity: sha512-2DQQ2OJaxjtyxGq5FmMlqb6hptsqMs2xoBiVRMkTS/rvyTrk1oQdKZ8ePwjtgX3nJ728ni3IXIyXV+vfGp4EBw== + integrity: sha512-fAMWcsguL0632eWrROp/vhPgI7sBj/JROWVPzpabwVkm9z3m1rQm6iLFn4qfkZL8Ozy6NVZPXOQ7EXmeU24byg== } engines: { node: '>=6' } dependencies: - '@sentry/types': 6.19.2 + '@sentry/types': 6.19.6 tslib: 1.14.1 dev: false @@ -400,20 +376,20 @@ packages: tiny-glob: 0.2.9 dev: true - /@sveltejs/kit/1.0.0-next.303_svelte@3.46.4: + /@sveltejs/kit/1.0.0-next.316_svelte@3.47.0: resolution: { - integrity: sha512-WdxDc8OiF1WEd/bEza7CBdzA+3qIcCi1GKBj/gieKX9I3N8iDJt/Cg2POrLo9wQoJ47nZcAd1eOhfr7XEX1aIQ== + integrity: sha512-oLjWOWzjriJD2t210r7ALuH/8ZADrJGsOODzRCRSJvRBCt0Q7VKVLqwKbM/RlZzD1k8Af2uRodQT11kP98hAIA== } engines: { node: '>=14.13' } hasBin: true peerDependencies: svelte: ^3.44.0 dependencies: - '@sveltejs/vite-plugin-svelte': 1.0.0-next.33_svelte@3.46.4+vite@2.8.0 + '@sveltejs/vite-plugin-svelte': 1.0.0-next.33_svelte@3.47.0+vite@2.9.1 sade: 1.7.4 - svelte: 3.46.4 - vite: 2.8.0 + svelte: 3.47.0 + vite: 2.9.1 transitivePeerDependencies: - diff-match-patch - less @@ -422,7 +398,7 @@ packages: - supports-color dev: true - /@sveltejs/vite-plugin-svelte/1.0.0-next.33_svelte@3.46.4+vite@2.8.0: + /@sveltejs/vite-plugin-svelte/1.0.0-next.33_svelte@3.47.0+vite@2.9.1: resolution: { integrity: sha512-aj0h2+ZixgT+yoJFIs8dRRw/Cj9tgNu3+hY4CJikpa04mfhR61wXqJFfi2ZEFMUvFda5nCxKYIChFkc6wq5fJA== @@ -441,13 +417,33 @@ packages: kleur: 4.1.4 magic-string: 0.25.7 require-relative: 0.8.7 - svelte: 3.46.4 - svelte-hmr: 0.14.9_svelte@3.46.4 - vite: 2.8.0 + svelte: 3.47.0 + svelte-hmr: 0.14.9_svelte@3.47.0 + vite: 2.9.1 transitivePeerDependencies: - supports-color dev: true + /@sveltekit-i18n/base/1.1.1_svelte@3.46.4: + resolution: + { + integrity: sha512-J/sMU0OwS3dCLOuilHMBqu8vZHuuXiNV9vFJx8Nb4/b5BlR/KCZ4bCXI8wZR02GHeCOYKZxWus07CM1scxa/jw== + } + peerDependencies: + svelte: ^3.x + dependencies: + svelte: 3.46.4 + optionalDependencies: + '@sveltekit-i18n/parser-default': 1.0.3 + dev: true + + /@sveltekit-i18n/parser-default/1.0.3: + resolution: + { + integrity: sha512-HheveklTjp3hxpYQhoHfyA6B4bQaUeSV5MQf2usIv/58UF2jY/YqhCAWj9bDBjufbuZc5pSz4BXvdX3WVT+viA== + } + dev: true + /@szmarczak/http-timer/5.0.1: resolution: { @@ -486,15 +482,6 @@ packages: } dev: true - /@types/bcrypt/5.0.0: - resolution: - { - integrity: sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw== - } - dependencies: - '@types/node': 17.0.23 - dev: true - /@types/cacheable-request/6.0.2: resolution: { @@ -503,7 +490,7 @@ packages: dependencies: '@types/http-cache-semantics': 4.0.1 '@types/keyv': 3.1.3 - '@types/node': 17.0.23 + '@types/node': 17.0.25 '@types/responselike': 1.0.0 dev: false @@ -561,7 +548,7 @@ packages: integrity: sha512-FXCJgyyN3ivVgRoml4h94G/p3kY+u/B86La+QptcqJaWtBWtmc6TtkNfS40n9bIvyLteHh7zXOtgbobORKPbDg== } dependencies: - '@types/node': 17.0.23 + '@types/node': 17.0.25 dev: false /@types/node-forge/1.0.1: @@ -570,22 +557,15 @@ packages: integrity: sha512-96ELNKv9tQJ19afdBUiM5iDw7OYEc53iUc51gAPR2aGaqRsO1DBROjqgZRjZa1tkPj7TnEOR0EnyAX6iryGkzA== } dependencies: - '@types/node': 17.0.23 + '@types/node': 17.0.25 dev: true - /@types/node/17.0.23: + /@types/node/17.0.25: resolution: { - integrity: sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw== + integrity: sha512-wANk6fBrUwdpY4isjWrKTufkrXdu1D2YHCot2fD/DfWxF5sMrVSA+KN7ydckvaTCh0HiqX9IVl0L5/ZoXg5M7w== } - /@types/parse-json/4.0.0: - resolution: - { - integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== - } - dev: true - /@types/pug/2.0.5: resolution: { @@ -599,7 +579,7 @@ packages: integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== } dependencies: - '@types/node': 17.0.23 + '@types/node': 17.0.25 dev: false /@types/sass/1.16.1: @@ -608,7 +588,7 @@ packages: integrity: sha512-iZUcRrGuz/Tbg3loODpW7vrQJkUtpY2fFSf4ELqqkApcS2TkZ1msk7ie8iZPB86lDOP8QOTTmuvWjc5S0R9OjQ== } dependencies: - '@types/node': 17.0.23 + '@types/node': 17.0.25 dev: true /@types/ssh2-streams/0.1.9: @@ -763,13 +743,6 @@ packages: } dev: true - /abbrev/1.1.1: - resolution: - { - integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== - } - dev: false - /acorn-jsx/5.3.2_acorn@7.4.1: resolution: { @@ -896,17 +869,13 @@ packages: engines: { node: '>=0.10.0' } dev: true - /ansi-regex/3.0.0: - resolution: { integrity: sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= } - engines: { node: '>=4' } - dev: false - /ansi-regex/5.0.1: resolution: { integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== } engines: { node: '>=8' } + dev: true /ansi-regex/6.0.1: resolution: @@ -960,24 +929,6 @@ packages: picomatch: 2.3.0 dev: true - /aproba/2.0.0: - resolution: - { - integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== - } - dev: false - - /are-we-there-yet/2.0.0: - resolution: - { - integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== - } - engines: { node: '>=10' } - dependencies: - delegates: 1.0.0 - readable-stream: 3.6.0 - dev: false - /arg/4.1.3: resolution: { @@ -1697,18 +1648,8 @@ packages: tweetnacl: 0.14.5 dev: false - /bcrypt/5.0.1: - resolution: - { - integrity: sha512-9BTgmrhZM2t1bNuDtrtIMVSmmxZBrJ71n8Wg+YgdjHuIWYF7SjjmCPZFB+/5i/o/PIeRpwVJR3P+NrpIItUjqw== - } - engines: { node: '>= 10.0.0' } - requiresBuild: true - dependencies: - '@mapbox/node-pre-gyp': 1.0.6 - node-addon-api: 3.2.1 - transitivePeerDependencies: - - supports-color + /bcryptjs/2.4.3: + resolution: { integrity: sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms= } dev: false /binary-extensions/2.2.0: @@ -1782,16 +1723,16 @@ packages: ieee754: 1.2.1 dev: false - /bullmq/1.78.1: + /bullmq/1.80.4: resolution: { - integrity: sha512-er45mM8nGhgA83EVCJ4PNxPyDSzakvoxeFGU4vdSgYeB+SbeFQAlJYmAC50Ms7YFPstm1LeinbVZ+oX/BmBzOg== + integrity: sha512-j3PyjU16gqmb3Md9QjMInJdbMvxIlSJx7mojtoP06LV9MfhzW75DkDrpSuJlF0H+0+u6MViV4hhaGTxky5OJWw== } dependencies: cron-parser: 4.2.1 get-port: 5.1.1 glob: 7.2.0 - ioredis: 4.28.3 + ioredis: 4.28.5 lodash: 4.17.21 msgpackr: 1.4.7 semver: 6.3.0 @@ -1915,14 +1856,6 @@ packages: } dev: false - /chownr/2.0.0: - resolution: - { - integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== - } - engines: { node: '>=10' } - dev: false - /clean-stack/2.2.0: resolution: { @@ -2007,14 +1940,6 @@ packages: } dev: true - /color-support/1.1.3: - resolution: - { - integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== - } - hasBin: true - dev: false - /colorette/2.0.16: resolution: { @@ -2040,10 +1965,6 @@ packages: /concat-map/0.0.1: resolution: { integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= } - /console-control-strings/1.1.0: - resolution: { integrity: sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= } - dev: false - /convert-source-map/1.8.0: resolution: { @@ -2061,17 +1982,12 @@ packages: engines: { node: '>= 0.6' } dev: false - /cooltipz-css/2.1.0: + /cookie/0.5.0: resolution: { - integrity: sha512-howXMSGn4tYvroGUad5jg1QFKUMxxc2GiQSXrv6TQBIMmhrf8t//hz1Fx8eyc5t93SS9ZzUa5rHbPtjBTg5j1w== + integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== } - engines: { node: '>=16', npm: '>=8' } - peerDependencies: - sass: 1.x - peerDependenciesMeta: - sass: - optional: true + engines: { node: '>= 0.6' } dev: false /core-js/2.6.12: @@ -2083,20 +1999,6 @@ packages: requiresBuild: true dev: true - /cosmiconfig/7.0.1: - resolution: - { - integrity: sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ== - } - engines: { node: '>=10' } - dependencies: - '@types/parse-json': 4.0.0 - import-fresh: 3.3.0 - parse-json: 5.2.0 - path-type: 4.0.0 - yaml: 1.10.2 - dev: true - /cpu-features/0.0.2: resolution: { @@ -2183,10 +2085,10 @@ packages: } dev: false - /dayjs/1.11.0: + /dayjs/1.11.1: resolution: { - integrity: sha512-JLC809s6Y948/FuCZPm5IX8rRhQwOiyMb2TfVVQEixG7P8Lm/gt5S7yoQZmC8x1UehI9Pb7sksEt4xx14m+7Ug== + integrity: sha512-ER7EjqVAMkRRsxNCC5YqJ9d9VQYuWdGt7aiH2qA5R5wt8ZmWaP2dLUSIK6y/kVzLMlmh1Tvu5xUf4M/wdGJ5KA== } dev: false @@ -2258,10 +2160,6 @@ packages: resolution: { integrity: sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM= } dev: true - /delegates/1.0.0: - resolution: { integrity: sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= } - dev: false - /denque/1.5.1: resolution: { @@ -2285,12 +2183,6 @@ packages: engines: { node: '>=8' } dev: true - /detect-libc/1.0.3: - resolution: { integrity: sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= } - engines: { node: '>=0.10' } - hasBin: true - dev: false - /detective/5.2.0: resolution: { @@ -2424,6 +2316,7 @@ packages: { integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== } + dev: true /emoji-regex/9.2.2: resolution: @@ -2451,23 +2344,26 @@ packages: ansi-colors: 4.1.1 dev: true - /error-ex/1.3.2: - resolution: - { - integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - } - dependencies: - is-arrayish: 0.2.1 - dev: true - /es6-promise/3.3.1: resolution: { integrity: sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM= } dev: true - /esbuild-android-arm64/0.14.21: + /esbuild-android-64/0.14.34: resolution: { - integrity: sha512-Bqgld1TY0wZv8TqiQmVxQFgYzz8ZmyzT7clXBDZFkOOdRybzsnj8AZuK1pwcLVA7Ya6XncHgJqIao7NFd3s0RQ== + integrity: sha512-XfxcfJqmMYsT/LXqrptzFxmaR3GWzXHDLdFNIhm6S00zPaQF1TBBWm+9t0RZ6LRR7iwH57DPjaOeW20vMqI4Yw== + } + engines: { node: '>=12' } + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /esbuild-android-arm64/0.14.34: + resolution: + { + integrity: sha512-T02+NXTmSRL1Mc6puz+R9CB54rSPICkXKq6+tw8B6vxZFnCPzbJxgwIX4kcluz9p8nYBjF3+lSilTGWb7+Xgew== } engines: { node: '>=12' } cpu: [arm64] @@ -2476,10 +2372,10 @@ packages: dev: true optional: true - /esbuild-darwin-64/0.14.21: + /esbuild-darwin-64/0.14.34: resolution: { - integrity: sha512-j+Eg+e13djzyYINVvAbOo2/zvZ2DivuJJTaBrJnJHSD7kUNuGHRkHoSfFjbI80KHkn091w350wdmXDNSgRjfYQ== + integrity: sha512-pLRip2Bh4Ng7Bf6AMgCrSp3pPe/qZyf11h5Qo2mOfJqLWzSVjxrXW+CFRJfrOVP7TCnh/gmZSM2AFdCPB72vtw== } engines: { node: '>=12' } cpu: [x64] @@ -2488,10 +2384,10 @@ packages: dev: true optional: true - /esbuild-darwin-arm64/0.14.21: + /esbuild-darwin-arm64/0.14.34: resolution: { - integrity: sha512-nDNTKWDPI0RuoPj5BhcSB2z5EmZJJAyRtZLIjyXSqSpAyoB8eyAKXl4lB8U2P78Fnh4Lh1le/fmpewXE04JhBQ== + integrity: sha512-vpidSJEBxx6lf1NWgXC+DCmGqesJuZ5Y8aQVVsaoO4i8tRXbXb0whChRvop/zd3nfNM4dIl5EXAky0knRX5I6w== } engines: { node: '>=12' } cpu: [arm64] @@ -2500,10 +2396,10 @@ packages: dev: true optional: true - /esbuild-freebsd-64/0.14.21: + /esbuild-freebsd-64/0.14.34: resolution: { - integrity: sha512-zIurkCHXhxELiDZtLGiexi8t8onQc2LtuE+S7457H/pP0g0MLRKMrsn/IN4LDkNe6lvBjuoZZi2OfelOHn831g== + integrity: sha512-m0HBjePhe0hAQJgtMRMNV9kMgIyV4/qSnzPx42kRMQBcPhgjAq1JRu4Il26czC+9FgpMbFkUktb07f/Lwnc6CA== } engines: { node: '>=12' } cpu: [x64] @@ -2512,10 +2408,10 @@ packages: dev: true optional: true - /esbuild-freebsd-arm64/0.14.21: + /esbuild-freebsd-arm64/0.14.34: resolution: { - integrity: sha512-wdxMmkJfbwcN+q85MpeUEamVZ40FNsBa9mPq8tAszDn8TRT2HoJvVRADPIIBa9SWWwlDChIMjkDKAnS3KS/sPA== + integrity: sha512-cpRc2B94L1KvMPPYB4D6G39jLqpKlD3noAMY4/e86iXXXkhUYJJEtTuyNFTa9JRpWM0xCAp4mxjHjoIiLuoCLA== } engines: { node: '>=12' } cpu: [arm64] @@ -2524,10 +2420,10 @@ packages: dev: true optional: true - /esbuild-linux-32/0.14.21: + /esbuild-linux-32/0.14.34: resolution: { - integrity: sha512-fmxvyzOPPh2xiEHojpCeIQP6pXcoKsWbz3ryDDIKLOsk4xp3GbpHIEAWP0xTeuhEbendmvBDVKbAVv3PnODXLg== + integrity: sha512-8nQaEaoW7MH/K/RlozJa+lE1ejHIr8fuPIHhc513UebRav7HtXgQvxHQ6VZRUkWtep23M6dd7UqhwO1tMOfzQQ== } engines: { node: '>=12' } cpu: [ia32] @@ -2536,10 +2432,10 @@ packages: dev: true optional: true - /esbuild-linux-64/0.14.21: + /esbuild-linux-64/0.14.34: resolution: { - integrity: sha512-edZyNOv1ql+kpmlzdqzzDjRQYls+tSyi4QFi+PdBhATJFUqHsnNELWA9vMSzAaInPOEaVUTA5Ml28XFChcy4DA== + integrity: sha512-Y3of4qQoLLlAgf042MlrY1P+7PnN9zWj8nVtw9XQG5hcLOZLz7IKpU35oeu7n4wvyaZHwvQqDJ93gRLqdJekcQ== } engines: { node: '>=12' } cpu: [x64] @@ -2548,10 +2444,10 @@ packages: dev: true optional: true - /esbuild-linux-arm/0.14.21: + /esbuild-linux-arm/0.14.34: resolution: { - integrity: sha512-aSU5pUueK6afqmLQsbU+QcFBT62L+4G9hHMJDHWfxgid6hzhSmfRH9U/f+ymvxsSTr/HFRU4y7ox8ZyhlVl98w== + integrity: sha512-9lpq1NcJqssAF7alCO6zL3gvBVVt/lKw4oetUM7OgNnRX0OWpB+ZIO9FwCrSj/dMdmgDhPLf+119zB8QxSMmAg== } engines: { node: '>=12' } cpu: [arm] @@ -2560,10 +2456,10 @@ packages: dev: true optional: true - /esbuild-linux-arm64/0.14.21: + /esbuild-linux-arm64/0.14.34: resolution: { - integrity: sha512-t5qxRkq4zdQC0zXpzSB2bTtfLgOvR0C6BXYaRE/6/k8/4SrkZcTZBeNu+xGvwCU4b5dU9ST9pwIWkK6T1grS8g== + integrity: sha512-IlWaGtj9ir7+Nrume1DGcyzBDlK8GcnJq0ANKwcI9pVw8tqr+6GD0eqyF9SF1mR8UmAp+odrx1H5NdR2cHdFHA== } engines: { node: '>=12' } cpu: [arm64] @@ -2572,10 +2468,10 @@ packages: dev: true optional: true - /esbuild-linux-mips64le/0.14.21: + /esbuild-linux-mips64le/0.14.34: resolution: { - integrity: sha512-jLZLQGCNlUsmIHtGqNvBs3zN+7a4D9ckf0JZ+jQTwHdZJ1SgV9mAjbB980OFo66LoY+WeM7t3WEnq3FjI1zw4A== + integrity: sha512-k3or+01Rska1AjUyNjA4buEwB51eyN/xPQAoOx1CjzAQC3l8rpjUDw55kXyL63O/1MUi4ISvtNtl8gLwdyEcxw== } engines: { node: '>=12' } cpu: [mips64el] @@ -2584,10 +2480,10 @@ packages: dev: true optional: true - /esbuild-linux-ppc64le/0.14.21: + /esbuild-linux-ppc64le/0.14.34: resolution: { - integrity: sha512-4TWxpK391en2UBUw6GSrukToTDu6lL9vkm3Ll40HrI08WG3qcnJu7bl8e1+GzelDsiw1QmfAY/nNvJ6iaHRpCQ== + integrity: sha512-+qxb8M9FfM2CJaVU7GgYpJOHM1ngQOx+/VrtBjb4C8oVqaPcESCeg2anjl+HRZy8VpYc71q/iBYausPPbJ+Keg== } engines: { node: '>=12' } cpu: [ppc64] @@ -2596,10 +2492,10 @@ packages: dev: true optional: true - /esbuild-linux-riscv64/0.14.21: + /esbuild-linux-riscv64/0.14.34: resolution: { - integrity: sha512-fElngqOaOfTsF+u+oetDLHsPG74vB2ZaGZUqmGefAJn3a5z9Z2pNa4WpVbbKgHpaAAy5tWM1m1sbGohj6Ki6+Q== + integrity: sha512-Y717ltBdQ5j5sZIHdy1DV9kieo0wMip0dCmVSTceowCPYSn1Cg33Kd6981+F/3b9FDMzNWldZFOBRILViENZSA== } engines: { node: '>=12' } cpu: [riscv64] @@ -2608,10 +2504,10 @@ packages: dev: true optional: true - /esbuild-linux-s390x/0.14.21: + /esbuild-linux-s390x/0.14.34: resolution: { - integrity: sha512-brleZ6R5fYv0qQ7ZBwenQmP6i9TdvJCB092c/3D3pTLQHBGHJb5zWgKxOeS7bdHzmLy6a6W7GbFk6QKpjyD6QA== + integrity: sha512-bDDgYO4LhL4+zPs+WcBkXph+AQoPcQRTv18FzZS0WhjfH8TZx2QqlVPGhmhZ6WidrY+jKthUqO6UhGyIb4MpmA== } engines: { node: '>=12' } cpu: [s390x] @@ -2620,10 +2516,10 @@ packages: dev: true optional: true - /esbuild-netbsd-64/0.14.21: + /esbuild-netbsd-64/0.14.34: resolution: { - integrity: sha512-nCEgsLCQ8RoFWVV8pVI+kX66ICwbPP/M9vEa0NJGIEB/Vs5sVGMqkf67oln90XNSkbc0bPBDuo4G6FxlF7PN8g== + integrity: sha512-cfaFGXdRt0+vHsjNPyF0POM4BVSHPSbhLPe8mppDc7GDDxjIl08mV1Zou14oDWMp/XZMjYN1kWYRSfftiD0vvQ== } engines: { node: '>=12' } cpu: [x64] @@ -2632,10 +2528,10 @@ packages: dev: true optional: true - /esbuild-openbsd-64/0.14.21: + /esbuild-openbsd-64/0.14.34: resolution: { - integrity: sha512-h9zLMyVD0T73MDTVYIb/qUTokwI6EJH9O6wESuTNq6+XpMSr6C5aYZ4fvFKdNELW+Xsod+yDS2hV2JTUAbFrLA== + integrity: sha512-vmy9DxXVnRiI14s8GKuYBtess+EVcDALkbpTqd5jw4XITutIzyB7n4x0Tj5utAkKsgZJB22lLWGekr0ABnSLow== } engines: { node: '>=12' } cpu: [x64] @@ -2644,10 +2540,10 @@ packages: dev: true optional: true - /esbuild-sunos-64/0.14.21: + /esbuild-sunos-64/0.14.34: resolution: { - integrity: sha512-Kl+7Cot32qd9oqpLdB1tEGXEkjBlijrIxMJ0+vlDFaqsODutif25on0IZlFxEBtL2Gosd4p5WCV1U7UskNQfXA== + integrity: sha512-eNPVatNET1F7tRMhii7goL/eptfxc0ALRjrj9SPFNqp0zmxrehBFD6BaP3R4LjMn6DbMO0jOAnTLFKr8NqcJAA== } engines: { node: '>=12' } cpu: [x64] @@ -2656,10 +2552,10 @@ packages: dev: true optional: true - /esbuild-windows-32/0.14.21: + /esbuild-windows-32/0.14.34: resolution: { - integrity: sha512-V7vnTq67xPBUCk/9UtlolmQ798Ecjdr1ZoI1vcSgw7M82aSSt0eZdP6bh5KAFZU8pxDcx3qoHyWQfHYr11f22A== + integrity: sha512-EFhpXyHEcnqWYe2rAHFd8dRw8wkrd9U+9oqcyoEL84GbanAYjiiIjBZsnR8kl0sCQ5w6bLpk7vCEIA2VS32Vcg== } engines: { node: '>=12' } cpu: [ia32] @@ -2668,10 +2564,10 @@ packages: dev: true optional: true - /esbuild-windows-64/0.14.21: + /esbuild-windows-64/0.14.34: resolution: { - integrity: sha512-kDgHjKOHwjfJDCyRGELzVxiP/RBJBTA+wyspf78MTTJQkyPuxH2vChReNdWc+dU2S4gIZFHMdP1Qrl/k22ZmaA== + integrity: sha512-a8fbl8Ky7PxNEjf1aJmtxdDZj32/hC7S1OcA2ckEpCJRTjiKslI9vAdPpSjrKIWhws4Galpaawy0nB7fjHYf5Q== } engines: { node: '>=12' } cpu: [x64] @@ -2680,10 +2576,10 @@ packages: dev: true optional: true - /esbuild-windows-arm64/0.14.21: + /esbuild-windows-arm64/0.14.34: resolution: { - integrity: sha512-8Sbo0zpzgwWrwjQYLmHF78f7E2xg5Ve63bjB2ng3V2aManilnnTGaliq2snYg+NOX60+hEvJHRdVnuIAHW0lVw== + integrity: sha512-EYvmKbSa2B3sPnpC28UEu9jBK5atGV4BaVRE7CYGUci2Hlz4AvtV/LML+TcDMT6gBgibnN2gcltWclab3UutMg== } engines: { node: '>=12' } cpu: [arm64] @@ -2692,34 +2588,35 @@ packages: dev: true optional: true - /esbuild/0.14.21: + /esbuild/0.14.34: resolution: { - integrity: sha512-7WEoNMBJdLN993dr9h0CpFHPRc3yFZD+EAVY9lg6syJJ12gc5fHq8d75QRExuhnMkT2DaRiIKFThRvDWP+fO+A== + integrity: sha512-QIWdPT/gFF6hCaf4m7kP0cJ+JIuFkdHibI7vVFvu3eJS1HpVmYHWDulyN5WXwbRA0SX/7ZDaJ/1DH8SdY9xOJg== } engines: { node: '>=12' } hasBin: true requiresBuild: true optionalDependencies: - esbuild-android-arm64: 0.14.21 - esbuild-darwin-64: 0.14.21 - esbuild-darwin-arm64: 0.14.21 - esbuild-freebsd-64: 0.14.21 - esbuild-freebsd-arm64: 0.14.21 - esbuild-linux-32: 0.14.21 - esbuild-linux-64: 0.14.21 - esbuild-linux-arm: 0.14.21 - esbuild-linux-arm64: 0.14.21 - esbuild-linux-mips64le: 0.14.21 - esbuild-linux-ppc64le: 0.14.21 - esbuild-linux-riscv64: 0.14.21 - esbuild-linux-s390x: 0.14.21 - esbuild-netbsd-64: 0.14.21 - esbuild-openbsd-64: 0.14.21 - esbuild-sunos-64: 0.14.21 - esbuild-windows-32: 0.14.21 - esbuild-windows-64: 0.14.21 - esbuild-windows-arm64: 0.14.21 + esbuild-android-64: 0.14.34 + esbuild-android-arm64: 0.14.34 + esbuild-darwin-64: 0.14.34 + esbuild-darwin-arm64: 0.14.34 + esbuild-freebsd-64: 0.14.34 + esbuild-freebsd-arm64: 0.14.34 + esbuild-linux-32: 0.14.34 + esbuild-linux-64: 0.14.34 + esbuild-linux-arm: 0.14.34 + esbuild-linux-arm64: 0.14.34 + esbuild-linux-mips64le: 0.14.34 + esbuild-linux-ppc64le: 0.14.34 + esbuild-linux-riscv64: 0.14.34 + esbuild-linux-s390x: 0.14.34 + esbuild-netbsd-64: 0.14.34 + esbuild-openbsd-64: 0.14.34 + esbuild-sunos-64: 0.14.34 + esbuild-windows-32: 0.14.34 + esbuild-windows-64: 0.14.34 + esbuild-windows-arm64: 0.14.34 dev: true /escalade/3.1.1: @@ -2755,7 +2652,7 @@ packages: eslint: 7.32.0 dev: true - /eslint-plugin-svelte3/3.4.1_eslint@7.32.0+svelte@3.46.4: + /eslint-plugin-svelte3/3.4.1_eslint@7.32.0+svelte@3.47.0: resolution: { integrity: sha512-7p59WG8qV8L6wLdl4d/c3mdjkgVglQCdv5XOTk/iNPBKXuuV+Q0eFP5Wa6iJd/G2M1qR3BkLPEzaANOqKAZczw== @@ -2766,7 +2663,7 @@ packages: svelte: ^3.2.0 dependencies: eslint: 7.32.0 - svelte: 3.46.4 + svelte: 3.47.0 dev: true /eslint-scope/5.1.1: @@ -3066,16 +2963,6 @@ packages: } dev: false - /fs-minipass/2.1.0: - resolution: - { - integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== - } - engines: { node: '>= 8' } - dependencies: - minipass: 3.1.5 - dev: false - /fs.realpath/1.0.0: resolution: { integrity: sha1-FQStJSMVjKpA20onh8sBQRmU6k8= } @@ -3110,24 +2997,6 @@ packages: resolution: { integrity: sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= } dev: true - /gauge/3.0.1: - resolution: - { - integrity: sha512-6STz6KdQgxO4S/ko+AbjlFGGdGcknluoqU+79GOFCDqqyYj5OanQf9AjxwN0jCidtT+ziPMmPSt9E4hfQ0CwIQ== - } - engines: { node: '>=10' } - dependencies: - aproba: 2.0.0 - color-support: 1.1.3 - console-control-strings: 1.1.0 - has-unicode: 2.0.1 - object-assign: 4.1.1 - signal-exit: 3.0.5 - string-width: 2.1.1 - strip-ansi: 4.0.0 - wide-align: 1.1.5 - dev: false - /generate-password/1.7.0: resolution: { @@ -3248,10 +3117,10 @@ packages: } dev: true - /got/12.0.2: + /got/12.0.3: resolution: { - integrity: sha512-Zi4yHiqCgaorUbknr/RHFBsC3XqjSodaw0F3qxlqAqyj+OGYZl37/uy01R0qz++KANKQYdY5FHJ0okXZpEzwWQ== + integrity: sha512-hmdcXi/S0gcAtDg4P8j/rM7+j3o1Aq6bXhjxkDhRY2ipe7PHpvx/14DgTY2czHOLaGeU8VRvRecidwfu9qdFug== } engines: { node: '>=14.16' } dependencies: @@ -3297,10 +3166,6 @@ packages: engines: { node: '>=8' } dev: true - /has-unicode/2.0.1: - resolution: { integrity: sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= } - dev: false - /has/1.0.3: resolution: { @@ -3390,16 +3255,6 @@ packages: engines: { node: '>= 4' } dev: true - /import-cwd/3.0.0: - resolution: - { - integrity: sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg== - } - engines: { node: '>=8' } - dependencies: - import-from: 3.0.0 - dev: true - /import-fresh/3.3.0: resolution: { @@ -3411,16 +3266,6 @@ packages: resolve-from: 4.0.0 dev: true - /import-from/3.0.0: - resolution: - { - integrity: sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ== - } - engines: { node: '>=8' } - dependencies: - resolve-from: 5.0.0 - dev: true - /imurmurhash/0.1.4: resolution: { integrity: sha1-khi5srkoojixPcT7a21XbyMUU+o= } engines: { node: '>=0.8.19' } @@ -3455,10 +3300,10 @@ packages: loose-envify: 1.4.0 dev: true - /ioredis/4.28.3: + /ioredis/4.28.5: resolution: { - integrity: sha512-9JOWVgBnuSxpIgfpjc1OeY1OLmA4t2KOWWURTDRXky+eWO0LZhI33pQNT9gYxANUXfh5p/zYephYni6GPRsksQ== + integrity: sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A== } engines: { node: '>=6' } dependencies: @@ -3477,10 +3322,6 @@ packages: - supports-color dev: false - /is-arrayish/0.2.1: - resolution: { integrity: sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= } - dev: true - /is-binary-path/2.1.0: resolution: { @@ -3513,17 +3354,13 @@ packages: engines: { node: '>=0.10.0' } dev: true - /is-fullwidth-code-point/2.0.0: - resolution: { integrity: sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= } - engines: { node: '>=4' } - dev: false - /is-fullwidth-code-point/3.0.0: resolution: { integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== } engines: { node: '>=8' } + dev: true /is-fullwidth-code-point/4.0.0: resolution: @@ -3619,13 +3456,6 @@ packages: } dev: false - /json-parse-even-better-errors/2.3.1: - resolution: - { - integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== - } - dev: true - /json-schema-traverse/0.4.1: resolution: { @@ -3725,14 +3555,18 @@ packages: engines: { node: '>=10' } dev: true - /lines-and-columns/1.1.6: - resolution: { integrity: sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= } - dev: true - - /lint-staged/12.3.7: + /lilconfig/2.0.5: resolution: { - integrity: sha512-/S4D726e2GIsDVWIk1XGvheCaDm1SJRQp8efamZFWJxQMVEbOwSysp7xb49Oo73KYCdy97mIWinhlxcoNqIfIQ== + integrity: sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg== + } + engines: { node: '>=10' } + dev: true + + /lint-staged/12.4.0: + resolution: + { + integrity: sha512-3X7MR0h9b7qf4iXf/1n7RlVAx+EzpAZXoCEMhVSpaBlgKDfH2ewf+QUm7BddFyq29v4dgPP+8+uYpWuSWx035A== } engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } hasBin: true @@ -3895,6 +3729,7 @@ packages: engines: { node: '>=10' } dependencies: yallist: 4.0.0 + dev: true /lru_map/0.3.3: resolution: { integrity: sha1-tcg1G5Rky9dQM1p5ZQoOwOVhGN0= } @@ -3916,16 +3751,6 @@ packages: sourcemap-codec: 1.4.8 dev: true - /make-dir/3.1.0: - resolution: - { - integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== - } - engines: { node: '>=8' } - dependencies: - semver: 6.3.0 - dev: false - /make-error/1.3.6: resolution: { @@ -4006,27 +3831,6 @@ packages: } dev: true - /minipass/3.1.5: - resolution: - { - integrity: sha512-+8NzxD82XQoNKNrl1d/FSi+X8wAEWR+sbYAfIvub4Nz0d22plFG72CEVVaufV8PNf4qSslFTD8VMOxNVhHCjTw== - } - engines: { node: '>=8' } - dependencies: - yallist: 4.0.0 - dev: false - - /minizlib/2.1.2: - resolution: - { - integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== - } - engines: { node: '>= 8' } - dependencies: - minipass: 3.1.5 - yallist: 4.0.0 - dev: false - /mkdirp-classic/0.5.3: resolution: { @@ -4044,15 +3848,6 @@ packages: minimist: 1.2.5 dev: true - /mkdirp/1.0.4: - resolution: - { - integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== - } - engines: { node: '>=10' } - hasBin: true - dev: false - /mri/1.2.0: resolution: { @@ -4121,27 +3916,10 @@ packages: resolution: { integrity: sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= } dev: true - /node-addon-api/3.2.1: + /node-forge/1.3.1: resolution: { - integrity: sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== - } - dev: false - - /node-fetch/2.6.6: - resolution: - { - integrity: sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA== - } - engines: { node: 4.x || >=6.0.0 } - dependencies: - whatwg-url: 5.0.0 - dev: false - - /node-forge/1.3.0: - resolution: - { - integrity: sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA== + integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== } engines: { node: '>= 6.13.0' } dev: false @@ -4169,17 +3947,6 @@ packages: } dev: false - /nopt/5.0.0: - resolution: - { - integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== - } - engines: { node: '>=6' } - hasBin: true - dependencies: - abbrev: 1.1.1 - dev: false - /normalize-path/3.0.0: resolution: { @@ -4211,27 +3978,10 @@ packages: path-key: 3.1.1 dev: true - /npmlog/5.0.1: + /object-hash/3.0.0: resolution: { - integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw== - } - dependencies: - are-we-there-yet: 2.0.0 - console-control-strings: 1.1.0 - gauge: 3.0.1 - set-blocking: 2.0.0 - dev: false - - /object-assign/4.1.1: - resolution: { integrity: sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= } - engines: { node: '>=0.10.0' } - dev: false - - /object-hash/2.2.0: - resolution: - { - integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== + integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== } engines: { node: '>= 6' } dev: true @@ -4329,19 +4079,6 @@ packages: callsites: 3.1.0 dev: true - /parse-json/5.2.0: - resolution: - { - integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== - } - engines: { node: '>=8' } - dependencies: - '@babel/code-frame': 7.12.11 - error-ex: 1.3.2 - json-parse-even-better-errors: 2.3.1 - lines-and-columns: 1.1.6 - dev: true - /path-is-absolute/1.0.1: resolution: { integrity: sha1-F0uSaHNVNP+8es5r9TpanhtcX18= } engines: { node: '>=0.10.0' } @@ -4405,21 +4142,24 @@ packages: postcss: 8.4.12 dev: true - /postcss-load-config/3.1.0_ts-node@10.7.0: + /postcss-load-config/3.1.4_postcss@8.4.12+ts-node@10.7.0: resolution: { - integrity: sha512-ipM8Ds01ZUophjDTQYSVP70slFSYg3T0/zyfII5vzhN6V57YSxMgG5syXuwi5VtS8wSf3iL30v0uBdoIVx4Q0g== + integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg== } engines: { node: '>= 10' } peerDependencies: + postcss: '>=8.0.9' ts-node: '>=9.0.0' peerDependenciesMeta: + postcss: + optional: true ts-node: optional: true dependencies: - import-cwd: 3.0.0 - lilconfig: 2.0.4 - ts-node: 10.7.0_ee885bc7281b682b6adbed6ae09ee090 + lilconfig: 2.0.5 + postcss: 8.4.12 + ts-node: 10.7.0_de7c86b0cde507c63a0402da5b982bd3 yaml: 1.10.2 dev: true @@ -4433,13 +4173,13 @@ packages: postcss: ^8.2.14 dependencies: postcss: 8.4.12 - postcss-selector-parser: 6.0.9 + postcss-selector-parser: 6.0.10 dev: true - /postcss-selector-parser/6.0.9: + /postcss-selector-parser/6.0.10: resolution: { - integrity: sha512-UO3SgnZOVTwu4kyLR22UQ1xZh086RyNZppb7lLAKBFK8a32ttG5i87Y/P3+2bRSjZNyJ1B7hfFNo273tKe9YxQ== + integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w== } engines: { node: '>=4' } dependencies: @@ -4474,35 +4214,35 @@ packages: engines: { node: '>= 0.8.0' } dev: true - /prettier-plugin-svelte/2.6.0_prettier@2.6.1+svelte@3.46.4: + /prettier-plugin-svelte/2.7.0_prettier@2.6.2+svelte@3.47.0: resolution: { - integrity: sha512-NPSRf6Y5rufRlBleok/pqg4+1FyGsL0zYhkYP6hnueeL1J/uCm3OfOZPsLX4zqD9VAdcXfyEL2PYqGv8ZoOSfA== + integrity: sha512-fQhhZICprZot2IqEyoiUYLTRdumULGRvw0o4dzl5jt0jfzVWdGqeYW27QTWAeXhoupEZJULmNoH3ueJwUWFLIA== } peerDependencies: prettier: ^1.16.4 || ^2.0.0 svelte: ^3.2.0 dependencies: - prettier: 2.6.1 - svelte: 3.46.4 + prettier: 2.6.2 + svelte: 3.47.0 dev: true - /prettier-plugin-tailwindcss/0.1.8_prettier@2.6.1: + /prettier-plugin-tailwindcss/0.1.10_prettier@2.6.2: resolution: { - integrity: sha512-hwarSBCswAXa+kqYtaAkFr3Vop9o04WOyZs0qo3NyvW8L7f1rif61wRyq0+ArmVThOuRBcJF5hjGXYk86cwemg== + integrity: sha512-ooDGNuXUjgCXfShliVYQ6+0iXqUFXn+zdNInPe0WZN9qINt9srbLGFGY5jeVL4MXtY20/4S8JaBcd8l6N6NfCQ== } engines: { node: '>=12.17.0' } peerDependencies: prettier: '>=2.2.0' dependencies: - prettier: 2.6.1 + prettier: 2.6.2 dev: true - /prettier/2.6.1: + /prettier/2.6.2: resolution: { - integrity: sha512-8UVbTBYGwN37Bs9LERmxCPjdvPxlEowx2urIL6urHzdb3SDq4B/Z6xLFCblrSnE4iKWcS6ziJ3aOYrc1kz/E2A== + integrity: sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew== } engines: { node: '>=10.13.0' } hasBin: true @@ -4699,14 +4439,6 @@ packages: engines: { node: '>=4' } dev: true - /resolve-from/5.0.0: - resolution: - { - integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== - } - engines: { node: '>=8' } - dev: true - /resolve/1.22.0: resolution: { @@ -4772,6 +4504,7 @@ packages: hasBin: true dependencies: glob: 7.2.0 + dev: true /rollup/2.61.1: resolution: @@ -4867,10 +4600,7 @@ packages: hasBin: true dependencies: lru-cache: 6.0.0 - - /set-blocking/2.0.0: - resolution: { integrity: sha1-BF+XgtARrppoA93TgrJDkrPYkPc= } - dev: false + dev: true /shebang-command/1.2.0: resolution: { integrity: sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= } @@ -4905,6 +4635,7 @@ packages: { integrity: sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ== } + dev: true /slash/1.0.0: resolution: { integrity: sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU= } @@ -5039,17 +4770,6 @@ packages: engines: { node: '>=0.6.19' } dev: true - /string-width/2.1.1: - resolution: - { - integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== - } - engines: { node: '>=4' } - dependencies: - is-fullwidth-code-point: 2.0.0 - strip-ansi: 4.0.0 - dev: false - /string-width/4.2.3: resolution: { @@ -5060,6 +4780,7 @@ packages: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + dev: true /string-width/5.1.0: resolution: @@ -5089,13 +4810,6 @@ packages: ansi-regex: 2.1.1 dev: true - /strip-ansi/4.0.0: - resolution: { integrity: sha1-qEeQIusaw2iocTibY1JixQXuNo8= } - engines: { node: '>=4' } - dependencies: - ansi-regex: 3.0.0 - dev: false - /strip-ansi/6.0.1: resolution: { @@ -5104,6 +4818,7 @@ packages: engines: { node: '>=8' } dependencies: ansi-regex: 5.0.1 + dev: true /strip-ansi/7.0.1: resolution: @@ -5182,10 +4897,10 @@ packages: engines: { node: '>= 0.4' } dev: true - /svelte-check/2.4.6_postcss@8.4.12+svelte@3.46.4: + /svelte-check/2.7.0_postcss@8.4.12+svelte@3.47.0: resolution: { - integrity: sha512-luzdly7RJhyXucQe8ID/7CqDgXdMrPYXmyZBjCbp+cixzTopZotuWevrB5hWDOnOU19m2cyetigIIa7WDHnCmQ== + integrity: sha512-GrvG24j0+i8AOm0k0KyJ6Dqc+TAR2yzB7rtS4nljHStunVxCTr/1KYlv4EsOeoqtHLzeWMOd5D2O6nDdP/yw4A== } hasBin: true peerDependencies: @@ -5194,12 +4909,11 @@ packages: chokidar: 3.5.3 fast-glob: 3.2.11 import-fresh: 3.3.0 - minimist: 1.2.5 picocolors: 1.0.0 sade: 1.7.4 source-map: 0.7.3 - svelte: 3.46.4 - svelte-preprocess: 4.10.4_296873641a0ad9f42fe92172d27bcedd + svelte: 3.47.0 + svelte-preprocess: 4.10.6_41810887ae6c6d59323116f47e33fa38 typescript: 4.6.3 transitivePeerDependencies: - '@babel/core' @@ -5214,7 +4928,7 @@ packages: - sugarss dev: true - /svelte-hmr/0.14.9_svelte@3.46.4: + /svelte-hmr/0.14.9_svelte@3.47.0: resolution: { integrity: sha512-bKE9+4qb4sAnA+TKHiYurUl970rjA0XmlP9TEP7K/ncyWz3m81kA4HOgmlZK/7irGK7gzZlaPDI3cmf8fp/+tg== @@ -5222,20 +4936,20 @@ packages: peerDependencies: svelte: '>=3.19.0' dependencies: - svelte: 3.46.4 + svelte: 3.47.0 dev: true - /svelte-kit-cookie-session/2.1.2: + /svelte-kit-cookie-session/2.1.3: resolution: { - integrity: sha512-PfxIWDhiyYWu7iKlL0GHpmwDrdFh+rX/WmBzOuvctF25UqngIo9MCiegWBSBLE1RBwNs5UqaIeI8+vligmY07g== + integrity: sha512-7Xk3CNbpLAi1KodlsV5W5jULQ2NxQunaXtAYqAuzIEXIq2EwC4oDa25kdmHjNe33epV0t4r0WwxZOuSdJPsapg== } dev: false - /svelte-preprocess/4.10.4_296873641a0ad9f42fe92172d27bcedd: + /svelte-preprocess/4.10.6_41810887ae6c6d59323116f47e33fa38: resolution: { - integrity: sha512-fuwol0N4UoHsNQolLFbMqWivqcJ9N0vfWO9IuPAiX/5okfoGXURyJ6nECbuEIv0nU3M8Xe2I1ONNje2buk7l6A== + integrity: sha512-I2SV1w/AveMvgIQlUF/ZOO3PYVnhxfcpNyGt8pxpUVhPfyfL/CZBkkw/KPfuFix5FJ9TnnNYMhACK3DtSaYVVQ== } engines: { node: '>= 9.11.2' } requiresBuild: true @@ -5283,7 +4997,7 @@ packages: postcss: 8.4.12 sorcery: 0.10.0 strip-indent: 3.0.0 - svelte: 3.46.4 + svelte: 3.47.0 typescript: 4.6.3 dev: true @@ -5294,14 +5008,27 @@ packages: } dev: true - /svelte/3.46.4: + /svelte/3.47.0: resolution: { - integrity: sha512-qKJzw6DpA33CIa+C/rGp4AUdSfii0DOTCzj/2YpSKKayw5WGSS624Et9L1nU1k2OVRS9vaENQXp2CVZNU+xvIg== + integrity: sha512-4JaJp3HEoTCGARRWZQIZDUanhYv0iyoHikklVHVLH9xFE9db22g4TDv7CPeNA8HD1JgjXI1vlhR1JZvvhaTu2Q== } engines: { node: '>= 8' } dev: true + /sveltekit-i18n/2.1.2_svelte@3.46.4: + resolution: + { + integrity: sha512-s5YxcbNd2EWNZaZR1A4Drt8s53E4fpUkN4XIWd3VRpw1pihZVWssqmBW1qkjQ6AB0kiu1Qwule+vt1HkbQOjrg== + } + peerDependencies: + svelte: ^3.x + dependencies: + '@sveltekit-i18n/base': 1.1.1_svelte@3.46.4 + '@sveltekit-i18n/parser-default': 1.0.3 + svelte: 3.46.4 + dev: true + /table/6.7.2: resolution: { @@ -5317,7 +5044,7 @@ packages: strip-ansi: 6.0.1 dev: true - /tailwindcss-scrollbar/0.1.0_tailwindcss@3.0.23: + /tailwindcss-scrollbar/0.1.0_tailwindcss@3.0.24: resolution: { integrity: sha512-egipxw4ooQDh94x02XQpPck0P0sfwazwoUGfA9SedPATIuYDR+6qe8d31Gl7YsSMRiOKDkkqfI0kBvEw9lT/Hg== @@ -5325,38 +5052,35 @@ packages: peerDependencies: tailwindcss: '>= 2.x.x' dependencies: - tailwindcss: 3.0.23_b89136460714832cdda11d1e9d57d1ff + tailwindcss: 3.0.24_ts-node@10.7.0 dev: false - /tailwindcss/3.0.23_b89136460714832cdda11d1e9d57d1ff: + /tailwindcss/3.0.24_ts-node@10.7.0: resolution: { - integrity: sha512-+OZOV9ubyQ6oI2BXEhzw4HrqvgcARY38xv3zKcjnWtMIZstEsXdI9xftd1iB7+RbOnj2HOEzkA0OyB5BaSxPQA== + integrity: sha512-H3uMmZNWzG6aqmg9q07ZIRNIawoiEcNFKDfL+YzOPuPsXuDXxJxB9icqzLgdzKNwjG3SAro2h9SYav8ewXNgig== } engines: { node: '>=12.13.0' } hasBin: true - peerDependencies: - autoprefixer: ^10.0.2 dependencies: arg: 5.0.1 - autoprefixer: 10.4.4_postcss@8.4.12 - chalk: 4.1.2 chokidar: 3.5.3 color-name: 1.1.4 - cosmiconfig: 7.0.1 detective: 5.2.0 didyoumean: 1.2.2 dlv: 1.1.3 fast-glob: 3.2.11 glob-parent: 6.0.2 is-glob: 4.0.3 + lilconfig: 2.0.5 normalize-path: 3.0.0 - object-hash: 2.2.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.0_ts-node@10.7.0 + 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-selector-parser: 6.0.9 + postcss-selector-parser: 6.0.10 postcss-value-parser: 4.2.0 quick-lru: 5.1.1 resolve: 1.22.0 @@ -5390,21 +5114,6 @@ packages: readable-stream: 3.6.0 dev: false - /tar/6.1.11: - resolution: - { - integrity: sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== - } - engines: { node: '>= 10' } - dependencies: - chownr: 2.0.0 - fs-minipass: 2.1.0 - minipass: 3.1.5 - minizlib: 2.1.2 - mkdirp: 1.0.4 - yallist: 4.0.0 - dev: false - /text-table/0.2.0: resolution: { integrity: sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= } dev: true @@ -5438,16 +5147,12 @@ packages: is-number: 7.0.0 dev: true - /tr46/0.0.3: - resolution: { integrity: sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= } - dev: false - /trim-right/1.0.1: resolution: { integrity: sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= } engines: { node: '>=0.10.0' } dev: true - /ts-node/10.7.0_ee885bc7281b682b6adbed6ae09ee090: + /ts-node/10.7.0_de7c86b0cde507c63a0402da5b982bd3: resolution: { integrity: sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A== @@ -5469,7 +5174,7 @@ packages: '@tsconfig/node12': 1.0.9 '@tsconfig/node14': 1.0.1 '@tsconfig/node16': 1.0.2 - '@types/node': 17.0.23 + '@types/node': 17.0.25 acorn: 8.5.0 acorn-walk: 8.2.0 arg: 4.1.3 @@ -5597,10 +5302,10 @@ packages: } dev: true - /vite/2.8.0: + /vite/2.9.1: resolution: { - integrity: sha512-ed5rjyeysttuPJX/aKSA0gTB/8ZKLM5xF6FtEuKy1B9DiQbDNFMVMQxnb9JesgBPUMMIJxC8w5KZ/KNWLKFXoA== + integrity: sha512-vSlsSdOYGcYEJfkQ/NeLXgnRv5zZfpAsdztkIrs7AZHV8RCMZQkwjo4DS5BnrYTqoWqLoUe1Cah4aVO4oNNqCQ== } engines: { node: '>=12.2.0' } hasBin: true @@ -5616,7 +5321,7 @@ packages: stylus: optional: true dependencies: - esbuild: 0.14.21 + esbuild: 0.14.34 postcss: 8.4.12 resolve: 1.22.0 rollup: 2.61.1 @@ -5624,17 +5329,6 @@ packages: fsevents: 2.3.2 dev: true - /webidl-conversions/3.0.1: - resolution: { integrity: sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= } - dev: false - - /whatwg-url/5.0.0: - resolution: { integrity: sha1-lmRU6HZUYuN2RNNib2dCzotwll0= } - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - dev: false - /which/1.3.1: resolution: { @@ -5655,15 +5349,6 @@ packages: dependencies: isexe: 2.0.0 - /wide-align/1.1.5: - resolution: - { - integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== - } - dependencies: - string-width: 4.2.3 - dev: false - /word-wrap/1.2.3: resolution: { @@ -5716,6 +5401,7 @@ packages: { integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== } + dev: true /yaml/1.10.2: resolution: diff --git a/prisma/migrations/20220418214843_persistent_storage_services/migration.sql b/prisma/migrations/20220418214843_persistent_storage_services/migration.sql new file mode 100644 index 000000000..f85fd31df --- /dev/null +++ b/prisma/migrations/20220418214843_persistent_storage_services/migration.sql @@ -0,0 +1,12 @@ +-- CreateTable +CREATE TABLE "ServicePersistentStorage" ( + "id" TEXT NOT NULL PRIMARY KEY, + "serviceId" TEXT NOT NULL, + "path" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ServicePersistentStorage_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "ServicePersistentStorage_serviceId_path_key" ON "ServicePersistentStorage"("serviceId", "path"); diff --git a/prisma/migrations/20220419203408_multiply_dockerfile_locations/migration.sql b/prisma/migrations/20220419203408_multiply_dockerfile_locations/migration.sql new file mode 100644 index 000000000..ce32f0844 --- /dev/null +++ b/prisma/migrations/20220419203408_multiply_dockerfile_locations/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Application" ADD COLUMN "dockerFileLocation" TEXT; diff --git a/prisma/migrations/20220420202031_deno_configurations/migration.sql b/prisma/migrations/20220420202031_deno_configurations/migration.sql new file mode 100644 index 000000000..9d0e832dc --- /dev/null +++ b/prisma/migrations/20220420202031_deno_configurations/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "Application" ADD COLUMN "denoMainFile" TEXT; +ALTER TABLE "Application" ADD COLUMN "denoOptions" TEXT; diff --git a/prisma/migrations/20220420210057_branch_for_builds/migration.sql b/prisma/migrations/20220420210057_branch_for_builds/migration.sql new file mode 100644 index 000000000..8cd8e653d --- /dev/null +++ b/prisma/migrations/20220420210057_branch_for_builds/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Build" ADD COLUMN "branch" TEXT; diff --git a/prisma/migrations/20220425071132_umami/migration.sql b/prisma/migrations/20220425071132_umami/migration.sql new file mode 100644 index 000000000..259fb76cd --- /dev/null +++ b/prisma/migrations/20220425071132_umami/migration.sql @@ -0,0 +1,17 @@ +-- CreateTable +CREATE TABLE "Umami" ( + "id" TEXT NOT NULL PRIMARY KEY, + "serviceId" TEXT NOT NULL, + "postgresqlUser" TEXT NOT NULL, + "postgresqlPassword" TEXT NOT NULL, + "postgresqlDatabase" TEXT NOT NULL, + "postgresqlPublicPort" INTEGER, + "umamiAdminPassword" TEXT NOT NULL, + "hashSalt" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "Umami_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "Umami_serviceId_key" ON "Umami"("serviceId"); diff --git a/prisma/migrations/20220425075326_auto_update_coolify/migration.sql b/prisma/migrations/20220425075326_auto_update_coolify/migration.sql new file mode 100644 index 000000000..a102973ee --- /dev/null +++ b/prisma/migrations/20220425075326_auto_update_coolify/migration.sql @@ -0,0 +1,22 @@ +-- 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, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); +INSERT INTO "new_Setting" ("createdAt", "dualCerts", "fqdn", "id", "isRegistrationEnabled", "maxPort", "minPort", "proxyHash", "proxyPassword", "proxyUser", "updatedAt") SELECT "createdAt", "dualCerts", "fqdn", "id", "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/schema.prisma b/prisma/schema.prisma index 6df6f549e..6cd94b395 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1,5 +1,6 @@ generator client { - provider = "prisma-client-js" + provider = "prisma-client-js" + binaryTargets = ["native", "linux-musl"] } datasource db { @@ -17,6 +18,7 @@ model Setting { proxyPassword String proxyUser String proxyHash String? + isAutoUpdateEnabled Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } @@ -91,6 +93,9 @@ model Application { pythonWSGI String? pythonModule String? pythonVariable String? + dockerFileLocation String? + denoMainFile String? + denoOptions String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt settings ApplicationSettings? @@ -118,14 +123,25 @@ model ApplicationSettings { model ApplicationPersistentStorage { id String @id @default(cuid()) application Application @relation(fields: [applicationId], references: [id]) - applicationId String - path String + applicationId String + path String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([applicationId, path]) } +model ServicePersistentStorage { + id String @id @default(cuid()) + service Service @relation(fields: [serviceId], references: [id]) + serviceId String + path String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([serviceId, path]) +} + model Secret { id String @id @default(cuid()) name String @@ -169,6 +185,7 @@ model Build { githubAppId String? gitlabAppId String? commit String? + branch String? status String? @default("queued") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -267,17 +284,17 @@ model DatabaseSettings { } model Service { - id String @id @default(cuid()) + id String @id @default(cuid()) name String fqdn String? - dualCerts Boolean @default(false) + dualCerts Boolean @default(false) type String? version String? teams Team[] destinationDockerId String? - destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id]) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt plausibleAnalytics PlausibleAnalytics? minio Minio? vscodeserver Vscodeserver? @@ -285,6 +302,8 @@ model Service { ghost Ghost? serviceSecret ServiceSecret[] meiliSearch MeiliSearch? + persistentStorage ServicePersistentStorage[] + umami Umami? } model PlausibleAnalytics { @@ -369,3 +388,17 @@ model MeiliSearch { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } + +model Umami { + id String @id @default(cuid()) + serviceId String @unique + postgresqlUser String + postgresqlPassword String + postgresqlDatabase String + postgresqlPublicPort Int? + umamiAdminPassword String + hashSalt String + service Service @relation(fields: [serviceId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/prisma/seed.cjs b/prisma/seed.cjs index 61f3928f6..1e6b160c6 100644 --- a/prisma/seed.cjs +++ b/prisma/seed.cjs @@ -50,6 +50,20 @@ async function main() { } }); } + + // Set auto-update based on env variable + const isAutoUpdateEnabled = process.env['COOLIFY_AUTO_UPDATE'] === 'true'; + const settings = await prisma.setting.findFirst({}); + if (settings) { + await prisma.setting.update({ + where: { + id: settings.id + }, + data: { + isAutoUpdateEnabled + } + }); + } } main() .catch((e) => { diff --git a/src/app.d.ts b/src/app.d.ts index d432aef4a..ccdea3cee 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -6,7 +6,11 @@ declare namespace App { cookies: Record; } interface Platform {} - interface Session extends SessionData {} + interface Session extends SessionData { + whiteLabelDetails: { + icon: string | null; + }; + } interface Stuff { service: any; application: any; @@ -27,6 +31,7 @@ interface SessionData { userId?: string | null; teamId?: string | null; permission?: string; + lang?: string; isAdmin?: boolean; expires?: string | null; } diff --git a/src/hooks.ts b/src/hooks.ts index c72c7fd6c..50624b22f 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -6,8 +6,12 @@ import { getUserDetails, sentry } from '$lib/common'; import { version } from '$lib/common'; import cookie from 'cookie'; import { dev } from '$app/env'; +import { locales } from '$lib/translations'; const whiteLabeled = process.env['COOLIFY_WHITE_LABELED'] === 'true'; +const whiteLabelDetails = { + icon: (whiteLabeled && process.env['COOLIFY_WHITE_LABELED_ICON']) || null +}; export const handle = handleSession( { @@ -17,6 +21,24 @@ export const handle = handleSession( }, async function ({ event, resolve }) { let response; + + const { url, request } = event; + + // Get defined locales + const supportedLocales = locales.get(); + let locale; + + if (event.locals.cookies['lang']) { + locale = event.locals.cookies['lang']; + } else if (!locale) { + locale = `${`${request.headers.get('accept-language')}`.match( + /[a-zA-Z]+?(?=-|_|,|;)/ + )}`.toLowerCase(); + } + + // Set default locale if user preferred locale does not match + if (!supportedLocales.includes(locale)) locale = 'en'; + try { if (event.locals.cookies) { if (event.locals.cookies['kit.session']) { @@ -36,12 +58,14 @@ export const handle = handleSession( } response = await resolve(event, { - ssr: !event.url.pathname.startsWith('/webhooks/success') + ssr: !event.url.pathname.startsWith('/webhooks/success'), + transformPage: ({ html }) => html.replace(//, ``) }); } catch (error) { console.log(error); response = await resolve(event, { - ssr: !event.url.pathname.startsWith('/webhooks/success') + ssr: !event.url.pathname.startsWith('/webhooks/success'), + transformPage: ({ html }) => html.replace(//, ``) }); response.headers.append( 'Set-Cookie', @@ -64,16 +88,27 @@ export const handle = handleSession( expires: new Date('Thu, 01 Jan 1970 00:00:01 GMT') }) ); - } finally { - return response; } + + response.headers.append( + 'Set-Cookie', + cookie.serialize('lang', locale, { + path: '/', + sameSite: 'strict', + maxAge: 30 * 24 * 60 * 60 + }) + ); + + return response; } ); export const getSession: GetSession = function ({ locals }) { return { + lang: locals.cookies.lang, version, whiteLabeled, + whiteLabelDetails, ...locals.session.data }; }; diff --git a/src/lib/api.ts b/src/lib/api.ts index 42fe10a1e..ab145a2f7 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,9 +1,15 @@ -async function send({ method, path, data = {}, headers, timeout = 30000 }) { +async function send({ + method, + path, + data = {}, + headers, + timeout = 120000 +}): Promise> { const controller = new AbortController(); const id = setTimeout(() => controller.abort(), timeout); const opts = { method, headers: {}, body: null, signal: controller.signal }; if (Object.keys(data).length > 0) { - let parsedData = data; + const parsedData = data; for (const [key, value] of Object.entries(data)) { if (value === '') { parsedData[key] = null; @@ -43,18 +49,33 @@ async function send({ method, path, data = {}, headers, timeout = 30000 }) { return responseData; } -export function get(path, headers = {}): Promise { +export function get( + path: string, + headers?: Record +): Promise> { return send({ method: 'GET', path, headers }); } -export function del(path, data = {}, headers = {}): Promise { +export function del( + path: string, + data: Record, + headers?: Record +): Promise> { return send({ method: 'DELETE', path, data, headers }); } -export function post(path, data, headers = {}): Promise { +export function post( + path: string, + data: Record, + headers?: Record +): Promise> { return send({ method: 'POST', path, data, headers }); } -export function put(path, data, headers = {}): Promise { +export function put( + path: string, + data: Record, + headers?: Record +): Promise> { return send({ method: 'PUT', path, data, headers }); } diff --git a/src/lib/buildPacks/common.ts b/src/lib/buildPacks/common.ts index 65f4d6903..05313a3d2 100644 --- a/src/lib/buildPacks/common.ts +++ b/src/lib/buildPacks/common.ts @@ -91,7 +91,9 @@ export const setDefaultConfiguration = async (data) => { startCommand, buildCommand, publishDirectory, - baseDirectory + baseDirectory, + dockerFileLocation, + denoMainFile } = data; const template = scanningTemplates[buildPack]; if (!port) { @@ -102,14 +104,25 @@ export const setDefaultConfiguration = async (data) => { else if (buildPack === 'php') port = 80; else if (buildPack === 'python') port = 8000; } - if (!installCommand) installCommand = template?.installCommand || 'yarn install'; - if (!startCommand) startCommand = template?.startCommand || 'yarn start'; - if (!buildCommand) buildCommand = template?.buildCommand || null; + if (!installCommand && buildPack !== 'static') + installCommand = template?.installCommand || 'yarn install'; + if (!startCommand && buildPack !== 'static') + startCommand = template?.startCommand || 'yarn start'; + if (!buildCommand && buildPack !== 'static') buildCommand = template?.buildCommand || null; if (!publishDirectory) publishDirectory = template?.publishDirectory || null; if (baseDirectory) { if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`; if (!baseDirectory.endsWith('/')) baseDirectory = `${baseDirectory}/`; } + if (dockerFileLocation) { + if (!dockerFileLocation.startsWith('/')) dockerFileLocation = `/${dockerFileLocation}`; + if (dockerFileLocation.endsWith('/')) dockerFileLocation = dockerFileLocation.slice(0, -1); + } else { + dockerFileLocation = '/Dockerfile'; + } + if (!denoMainFile) { + denoMainFile = 'main.ts'; + } return { buildPack, @@ -118,7 +131,9 @@ export const setDefaultConfiguration = async (data) => { startCommand, buildCommand, publishDirectory, - baseDirectory + baseDirectory, + dockerFileLocation, + denoMainFile }; }; @@ -184,7 +199,11 @@ export async function copyBaseConfigurationFiles(buildPack, workdir, buildId, ap } ` ); - await saveBuildLog({ line: 'Copied default configuration file.', buildId, applicationId }); + await saveBuildLog({ + line: 'Copied default configuration file for Nginx.', + buildId, + applicationId + }); } } catch (error) { console.log(error); diff --git a/src/lib/buildPacks/deno.ts b/src/lib/buildPacks/deno.ts new file mode 100644 index 000000000..2e5569438 --- /dev/null +++ b/src/lib/buildPacks/deno.ts @@ -0,0 +1,54 @@ +import { buildImage } from '$lib/docker'; +import { promises as fs } from 'fs'; + +const createDockerfile = async (data, image): Promise => { + const { workdir, port, baseDirectory, secrets, pullmergeRequestId, denoMainFile, denoOptions } = + data; + const Dockerfile: Array = []; + + let depsFound = false; + try { + await fs.readFile(`${workdir}${baseDirectory || ''}/deps.ts`); + depsFound = true; + } catch (error) {} + + Dockerfile.push(`FROM ${image}`); + Dockerfile.push('WORKDIR /app'); + Dockerfile.push(`LABEL coolify.image=true`); + if (secrets.length > 0) { + secrets.forEach((secret) => { + if (secret.isBuildSecret) { + if (pullmergeRequestId) { + if (secret.isPRMRSecret) { + Dockerfile.push(`ARG ${secret.name}=${secret.value}`); + } + } else { + if (!secret.isPRMRSecret) { + Dockerfile.push(`ARG ${secret.name}=${secret.value}`); + } + } + } + }); + } + if (depsFound) { + Dockerfile.push(`COPY .${baseDirectory || ''}/deps.ts /app`); + Dockerfile.push(`RUN deno cache deps.ts`); + } + Dockerfile.push(`COPY ${denoMainFile} /app`); + Dockerfile.push(`RUN deno cache ${denoMainFile}`); + Dockerfile.push(`COPY .${baseDirectory || ''} ./`); + Dockerfile.push(`ENV NO_COLOR true`); + Dockerfile.push(`EXPOSE ${port}`); + Dockerfile.push(`CMD deno run ${denoOptions ? denoOptions.split(' ') : ''} ${denoMainFile}`); + await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); +}; + +export default async function (data) { + try { + const image = 'denoland/deno:latest'; + await createDockerfile(data, image); + await buildImage(data); + } catch (error) { + throw error; + } +} diff --git a/src/lib/buildPacks/docker.ts b/src/lib/buildPacks/docker.ts index 371ffed82..1ddf81a60 100644 --- a/src/lib/buildPacks/docker.ts +++ b/src/lib/buildPacks/docker.ts @@ -10,15 +10,16 @@ export default async function ({ buildId, baseDirectory, secrets, - pullmergeRequestId + pullmergeRequestId, + dockerFileLocation }) { try { - let file = `${workdir}/Dockerfile`; + const file = `${workdir}${dockerFileLocation}`; + let dockerFileOut = `${workdir}`; if (baseDirectory) { - file = `${workdir}/${baseDirectory}/Dockerfile`; - workdir = `${workdir}/${baseDirectory}`; + dockerFileOut = `${workdir}${baseDirectory}`; + workdir = `${workdir}${baseDirectory}`; } - const Dockerfile: Array = (await fs.readFile(`${file}`, 'utf8')) .toString() .trim() @@ -26,20 +27,23 @@ export default async function ({ if (secrets.length > 0) { secrets.forEach((secret) => { if (secret.isBuildSecret) { - if (pullmergeRequestId) { - if (secret.isPRMRSecret) { - Dockerfile.push(`ARG ${secret.name}=${secret.value}`); - } - } else { - if (!secret.isPRMRSecret) { - Dockerfile.push(`ARG ${secret.name}=${secret.value}`); - } + if ( + (pullmergeRequestId && secret.isPRMRSecret) || + (!pullmergeRequestId && !secret.isPRMRSecret) + ) { + Dockerfile.unshift(`ARG ${secret.name}=${secret.value}`); + + Dockerfile.forEach((line, index) => { + if (line.startsWith('FROM')) { + Dockerfile.splice(index + 1, 0, `ARG ${secret.name}`); + } + }); } } }); } - await fs.writeFile(`${file}`, Dockerfile.join('\n')); - await buildImage({ applicationId, tag, workdir, docker, buildId, debug }); + await fs.writeFile(`${dockerFileOut}${dockerFileLocation}`, Dockerfile.join('\n')); + await buildImage({ applicationId, tag, workdir, docker, buildId, debug, dockerFileLocation }); } catch (error) { throw error; } diff --git a/src/lib/buildPacks/index.ts b/src/lib/buildPacks/index.ts index babbe8f17..ac31afc16 100644 --- a/src/lib/buildPacks/index.ts +++ b/src/lib/buildPacks/index.ts @@ -13,6 +13,7 @@ import rust from './rust'; import astro from './static'; import eleventy from './static'; import python from './python'; +import deno from './deno'; export { node, @@ -29,5 +30,6 @@ export { rust, astro, eleventy, - python + python, + deno }; diff --git a/src/lib/common.ts b/src/lib/common.ts index 57cdabba2..acbe6c88c 100644 --- a/src/lib/common.ts +++ b/src/lib/common.ts @@ -12,7 +12,8 @@ import { version as currentVersion } from '../../package.json'; import dayjs from 'dayjs'; import Cookie from 'cookie'; import os from 'os'; -import cuid from 'cuid'; +import type { RequestEvent } from '@sveltejs/kit/types/internal'; +import type { Job } from 'bullmq'; try { if (!dev) { @@ -25,7 +26,7 @@ try { initialScope: { tags: { appId: process.env['COOLIFY_APP_ID'], - 'os.arch': os.arch(), + 'os.arch': getOsArch(), 'os.platform': os.platform(), 'os.release': os.release() } @@ -45,37 +46,30 @@ const customConfig: Config = { export const version = currentVersion; export const asyncExecShell = util.promisify(child.exec); -export const asyncSleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay)); - +export const asyncSleep = (delay: number): Promise => + new Promise((resolve) => setTimeout(resolve, delay)); export const sentry = Sentry; -export const uniqueName = () => uniqueNamesGenerator(customConfig); +export const uniqueName = (): string => uniqueNamesGenerator(customConfig); -export const saveBuildLog = async ({ line, buildId, applicationId }) => { - if (line) { - if (line.includes('ghs_')) { - const regex = /ghs_.*@/g; - line = line.replace(regex, '@'); - } - const addTimestamp = `${generateTimestamp()} ${line}`; - return await buildLogQueue.add(buildId, { buildId, line: addTimestamp, applicationId }); +export const saveBuildLog = async ({ + line, + buildId, + applicationId +}: { + line: string; + buildId: string; + applicationId: string; +}): Promise => { + if (line && typeof line === 'string' && line.includes('ghs_')) { + const regex = /ghs_.*@/g; + line = line.replace(regex, '@'); } + const addTimestamp = `${generateTimestamp()} ${line}`; + return await buildLogQueue.add(buildId, { buildId, line: addTimestamp, applicationId }); }; -export const isTeamIdTokenAvailable = (request) => { - const cookie = request.headers.cookie - ?.split(';') - .map((s) => s.trim()) - .find((s) => s.startsWith('teamId=')) - ?.split('=')[1]; - if (!cookie) { - return getTeam(request); - } else { - return cookie; - } -}; - -export const getTeam = (event) => { +export const getTeam = (event: RequestEvent): string | null => { const cookies = Cookie.parse(event.request.headers.get('cookie')); if (cookies?.teamId) { return cookies.teamId; @@ -85,14 +79,28 @@ export const getTeam = (event) => { return null; }; -export const getUserDetails = async (event, isAdminRequired = true) => { +export const getUserDetails = async ( + event: RequestEvent, + isAdminRequired = true +): Promise<{ + teamId: string; + userId: string; + permission: string; + status: number; + body: { message: string }; +}> => { const teamId = getTeam(event); const userId = event?.locals?.session?.data?.userId || null; - const { permission = 'read' } = await db.prisma.permission.findFirst({ - where: { teamId, userId }, - select: { permission: true }, - rejectOnNotFound: true - }); + 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; + } + const payload = { teamId, userId, @@ -112,11 +120,11 @@ export const getUserDetails = async (event, isAdminRequired = true) => { return payload; }; -export function getEngine(engine) { +export function getEngine(engine: string): string { return engine === '/var/run/docker.sock' ? 'unix:///var/run/docker.sock' : engine; } -export async function removeContainer(id, engine) { +export async function removeContainer(id: string, engine: string): Promise { const host = getEngine(engine); try { const { stdout } = await asyncExecShell( @@ -132,11 +140,23 @@ export async function removeContainer(id, engine) { } } -export const removeDestinationDocker = async ({ id, engine }) => { +export const removeDestinationDocker = async ({ + id, + engine +}: { + id: string; + engine: string; +}): Promise => { return await removeContainer(id, engine); }; -export const createDirectories = async ({ repository, buildId }) => { +export const createDirectories = async ({ + repository, + buildId +}: { + repository: string; + buildId: string; +}): Promise<{ workdir: string; repodir: string }> => { const repodir = `/tmp/build-sources/${repository}/`; const workdir = `/tmp/build-sources/${repository}/${buildId}`; @@ -148,20 +168,14 @@ export const createDirectories = async ({ repository, buildId }) => { }; }; -export function generateTimestamp() { +export function generateTimestamp(): string { return `${dayjs().format('HH:mm:ss.SSS')} `; } -export function getDomain(domain) { +export function getDomain(domain: string): string { return domain?.replace('https://', '').replace('http://', ''); } -export function dashify(str: string, options?: any): string { - if (typeof str !== 'string') return str; - return str - .trim() - .replace(/\W/g, (m) => (/[À-ž]/.test(m) ? m : '-')) - .replace(/^-+|-+$/g, '') - .replace(/-{2,}/g, (m) => (options && options.condense ? '-' : m)) - .toLowerCase(); +export function getOsArch() { + return os.arch(); } diff --git a/src/lib/components/ServiceLinks.svelte b/src/lib/components/ServiceLinks.svelte index a3b4ce2cd..980505119 100644 --- a/src/lib/components/ServiceLinks.svelte +++ b/src/lib/components/ServiceLinks.svelte @@ -6,6 +6,7 @@ import N8n from './svg/services/N8n.svelte'; import NocoDb from './svg/services/NocoDB.svelte'; import PlausibleAnalytics from './svg/services/PlausibleAnalytics.svelte'; + import Umami from './svg/services/Umami.svelte'; import UptimeKuma from './svg/services/UptimeKuma.svelte'; import VaultWarden from './svg/services/VaultWarden.svelte'; import VsCodeServer from './svg/services/VSCodeServer.svelte'; @@ -52,4 +53,8 @@ +{:else if service.type === 'umami'} + + + {/if} diff --git a/src/lib/components/common.ts b/src/lib/components/common.ts index 2bd962c4c..760653cfa 100644 --- a/src/lib/components/common.ts +++ b/src/lib/components/common.ts @@ -19,7 +19,7 @@ export const staticDeployments = [ 'astro', 'eleventy' ]; -export const notNodeDeployments = ['php', 'docker', 'rust', 'python']; +export const notNodeDeployments = ['php', 'docker', 'rust', 'python', 'deno']; export function getDomain(domain) { return domain?.replace('https://', '').replace('http://', ''); @@ -180,5 +180,16 @@ export const supportedServiceTypesAndVersions = [ ports: { main: 7700 } + }, + { + name: 'umami', + fancyName: 'Umami', + baseImage: 'ghcr.io/mikecao/umami', + images: ['postgres:12-alpine'], + versions: ['postgresql-latest'], + recommendedVersion: 'postgresql-latest', + ports: { + main: 3000 + } } ]; diff --git a/src/lib/components/svg/applications/Deno.svelte b/src/lib/components/svg/applications/Deno.svelte new file mode 100644 index 000000000..4e1794226 --- /dev/null +++ b/src/lib/components/svg/applications/Deno.svelte @@ -0,0 +1,24 @@ + diff --git a/src/lib/components/svg/services/Umami.svelte b/src/lib/components/svg/services/Umami.svelte new file mode 100644 index 000000000..ac0df85af --- /dev/null +++ b/src/lib/components/svg/services/Umami.svelte @@ -0,0 +1,85 @@ + + + + Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + diff --git a/src/lib/components/templates.ts b/src/lib/components/templates.ts index b82b12f51..2336afb7d 100644 --- a/src/lib/components/templates.ts +++ b/src/lib/components/templates.ts @@ -153,6 +153,16 @@ export function findBuildPack(pack, packageManager = 'npm') { port: 8000 }; } + if (pack === 'deno') { + return { + ...metaData, + installCommand: null, + buildCommand: null, + startCommand: null, + publishDirectory: null, + port: 8000 + }; + } return { name: 'node', fancyName: 'Node.js', @@ -262,6 +272,12 @@ export const buildPacks = [ fancyName: 'Python', hoverColor: 'hover:bg-green-700', color: 'bg-green-700' + }, + { + name: 'deno', + fancyName: 'Deno', + hoverColor: 'hover:bg-green-700', + color: 'bg-green-700' } ]; export const scanningTemplates = { diff --git a/src/lib/crypto.ts b/src/lib/crypto.ts index 861a25510..dfb38991f 100644 --- a/src/lib/crypto.ts +++ b/src/lib/crypto.ts @@ -1,13 +1,13 @@ import crypto from 'crypto'; const algorithm = 'aes-256-ctr'; -export const base64Encode = (text: string) => { +export const base64Encode = (text: string): string => { return Buffer.from(text).toString('base64'); }; -export const base64Decode = (text: string) => { +export const base64Decode = (text: string): string => { return Buffer.from(text, 'base64').toString('ascii'); }; -export const encrypt = (text: string) => { +export const encrypt = (text: string): string => { if (text) { const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv(algorithm, process.env['COOLIFY_SECRET_KEY'], iv); @@ -19,7 +19,7 @@ export const encrypt = (text: string) => { } }; -export const decrypt = (hashString: string) => { +export const decrypt = (hashString: string): string => { if (hashString) { const hash: Hash = JSON.parse(hashString); const decipher = crypto.createDecipheriv( diff --git a/src/lib/database/applications.ts b/src/lib/database/applications.ts index 7177d0136..de8a7e75e 100644 --- a/src/lib/database/applications.ts +++ b/src/lib/database/applications.ts @@ -1,10 +1,19 @@ import { decrypt, encrypt } from '$lib/crypto'; import { asyncExecShell, getEngine } from '$lib/common'; -import { getDomain, removeDestinationDocker } from '$lib/common'; +import { removeDestinationDocker } from '$lib/common'; import { prisma } from './common'; -export async function listApplications(teamId) { +import type { + DestinationDocker, + GitSource, + Secret, + ApplicationSettings, + Application, + ApplicationPersistentStorage +} from '@prisma/client'; + +export async function listApplications(teamId: string): Promise { if (teamId === '0') { return await prisma.application.findMany({ include: { teams: true } }); } @@ -14,7 +23,13 @@ export async function listApplications(teamId) { }); } -export async function newApplication({ name, teamId }) { +export async function newApplication({ + name, + teamId +}: { + name: string; + teamId: string; +}): Promise { return await prisma.application.create({ data: { name, @@ -24,34 +39,17 @@ export async function newApplication({ name, teamId }) { }); } -export async function importApplication({ - name, - teamId, - fqdn, - port, - buildCommand, - startCommand, - installCommand -}) { - return await prisma.application.create({ - data: { - name, - fqdn, - port, - buildCommand, - startCommand, - installCommand, - teams: { connect: { id: teamId } } - } - }); -} - -export async function removeApplication({ id, teamId }) { - const { fqdn, destinationDockerId, destinationDocker } = await prisma.application.findUnique({ +export async function removeApplication({ + id, + teamId +}: { + id: string; + teamId: string; +}): Promise { + const { destinationDockerId, destinationDocker } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } }); - const domain = getDomain(fqdn); if (destinationDockerId) { const host = getEngine(destinationDocker.engine); const { stdout: containers } = await asyncExecShell( @@ -62,7 +60,6 @@ export async function removeApplication({ id, teamId }) { for (const container of containersArray) { const containerObj = JSON.parse(container); const id = containerObj.ID; - const preview = containerObj.Image.split('-')[1]; await removeDestinationDocker({ id, engine: destinationDocker.engine }); } } @@ -80,9 +77,23 @@ export async function removeApplication({ id, teamId }) { } } -export async function getApplicationWebhook({ projectId, branch }) { +export async function getApplicationWebhook({ + projectId, + branch +}: { + projectId: number; + branch: string; +}): Promise< + Application & { + destinationDocker: DestinationDocker; + settings: ApplicationSettings; + gitSource: GitSource; + secrets: Secret[]; + persistentStorage: ApplicationPersistentStorage[]; + } +> { try { - let application = await prisma.application.findFirst({ + const application = await prisma.application.findFirst({ where: { projectId, branch, settings: { autodeploy: true } }, include: { destinationDocker: true, @@ -131,16 +142,17 @@ export async function getApplicationWebhook({ projectId, branch }) { throw { status: 404, body: { message: e.message } }; } } -export async function getApplicationById({ id }) { - const body = await prisma.application.findFirst({ - where: { id }, - include: { destinationDocker: true } - }); - return { ...body }; -} -export async function getApplication({ id, teamId }) { - let body = {}; +export async function getApplication({ id, teamId }: { id: string; teamId: string }): Promise< + Application & { + destinationDocker: DestinationDocker; + settings: ApplicationSettings; + gitSource: GitSource; + secrets: Secret[]; + persistentStorage: ApplicationPersistentStorage[]; + } +> { + let body; if (teamId === '0') { body = await prisma.application.findFirst({ where: { id }, @@ -194,7 +206,14 @@ export async function configureGitRepository({ projectId, webhookToken, autodeploy -}) { +}: { + id: string; + repository: string; + branch: string; + projectId: number; + webhookToken: string; + autodeploy: boolean; +}): Promise { if (webhookToken) { const encryptedWebhookToken = encrypt(webhookToken); await prisma.application.update({ @@ -224,7 +243,10 @@ export async function configureGitRepository({ } } -export async function configureBuildPack({ id, buildPack }) { +export async function configureBuildPack({ + id, + buildPack +}: Pick): Promise { return await prisma.application.update({ where: { id }, data: { buildPack } }); } @@ -242,8 +264,29 @@ export async function configureApplication({ publishDirectory, pythonWSGI, pythonModule, - pythonVariable -}) { + pythonVariable, + dockerFileLocation, + denoMainFile, + denoOptions +}: { + id: string; + buildPack: string; + name: string; + fqdn: string; + port: number; + exposePort: number; + installCommand: string; + buildCommand: string; + startCommand: string; + baseDirectory: string; + publishDirectory: string; + pythonWSGI: string; + pythonModule: string; + pythonVariable: string; + dockerFileLocation: string; + denoMainFile: string; + denoOptions: string; +}): Promise { return await prisma.application.update({ where: { id }, data: { @@ -259,16 +302,32 @@ export async function configureApplication({ publishDirectory, pythonWSGI, pythonModule, - pythonVariable + pythonVariable, + dockerFileLocation, + denoMainFile, + denoOptions } }); } -export async function checkDoubleBranch(branch, projectId) { +export async function checkDoubleBranch(branch: string, projectId: number): Promise { const applications = await prisma.application.findMany({ where: { branch, projectId } }); return applications.length > 1; } -export async function setApplicationSettings({ id, debug, previews, dualCerts, autodeploy }) { + +export async function setApplicationSettings({ + id, + debug, + previews, + dualCerts, + autodeploy +}: { + id: string; + debug: boolean; + previews: boolean; + dualCerts: boolean; + autodeploy: boolean; +}): Promise { return await prisma.application.update({ where: { id }, data: { settings: { update: { debug, previews, dualCerts, autodeploy } } }, @@ -276,29 +335,6 @@ export async function setApplicationSettings({ id, debug, previews, dualCerts, a }); } -export async function createBuild({ - id, - applicationId, - destinationDockerId, - gitSourceId, - githubAppId, - gitlabAppId, - type -}) { - return await prisma.build.create({ - data: { - id, - applicationId, - destinationDockerId, - gitSourceId, - githubAppId, - gitlabAppId, - status: 'running', - type - } - }); -} - -export async function getPersistentStorage(id) { +export async function getPersistentStorage(id: string): Promise { return await prisma.applicationPersistentStorage.findMany({ where: { applicationId: id } }); } diff --git a/src/lib/database/checks.ts b/src/lib/database/checks.ts index 534d81966..bbe773ae5 100644 --- a/src/lib/database/checks.ts +++ b/src/lib/database/checks.ts @@ -1,7 +1,16 @@ import { getDomain } from '$lib/common'; import { prisma } from './common'; +import type { Application, ServiceSecret, DestinationDocker, Secret } from '@prisma/client'; -export async function isBranchAlreadyUsed({ repository, branch, id }) { +export async function isBranchAlreadyUsed({ + repository, + branch, + id +}: { + id: string; + repository: string; + branch: string; +}): Promise { const application = await prisma.application.findUnique({ where: { id }, include: { gitSource: true } @@ -11,18 +20,42 @@ export async function isBranchAlreadyUsed({ repository, branch, id }) { }); } -export async function isDockerNetworkExists({ network }) { +export async function isDockerNetworkExists({ + network +}: { + network: string; +}): Promise { return await prisma.destinationDocker.findFirst({ where: { network } }); } -export async function isServiceSecretExists({ id, name }) { +export async function isServiceSecretExists({ + id, + name +}: { + id: string; + name: string; +}): Promise { return await prisma.serviceSecret.findFirst({ where: { name, serviceId: id } }); } -export async function isSecretExists({ id, name, isPRMRSecret }) { +export async function isSecretExists({ + id, + name, + isPRMRSecret +}: { + id: string; + name: string; + isPRMRSecret: boolean; +}): Promise { return await prisma.secret.findFirst({ where: { name, applicationId: id, isPRMRSecret } }); } -export async function isDomainConfigured({ id, fqdn }) { +export async function isDomainConfigured({ + id, + fqdn +}: { + id: string; + fqdn: string; +}): Promise { const domain = getDomain(fqdn); const nakedDomain = domain.replace('www.', ''); const foundApp = await prisma.application.findFirst({ @@ -55,6 +88,5 @@ export async function isDomainConfigured({ id, fqdn }) { }, select: { fqdn: true } }); - if (foundApp || foundService || coolifyFqdn) return true; - return false; + return !!(foundApp || foundService || coolifyFqdn); } diff --git a/src/lib/database/common.ts b/src/lib/database/common.ts index 9d588fcc4..968208f55 100644 --- a/src/lib/database/common.ts +++ b/src/lib/database/common.ts @@ -6,11 +6,12 @@ import { } from '$lib/components/common'; import * as Prisma from '@prisma/client'; import { default as ProdPrisma } from '@prisma/client'; -import type { PrismaClientOptions } from '@prisma/client/runtime'; +import type { Database, DatabaseSettings } from '@prisma/client'; import generator from 'generate-password'; import forge from 'node-forge'; +import getPort, { portNumbers } from 'get-port'; -export function generatePassword(length = 24) { +export function generatePassword(length = 24): string { return generator.generate({ length, numbers: true, @@ -30,8 +31,14 @@ export const prisma = new PrismaClient({ rejectOnNotFound: false }); -export function ErrorHandler(e) { - if (e! instanceof Error) { +export function ErrorHandler(e: { + stdout?; + message?: string; + status?: number; + name?: string; + error?: string; +}): { status: number; body: { message: string; error: string } } { + if (e && e instanceof Error) { e = new Error(e.toString()); } let truncatedError = e; @@ -39,8 +46,7 @@ export function ErrorHandler(e) { truncatedError = e.stdout; } if (e.message?.includes('docker run')) { - let truncatedArray = []; - truncatedArray = truncatedError.message.split('-').filter((line) => { + const truncatedArray: string[] = truncatedError.message.split('-').filter((line) => { if (!line.startsWith('e ')) { return line; } @@ -68,11 +74,11 @@ export function ErrorHandler(e) { payload.body.message = 'Already exists. Choose another name.'; } } - // console.error(e) return payload; } + export async function generateSshKeyPair(): Promise<{ publicKey: string; privateKey: string }> { - return await new Promise(async (resolve, reject) => { + return await new Promise((resolve, reject) => { forge.pki.rsa.generateKeyPair({ bits: 4096, workers: -1 }, function (err, keys) { if (keys) { resolve({ @@ -86,35 +92,94 @@ export async function generateSshKeyPair(): Promise<{ publicKey: string; private }); } -export function getVersions(type) { +export function getVersions(type: string): string[] { const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type); if (found) { return found.versions; } return []; } -export function getDatabaseImage(type) { + +export function getDatabaseImage(type: string): string { const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type); if (found) { return found.baseImage; } return ''; } -export function getServiceImage(type) { + +export function getServiceImage(type: string): string { const found = supportedServiceTypesAndVersions.find((t) => t.name === type); if (found) { return found.baseImage; } return ''; } -export function getServiceImages(type) { + +export function getServiceImages(type: string): string[] { const found = supportedServiceTypesAndVersions.find((t) => t.name === type); if (found) { return found.images; } return []; } -export function generateDatabaseConfiguration(database) { + +export function generateDatabaseConfiguration(database: Database & { settings: DatabaseSettings }): + | { + volume: string; + image: string; + ulimits: Record; + privatePort: number; + environmentVariables: { + MYSQL_DATABASE: string; + MYSQL_PASSWORD: string; + MYSQL_ROOT_USER: string; + MYSQL_USER: string; + MYSQL_ROOT_PASSWORD: string; + }; + } + | { + volume: string; + image: string; + ulimits: Record; + privatePort: number; + environmentVariables: { + MONGODB_ROOT_USER: string; + MONGODB_ROOT_PASSWORD: string; + }; + } + | { + volume: string; + image: string; + ulimits: Record; + privatePort: number; + environmentVariables: { + POSTGRESQL_POSTGRES_PASSWORD: string; + POSTGRESQL_USERNAME: string; + POSTGRESQL_PASSWORD: string; + POSTGRESQL_DATABASE: string; + }; + } + | { + volume: string; + image: string; + ulimits: Record; + privatePort: number; + environmentVariables: { + REDIS_AOF_ENABLED: string; + REDIS_PASSWORD: string; + }; + } + | { + volume: string; + image: string; + ulimits: Record; + privatePort: number; + environmentVariables: { + COUCHDB_PASSWORD: string; + COUCHDB_USER: string; + }; + } { const { id, dbUser, @@ -129,7 +194,6 @@ export function generateDatabaseConfiguration(database) { const baseImage = getDatabaseImage(type); if (type === 'mysql') { return { - // url: `mysql://${dbUser}:${dbUserPassword}@${id}:${isPublic ? port : 3306}/${defaultDatabase}`, privatePort: 3306, environmentVariables: { MYSQL_USER: dbUser, @@ -144,7 +208,6 @@ export function generateDatabaseConfiguration(database) { }; } else if (type === 'mongodb') { return { - // url: `mongodb://${dbUser}:${dbUserPassword}@${id}:${isPublic ? port : 27017}/${defaultDatabase}`, privatePort: 27017, environmentVariables: { MONGODB_ROOT_USER: rootUser, @@ -156,7 +219,6 @@ export function generateDatabaseConfiguration(database) { }; } else if (type === 'postgresql') { return { - // url: `psql://${dbUser}:${dbUserPassword}@${id}:${isPublic ? port : 5432}/${defaultDatabase}`, privatePort: 5432, environmentVariables: { POSTGRESQL_POSTGRES_PASSWORD: rootUserPassword, @@ -170,7 +232,6 @@ export function generateDatabaseConfiguration(database) { }; } else if (type === 'redis') { return { - // url: `redis://${dbUser}:${dbUserPassword}@${id}:${isPublic ? port : 6379}/${defaultDatabase}`, privatePort: 6379, environmentVariables: { REDIS_PASSWORD: dbUserPassword, @@ -182,7 +243,6 @@ export function generateDatabaseConfiguration(database) { }; } else if (type === 'couchdb') { return { - // url: `couchdb://${dbUser}:${dbUserPassword}@${id}:${isPublic ? port : 5984}/${defaultDatabase}`, privatePort: 5984, environmentVariables: { COUCHDB_PASSWORD: dbUserPassword, @@ -193,18 +253,30 @@ export function generateDatabaseConfiguration(database) { ulimits: {} }; } - // } else if (type === 'clickhouse') { - // return { - // url: `clickhouse://${dbUser}:${dbUserPassword}@${id}:${port}/${defaultDatabase}`, - // privatePort: 9000, - // image: `bitnami/clickhouse-server:${version}`, - // volume: `${id}-${type}-data:/var/lib/clickhouse`, - // ulimits: { - // nofile: { - // soft: 262144, - // hard: 262144 - // } - // } - // } - // } +} + +export async function getFreePort() { + const data = await prisma.setting.findFirst(); + const { minPort, maxPort } = data; + + const dbUsed = await ( + await prisma.database.findMany({ + where: { publicPort: { not: null } }, + select: { publicPort: true } + }) + ).map((a) => a.publicPort); + const wpFtpUsed = await ( + await prisma.wordpress.findMany({ + where: { ftpPublicPort: { not: null } }, + select: { ftpPublicPort: true } + }) + ).map((a) => a.ftpPublicPort); + const wpUsed = await ( + await prisma.wordpress.findMany({ + where: { mysqlPublicPort: { not: null } }, + select: { mysqlPublicPort: true } + }) + ).map((a) => a.mysqlPublicPort); + const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed]; + return await getPort({ port: portNumbers(minPort, maxPort), exclude: usedPorts }); } diff --git a/src/lib/database/databases.ts b/src/lib/database/databases.ts index 9132fc2b2..388047ac3 100644 --- a/src/lib/database/databases.ts +++ b/src/lib/database/databases.ts @@ -1,12 +1,11 @@ import { decrypt, encrypt } from '$lib/crypto'; -import * as db from '$lib/database'; import cuid from 'cuid'; import { generatePassword } from '.'; -import { prisma, ErrorHandler } from './common'; -import getPort, { portNumbers } from 'get-port'; +import { prisma } from './common'; import { asyncExecShell, getEngine, removeContainer } from '$lib/common'; +import type { Database, DatabaseSettings, DestinationDocker } from '@prisma/client'; -export async function listDatabases(teamId) { +export async function listDatabases(teamId: string): Promise { if (teamId === '0') { return await prisma.database.findMany({ include: { teams: true } }); } else { @@ -16,7 +15,14 @@ export async function listDatabases(teamId) { }); } } -export async function newDatabase({ name, teamId }) { + +export async function newDatabase({ + name, + teamId +}: { + name: string; + teamId: string; +}): Promise { const dbUser = cuid(); const dbUserPassword = encrypt(generatePassword()); const rootUser = cuid(); @@ -37,8 +43,14 @@ export async function newDatabase({ name, teamId }) { }); } -export async function getDatabase({ id, teamId }) { - let body = {}; +export async function getDatabase({ + id, + teamId +}: { + id: string; + teamId: string; +}): Promise { + let body; if (teamId === '0') { body = await prisma.database.findFirst({ where: { id }, @@ -50,20 +62,25 @@ export async function getDatabase({ id, teamId }) { include: { destinationDocker: true, settings: true } }); } - if (body.dbUserPassword) body.dbUserPassword = decrypt(body.dbUserPassword); if (body.rootUserPassword) body.rootUserPassword = decrypt(body.rootUserPassword); - return { ...body }; + return body; } -export async function removeDatabase({ id }) { +export async function removeDatabase({ id }: { id: string }): Promise { await prisma.databaseSettings.deleteMany({ where: { databaseId: id } }); await prisma.database.delete({ where: { id } }); return; } -export async function configureDatabaseType({ id, type }) { +export async function configureDatabaseType({ + id, + type +}: { + id: string; + type: string; +}): Promise { return await prisma.database.update({ where: { id }, data: { type } @@ -79,7 +96,7 @@ export async function setDatabase({ version?: string; isPublic?: boolean; appendOnly?: boolean; -}) { +}): Promise { return await prisma.database.update({ where: { id }, data: { @@ -97,7 +114,16 @@ export async function updateDatabase({ rootUser, rootUserPassword, version -}) { +}: { + id: string; + name: string; + defaultDatabase: string; + dbUser: string; + dbUserPassword: string; + rootUser: string; + rootUserPassword: string; + version: string; +}): Promise { const encryptedDbUserPassword = dbUserPassword && encrypt(dbUserPassword); const encryptedRootUserPassword = rootUserPassword && encrypt(rootUserPassword); return await prisma.database.update({ @@ -114,7 +140,9 @@ export async function updateDatabase({ }); } -export async function stopDatabase(database) { +export async function stopDatabase( + database: Database & { destinationDocker: DestinationDocker } +): Promise { let everStarted = false; const { id, diff --git a/src/lib/database/destinations.ts b/src/lib/database/destinations.ts index 7b2570d93..2f6cdb488 100644 --- a/src/lib/database/destinations.ts +++ b/src/lib/database/destinations.ts @@ -1,11 +1,22 @@ import { asyncExecShell, getEngine } from '$lib/common'; -import { decrypt, encrypt } from '$lib/crypto'; import { dockerInstance } from '$lib/docker'; import { startCoolifyProxy } from '$lib/haproxy'; import { getDatabaseImage } from '.'; import { prisma } from './common'; +import type { DestinationDocker, Service, Application, Prisma } from '@prisma/client'; +import type { CreateDockerDestination } from '$lib/types/destinations'; -export async function listDestinations(teamId) { +type DestinationConfigurationObject = { + id: string; + destinationId: string; +}; + +type FindDestinationFromTeam = { + id: string; + teamId: string; +}; + +export async function listDestinations(teamId: string): Promise { if (teamId === '0') { return await prisma.destinationDocker.findMany({ include: { teams: true } }); } @@ -15,19 +26,28 @@ export async function listDestinations(teamId) { }); } -export async function configureDestinationForService({ id, destinationId }) { +export async function configureDestinationForService({ + id, + destinationId +}: DestinationConfigurationObject): Promise { return await prisma.service.update({ where: { id }, data: { destinationDocker: { connect: { id: destinationId } } } }); } -export async function configureDestinationForApplication({ id, destinationId }) { +export async function configureDestinationForApplication({ + id, + destinationId +}: DestinationConfigurationObject): Promise { return await prisma.application.update({ where: { id }, data: { destinationDocker: { connect: { id: destinationId } } } }); } -export async function configureDestinationForDatabase({ id, destinationId }) { +export async function configureDestinationForDatabase({ + id, + destinationId +}: DestinationConfigurationObject): Promise { await prisma.database.update({ where: { id }, data: { destinationDocker: { connect: { id: destinationId } } } @@ -48,7 +68,12 @@ export async function configureDestinationForDatabase({ id, destinationId }) { } } } -export async function updateDestination({ id, name, engine, network }) { +export async function updateDestination({ + id, + name, + engine, + network +}: Pick): Promise { return await prisma.destinationDocker.update({ where: { id }, data: { name, engine, network } }); } @@ -58,13 +83,8 @@ export async function newRemoteDestination({ engine, network, isCoolifyProxyUsed, - remoteEngine, - ipAddress, - user, - port, - sshPrivateKey -}) { - const encryptedPrivateKey = encrypt(sshPrivateKey); + remoteEngine +}: CreateDockerDestination): Promise { const destination = await prisma.destinationDocker.create({ data: { name, @@ -72,16 +92,18 @@ export async function newRemoteDestination({ engine, network, isCoolifyProxyUsed, - remoteEngine, - ipAddress, - user, - port, - sshPrivateKey: encryptedPrivateKey + remoteEngine } }); return destination.id; } -export async function newLocalDestination({ name, teamId, engine, network, isCoolifyProxyUsed }) { +export async function newLocalDestination({ + name, + teamId, + engine, + network, + isCoolifyProxyUsed +}: CreateDockerDestination): Promise { const host = getEngine(engine); const docker = dockerInstance({ destinationDocker: { engine, network } }); const found = await docker.engine.listNetworks({ filters: { name: [`^${network}$`] } }); @@ -99,18 +121,14 @@ export async function newLocalDestination({ name, teamId, engine, network, isCoo (destination) => destination.network !== network && destination.isCoolifyProxyUsed === true ); if (proxyConfigured) { - if (proxyConfigured.isCoolifyProxyUsed) { - isCoolifyProxyUsed = true; - } else { - isCoolifyProxyUsed = false; - } + isCoolifyProxyUsed = !!proxyConfigured.isCoolifyProxyUsed; } await prisma.destinationDocker.updateMany({ where: { engine }, data: { isCoolifyProxyUsed } }); } if (isCoolifyProxyUsed) await startCoolifyProxy(engine); return destination.id; } -export async function removeDestination({ id }) { +export async function removeDestination({ id }: Pick): Promise { const destination = await prisma.destinationDocker.delete({ where: { id } }); if (destination.isCoolifyProxyUsed) { const host = getEngine(destination.engine); @@ -127,8 +145,11 @@ export async function removeDestination({ id }) { } } -export async function getDestination({ id, teamId }) { - let destination = {}; +export async function getDestination({ + id, + teamId +}: FindDestinationFromTeam): Promise { + let destination; if (teamId === '0') { destination = await prisma.destinationDocker.findFirst({ where: { id } @@ -141,13 +162,22 @@ export async function getDestination({ id, teamId }) { return destination; } -export async function getDestinationByApplicationId({ id, teamId }) { +export async function getDestinationByApplicationId({ + id, + teamId +}: FindDestinationFromTeam): Promise { return await prisma.destinationDocker.findFirst({ where: { application: { some: { id } }, teams: { some: { id: teamId } } } }); } -export async function setDestinationSettings({ engine, isCoolifyProxyUsed }) { +export async function setDestinationSettings({ + engine, + isCoolifyProxyUsed +}: { + engine: string; + isCoolifyProxyUsed: boolean; +}): Promise { return await prisma.destinationDocker.updateMany({ where: { engine }, data: { isCoolifyProxyUsed } diff --git a/src/lib/database/gitSources.ts b/src/lib/database/gitSources.ts index b8279c268..a0c0b94ed 100644 --- a/src/lib/database/gitSources.ts +++ b/src/lib/database/gitSources.ts @@ -1,7 +1,10 @@ import { decrypt, encrypt } from '$lib/crypto'; import { prisma } from './common'; +import type { GithubApp, GitlabApp, GitSource, Prisma, Application } from '@prisma/client'; -export async function listSources(teamId) { +export async function listSources( + teamId: string | Prisma.StringFilter +): Promise<(GitSource & { githubApp?: GithubApp; gitlabApp?: GitlabApp })[]> { if (teamId === '0') { return await prisma.gitSource.findMany({ include: { githubApp: true, gitlabApp: true, teams: true } @@ -13,7 +16,13 @@ export async function listSources(teamId) { }); } -export async function newSource({ teamId, name }) { +export async function newSource({ + name, + teamId +}: { + name: string; + teamId: string; +}): Promise { return await prisma.gitSource.create({ data: { name, @@ -21,7 +30,7 @@ export async function newSource({ teamId, name }) { } }); } -export async function removeSource({ id }) { +export async function removeSource({ id }: { id: string }): Promise { const source = await prisma.gitSource.delete({ where: { id }, include: { githubApp: true, gitlabApp: true } @@ -30,8 +39,14 @@ export async function removeSource({ id }) { if (source.gitlabAppId) await prisma.gitlabApp.delete({ where: { id: source.gitlabAppId } }); } -export async function getSource({ id, teamId }) { - let body = {}; +export async function getSource({ + id, + teamId +}: { + id: string; + teamId: string; +}): Promise { + let body; if (teamId === '0') { body = await prisma.gitSource.findFirst({ where: { id }, @@ -51,8 +66,11 @@ export async function getSource({ id, teamId }) { if (body?.gitlabApp?.appSecret) body.gitlabApp.appSecret = decrypt(body.gitlabApp.appSecret); return body; } -export async function addGitHubSource({ id, teamId, type, name, htmlUrl, apiUrl }) { - await prisma.gitSource.update({ where: { id }, data: { type, name, htmlUrl, apiUrl } }); +export async function addGitHubSource({ id, teamId, type, name, htmlUrl, apiUrl, organization }) { + await prisma.gitSource.update({ + where: { id }, + data: { type, name, htmlUrl, apiUrl, organization } + }); return await prisma.githubApp.create({ data: { teams: { connect: { id: teamId } }, @@ -72,7 +90,7 @@ export async function addGitLabSource({ appSecret, groupName }) { - const encrptedAppSecret = encrypt(appSecret); + const encryptedAppSecret = encrypt(appSecret); await prisma.gitSource.update({ where: { id }, data: { type, apiUrl, htmlUrl, name } }); return await prisma.gitlabApp.create({ data: { @@ -80,19 +98,35 @@ export async function addGitLabSource({ appId, oauthId, groupName, - appSecret: encrptedAppSecret, + appSecret: encryptedAppSecret, gitSource: { connect: { id } } } }); } -export async function configureGitsource({ id, gitSourceId }) { +export async function configureGitsource({ + id, + gitSourceId +}: { + id: string; + gitSourceId: string; +}): Promise { return await prisma.application.update({ where: { id }, data: { gitSource: { connect: { id: gitSourceId } } } }); } -export async function updateGitsource({ id, name, htmlUrl, apiUrl }) { +export async function updateGitsource({ + id, + name, + htmlUrl, + apiUrl +}: { + id: string; + name: string; + htmlUrl: string; + apiUrl: string; +}): Promise { return await prisma.gitSource.update({ where: { id }, data: { name, htmlUrl, apiUrl } diff --git a/src/lib/database/github.ts b/src/lib/database/github.ts index 339b8e008..182ad4529 100644 --- a/src/lib/database/github.ts +++ b/src/lib/database/github.ts @@ -1,7 +1,15 @@ import { decrypt, encrypt } from '$lib/crypto'; import { prisma } from './common'; +import type { GithubApp } from '@prisma/client'; -export async function addInstallation({ gitSourceId, installation_id }) { +// TODO: We should change installation_id to be camelCase +export async function addInstallation({ + gitSourceId, + installation_id +}: { + gitSourceId: string; + installation_id: string; +}): Promise { const source = await prisma.gitSource.findUnique({ where: { id: gitSourceId }, include: { githubApp: true } @@ -12,8 +20,12 @@ export async function addInstallation({ gitSourceId, installation_id }) { }); } -export async function getUniqueGithubApp({ githubAppId }) { - let body = await prisma.githubApp.findUnique({ where: { id: githubAppId } }); +export async function getUniqueGithubApp({ + githubAppId +}: { + githubAppId: string; +}): Promise { + const body = await prisma.githubApp.findUnique({ where: { id: githubAppId } }); if (body.privateKey) body.privateKey = decrypt(body.privateKey); return body; } @@ -26,7 +38,15 @@ export async function createGithubApp({ pem, webhook_secret, state -}) { +}: { + id: number; + client_id: string; + slug: string; + client_secret: string; + pem: string; + webhook_secret: string; + state: string; +}): Promise { const encryptedClientSecret = encrypt(client_secret); const encryptedWebhookSecret = encrypt(webhook_secret); const encryptedPem = encrypt(pem); diff --git a/src/lib/database/gitlab.ts b/src/lib/database/gitlab.ts index eb9bdfb1b..8bb6a9c9b 100644 --- a/src/lib/database/gitlab.ts +++ b/src/lib/database/gitlab.ts @@ -1,7 +1,14 @@ import { encrypt } from '$lib/crypto'; import { generateSshKeyPair, prisma } from './common'; +import type { GitlabApp } from '@prisma/client'; -export async function updateDeployKey({ id, deployKeyId }) { +export async function updateDeployKey({ + id, + deployKeyId +}: { + id: string; + deployKeyId: number; +}): Promise { const application = await prisma.application.findUnique({ where: { id }, include: { gitSource: { include: { gitlabApp: true } } } @@ -11,14 +18,24 @@ export async function updateDeployKey({ id, deployKeyId }) { data: { deployKeyId } }); } -export async function getSshKey({ id }) { +export async function getSshKey({ + id +}: { + id: string; +}): Promise<{ status: number; body: { publicKey: string } }> { const application = await prisma.application.findUnique({ where: { id }, include: { gitSource: { include: { gitlabApp: true } } } }); return { status: 200, body: { publicKey: application.gitSource.gitlabApp.publicSshKey } }; } -export async function generateSshKey({ id }) { +export async function generateSshKey({ + id +}: { + id: string; +}): Promise< + { status: number; body: { publicKey: string } } | { status: number; body?: undefined } +> { const application = await prisma.application.findUnique({ where: { id }, include: { gitSource: { include: { gitlabApp: true } } } diff --git a/src/lib/database/logs.ts b/src/lib/database/logs.ts index 9a71f51e4..74f50b50b 100644 --- a/src/lib/database/logs.ts +++ b/src/lib/database/logs.ts @@ -1,6 +1,13 @@ +import type { BuildLog } from '@prisma/client'; import { prisma, ErrorHandler } from './common'; -export async function listLogs({ buildId, last = 0 }) { +export async function listLogs({ + buildId, + last = 0 +}: { + buildId: string; + last: number; +}): Promise { try { const body = await prisma.buildLog.findMany({ where: { buildId, time: { gt: last } }, diff --git a/src/lib/database/secrets.ts b/src/lib/database/secrets.ts index 835eb4def..2aade4a2f 100644 --- a/src/lib/database/secrets.ts +++ b/src/lib/database/secrets.ts @@ -1,7 +1,8 @@ import { encrypt, decrypt } from '$lib/crypto'; import { prisma } from './common'; +import type { ServiceSecret, Secret, Prisma } from '@prisma/client'; -export async function listServiceSecrets(serviceId: string) { +export async function listServiceSecrets(serviceId: string): Promise { let secrets = await prisma.serviceSecret.findMany({ where: { serviceId }, orderBy: { createdAt: 'desc' } @@ -14,7 +15,7 @@ export async function listServiceSecrets(serviceId: string) { return secrets; } -export async function listSecrets(applicationId: string) { +export async function listSecrets(applicationId: string): Promise { let secrets = await prisma.secret.findMany({ where: { applicationId }, orderBy: { createdAt: 'desc' } @@ -27,20 +28,48 @@ export async function listSecrets(applicationId: string) { return secrets; } -export async function createServiceSecret({ id, name, value }) { +export async function createServiceSecret({ + id, + name, + value +}: { + id: string; + name: string; + value: string; +}): Promise { value = encrypt(value); return await prisma.serviceSecret.create({ data: { name, value, service: { connect: { id } } } }); } -export async function createSecret({ id, name, value, isBuildSecret, isPRMRSecret }) { +export async function createSecret({ + id, + name, + value, + isBuildSecret, + isPRMRSecret +}: { + id: string; + name: string; + value: string; + isBuildSecret: boolean; + isPRMRSecret: boolean; +}): Promise { value = encrypt(value); return await prisma.secret.create({ data: { name, value, isBuildSecret, isPRMRSecret, application: { connect: { id } } } }); } -export async function updateServiceSecret({ id, name, value }) { +export async function updateServiceSecret({ + id, + name, + value +}: { + id: string; + name: string; + value: string; +}): Promise { value = encrypt(value); const found = await prisma.serviceSecret.findFirst({ where: { serviceId: id, name } }); @@ -55,7 +84,19 @@ export async function updateServiceSecret({ id, name, value }) { }); } } -export async function updateSecret({ id, name, value, isBuildSecret, isPRMRSecret }) { +export async function updateSecret({ + id, + name, + value, + isBuildSecret, + isPRMRSecret +}: { + id: string; + name: string; + value: string; + isBuildSecret: boolean; + isPRMRSecret: boolean; +}): Promise { value = encrypt(value); const found = await prisma.secret.findFirst({ where: { applicationId: id, name, isPRMRSecret } }); @@ -71,10 +112,22 @@ export async function updateSecret({ id, name, value, isBuildSecret, isPRMRSecre } } -export async function removeServiceSecret({ id, name }) { +export async function removeServiceSecret({ + id, + name +}: { + id: string; + name: string; +}): Promise { return await prisma.serviceSecret.deleteMany({ where: { serviceId: id, name } }); } -export async function removeSecret({ id, name }) { +export async function removeSecret({ + id, + name +}: { + id: string; + name: string; +}): Promise { return await prisma.secret.deleteMany({ where: { applicationId: id, name } }); } diff --git a/src/lib/database/services.ts b/src/lib/database/services.ts index 0aec569b6..4cb2e8b22 100644 --- a/src/lib/database/services.ts +++ b/src/lib/database/services.ts @@ -1,10 +1,28 @@ -import { asyncExecShell, getEngine } from '$lib/common'; import { decrypt, encrypt } from '$lib/crypto'; +import type { Minio, Prisma, Service } from '@prisma/client'; import cuid from 'cuid'; import { generatePassword } from '.'; import { prisma } from './common'; -export async function listServices(teamId) { +const include: Prisma.ServiceInclude = { + destinationDocker: true, + persistentStorage: true, + serviceSecret: true, + minio: true, + plausibleAnalytics: true, + vscodeserver: true, + wordpress: true, + ghost: true, + meiliSearch: true, + umami: true +}; +export async function listServicesWithIncludes() { + return await prisma.service.findMany({ + include, + orderBy: { createdAt: 'desc' } + }); +} +export async function listServices(teamId: string): Promise { if (teamId === '0') { return await prisma.service.findMany({ include: { teams: true } }); } else { @@ -15,22 +33,18 @@ export async function listServices(teamId) { } } -export async function newService({ name, teamId }) { +export async function newService({ + name, + teamId +}: { + name: string; + teamId: string; +}): Promise { return await prisma.service.create({ data: { name, teams: { connect: { id: teamId } } } }); } -export async function getService({ id, teamId }) { - let body = {}; - const include = { - destinationDocker: true, - plausibleAnalytics: true, - minio: true, - vscodeserver: true, - wordpress: true, - ghost: true, - serviceSecret: true, - meiliSearch: true - }; +export async function getService({ id, teamId }: { id: string; teamId: string }): Promise { + let body; if (teamId === '0') { body = await prisma.service.findFirst({ where: { id }, @@ -43,6 +57,12 @@ export async function getService({ id, teamId }) { }); } + if (body?.serviceSecret.length > 0) { + body.serviceSecret = body.serviceSecret.map((s) => { + s.value = decrypt(s.value); + return s; + }); + } if (body.plausibleAnalytics?.postgresqlPassword) body.plausibleAnalytics.postgresqlPassword = decrypt( body.plausibleAnalytics.postgresqlPassword @@ -69,21 +89,26 @@ export async function getService({ id, teamId }) { if (body.meiliSearch?.masterKey) body.meiliSearch.masterKey = decrypt(body.meiliSearch.masterKey); - if (body?.serviceSecret.length > 0) { - body.serviceSecret = body.serviceSecret.map((s) => { - s.value = decrypt(s.value); - return s; - }); - } - if (body.wordpress?.ftpPassword) { - body.wordpress.ftpPassword = decrypt(body.wordpress.ftpPassword); - } + if (body.wordpress?.ftpPassword) body.wordpress.ftpPassword = decrypt(body.wordpress.ftpPassword); + + if (body.umami?.postgresqlPassword) + body.umami.postgresqlPassword = decrypt(body.umami.postgresqlPassword); + if (body.umami?.umamiAdminPassword) + body.umami.umamiAdminPassword = decrypt(body.umami.umamiAdminPassword); + if (body.umami?.hashSalt) body.umami.hashSalt = decrypt(body.umami.hashSalt); + const settings = await prisma.setting.findFirst(); return { ...body, settings }; } -export async function configureServiceType({ id, type }) { +export async function configureServiceType({ + id, + type +}: { + id: string; + type: string; +}): Promise { if (type === 'plausibleanalytics') { const password = encrypt(generatePassword()); const postgresqlUser = cuid(); @@ -197,48 +222,184 @@ export async function configureServiceType({ id, type }) { meiliSearch: { create: { masterKey } } } }); + } else if (type === 'umami') { + const umamiAdminPassword = encrypt(generatePassword()); + const postgresqlUser = cuid(); + const postgresqlPassword = encrypt(generatePassword()); + const postgresqlDatabase = 'umami'; + const hashSalt = encrypt(generatePassword(64)); + await prisma.service.update({ + where: { id }, + data: { + type, + umami: { + create: { + umamiAdminPassword, + postgresqlDatabase, + postgresqlPassword, + postgresqlUser, + hashSalt + } + } + } + }); } } -export async function setServiceVersion({ id, version }) { + +export async function setServiceVersion({ + id, + version +}: { + id: string; + version: string; +}): Promise { return await prisma.service.update({ where: { id }, data: { version } }); } -export async function setServiceSettings({ id, dualCerts }) { +export async function setServiceSettings({ + id, + dualCerts +}: { + id: string; + dualCerts: boolean; +}): Promise { return await prisma.service.update({ where: { id }, data: { dualCerts } }); } -export async function updatePlausibleAnalyticsService({ id, fqdn, email, username, name }) { +export async function updatePlausibleAnalyticsService({ + id, + fqdn, + email, + username, + name +}: { + id: string; + fqdn: string; + name: string; + email: string; + username: string; +}): Promise { await prisma.plausibleAnalytics.update({ where: { serviceId: id }, data: { email, username } }); await prisma.service.update({ where: { id }, data: { name, fqdn } }); } -export async function updateService({ id, fqdn, name }) { + +export async function updateService({ + id, + fqdn, + name +}: { + id: string; + fqdn: string; + name: string; +}): Promise { return await prisma.service.update({ where: { id }, data: { fqdn, name } }); } -export async function updateWordpress({ id, fqdn, name, mysqlDatabase, extraConfig }) { + +export async function updateLanguageToolService({ + id, + fqdn, + name +}: { + id: string; + fqdn: string; + name: string; +}): Promise { + return await prisma.service.update({ where: { id }, data: { fqdn, name } }); +} + +export async function updateMeiliSearchService({ + id, + fqdn, + name +}: { + id: string; + fqdn: string; + name: string; +}): Promise { + return await prisma.service.update({ where: { id }, data: { fqdn, name } }); +} + +export async function updateVaultWardenService({ + id, + fqdn, + name +}: { + id: string; + fqdn: string; + name: string; +}): Promise { + return await prisma.service.update({ where: { id }, data: { fqdn, name } }); +} + +export async function updateVsCodeServer({ + id, + fqdn, + name +}: { + id: string; + fqdn: string; + name: string; +}): Promise { + return await prisma.service.update({ where: { id }, data: { fqdn, name } }); +} + +export async function updateWordpress({ + id, + fqdn, + name, + mysqlDatabase, + extraConfig +}: { + id: string; + fqdn: string; + name: string; + mysqlDatabase: string; + extraConfig: string; +}): Promise { return await prisma.service.update({ where: { id }, data: { fqdn, name, wordpress: { update: { mysqlDatabase, extraConfig } } } }); } -export async function updateMinioService({ id, publicPort }) { + +export async function updateMinioService({ + id, + publicPort +}: { + id: string; + publicPort: number; +}): Promise { return await prisma.minio.update({ where: { serviceId: id }, data: { publicPort } }); } -export async function updateGhostService({ id, fqdn, name, mariadbDatabase }) { + +export async function updateGhostService({ + id, + fqdn, + name, + mariadbDatabase +}: { + id: string; + fqdn: string; + name: string; + mariadbDatabase: string; +}): Promise { return await prisma.service.update({ where: { id }, data: { fqdn, name, ghost: { update: { mariadbDatabase } } } }); } -export async function removeService({ id }) { +export async function removeService({ id }: { id: string }): Promise { + await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id } }); await prisma.meiliSearch.deleteMany({ where: { serviceId: id } }); await prisma.ghost.deleteMany({ where: { serviceId: id } }); + await prisma.umami.deleteMany({ where: { serviceId: id } }); await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } }); await prisma.minio.deleteMany({ where: { serviceId: id } }); await prisma.vscodeserver.deleteMany({ where: { serviceId: id } }); diff --git a/src/lib/database/settings.ts b/src/lib/database/settings.ts index 31d6b3c04..2d0395d88 100644 --- a/src/lib/database/settings.ts +++ b/src/lib/database/settings.ts @@ -1,8 +1,9 @@ import { decrypt } from '$lib/crypto'; import { prisma } from './common'; +import type { Setting } from '@prisma/client'; -export async function listSettings() { - let settings = await prisma.setting.findFirst({}); +export async function listSettings(): Promise { + const settings = await prisma.setting.findFirst({}); if (settings.proxyPassword) settings.proxyPassword = decrypt(settings.proxyPassword); return settings; } diff --git a/src/lib/database/teams.ts b/src/lib/database/teams.ts index dd9a14cff..45d8fb5db 100644 --- a/src/lib/database/teams.ts +++ b/src/lib/database/teams.ts @@ -1,9 +1,10 @@ +import type { Team, Permission } from '@prisma/client'; import { prisma } from './common'; -export async function listTeams() { +export async function listTeams(): Promise { return await prisma.team.findMany(); } -export async function newTeam({ name, userId }) { +export async function newTeam({ name, userId }: { name: string; userId: string }): Promise { return await prisma.team.create({ data: { name, @@ -12,7 +13,11 @@ export async function newTeam({ name, userId }) { } }); } -export async function getMyTeams({ userId }) { +export async function getMyTeams({ + userId +}: { + userId: string; +}): Promise<(Permission & { team: Team & { _count: { users: number } } })[]> { return await prisma.permission.findMany({ where: { userId }, include: { team: { include: { _count: { select: { users: true } } } } } diff --git a/src/lib/database/users.ts b/src/lib/database/users.ts index 36b130b35..b74c8de4c 100644 --- a/src/lib/database/users.ts +++ b/src/lib/database/users.ts @@ -1,16 +1,30 @@ import cuid from 'cuid'; -import bcrypt from 'bcrypt'; +import bcrypt from 'bcryptjs'; import { prisma } from './common'; import { asyncExecShell, uniqueName } from '$lib/common'; - import * as db from '$lib/database'; import { startCoolifyProxy } from '$lib/haproxy'; -export async function hashPassword(password: string) { +import type { User } from '@prisma/client'; + +export async function hashPassword(password: string): Promise { const saltRounds = 15; return bcrypt.hash(password, saltRounds); } -export async function login({ email, password, isLogin }) { + +export async function login({ + email, + password, + isLogin +}: { + email: string; + password: string; + isLogin: boolean; +}): Promise<{ + status: number; + headers: { 'Set-Cookie': string }; + body: { userId: string; teamId: string; permission: string; isAdmin: boolean }; +}> { const users = await prisma.user.count(); const userFound = await prisma.user.findUnique({ where: { email }, @@ -32,8 +46,12 @@ export async function login({ email, password, isLogin }) { if (users === 0) { await prisma.setting.update({ where: { id }, data: { isRegistrationEnabled: false } }); // Create default network & start Coolify Proxy - await asyncExecShell(`docker network create --attachable coolify`); - await startCoolifyProxy('/var/run/docker.sock'); + try { + await asyncExecShell(`docker network create --attachable coolify`); + } catch (error) {} + try { + await startCoolifyProxy('/var/run/docker.sock'); + } catch (error) {} uid = '0'; } @@ -140,6 +158,6 @@ export async function login({ email, password, isLogin }) { }; } -export async function getUser({ userId }) { +export async function getUser({ userId }: { userId: string }): Promise { return await prisma.user.findUnique({ where: { id: userId } }); } diff --git a/src/lib/docker.ts b/src/lib/docker.ts index 04f68ac48..d3c040c6a 100644 --- a/src/lib/docker.ts +++ b/src/lib/docker.ts @@ -85,7 +85,8 @@ export async function buildImage({ docker, buildId, isCache = false, - debug = false + debug = false, + dockerFileLocation = '/Dockerfile' }) { if (isCache) { await saveBuildLog({ line: `Building cache image started.`, buildId, applicationId }); @@ -103,11 +104,12 @@ export async function buildImage({ const stream = await docker.engine.buildImage( { src: ['.'], context: workdir }, { - dockerfile: isCache ? 'Dockerfile-cache' : 'Dockerfile', + dockerfile: isCache ? `${dockerFileLocation}-cache` : dockerFileLocation, t: `${applicationId}:${tag}${isCache ? '-cache' : ''}` } ); await streamEvents({ stream, docker, buildId, applicationId, debug }); + await saveBuildLog({ line: `Building image successful!`, buildId, applicationId }); } export function dockerInstance({ destinationDocker }): { engine: Dockerode; network: string } { diff --git a/src/lib/form.ts b/src/lib/form.ts index 08099576d..5a7d65e23 100644 --- a/src/lib/form.ts +++ b/src/lib/form.ts @@ -1,5 +1,6 @@ import { toast } from '@zerodevx/svelte-toast'; -export function errorNotification(message: string) { + +export function errorNotification(message: string): void { console.error(message); if (typeof message !== 'string') { toast.push('Ooops, something is not okay, are you okay?'); @@ -30,7 +31,7 @@ export function enhance( e.preventDefault(); let body = new FormData(form); - let parsedData = body; + const parsedData = body; body.forEach((data, key) => { if (data === '' || data === null) parsedData.delete(key); diff --git a/src/lib/haproxy/configuration.ts b/src/lib/haproxy/configuration.ts index 831d9490b..f2f9f500b 100644 --- a/src/lib/haproxy/configuration.ts +++ b/src/lib/haproxy/configuration.ts @@ -1,16 +1,16 @@ import { dev } from '$app/env'; -import got from 'got'; +import got, { type Got } from 'got'; +import * as db from '$lib/database'; import mustache from 'mustache'; import crypto from 'crypto'; - -import * as db from '$lib/database'; import { checkContainer, checkHAProxy } from '.'; import { asyncExecShell, getDomain, getEngine } from '$lib/common'; import { supportedServiceTypesAndVersions } from '$lib/components/common'; +import { listServicesWithIncludes } from '$lib/database'; const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555'; -let template = `program api +const template = `program api command /usr/bin/dataplaneapi -f /usr/local/etc/haproxy/dataplaneapi.hcl --userlist haproxy-dataplaneapi no option start-on-reload @@ -21,10 +21,10 @@ global defaults mode http log global - timeout http-request 60s + timeout http-request 120s timeout connect 10s - timeout client 60s - timeout server 60s + timeout client 120s + timeout server 120s userlist haproxy-dataplaneapi user admin insecure-password "\${HAPROXY_PASSWORD}" @@ -128,7 +128,8 @@ backend {{domain}} server {{id}} {{id}}:{{port}} check fall 10 {{/coolify}} `; -export async function haproxyInstance() { + +export async function haproxyInstance(): Promise { const { proxyPassword } = await db.listSettings(); return got.extend({ prefixUrl: url, @@ -137,31 +138,87 @@ export async function haproxyInstance() { }); } -export async function configureHAProxy() { +export async function configureHAProxy(): Promise { const haproxy = await haproxyInstance(); await checkHAProxy(haproxy); - try { - const data = { - applications: [], - services: [], - coolify: [] - }; - const applications = await db.prisma.application.findMany({ - include: { destinationDocker: true, settings: true } - }); - for (const application of applications) { - const { - fqdn, - id, - port, - destinationDocker, - destinationDockerId, - settings: { previews }, - updatedAt - } = application; - if (destinationDockerId) { - const { engine, network } = destinationDocker; + const data = { + applications: [], + services: [], + coolify: [] + }; + const applications = await db.prisma.application.findMany({ + include: { destinationDocker: true, settings: true } + }); + for (const application of applications) { + const { + fqdn, + id, + port, + destinationDocker, + destinationDockerId, + settings: { previews }, + updatedAt + } = application; + if (destinationDockerId) { + const { engine, network } = destinationDocker; + const isRunning = await checkContainer(engine, id); + if (fqdn) { + const domain = getDomain(fqdn); + const isHttps = fqdn.startsWith('https://'); + const isWWW = fqdn.includes('www.'); + const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`; + if (isRunning) { + data.applications.push({ + id, + port: port || 3000, + domain, + isRunning, + isHttps, + redirectValue, + redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain, + updatedAt: updatedAt.getTime() + }); + } + 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}`; + data.applications.push({ + id: container, + port: port || 3000, + domain: previewDomain, + isRunning, + isHttps, + redirectValue, + redirectTo: isWWW ? previewDomain.replace('www.', '') : 'www.' + previewDomain, + updatedAt: updatedAt.getTime() + }); + } + } + } + } + } + } + const services = await listServicesWithIncludes(); + + for (const service of services) { + const { fqdn, id, type, destinationDocker, destinationDockerId, updatedAt } = 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); @@ -169,9 +226,10 @@ export async function configureHAProxy() { const isWWW = fqdn.includes('www.'); const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`; if (isRunning) { - data.applications.push({ + data.services.push({ id, - port: port || 3000, + port, + publicPort, domain, isRunning, isHttps, @@ -180,108 +238,38 @@ export async function configureHAProxy() { updatedAt: updatedAt.getTime() }); } - 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) { - let previewDomain = `${container.split('-')[1]}.${domain}`; - data.applications.push({ - id: container, - port: port || 3000, - domain: previewDomain, - isRunning, - isHttps, - redirectValue, - redirectTo: isWWW ? previewDomain.replace('www.', '') : 'www.' + previewDomain, - updatedAt: updatedAt.getTime() - }); - } - } - } } } } - const services = await db.prisma.service.findMany({ - include: { - destinationDocker: true, - minio: true, - plausibleAnalytics: true, - vscodeserver: true, - wordpress: true, - ghost: true + } + const { fqdn } = await db.prisma.setting.findFirst(); + if (fqdn) { + const domain = getDomain(fqdn); + const isHttps = fqdn.startsWith('https://'); + const isWWW = fqdn.includes('www.'); + const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`; + data.coolify.push({ + id: dev ? 'host.docker.internal' : 'coolify', + port: 3000, + domain, + isHttps, + redirectValue, + redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain + }); + } + const output = mustache.render(template, data); + const newHash = crypto.createHash('md5').update(output).digest('hex'); + const { proxyHash, id } = await db.listSettings(); + if (proxyHash !== newHash) { + await db.prisma.setting.update({ where: { id }, data: { proxyHash: newHash } }); + await haproxy.post(`v2/services/haproxy/configuration/raw`, { + searchParams: { + skip_version: true + }, + body: output, + headers: { + 'Content-Type': 'text/plain' } }); - - for (const service of services) { - const { fqdn, id, type, destinationDocker, destinationDockerId, updatedAt } = 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 isHttps = fqdn.startsWith('https://'); - const isWWW = fqdn.includes('www.'); - const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`; - if (isRunning) { - data.services.push({ - id, - port, - publicPort, - domain, - isRunning, - isHttps, - redirectValue, - redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain, - updatedAt: updatedAt.getTime() - }); - } - } - } - } - } - const { fqdn } = await db.prisma.setting.findFirst(); - if (fqdn) { - const domain = getDomain(fqdn); - const isHttps = fqdn.startsWith('https://'); - const isWWW = fqdn.includes('www.'); - const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`; - data.coolify.push({ - id: dev ? 'host.docker.internal' : 'coolify', - port: 3000, - domain, - isHttps, - redirectValue, - redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain - }); - } - const output = mustache.render(template, data); - const newHash = crypto.createHash('md5').update(output).digest('hex'); - const { proxyHash, id } = await db.listSettings(); - if (proxyHash !== newHash) { - await db.prisma.setting.update({ where: { id }, data: { proxyHash: newHash } }); - await haproxy.post(`v2/services/haproxy/configuration/raw`, { - searchParams: { - skip_version: true - }, - body: output, - headers: { - 'Content-Type': 'text/plain' - } - }); - } - } catch (error) { - throw error; } } diff --git a/src/lib/haproxy/index.ts b/src/lib/haproxy/index.ts index 98a1453f3..5b713e343 100644 --- a/src/lib/haproxy/index.ts +++ b/src/lib/haproxy/index.ts @@ -1,7 +1,8 @@ import { dev } from '$app/env'; import { asyncExecShell, getEngine } from '$lib/common'; -import got from 'got'; +import got, { type Got, type Response } from 'got'; import * as db from '$lib/database'; +import type { DestinationDocker } from '@prisma/client'; const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555'; @@ -9,7 +10,7 @@ export const defaultProxyImage = `coolify-haproxy-alpine:latest`; export const defaultProxyImageTcp = `coolify-haproxy-tcp-alpine:latest`; export const defaultProxyImageHttp = `coolify-haproxy-http-alpine:latest`; -export async function haproxyInstance() { +export async function haproxyInstance(): Promise { const { proxyPassword } = await db.listSettings(); return got.extend({ prefixUrl: url, @@ -17,6 +18,7 @@ export async function haproxyInstance() { password: proxyPassword }); } + export async function getRawConfiguration(): Promise { return await (await haproxyInstance()).get(`v2/services/haproxy/configuration/raw`).json(); } @@ -43,11 +45,12 @@ export async function getNextTransactionId(): Promise { return newTransaction.id; } -export async function completeTransaction(transactionId) { +export async function completeTransaction(transactionId: string): Promise> { const haproxy = await haproxyInstance(); return await haproxy.put(`v2/services/haproxy/transactions/${transactionId}`); } -export async function deleteProxy({ id }) { + +export async function deleteProxy({ id }: { id: string }): Promise { const haproxy = await haproxyInstance(); await checkHAProxy(haproxy); @@ -77,11 +80,12 @@ export async function deleteProxy({ id }) { } } -export async function reloadHaproxy(engine) { +export async function reloadHaproxy(engine: string): Promise<{ stdout: string; stderr: string }> { const host = getEngine(engine); return await asyncExecShell(`DOCKER_HOST=${host} docker exec coolify-haproxy kill -HUP 1`); } -export async function checkHAProxy(haproxy?: any) { + +export async function checkHAProxy(haproxy?: Got): Promise { if (!haproxy) haproxy = await haproxyInstance(); try { await haproxy.get('v2/info'); @@ -93,7 +97,10 @@ export async function checkHAProxy(haproxy?: any) { } } -export async function stopTcpHttpProxy(destinationDocker, publicPort) { +export async function stopTcpHttpProxy( + destinationDocker: DestinationDocker, + publicPort: number +): Promise<{ stdout: string; stderr: string } | Error> { const { engine } = destinationDocker; const host = getEngine(engine); const containerName = `haproxy-for-${publicPort}`; @@ -108,16 +115,22 @@ export async function stopTcpHttpProxy(destinationDocker, publicPort) { return error; } } -export async function startTcpProxy(destinationDocker, id, publicPort, privatePort, volume = null) { +export async function startTcpProxy( + destinationDocker: DestinationDocker, + id: string, + publicPort: number, + privatePort: number, + volume?: string +): Promise<{ stdout: string; stderr: string } | Error> { const { network, engine } = destinationDocker; const host = getEngine(engine); const containerName = `haproxy-for-${publicPort}`; - const found = await checkContainer(engine, containerName); - const foundDB = await checkContainer(engine, id); + const found = await checkContainer(engine, containerName, true); + const foundDependentContainer = await checkContainer(engine, id, true); try { - if (foundDB && !found) { + if (foundDependentContainer && !found) { const { stdout: Config } = await asyncExecShell( `DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'` ); @@ -128,20 +141,31 @@ export async function startTcpProxy(destinationDocker, id, publicPort, privatePo } -d coollabsio/${defaultProxyImageTcp}` ); } + 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, id, publicPort, privatePort) { + +export async function startHttpProxy( + destinationDocker: DestinationDocker, + id: string, + publicPort: number, + privatePort: number +): Promise<{ stdout: string; stderr: string } | Error> { const { network, engine } = destinationDocker; const host = getEngine(engine); const containerName = `haproxy-for-${publicPort}`; - const found = await checkContainer(engine, containerName); - const foundDB = await checkContainer(engine, id); + const found = await checkContainer(engine, containerName, true); + const foundDependentContainer = await checkContainer(engine, id, true); try { - if (foundDB && !found) { + if (foundDependentContainer && !found) { const { stdout: Config } = await asyncExecShell( `DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'` ); @@ -150,13 +174,19 @@ export async function startHttpProxy(destinationDocker, id, publicPort, privateP `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/${defaultProxyImageHttp}` ); } + if (!foundDependentContainer && found) { + return await asyncExecShell( + `DOCKER_HOST=${host} docker stop -t 0 ${containerName} && docker rm ${containerName}` + ); + } } catch (error) { return error; } } -export async function startCoolifyProxy(engine) { + +export async function startCoolifyProxy(engine: string): Promise { const host = getEngine(engine); - const found = await checkContainer(engine, 'coolify-haproxy'); + const found = await checkContainer(engine, 'coolify-haproxy', true); const { proxyPassword, proxyUser, id } = await db.listSettings(); if (!found) { const { stdout: Config } = await asyncExecShell( @@ -170,7 +200,26 @@ export async function startCoolifyProxy(engine) { } await configureNetworkCoolifyProxy(engine); } -export async function checkContainer(engine, container) { + +export async function isContainerExited(engine: string, containerName: string): Promise { + let isExited = false; + const host = getEngine(engine); + try { + const { stdout } = await asyncExecShell( + `DOCKER_HOST="${host}" docker inspect -f '{{.State.Status}}' ${containerName}` + ); + if (stdout.trim() === 'exited') { + isExited = true; + } + } catch (error) {} + + return isExited; +} +export async function checkContainer( + engine: string, + container: string, + remove: boolean = false +): Promise { const host = getEngine(engine); let containerFound = false; @@ -180,8 +229,11 @@ export async function checkContainer(engine, container) { ); const parsedStdout = JSON.parse(stdout); const status = parsedStdout.Status; - const isRunning = status === 'running' ? true : false; - if (status === 'exited' || status === 'created') { + const isRunning = status === 'running'; + if (status === 'created') { + await asyncExecShell(`DOCKER_HOST="${host}" docker rm ${container}`); + } + if (remove && status === 'exited') { await asyncExecShell(`DOCKER_HOST="${host}" docker rm ${container}`); } if (isRunning) { @@ -193,7 +245,9 @@ export async function checkContainer(engine, container) { return containerFound; } -export async function stopCoolifyProxy(engine) { +export async function stopCoolifyProxy( + engine: string +): Promise<{ stdout: string; stderr: string } | Error> { const host = getEngine(engine); const found = await checkContainer(engine, 'coolify-haproxy'); await db.setDestinationSettings({ engine, isCoolifyProxyUsed: false }); @@ -210,16 +264,18 @@ export async function stopCoolifyProxy(engine) { } } -export async function configureNetworkCoolifyProxy(engine) { +export async function configureNetworkCoolifyProxy(engine: string): Promise { const host = getEngine(engine); const destinations = await db.prisma.destinationDocker.findMany({ where: { engine } }); - destinations.forEach(async (destination) => { - try { + const { stdout: networks } = await asyncExecShell( + `DOCKER_HOST="${host}" docker ps -a --filter name=coolify-haproxy --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-haproxy` ); - } catch (err) { - // TODO: handle error } - }); + } } diff --git a/src/lib/importers/github.ts b/src/lib/importers/github.ts index 53754205e..a08fb87b4 100644 --- a/src/lib/importers/github.ts +++ b/src/lib/importers/github.ts @@ -2,11 +2,9 @@ import { asyncExecShell, saveBuildLog } from '$lib/common'; import got from 'got'; import jsonwebtoken from 'jsonwebtoken'; import * as db from '$lib/database'; -import { ErrorHandler } from '$lib/database'; export default async function ({ applicationId, - debug, workdir, githubAppId, repository, @@ -14,7 +12,16 @@ export default async function ({ htmlUrl, branch, buildId -}): Promise { +}: { + applicationId: string; + workdir: string; + githubAppId: string; + repository: string; + apiUrl: string; + htmlUrl: string; + branch: string; + buildId: string; +}): Promise { const url = htmlUrl.replace('https://', '').replace('http://', ''); await saveBuildLog({ line: 'GitHub importer started.', buildId, applicationId }); const { privateKey, appId, installationId } = await db.getUniqueGithubApp({ githubAppId }); diff --git a/src/lib/importers/gitlab.ts b/src/lib/importers/gitlab.ts index 0cf57d98a..966b22c96 100644 --- a/src/lib/importers/gitlab.ts +++ b/src/lib/importers/gitlab.ts @@ -9,7 +9,16 @@ export default async function ({ branch, buildId, privateSshKey -}): Promise { +}: { + applicationId: string; + workdir: string; + repository: string; + htmlUrl: string; + branch: string; + buildId: string; + repodir: string; + privateSshKey: string; +}): Promise { const url = htmlUrl.replace('https://', '').replace('http://', '').replace(/\/$/, ''); await saveBuildLog({ line: 'GitLab importer started.', buildId, applicationId }); await asyncExecShell(`echo '${privateSshKey}' > ${repodir}/id.rsa`); diff --git a/src/lib/lang.json b/src/lib/lang.json new file mode 100644 index 000000000..fdfc5bf80 --- /dev/null +++ b/src/lib/lang.json @@ -0,0 +1,4 @@ +{ + "fr": "Français", + "en": "English" +} diff --git a/src/lib/letsencrypt/index.ts b/src/lib/letsencrypt/index.ts index fb1fa46ac..7682ecb27 100644 --- a/src/lib/letsencrypt/index.ts +++ b/src/lib/letsencrypt/index.ts @@ -6,9 +6,14 @@ import cuid from 'cuid'; import fs from 'fs/promises'; import getPort, { portNumbers } from 'get-port'; import { supportedServiceTypesAndVersions } from '$lib/components/common'; +import { promises as dns } from 'dns'; +import { listServicesWithIncludes } from '$lib/database'; -export async function letsEncrypt(domain, id = null, isCoolify = false) { +export async function letsEncrypt(domain: string, id?: string, isCoolify = false): Promise { try { + const certbotImage = + process.arch === 'x64' ? 'certbot/certbot' : 'certbot/certbot:arm64v8-latest'; + const data = await db.prisma.setting.findFirst(); const { minPort, maxPort } = data; @@ -62,7 +67,7 @@ export async function letsEncrypt(domain, id = null, isCoolify = false) { if (found) return; await asyncExecShell( - `DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p 9080:${randomPort} -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs certonly --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port ${randomPort} -d ${nakedDomain} -d ${wwwDomain} --expand --agree-tos --non-interactive --register-unsafely-without-email ${ + `DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p 9080:${randomPort} -v "coolify-letsencrypt:/etc/letsencrypt" ${certbotImage} --logs-dir /etc/letsencrypt/logs certonly --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port ${randomPort} -d ${nakedDomain} -d ${wwwDomain} --expand --agree-tos --non-interactive --register-unsafely-without-email ${ dev ? '--test-cert' : '' }` ); @@ -82,7 +87,7 @@ export async function letsEncrypt(domain, id = null, isCoolify = false) { } if (found) return; await asyncExecShell( - `DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p 9080:${randomPort} -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs certonly --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port ${randomPort} -d ${domain} --expand --agree-tos --non-interactive --register-unsafely-without-email ${ + `DOCKER_HOST=${host} docker run --rm --name certbot-${randomCuid} -p 9080:${randomPort} -v "coolify-letsencrypt:/etc/letsencrypt" ${certbotImage} --logs-dir /etc/letsencrypt/logs certonly --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port ${randomPort} -d ${domain} --expand --agree-tos --non-interactive --register-unsafely-without-email ${ dev ? '--test-cert' : '' }` ); @@ -98,7 +103,7 @@ export async function letsEncrypt(domain, id = null, isCoolify = false) { } } -export async function generateSSLCerts() { +export async function generateSSLCerts(): Promise { const ssls = []; const applications = await db.prisma.application.findMany({ include: { destinationDocker: true, settings: true }, @@ -131,7 +136,7 @@ export async function generateSSLCerts() { .map((c) => c.replace(/"/g, '')); if (containers.length > 0) { for (const container of containers) { - let previewDomain = `${container.split('-')[1]}.${domain}`; + const previewDomain = `${container.split('-')[1]}.${domain}`; if (isHttps) ssls.push({ domain: previewDomain, id, isCoolify: false }); } } @@ -141,17 +146,7 @@ export async function generateSSLCerts() { console.log(`Error during generateSSLCerts with ${application.fqdn}: ${error}`); } } - const services = await db.prisma.service.findMany({ - include: { - destinationDocker: true, - minio: true, - plausibleAnalytics: true, - vscodeserver: true, - wordpress: true, - ghost: true - }, - orderBy: { createdAt: 'desc' } - }); + const services = await listServicesWithIncludes(); for (const service of services) { try { @@ -198,16 +193,44 @@ export async function generateSSLCerts() { file.endsWith('.pem') && certificates.push(file.replace(/\.pem$/, '')); } } + const resolver = new dns.Resolver({ timeout: 2000 }); + resolver.setServers(['8.8.8.8', '1.1.1.1']); + let ipv4, ipv6; + try { + ipv4 = await (await asyncExecShell(`curl -4s https://ifconfig.io`)).stdout; + } catch (error) {} + try { + ipv6 = await (await asyncExecShell(`curl -6s https://ifconfig.io`)).stdout; + } catch (error) {} for (const ssl of ssls) { if (!dev) { if ( certificates.includes(ssl.domain) || certificates.includes(ssl.domain.replace('www.', '')) ) { - console.log(`Certificate for ${ssl.domain} already exists`); + // console.log(`Certificate for ${ssl.domain} already exists`); } else { - console.log('Generating SSL for', ssl.domain); - await letsEncrypt(ssl.domain, ssl.id, ssl.isCoolify); + // Checking DNS entry before generating certificate + if (ipv4 || ipv6) { + let domains4 = []; + let domains6 = []; + try { + domains4 = await resolver.resolve4(ssl.domain); + } catch (error) {} + try { + domains6 = await resolver.resolve6(ssl.domain); + } catch (error) {} + if (domains4.length > 0 || domains6.length > 0) { + if ( + (ipv4 && domains4.includes(ipv4.replace('\n', ''))) || + (ipv6 && domains6.includes(ipv6.replace('\n', ''))) + ) { + console.log('Generating SSL for', ssl.domain); + return await letsEncrypt(ssl.domain, ssl.id, ssl.isCoolify); + } + } + } + console.log('DNS settings is incorrect for', ssl.domain, 'skipping.'); } } else { if ( @@ -216,7 +239,27 @@ export async function generateSSLCerts() { ) { console.log(`Certificate for ${ssl.domain} already exists`); } else { - console.log('Generating SSL for', ssl.domain); + // Checking DNS entry before generating certificate + if (ipv4 || ipv6) { + let domains4 = []; + let domains6 = []; + try { + domains4 = await resolver.resolve4(ssl.domain); + } catch (error) {} + try { + domains6 = await resolver.resolve6(ssl.domain); + } catch (error) {} + if (domains4.length > 0 || domains6.length > 0) { + if ( + (ipv4 && domains4.includes(ipv4.replace('\n', ''))) || + (ipv6 && domains6.includes(ipv6.replace('\n', ''))) + ) { + console.log('Generating SSL for', ssl.domain); + return; + } + } + } + console.log('DNS settings is incorrect for', ssl.domain, 'skipping.'); } } } diff --git a/src/lib/locales/en.json b/src/lib/locales/en.json new file mode 100644 index 000000000..d77d2a1c1 --- /dev/null +++ b/src/lib/locales/en.json @@ -0,0 +1,329 @@ +{ + "layout": { + "update_done": "Update completed.", + "wait_new_version_startup": "Waiting for the new version to start...", + "new_version": "New version reachable. Reloading...", + "switch_to_a_different_team": "Switch to a different team...", + "update_available": "Update available" + }, + "error": { + "you_can_find_your_way_back": "You can find your way back", + "here": "here", + "you_are_lost": "Ooops you are lost! But don't be afraid!" + }, + "index": { + "dashboard": "Dashboard", + "applications": "Applications", + "destinations": "Destinations", + "git_sources": "Git Sources", + "databases": "Databases", + "services": "Services", + "teams": "Teams", + "not_implemented_yet": "Not implemented yet", + "database": "Database", + "settings": "Settings", + "global_settings": "Global Settings", + "secret": "Secret", + "team": "Team", + "logout": "Logout" + }, + "login": { + "already_logged_in": "Already logged in...", + "authenticating": "Authenticating...", + "login": "Login" + }, + "forms": { + "password": "Password", + "email": "Email address", + "passwords_not_match": "Passwords do not match.", + "password_again": "Password again", + "save": "Save", + "saving": "Saving...", + "name": "Name", + "value": "Value", + "action": "Action", + "is_required": "is required.", + "add": "Add", + "set": "Set", + "remove": "Remove", + "path": "Path", + "confirm_continue": "Are you sure to continue?", + "must_be_stopped_to_modify": "Must be stopped to modify.", + "port": "Port", + "default": "default", + "base_directory": "Base Directory", + "publish_directory": "Publish Directory", + "generated_automatically_after_start": "Generated automatically after start", + "roots_password": "Root's Password", + "root_user": "Root User", + "eg": "eg", + "user": "User", + "loading": "Loading...", + "version": "Version", + "host": "Host", + "already_used_for": "{{type}} already used for", + "configuration": "Configuration", + "engine": "Engine", + "network": "Network", + "ip_address": "IP Address", + "ssh_private_key": "SSH Private Key", + "type": "Type", + "html_url": "HTML URL", + "api_url": "API URL", + "organization": "Organization", + "new_password": "New password", + "super_secure_new_password": "Super secure new password", + "submit": "Submit", + "default_email_address": "Default Email Address", + "default_password": "Default Password", + "username": "Username", + "root_db_user": "Root DB User", + "root_db_password": "Root DB Password", + "api_port": "API Port", + "verifying": "Verifying", + "verify_emails_without_smtp": "Verify emails without SMTP", + "extra_config": "Extra Config", + "select_a_service": "Select a Service", + "select_a_service_version": "Select a Service version", + "removing": "Removing...", + "remove_domain": "Remove domain", + "public_port_range": "Public Port Range", + "public_port_range_explainer": "Ports used to expose databases/services/internal services.
Add them to your firewall (if applicable).

You can specify a range of ports, eg: 9000-9100", + "no_actions_available": "No actions available", + "admin_api_key": "Admin API key" + }, + "register": { + "register": "Register", + "registering": "Registering...", + "first_user": "You are registering the first user. It will be the administrator of your Coolify instance." + }, + "reset": { + "reset_password": "Reset", + "invalid_secret_key": "Invalid secret key.", + "secret_key": "Secret Key", + "find_path_secret_key": "You can find it in ~/coolify/.env (COOLIFY_SECRET_KEY)" + }, + "application": { + "configuration": { + "buildpack": { + "choose_this_one": "Choose this one..." + }, + "branch_already_in_use": "This branch is already used by another application. Webhooks won't work in this case for both applications. Are you sure you want to use it?", + "no_repositories_configured": "No repositories configured for your Git Application.", + "configure_it_now": "Configure it now", + "loading_repositories": "Loading repositories ...", + "select_a_repository": "Please select a repository", + "loading_branches": "Loading branches ...", + "select_a_repository_first": "Please select a repository first", + "select_a_branch": "Please select a branch", + "loading_groups": "Loading groups...", + "select_a_group": "Please select a group", + "loading_projects": "Loading projects...", + "select_a_project": "Please select a project", + "no_projects_found": "No projects found", + "no_branches_found": "No branches found", + "configure_build_pack": "Configure Build Pack", + "scanning_repository_suggest_build_pack": "Scanning repository to suggest a build pack for you...", + "found_lock_file": "Found lock file for {{packageManager}}. Using it for predefined commands commands.", + "configure_destination": "Configure Destination", + "no_configurable_destination": "No configurable Destination found", + "select_a_repository_project": "Select a Repository / Project", + "select_a_git_source": "Select a Git Source", + "no_configurable_git": "No configurable Git Source found", + "configuration_missing": "Configuration missing" + }, + "build": { + "queued_waiting_exec": "Queued and waiting for execution.", + "build_logs_of": "Build logs of", + "running": "Running", + "queued": "Queued", + "finished_in": "Finished in", + "load_more": "Load More", + "no_logs": "No logs found", + "waiting_logs": "Waiting for the logs..." + }, + "preview": { + "need_during_buildtime": "Need during buildtime?", + "setup_secret_app_first": "You can add secrets to PR/MR deployments. Please add secrets to the application first.
Useful for creating staging environments.", + "values_overwriting_app_secrets": "These values overwrite application secrets in PR/MR deployments. Useful for creating staging environments.", + "redeploy": "Redeploy", + "no_previews_available": "No previews available" + }, + "secrets": { + "secret_saved": "Secret saved.", + "use_isbuildsecret": "Use isBuildSecret", + "secrets_for": "Secrets for" + }, + "storage": { + "path_is_required": "Path is required.", + "storage_saved": "Storage saved.", + "storage_updated": "Storage updated.", + "storage_deleted": "Storage deleted.", + "persistent_storage_explainer": "You can specify any folder that you want to be persistent across deployments.
This is useful for storing data such as a database (SQLite) or a cache." + }, + "deployment_queued": "Deployment queued.", + "confirm_to_delete": "Are you sure you would like to delete '{{name}}'?", + "stop_application": "Stop application", + "permission_denied_stop_application": "You do not have permission to stop the application.", + "rebuild_application": "Rebuild application", + "permission_denied_rebuild_application": "You do not have permission to rebuild application.", + "build_and_start_application": "Build and start application", + "permission_denied_build_and_start_application": "You do not have permission to Build and start application.", + "configurations": "Configurations", + "secret": "Secrets", + "persistent_storage": "Persistent Storage", + "previews": "Previews", + "logs": "Application Logs", + "build_logs": "Build Logs", + "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.", + "settings_saved": "Settings saved.", + "dns_not_set_partial_error": "DNS not set", + "git_source": "Git Source", + "git_repository": "Git Repository", + "build_pack": "Build Pack", + "destination": "Destination", + "application": "Application", + "url_fqdn": "URL (FQDN)", + "domain_fqdn": "Domain (FQDN)", + "https_explainer": "If you specify https, the application will be accessible only over https. SSL certificate will be generated for you.
If you specify www, the application will be redirected (302) from non-www and vice versa.

To modify the domain, you must first stop the application.

You must set your DNS to point to the server IP in advance.", + "ssl_www_and_non_www": "Generate SSL for www and non-www?", + "ssl_explainer": "It will generate certificates for both www and non-www.
You need to have both DNS entries set in advance.

Useful if you expect to have visitors on both.", + "install_command": "Install Command", + "build_command": "Build Command", + "start_command": "Start Command", + "directory_to_use_explainer": "Directory to use as the base for all commands.
Could be useful with monorepos.", + "publish_directory_explainer": "Directory containing all the assets for deployment.
For example: dist,_site or public.", + "features": "Features", + "enable_automatic_deployment": "Enable Automatic Deployment", + "enable_auto_deploy_webhooks": "Enable automatic deployment through webhooks.", + "enable_mr_pr_previews": "Enable MR/PR Previews", + "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.", + "cant_activate_auto_deploy_without_repo": "Cannot activate automatic deployments until only one application is defined for this repository / branch.", + "no_applications_found": "No applications found", + "secret__batch_dot_env": "Paste .env file", + "batch_secrets": "Batch add secrets" + }, + "general": "General", + "database": { + "default_database": "Default Database", + "generated_automatically_after_set_to_public": "Generated automatically after set to public", + "connection_string": "Connection String", + "set_public": "Set it public", + "warning_database_public": "Your database will be reachable over the internet.
Take security seriously in this case!", + "change_append_only_mode": "Change append only mode", + "warning_append_only": "Useful if you would like to restore redis data from a backup.
Database restart is required.", + "select_database_type": "Select a Database type", + "select_database_version": "Select a Database version", + "confirm_stop": "Are you sure you would like to stop {{name}}?", + "stop_database": "Stop database", + "permission_denied_stop_database": "You do not have permission to stop the database.", + "start_database": "Start database", + "permission_denied_start_database": "You do not have permission to start the database.", + "delete_database": "Delete Database", + "permission_denied_delete_database": "You do not have permission to delete a Database", + "no_databases_found": "No databases found" + }, + "destination": { + "delete_destination": "Delete Destination", + "permission_denied_delete_destination": "You do not have permission to delete this destination", + "add_to_coolify": "Add to Coolify", + "coolify_proxy_stopped": "Coolify Proxy stopped!", + "coolify_proxy_started": "Coolify Proxy started!", + "confirm_restart_proxy": "Are you sure you want to restart the proxy? Everything will be reconfigured in ~10 secs.", + "coolify_proxy_restarting": "Coolify Proxy restarting...", + "restarting_please_wait": "Restarting... please wait...", + "force_restart_proxy": "Force restart proxy", + "use_coolify_proxy": "Use Coolify Proxy?", + "no_destination_found": "No destination found", + "new_error_network_already_exists": "Network {{network}} already configured for another team!", + "new": { + "saving_and_configuring_proxy": "Saving and configuring proxy...", + "install_proxy": "This will install a proxy on the destination to allow you to access your applications and services without any manual configuration (recommended for Docker).

Databases will have their own proxy.", + "add_new_destination": "Add New Destination", + "predefined_destinations": "Predefined destinations" + } + }, + "sources": { + "local_docker": "Local Docker", + "remote_docker": "Remote Docker", + "organization_explainer": "Fill it if you would like to use an organization's as your Git Source. Otherwise your user will be used." + }, + "source": { + "new": { + "git_source": "Add New Git Source", + "official_providers": "Official providers" + }, + "no_git_sources_found": "No git sources found", + "delete_git_source": "Delete Git Source", + "permission_denied": "You do not have permission to delete a Git Source", + "create_new_app": "Create new {{name}} App", + "change_app_settings": "Change {{name}} App Settings", + "install_repositories": "Install Repositories", + "application_id": "Application ID", + "group_name": "Group Name", + "oauth_id": "OAuth ID", + "oauth_id_explainer": "The OAuth ID is the unique identifier of the GitLab application.
You can find it in the URL of your GitLab OAuth Application.", + "register_oauth_gitlab": "Register new OAuth application on GitLab", + "gitlab": { + "self_hosted": "Instance-wide application (self-hosted)", + "user_owned": "User owned application", + "group_owned": "Group owned application", + "gitlab_application_type": "GitLab Application Type", + "already_configured": "GitLab App is already configured." + }, + "github": { + "redirecting": "Redirecting to Github..." + } + }, + "services": { + "all_email_verified": "All email verified. You can login now.", + "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." + }, + "service": { + "stop_service": "Stop Service", + "permission_denied_stop_service": "You do not have permission to stop the service.", + "start_service": "Start Service", + "permission_denied_start_service": "You do not have permission to start the service.", + "delete_service": "Delete Service", + "permission_denied_delete_service": "You do not have permission to delete a service.", + "no_service": "No services found" + }, + "setting": { + "change_language": "Change Language", + "permission_denied": "You do not have permission to do this. \\nAsk an admin to modify your permissions.", + "domain_removed": "Domain removed", + "ssl_explainer": "If you specify https, Coolify will be accessible only over https. SSL certificate will be generated for you.
If you specify www, Coolify will be redirected (302) from non-www and vice versa.", + "must_remove_domain_before_changing": "Must remove the domain before you can change this setting.", + "registration_allowed": "Registration allowed?", + "registration_allowed_explainer": "Allow further registrations to the application.
It's turned off after the first registration.", + "coolify_proxy_settings": "Coolify Proxy Settings", + "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." + }, + "team": { + "pending_invitations": "Pending invitations", + "accept": "Accept", + "delete": "Delete", + "member": "member(s)", + "root": "(root)", + "invited_with_permissions": "Invited to {{teamName}} with {{permission}} permission.", + "members": "Members", + "root_team_explainer": "This is the root team. That means members of this group can manage instance wide settings and have all the priviliges in Coolify (imagine like root user on Linux).", + "permission": "Permission", + "you": "(You)", + "promote_to": "Promote to {{grade}}", + "revoke_invitation": "Revoke invitation", + "pending_invitation": "Pending invitation", + "invite_new_member": "Invite new member", + "send_invitation": "Send invitation", + "invite_only_register_explainer": "You can only invite registered users at the moment - will be extended soon.", + "admin": "Admin", + "read": "Read" + } +} diff --git a/src/lib/locales/fr.json b/src/lib/locales/fr.json new file mode 100644 index 000000000..36f10dfb4 --- /dev/null +++ b/src/lib/locales/fr.json @@ -0,0 +1,322 @@ +{ + "application": { + "application": "Application", + "build": { + "build_logs_of": "Créer des journaux de", + "finished_in": "Fini en", + "load_more": "Charger plus", + "no_logs": "Aucun journal trouvé", + "queued": "En file d'attente", + "queued_waiting_exec": "En file d'attente et en attente d'exécution.", + "running": "Fonctionnement", + "waiting_logs": "En attente des logs..." + }, + "build_and_start_application": "Build et démarrer l'application", + "build_command": "Commande Build", + "build_logs": "Créer des journaux", + "build_pack": "Pack de Build", + "cant_activate_auto_deploy_without_repo": "Impossible d'activer les déploiements automatiques tant qu'une seule application n'est pas définie pour ce dépôt/branche.", + "configuration": { + "branch_already_in_use": "Cette branche est déjà utilisée par une autre application. \nLes webhooks ne fonctionneront pas dans ce cas pour les deux applications. \nÊtes-vous sûr de vouloir l'utiliser ?", + "buildpack": { + "choose_this_one": "Choisir celui-ci..." + }, + "configuration_missing": "Configuration manquante", + "configure_build_pack": "Configurer le pack de build", + "configure_destination": "Configurer la destination", + "configure_it_now": "Configurez-le maintenant", + "found_lock_file": "Fichier .lock trouvé pour {{packageManager}}. \nL'utiliser pour les commandes prédéfinies.", + "loading_branches": "Chargement des branches...", + "loading_groups": "Chargement des groupes...", + "loading_projects": "Chargement des projets...", + "loading_repositories": "Chargement des dépôts Git...", + "no_branches_found": "Aucune branche trouvée", + "no_configurable_destination": "Aucune destination configurable trouvée", + "no_configurable_git": "Aucune source Git configurable trouvée", + "no_projects_found": "Aucun projet trouvé", + "no_repositories_configured": "Aucun dépôt Git configuré pour votre application.", + "scanning_repository_suggest_build_pack": "Analyse du dépôt pour vous suggérer un pack de Build...", + "select_a_branch": "Veuillez sélectionner une branche", + "select_a_git_source": "Sélectionnez une source Git", + "select_a_group": "Veuillez sélectionner un groupe", + "select_a_project": "Veuillez sélectionner un projet", + "select_a_repository": "Veuillez sélectionner un dépôt", + "select_a_repository_first": "Veuillez d'abord sélectionner un dépôt", + "select_a_repository_project": "Sélectionnez un dépôt / projet" + }, + "configurations": "Configurations", + "confirm_to_delete": "Voulez-vous vraiment supprimer '{{name}}'?", + "debug_logs": "Journaux de débogage", + "delete_application": "Supprimer l'application", + "deployment_queued": "Déploiement en file d'attente.", + "destination": "Destination", + "directory_to_use_explainer": "Répertoire à utiliser comme base pour toutes les commandes.
Pourrait être utile avec monorepos.", + "dns_not_set_error": "DNS non défini ou propagé pour {{domain}}.

Veuillez vérifier vos paramètres DNS.", + "dns_not_set_partial_error": "DNS non défini", + "domain_already_in_use": "Le domaine {{domain}} est déjà utilisé.", + "domain_fqdn": "Domaine (FQDN)", + "url_fqdn": "URL (FQDN)", + "enable_auto_deploy_webhooks": "Activez le déploiement automatique via des webhooks.", + "enable_automatic_deployment": "Activer le déploiement automatique", + "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.", + "features": "Caractéristiques", + "git_repository": "Dépôt Git", + "git_source": "Source Git", + "https_explainer": "Si vous spécifiez https, l'application sera accessible uniquement via https. \nUn certificat SSL sera généré pour vous.
Si vous spécifiez www, l'application sera redirigée (302) à partir de non-www et vice versa \n.

Pour modifier le domaine, vous devez d'abord arrêter l'application.

Vous devez configurer, en avance, votre DNS pour pointer vers l'IP du serveur.", + "install_command": "Commande d'installation", + "logs": "Journaux des applications", + "no_applications_found": "Aucune application trouvée", + "permission_denied_build_and_start_application": "Vous n'êtes pas autorisé à créer et à démarrer l'application.", + "permission_denied_delete_application": "Vous n'êtes pas autorisé à supprimer cette application", + "permission_denied_rebuild_application": "Vous n'êtes pas autorisé à re-build l'application.", + "permission_denied_stop_application": "Vous n'êtes pas autorisé à arrêter l'application.", + "persistent_storage": "Stockage persistant", + "preview": { + "need_during_buildtime": "Besoin pendant la build ?", + "no_previews_available": "Aucun aperçu disponible", + "redeploy": "Redéployer", + "setup_secret_app_first": "Vous pouvez ajouter des secrets aux déploiements PR/MR. \nVeuillez d'abord ajouter des secrets à l'application. \n
Utile pour créer des environnements de mise en scène.", + "values_overwriting_app_secrets": "Ces valeurs remplacent les secrets d'application dans les déploiements PR/MR. \nUtile pour créer des environnements de mise en scène." + }, + "previews": "Aperçus", + "publish_directory_explainer": "Répertoire contenant tous les actifs à déployer. \n
Par exemple : dist,_site ou public.", + "rebuild_application": "Re-build l'application", + "secret": "secrets", + "secrets": { + "secret_saved": "Secret enregistré.", + "secrets_for": "secrets pour", + "use_isbuildsecret": "Utiliser isBuildSecret" + }, + "settings_saved": "Paramètres sauvegardés.", + "ssl_explainer": "Il générera des certificats pour www et non-www. \n
Vous devez avoir les deux entrées DNS définies à l'avance.

Utile si vous prévoyez d'avoir des visiteurs sur les deux.", + "ssl_www_and_non_www": "Générer SSL pour www et non-www ?", + "start_command": "Démarrer la commande", + "stop_application": "Arrêter l'application", + "storage": { + "path_is_required": "Le chemin est requis.", + "persistent_storage_explainer": "Vous pouvez spécifier n'importe quel dossier que vous souhaitez conserver dans les déploiements. \n
Ceci est utile pour stocker des données telles qu'une base de données (SQLite) ou un cache.", + "storage_deleted": "Stockage supprimé.", + "storage_saved": "Stockage enregistré.", + "storage_updated": "Stockage mis à jour." + } + }, + "database": { + "change_append_only_mode": "Changer le mode d'ajout uniquement", + "confirm_stop": "Êtes-vous sûr de vouloir arrêter {{name}} ?", + "connection_string": "Connexion string", + "default_database": "Base de données par défaut", + "delete_database": "Supprimer la base de données", + "generated_automatically_after_set_to_public": "Généré automatiquement après avoir été défini sur public", + "no_databases_found": "Aucune base de données trouvée", + "permission_denied_delete_database": "Vous n'êtes pas autorisé à supprimer une base de données", + "permission_denied_start_database": "Vous n'êtes pas autorisé à démarrer la base de données.", + "permission_denied_stop_database": "Vous n'êtes pas autorisé à arrêter la base de données.", + "select_database_type": "Sélectionnez un type de base de données", + "select_database_version": "Sélectionnez une version de la base de données", + "set_public": "Rendre public", + "start_database": "Démarrer la base de données", + "stop_database": "Arrêter la base de données", + "warning_append_only": "Utile si vous souhaitez restaurer des données Redis à partir d'une sauvegarde.
Le redémarrage de la base de données est nécessaire.", + "warning_database_public": "Votre base de données sera accessible depuis Internet. \n
Prenez la sécurité au sérieux dans ce cas!" + }, + "destination": { + "add_to_coolify": "Ajouter à Coolify", + "confirm_restart_proxy": "Voulez-vous vraiment redémarrer le proxy? \nTout sera reconfiguré en ~10 secondes.", + "coolify_proxy_restarting": "Redémarrage du Proxy Coolify...", + "coolify_proxy_started": "Proxy Coolify démarré!", + "coolify_proxy_stopped": "Proxy Coolify arrêté!", + "delete_destination": "Supprimer le destinataire", + "force_restart_proxy": "Forcer le redémarrage du proxy", + "new": { + "add_new_destination": "Ajouter une nouvelle destination", + "install_proxy": "Cela installera un proxy sur la destination pour vous permettre d'accéder à vos applications et services sans aucune configuration manuelle (recommandé pour Docker).

Les bases de données auront leur propre proxy.", + "predefined_destinations": "Destinations prédéfinies", + "saving_and_configuring_proxy": "Enregistrement et configuration du proxy..." + }, + "new_error_network_already_exists": "Réseau {{network}} déjà configuré pour une autre équipe !", + "no_destination_found": "Aucune destination trouvée", + "permission_denied_delete_destination": "Vous n'êtes pas autorisé à supprimer cette destination", + "restarting_please_wait": "Redémarrage... veuillez patienter...", + "use_coolify_proxy": "Utiliser le Proxy Coolify ?" + }, + "error": { + "here": "ici", + "you_are_lost": "Oups vous êtes perdu ! \nMais n'ayez pas peur !", + "you_can_find_your_way_back": "Tu peux retrouver ton chemin" + }, + "forms": { + "action": "action", + "add": "Ajouter", + "already_used_for": "{{type}} déjà utilisé pour", + "api_port": "Port API", + "api_url": "URL de l'API", + "base_directory": "Répertoire de base", + "configuration": "Configuration", + "confirm_continue": "Êtes-vous sûr de continuer ?", + "default": "défaut", + "default_email_address": "Adresse e-mail par défaut", + "default_password": "Mot de passe par défaut", + "eg": "ex", + "email": "Adresse e-mail", + "engine": "Moteur", + "extra_config": "Configuration supplémentaire", + "generated_automatically_after_start": "Généré automatiquement après le démarrage", + "host": "Hôte", + "html_url": "URL HTML", + "ip_address": "Adresse IP", + "is_required": "est requis.", + "loading": "Chargement...", + "must_be_stopped_to_modify": "Doit être arrêté pour être modifié.", + "name": "Nom", + "network": "Réseau", + "new_password": "Nouveau mot de passe", + "no_actions_available": "Aucune action disponible", + "organization": "Organisation", + "password": "Mot de passe", + "password_again": "Mot de passe à nouveau", + "passwords_not_match": "Les mots de passe ne correspondent pas.", + "path": "Chemin", + "port": "Port", + "public_port_range": "Gamme de ports publics", + "public_port_range_explainer": "Ports utilisés pour exposer les bases de données/services/services internes.
Ajoutez-les à votre pare-feu (le cas échéant).

Vous pouvez spécifier une plage de ports, par exemple : 9000-9100", + "publish_directory": "Publier le répertoire", + "remove": "Retirer", + "remove_domain": "Supprimer le domaine", + "removing": "Suppression...", + "root_db_password": "Mot de passe root de la base de données", + "root_db_user": "Utilisateur root de la base de données", + "root_user": "Utilisateur root", + "roots_password": "Mot de passe de l'utilisateur root", + "save": "sauvegarder", + "saving": "Sauvegarde...", + "select_a_service": "Sélectionnez un service", + "select_a_service_version": "Sélectionnez une version de service", + "set": "Régler", + "ssh_private_key": "Clé privée SSH", + "submit": "Nous faire parvenir", + "super_secure_new_password": "Nouveau mot de passe super sécurisé", + "type": "Taper", + "user": "Utilisateur", + "username": "Nom d'utilisateur", + "value": "Valeur", + "verify_emails_without_smtp": "Vérifier les e-mails sans SMTP", + "verifying": "Vérification", + "version": "Version" + }, + "general": "Général", + "index": { + "applications": "Applications", + "dashboard": "Tableau de bord", + "database": "Base de données", + "databases": "Bases de données", + "destinations": "Destinations", + "git_sources": "Sources Git", + "global_settings": "Paramètres globaux", + "logout": "Se déconnecter", + "not_implemented_yet": "Pas encore implémenté", + "secret": "Secret", + "services": "Services", + "settings": "Réglages", + "team": "Équipe", + "teams": "Équipes" + }, + "layout": { + "new_version": "Nouvelle version accessible. \nRechargement...", + "switch_to_a_different_team": "Changer d'équipe...", + "update_available": "Mise à jour disponible", + "update_done": "Mise à jour terminée.", + "wait_new_version_startup": "En attendant le lancement de la nouvelle version..." + }, + "login": { + "already_logged_in": "Déjà connecté...", + "authenticating": "Authentification...", + "login": "Connexion" + }, + "register": { + "first_user": "Vous enregistrez le premier utilisateur. \nCe sera l'administrateur de votre instance Coolify.", + "register": "S'inscrire" + }, + "reset": { + "find_path_secret_key": "Vous pouvez le trouver dans ~/coolify/.env (COOLIFY_SECRET_KEY)", + "invalid_secret_key": "Clé secrète invalide.", + "reset_password": "Réinitialiser", + "secret_key": "Clef secrète" + }, + "service": { + "delete_service": "Supprimer le service", + "no_service": "Aucun service trouvé", + "permission_denied_delete_service": "Vous n'êtes pas autorisé à supprimer un service.", + "permission_denied_start_service": "Vous n'êtes pas autorisé à démarrer le service.", + "permission_denied_stop_service": "Vous n'êtes pas autorisé à arrêter le service.", + "start_service": "Démarrer le service", + "stop_service": "Stopper le service" + }, + "services": { + "all_email_verified": "Tous les e-mails sont vérifiés. \nVous pouvez vous connecter maintenant.", + "generate_www_non_www_ssl": "Il générera des certificats pour www et non-www. \n
Vous devez avoir les deux entrées DNS définies à l'avance.

Le service devra être redémarré." + }, + "setting": { + "coolify_proxy_settings": "Paramètres du proxy Coolify", + "credential_stat_explainer": "Identifiants pour la page statistiques.", + "domain_removed": "Domaine supprimé", + "must_remove_domain_before_changing": "Vous devez supprimer le domaine avant de pouvoir modifier ce paramètre.", + "permission_denied": "Vous n'avez pas la permission de faire cela. \n\\nDemandez à un administrateur de modifier vos autorisations.", + "registration_allowed": "Inscription autorisée ?", + "registration_allowed_explainer": "Autoriser d'autres inscriptions à l'application. \n
Il est désactivé après la première inscription.", + "ssl_explainer": "Si vous spécifiez https, Coolify sera accessible uniquement via https. \nUn certificat SSL sera généré pour vous.
Si vous spécifiez www, Coolify sera redirigé (302) à partir de non-www et vice versa." + }, + "source": { + "application_id": "ID d'application", + "change_app_settings": "Modifier les paramètres de l'application {{name}}", + "create_new_app": "Créer une nouvelle application {{name}}", + "delete_git_source": "Supprimer la source Git", + "github": { + "redirecting": "Redirection vers Github..." + }, + "gitlab": { + "already_configured": "L'application GitLab est déjà configurée.", + "gitlab_application_type": "Type d'application GitLab", + "group_owned": "Application détenue par le groupe", + "self_hosted": "Application à l'échelle de l'instance (auto-hébergée)", + "user_owned": "Application appartenant à l'utilisateur" + }, + "group_name": "Nom de groupe", + "install_repositories": "Installer les dépôts", + "new": { + "git_source": "Ajouter une nouvelle source Git", + "official_providers": "Fournisseurs officiels" + }, + "no_git_sources_found": "Aucune source git trouvée", + "oauth_id": "ID OAuth", + "oauth_id_explainer": "L'identifiant OAuth est l'identifiant unique de l'application GitLab. \n
Vous pouvez le trouver dans l'URL de votre application GitLab OAuth.", + "permission_denied": "Vous n'êtes pas autorisé à supprimer une source Git", + "register_oauth_gitlab": "Enregistrer une nouvelle application OAuth sur GitLab" + }, + "sources": { + "local_docker": "Docker local", + "organization_explainer": "Remplissez-le si vous souhaitez utiliser une organisation comme source Git. \nSinon, votre utilisateur sera utilisé.", + "remote_docker": "Station d'accueil à distance" + }, + "team": { + "accept": "J'accepte", + "admin": "Administrateur", + "delete": "Supprimer", + "invite_new_member": "Inviter un nouveau membre", + "invite_only_register_explainer": "Vous ne pouvez inviter que des utilisateurs enregistrés pour le moment - sera bientôt prolongé.", + "invited_with_permissions": "Invité à {{teamName}} avec {{permission}} autorisation.", + "member": "membre(s)", + "members": "Membres", + "pending_invitation": "Invitation en attente", + "pending_invitations": "Invitations en attente", + "permission": "Autorisation", + "promote_to": "Promouvoir à {{grade}}", + "read": "Lire", + "revoke_invitation": "Révoquer l'invitation", + "root": "(suprême)", + "root_team_explainer": "Il s'agit de l'équipe suprême. \nCela signifie que les membres de ce groupe peuvent gérer les paramètres à l'échelle de l'instance et avoir tous les privilèges dans Coolify (imaginez comme un utilisateur root sous Linux).", + "send_invitation": "Envoyer une invitation", + "you": "(Toi)" + } +} diff --git a/src/lib/queues/autoUpdater.ts b/src/lib/queues/autoUpdater.ts new file mode 100644 index 000000000..4bfb995c7 --- /dev/null +++ b/src/lib/queues/autoUpdater.ts @@ -0,0 +1,42 @@ +import { prisma } from '$lib/database'; +import { buildQueue } from '.'; +import got from 'got'; +import { asyncExecShell, version } from '$lib/common'; +import compare from 'compare-versions'; +import { dev } from '$app/env'; + +export default async function (): Promise { + try { + const currentVersion = version; + const { isAutoUpdateEnabled } = await prisma.setting.findFirst(); + if (isAutoUpdateEnabled) { + const versions = await got + .get( + `https://get.coollabs.io/versions.json?appId=${process.env['COOLIFY_APP_ID']}&version=${currentVersion}` + ) + .json(); + const latestVersion = versions['coolify'].main.version; + const isUpdateAvailable = compare(latestVersion, currentVersion); + if (isUpdateAvailable === 1) { + const activeCount = await buildQueue.getActiveCount(); + if (activeCount === 0) { + if (!dev) { + await buildQueue.pause(); + console.log(`Updating Coolify to ${latestVersion}.`); + await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`); + await asyncExecShell(`env | grep COOLIFY > .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"` + ); + } else { + await buildQueue.pause(); + console.log('Updating (not really in dev mode).'); + } + } + } + } + } catch (error) { + await buildQueue.resume(); + console.log(error); + } +} diff --git a/src/lib/queues/builder.ts b/src/lib/queues/builder.ts index 699ed4ed6..a6cf0af75 100644 --- a/src/lib/queues/builder.ts +++ b/src/lib/queues/builder.ts @@ -20,28 +20,22 @@ import { setDefaultConfiguration } from '$lib/buildPacks/common'; import yaml from 'js-yaml'; +import type { Job } from 'bullmq'; +import type { BuilderJob } from '$lib/types/builderJob'; + import type { ComposeFile } from '$lib/types/composeFile'; -export default async function (job) { - let { +export default async function (job: Job): Promise { + const { id: applicationId, repository, - branch, - buildPack, name, destinationDocker, destinationDockerId, gitSource, build_id: buildId, configHash, - port, - exposePort, - installCommand, - buildCommand, - startCommand, fqdn, - baseDirectory, - publishDirectory, projectId, secrets, phpModules, @@ -52,7 +46,21 @@ export default async function (job) { persistentStorage, pythonWSGI, pythonModule, - pythonVariable + pythonVariable, + denoOptions, + exposePort + } = job.data; + let { + branch, + buildPack, + port, + installCommand, + buildCommand, + startCommand, + baseDirectory, + publishDirectory, + dockerFileLocation, + denoMainFile } = job.data; const { debug } = settings; @@ -68,7 +76,7 @@ export default async function (job) { }); let imageId = applicationId; let domain = getDomain(fqdn); - let volumes = + const volumes = persistentStorage?.map((storage) => { return `${applicationId}${storage.path.replace(/\//gi, '-')}:${ buildPack !== 'docker' ? '/app' : '' @@ -103,8 +111,10 @@ export default async function (job) { buildCommand = configuration.buildCommand; publishDirectory = configuration.publishDirectory; baseDirectory = configuration.baseDirectory; + dockerFileLocation = configuration.dockerFileLocation; + denoMainFile = configuration.denoMainFile; - let commit = await importers[gitSource.type]({ + const commit = await importers[gitSource.type]({ applicationId, debug, workdir, @@ -179,6 +189,7 @@ export default async function (job) { } if (!imageFound || deployNeeded) { await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId); + console.log(exposePort ? `${exposePort}:${port}` : port); if (buildpacks[buildPack]) await buildpacks[buildPack]({ buildId, @@ -197,7 +208,7 @@ export default async function (job) { tag, workdir, docker, - port, + port: exposePort ? `${exposePort}:${port}` : port, installCommand, buildCommand, startCommand, @@ -206,15 +217,16 @@ export default async function (job) { phpModules, pythonWSGI, pythonModule, - pythonVariable + pythonVariable, + dockerFileLocation, + denoMainFile, + denoOptions }); else { await saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId }); throw new Error(`Build pack ${buildPack} not found.`); } - deployNeeded = true; } else { - deployNeeded = false; await saveBuildLog({ line: 'Nothing changed.', buildId, applicationId }); } @@ -250,7 +262,7 @@ export default async function (job) { repository, branch, projectId, - port, + port: exposePort ? `${exposePort}:${port}` : port, commit, installCommand, buildCommand, @@ -282,10 +294,21 @@ export default async function (job) { volumes, env_file: envFound ? [`${workdir}/.env`] : [], networks: [docker.network], - ports: exposePort ? [`${exposePort}:${port}`] : [], labels, depends_on: [], - restart: 'always' + restart: 'always', + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + // logging: { + // driver: 'fluentd', + // }, + deploy: { + restart_policy: { + condition: 'on-failure', + delay: '5s', + max_attempts: 3, + window: '120s' + } + } } }, networks: { diff --git a/src/lib/queues/cleanup.ts b/src/lib/queues/cleanup.ts index f89353531..d5dc49381 100644 --- a/src/lib/queues/cleanup.ts +++ b/src/lib/queues/cleanup.ts @@ -1,11 +1,10 @@ -import { dev } from '$app/env'; import { asyncExecShell, getEngine, version } from '$lib/common'; import { prisma } from '$lib/database'; -import { defaultProxyImageHttp, defaultProxyImageTcp } from '$lib/haproxy'; -export default async function () { +export default async function (): Promise { const destinationDockers = await prisma.destinationDocker.findMany(); - for (const destinationDocker of destinationDockers) { - const host = getEngine(destinationDocker.engine); + const engines = [...new Set(destinationDockers.map(({ engine }) => engine))]; + for (const engine of engines) { + const host = getEngine(engine); // Cleanup old coolify images try { let { stdout: images } = await asyncExecShell( @@ -16,56 +15,23 @@ export default async function () { await asyncExecShell(`DOCKER_HOST=${host} docker rmi -f ${images}`); } } catch (error) { - console.log(error); + //console.log(error); } try { await asyncExecShell(`DOCKER_HOST=${host} docker container prune -f`); } catch (error) { - console.log(error); + //console.log(error); } try { await asyncExecShell(`DOCKER_HOST=${host} docker image prune -f --filter "until=2h"`); } catch (error) { - console.log(error); + //console.log(error); + } + // Cleanup old images older than a day + try { + await asyncExecShell(`DOCKER_HOST=${host} docker image prune --filter "until=72h" -a -f`); + } catch (error) { + //console.log(error); } - // Tagging images with labels - // try { - // const images = [ - // `coollabsio/${defaultProxyImageTcp}`, - // `coollabsio/${defaultProxyImageHttp}`, - // 'certbot/certbot:latest', - // 'node:16.14.0-alpine', - // 'alpine:latest', - // 'nginx:stable-alpine', - // 'node:lts', - // 'php:apache', - // 'rust:latest' - // ]; - // for (const image of images) { - // try { - // await asyncExecShell(`DOCKER_HOST=${host} docker image inspect ${image}`); - // } catch (error) { - // await asyncExecShell( - // `DOCKER_HOST=${host} docker pull ${image} && echo "FROM ${image}" | docker build --label coolify.image="true" -t "${image}" -` - // ); - // } - // } - // } catch (error) {} - // if (!dev) { - // // Cleanup images that are not managed by coolify - // try { - // await asyncExecShell( - // `DOCKER_HOST=${host} docker image prune --filter 'label!=coolify.image=true' -a -f` - // ); - // } catch (error) { - // console.log(error); - // } - // // Cleanup old images >3 days - // try { - // await asyncExecShell(`DOCKER_HOST=${host} docker image prune --filter "until=72h" -a -f`); - // } catch (error) { - // console.log(error); - // } - // } } } diff --git a/src/lib/queues/index.ts b/src/lib/queues/index.ts index 736d76789..60097680d 100644 --- a/src/lib/queues/index.ts +++ b/src/lib/queues/index.ts @@ -1,6 +1,5 @@ import * as Bullmq from 'bullmq'; -import { default as ProdBullmq, Job, QueueEvents, QueueScheduler } from 'bullmq'; -import cuid from 'cuid'; +import { default as ProdBullmq, QueueScheduler } from 'bullmq'; import { dev } from '$app/env'; import { prisma } from '$lib/database'; @@ -8,8 +7,10 @@ import builder from './builder'; import logger from './logger'; import cleanup from './cleanup'; import proxy from './proxy'; +import proxyTcpHttp from './proxyTcpHttp'; import ssl from './ssl'; import sslrenewal from './sslrenewal'; +import autoUpdater from './autoUpdater'; import { asyncExecShell, saveBuildLog } from '$lib/common'; @@ -28,22 +29,28 @@ const connectionOptions = { } }; -const cron = async () => { +const cron = async (): Promise => { new QueueScheduler('proxy', connectionOptions); + new QueueScheduler('proxyTcpHttp', connectionOptions); new QueueScheduler('cleanup', connectionOptions); new QueueScheduler('ssl', connectionOptions); new QueueScheduler('sslRenew', connectionOptions); + new QueueScheduler('autoUpdater', connectionOptions); const queue = { proxy: new Queue('proxy', { ...connectionOptions }), + proxyTcpHttp: new Queue('proxyTcpHttp', { ...connectionOptions }), cleanup: new Queue('cleanup', { ...connectionOptions }), ssl: new Queue('ssl', { ...connectionOptions }), - sslRenew: new Queue('sslRenew', { ...connectionOptions }) + sslRenew: new Queue('sslRenew', { ...connectionOptions }), + autoUpdater: new Queue('autoUpdater', { ...connectionOptions }) }; await queue.proxy.drain(); + await queue.proxyTcpHttp.drain(); await queue.cleanup.drain(); await queue.ssl.drain(); await queue.sslRenew.drain(); + await queue.autoUpdater.drain(); new Worker( 'proxy', @@ -55,6 +62,16 @@ const cron = async () => { } ); + new Worker( + 'proxyTcpHttp', + async () => { + await proxyTcpHttp(); + }, + { + ...connectionOptions + } + ); + new Worker( 'ssl', async () => { @@ -85,22 +102,22 @@ const cron = async () => { } ); + new Worker( + 'autoUpdater', + async () => { + await autoUpdater(); + }, + { + ...connectionOptions + } + ); + await queue.proxy.add('proxy', {}, { repeat: { every: 10000 } }); + 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 } }); - - const events = { - proxy: new QueueEvents('proxy', { ...connectionOptions }), - ssl: new QueueEvents('ssl', { ...connectionOptions }) - }; - - events.proxy.on('completed', (data) => { - // console.log(data) - }); - events.ssl.on('completed', (data) => { - // console.log(data) - }); + await queue.autoUpdater.add('autoUpdater', {}, { repeat: { every: 60000 } }); }; cron().catch((error) => { console.log('cron failed to start'); @@ -113,6 +130,9 @@ const buildWorker = new Worker(buildQueueName, async (job) => await builder(job) concurrency: 1, ...connectionOptions }); +buildQueue.resume().catch((err) => { + console.log('Build queue failed to resume!', err); +}); buildWorker.on('completed', async (job: Bullmq.Job) => { try { @@ -121,7 +141,6 @@ buildWorker.on('completed', async (job: Bullmq.Job) => { setTimeout(async () => { await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'success' } }); }, 1234); - console.log(error); } finally { const workdir = `/tmp/build-sources/${job.data.repository}/${job.data.build_id}`; if (!dev) await asyncExecShell(`rm -fr ${workdir}`); @@ -137,7 +156,6 @@ buildWorker.on('failed', async (job: Bullmq.Job, failedReason) => { setTimeout(async () => { await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'failed' } }); }, 1234); - console.log(error); } finally { const workdir = `/tmp/build-sources/${job.data.repository}`; if (!dev) await asyncExecShell(`rm -fr ${workdir}`); diff --git a/src/lib/queues/logger.ts b/src/lib/queues/logger.ts index 14665b9ad..f751f8d6f 100644 --- a/src/lib/queues/logger.ts +++ b/src/lib/queues/logger.ts @@ -1,7 +1,8 @@ import { prisma } from '$lib/database'; import { dev } from '$app/env'; +import type { Job } from 'bullmq'; -export default async function (job) { +export default async function (job: Job): Promise { const { line, applicationId, buildId } = job.data; if (dev) console.debug(`[${applicationId}] ${line}`); await prisma.buildLog.create({ data: { line, buildId, time: Number(job.id), applicationId } }); diff --git a/src/lib/queues/proxy.ts b/src/lib/queues/proxy.ts index 339e60d60..db8742e74 100644 --- a/src/lib/queues/proxy.ts +++ b/src/lib/queues/proxy.ts @@ -1,7 +1,10 @@ import { ErrorHandler } from '$lib/database'; import { configureHAProxy } from '$lib/haproxy/configuration'; -export default async function () { +export default async function (): Promise { try { return await configureHAProxy(); } catch (error) { diff --git a/src/lib/queues/proxyTcpHttp.ts b/src/lib/queues/proxyTcpHttp.ts new file mode 100644 index 000000000..c268d2aa0 --- /dev/null +++ b/src/lib/queues/proxyTcpHttp.ts @@ -0,0 +1,55 @@ +import { ErrorHandler, generateDatabaseConfiguration, prisma } from '$lib/database'; +import { startCoolifyProxy, startHttpProxy, startTcpProxy } from '$lib/haproxy'; + +export default async function (): Promise { + try { + // Coolify Proxy + const localDocker = await prisma.destinationDocker.findFirst({ + where: { engine: '/var/run/docker.sock' } + }); + if (localDocker && localDocker.isCoolifyProxyUsed) { + await startCoolifyProxy('/var/run/docker.sock'); + } + // TCP Proxies + const databasesWithPublicPort = await prisma.database.findMany({ + where: { publicPort: { not: null } }, + include: { settings: true, destinationDocker: true } + }); + for (const database of databasesWithPublicPort) { + const { destinationDockerId, destinationDocker, publicPort, id } = database; + if (destinationDockerId) { + const { privatePort } = generateDatabaseConfiguration(database); + await startTcpProxy(destinationDocker, id, publicPort, privatePort); + } + } + const wordpressWithFtp = await prisma.wordpress.findMany({ + where: { ftpPublicPort: { not: null } }, + include: { service: { include: { destinationDocker: true } } } + }); + for (const ftp of wordpressWithFtp) { + const { service, ftpPublicPort } = ftp; + const { destinationDockerId, destinationDocker, id } = service; + if (destinationDockerId) { + await startTcpProxy(destinationDocker, `${id}-ftp`, ftpPublicPort, 22); + } + } + + // HTTP Proxies + const minioInstances = await prisma.minio.findMany({ + where: { publicPort: { not: null } }, + include: { service: { include: { destinationDocker: true } } } + }); + for (const minio of minioInstances) { + const { service, publicPort } = minio; + const { destinationDockerId, destinationDocker, id } = service; + if (destinationDockerId) { + await startHttpProxy(destinationDocker, id, publicPort, 9000); + } + } + } catch (error) { + return ErrorHandler(error.response?.body || error); + } +} diff --git a/src/lib/queues/ssl.ts b/src/lib/queues/ssl.ts index f041153d6..84b608fae 100644 --- a/src/lib/queues/ssl.ts +++ b/src/lib/queues/ssl.ts @@ -1,6 +1,6 @@ import { generateSSLCerts } from '$lib/letsencrypt'; -export default async function () { +export default async function (): Promise { try { return await generateSSLCerts(); } catch (error) { diff --git a/src/lib/queues/sslrenewal.ts b/src/lib/queues/sslrenewal.ts index 807aa34c5..4af5bae64 100644 --- a/src/lib/queues/sslrenewal.ts +++ b/src/lib/queues/sslrenewal.ts @@ -1,13 +1,9 @@ import { asyncExecShell } from '$lib/common'; import { reloadHaproxy } from '$lib/haproxy'; -export default async function () { - try { - 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'); - } catch (error) { - throw error; - } +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'); } diff --git a/src/lib/settings.ts b/src/lib/settings.ts index 211852c06..4e13f4f0f 100644 --- a/src/lib/settings.ts +++ b/src/lib/settings.ts @@ -1,8 +1,6 @@ export const publicPaths = [ '/login', '/register', - '/reset', - '/reset/password', '/webhooks/success', '/webhooks/github', '/webhooks/github/install', diff --git a/src/lib/store.ts b/src/lib/store.ts index d8d1be6ce..1473d93b2 100644 --- a/src/lib/store.ts +++ b/src/lib/store.ts @@ -1,6 +1,8 @@ -import { writable } from 'svelte/store'; +import { writable, type Writable } from 'svelte/store'; -export const gitTokens = writable({ - githubToken: null, - gitlabToken: null -}); +export const gitTokens: Writable<{ githubToken: string | null; gitlabToken: string | null }> = + writable({ + githubToken: null, + gitlabToken: null + }); +export const disabledButton: Writable = writable(false); diff --git a/src/lib/translations.ts b/src/lib/translations.ts new file mode 100644 index 000000000..3ead4e1ec --- /dev/null +++ b/src/lib/translations.ts @@ -0,0 +1,25 @@ +import i18n from 'sveltekit-i18n'; +import lang from './lang.json'; + +/** @type {import('sveltekit-i18n').Config} */ +export const config = { + fallbackLocale: 'en', + translations: { + en: { lang }, + fr: { lang } + }, + loaders: [ + { + locale: 'en', + key: '', + loader: async () => (await import('./locales/en.json')).default + }, + { + locale: 'fr', + key: '', + loader: async () => (await import('./locales/fr.json')).default + } + ] +}; + +export const { t, locales, locale, loadTranslations } = new i18n(config); diff --git a/src/lib/types/builderJob.ts b/src/lib/types/builderJob.ts new file mode 100644 index 000000000..606a57ece --- /dev/null +++ b/src/lib/types/builderJob.ts @@ -0,0 +1,55 @@ +import type { DestinationDocker, GithubApp, GitlabApp, GitSource, Secret } from '@prisma/client'; + +export type BuilderJob = { + build_id: string; + type: BuildType; + id: string; + name: string; + fqdn: string; + repository: string; + configHash: unknown; + branch: string; + buildPack: BuildPackName; + projectId: number; + port: number; + exposePort?: number; + installCommand: string; + buildCommand?: string; + startCommand?: string; + baseDirectory: string; + publishDirectory: string; + phpModules: string; + pythonWSGI: string; + pythonModule: string; + pythonVariable: string; + dockerFileLocation: string; + denoMainFile: string; + denoOptions: string; + createdAt: string; + updatedAt: string; + destinationDockerId: string; + destinationDocker: DestinationDocker; + gitSource: GitSource & { githubApp?: GithubApp; gitlabApp?: GitlabApp }; + settings: BuilderJobSettings; + secrets: Secret[]; + persistentStorage: { path: string }[]; + pullmergeRequestId?: unknown; + sourceBranch?: string; +}; + +// TODO: Add the other build types +export type BuildType = 'manual'; + +// TODO: Add the other buildpack names +export type BuildPackName = 'node' | 'docker'; + +export type BuilderJobSettings = { + id: string; + applicationId: string; + dualCerts: boolean; + debug: boolean; + previews: boolean; + autodeploy: boolean; + createdAt: string; + updatedAt: string; +}; diff --git a/src/lib/types/composeFile.ts b/src/lib/types/composeFile.ts index 1e2a0723c..724a53b69 100644 --- a/src/lib/types/composeFile.ts +++ b/src/lib/types/composeFile.ts @@ -18,11 +18,20 @@ export type ComposeFileService = { restart: ComposeFileRestartOption; depends_on?: string[]; command?: string; + ports?: string[]; build?: { context: string; dockerfile: string; args?: Record; }; + deploy?: { + restart_policy?: { + condition?: string; + delay?: string; + max_attempts?: number; + window?: string; + }; + }; }; export type ComposerFileVersion = diff --git a/src/lib/types/destinations.ts b/src/lib/types/destinations.ts new file mode 100644 index 000000000..c5a56d772 --- /dev/null +++ b/src/lib/types/destinations.ts @@ -0,0 +1,8 @@ +export type CreateDockerDestination = { + name: string; + engine: string; + remoteEngine: boolean; + network: string; + isCoolifyProxyUsed: boolean; + teamId: string; +}; diff --git a/src/routes/__error.svelte b/src/routes/__error.svelte index 6e9d9f184..1ac61d6bf 100644 --- a/src/routes/__error.svelte +++ b/src/routes/__error.svelte @@ -12,15 +12,18 @@
{status}
-
Ooops you are lost! But don't be afraid!
+
{$t('error.you_are_lost')}
- You can find your way back here + {$t('error.you_can_find_your_way_back')} + {$t('error.here')}
 	import type { Load } from '@sveltejs/kit';
 	import { publicPaths } from '$lib/settings';
-
+	import { locale, loadTranslations } from '$lib/translations';
 	export const load: Load = async ({ fetch, url, session }) => {
+		const { pathname } = url;
+
+		const defaultLocale = 'en';
+		const sessionLocale = session.lang;
+		const initLocale = sessionLocale || locale.get() || defaultLocale;
+		await loadTranslations(initLocale, pathname);
+
 		if (!session.userId && !publicPaths.includes(url.pathname)) {
 			return {
 				status: 302,
@@ -39,7 +46,6 @@
 	import { asyncSleep } from '$lib/components/common';
 	import { del, get, post } from '$lib/api';
 	import { browser, dev } from '$app/env';
-
 	let isUpdateAvailable = false;
 
 	let updateStatus = {
@@ -176,7 +182,7 @@
 				
 								
 							{:else if updateStatus.success}
-								
 							{:else}
-								
 					
 				
-
 				{#if $session.teamId === '0'}
 					
diff --git a/src/routes/applications/[id]/__layout.svelte b/src/routes/applications/[id]/__layout.svelte
index 7b997fae4..bf634edde 100644
--- a/src/routes/applications/[id]/__layout.svelte
+++ b/src/routes/applications/[id]/__layout.svelte
@@ -17,7 +17,7 @@
 		const endpoint = `/applications/${params.id}.json`;
 		const res = await fetch(endpoint);
 		if (res.ok) {
-			let { application, isRunning, appId, githubToken, gitlabToken } = await res.json();
+			let { application, isRunning, isExited, appId, githubToken, gitlabToken } = await res.json();
 			if (!application || Object.entries(application).length === 0) {
 				return {
 					status: 302,
@@ -46,6 +46,7 @@
 				props: {
 					application,
 					isRunning,
+					isExited,
 					githubToken,
 					gitlabToken
 				},
@@ -67,27 +68,39 @@
 
 
 
 
diff --git a/src/routes/sources/[id]/check.json.ts b/src/routes/sources/[id]/check.json.ts
index fe7d8e9c0..89084fdd7 100644
--- a/src/routes/sources/[id]/check.json.ts
+++ b/src/routes/sources/[id]/check.json.ts
@@ -1,6 +1,7 @@
 import { 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';
 
 export const post: RequestHandler = async (event) => {
@@ -11,7 +12,7 @@ export const post: RequestHandler = async (event) => {
 		const found = await db.prisma.gitlabApp.findFirst({ where: { oauthId: Number(oauthId) } });
 		if (found) {
 			throw {
-				message: `GitLab App is already configured.`
+				message: t.get('source.gitlab.already_configured')
 			};
 		}
 		return { status: 200 };
diff --git a/src/routes/sources/[id]/github.json.ts b/src/routes/sources/[id]/github.json.ts
index e0f1d3b61..14da66c0a 100644
--- a/src/routes/sources/[id]/github.json.ts
+++ b/src/routes/sources/[id]/github.json.ts
@@ -9,8 +9,8 @@ export const post: RequestHandler = async (event) => {
 	const { id } = event.params;
 
 	try {
-		let { type, name, htmlUrl, apiUrl } = await event.request.json();
-		await db.addGitHubSource({ id, teamId, type, name, htmlUrl, apiUrl });
+		let { type, name, htmlUrl, apiUrl, organization } = await event.request.json();
+		await db.addGitHubSource({ id, teamId, type, name, htmlUrl, apiUrl, organization });
 		return { status: 201 };
 	} catch (error) {
 		return ErrorHandler(error);
diff --git a/src/routes/sources/[id]/index.svelte b/src/routes/sources/[id]/index.svelte
index 015c98a1f..7538dbbbd 100644
--- a/src/routes/sources/[id]/index.svelte
+++ b/src/routes/sources/[id]/index.svelte
@@ -30,6 +30,8 @@
 
 
 
-
Git Source
+
{$t('application.git_source')}
> {source.name}
diff --git a/src/routes/sources/[id]/newGithubApp.svelte b/src/routes/sources/[id]/newGithubApp.svelte index b6a74c3ba..e3215b380 100644 --- a/src/routes/sources/[id]/newGithubApp.svelte +++ b/src/routes/sources/[id]/newGithubApp.svelte @@ -31,12 +31,12 @@
- Redirecting to Github... + {$t('source.github.redirecting')}
diff --git a/src/routes/sources/index.svelte b/src/routes/sources/index.svelte index 191eb8857..72489bdea 100644 --- a/src/routes/sources/index.svelte +++ b/src/routes/sources/index.svelte @@ -39,10 +39,11 @@ const { id } = await post('/sources/new', {}); return await goto(`/sources/${id}`, { replaceState: true }); } + import { t } from '$lib/translations';
-
Git Sources
+
{$t('index.git_sources')}
{#if $session.isAdmin}