From 9b7aacc93407a8004f86c3298b1da55d67da7261 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 29 Sep 2024 02:28:58 -0600 Subject: [PATCH 01/88] refactor(server): split logic in to packages --- apps/api/docker-compose.yml | 21 + apps/api/package.json | 19 +- apps/api/src/index.ts | 101 +- apps/api/src/test.ts | 82 + apps/api/tsconfig.json | 22 +- .../settings/servers/setup-server.tsx | 1 - apps/dokploy/next.config.mjs | 77 +- apps/dokploy/package.json | 1 + apps/dokploy/pages/api/[...trpc].ts | 2 + apps/dokploy/server/api/routers/admin.ts | 13 +- .../dokploy/server/api/routers/application.ts | 4 +- apps/dokploy/server/api/routers/mariadb.ts | 2 +- apps/dokploy/server/api/routers/mongo.ts | 2 +- apps/dokploy/server/api/routers/mysql.ts | 2 +- apps/dokploy/server/api/routers/port.ts | 2 +- apps/dokploy/server/api/routers/postgres.ts | 2 +- apps/dokploy/server/api/routers/project.ts | 2 +- apps/dokploy/server/api/routers/redis.ts | 2 +- .../server/api/services/application.ts | 2 +- apps/dokploy/server/api/services/compose.ts | 2 +- apps/dokploy/server/api/services/mariadb.ts | 2 +- apps/dokploy/server/api/services/mongo.ts | 2 +- apps/dokploy/server/api/services/mysql.ts | 2 +- apps/dokploy/server/api/services/postgres.ts | 2 +- apps/dokploy/server/api/services/redis.ts | 2 +- apps/dokploy/server/api/services/server.ts | 1 + apps/dokploy/server/db/index.ts | 6 + apps/dokploy/server/db/schema/index.ts | 31 +- .../server/utils/access-log/handler.ts | 1 - .../server/utils/servers/connection.ts | 3 - packages/builders/esbuild.config.ts | 25 + packages/builders/package.json | 156 + packages/builders/src/auth/auth.ts | 113 + packages/builders/src/auth/random-password.ts | 20 + packages/builders/src/auth/token.ts | 49 + packages/builders/src/constants/index.ts | 39 + packages/builders/src/db/drizzle.config.ts | 14 + packages/builders/src/db/index.ts | 28 + packages/builders/src/db/migration.ts | 21 + packages/builders/src/db/reset.ts | 23 + .../builders/src}/db/schema/admin.ts | 0 .../builders/src}/db/schema/application.ts | 19 +- .../builders/src}/db/schema/auth.ts | 0 .../builders/src}/db/schema/backups.ts | 0 .../builders/src}/db/schema/bitbucket.ts | 0 .../builders/src}/db/schema/certificate.ts | 0 .../builders/src}/db/schema/compose.ts | 7 +- .../builders/src}/db/schema/deployment.ts | 0 .../builders/src}/db/schema/destination.ts | 0 .../builders/src}/db/schema/domain.ts | 2 +- .../builders/src}/db/schema/git-provider.ts | 0 .../builders/src}/db/schema/github.ts | 0 .../builders/src}/db/schema/gitlab.ts | 0 packages/builders/src/db/schema/index.ts | 31 + .../builders/src}/db/schema/mariadb.ts | 1 - .../builders/src}/db/schema/mongo.ts | 0 .../builders/src}/db/schema/mount.ts | 0 .../builders/src}/db/schema/mysql.ts | 0 .../builders/src}/db/schema/notification.ts | 0 .../builders/src}/db/schema/port.ts | 0 .../builders/src}/db/schema/postgres.ts | 0 .../builders/src}/db/schema/project.ts | 0 .../builders/src}/db/schema/redirects.ts | 0 .../builders/src}/db/schema/redis.ts | 0 .../builders/src}/db/schema/registry.ts | 0 .../builders/src}/db/schema/security.ts | 0 .../builders/src}/db/schema/server.ts | 17 +- .../builders/src}/db/schema/session.ts | 0 .../builders/src}/db/schema/shared.ts | 0 .../builders/src}/db/schema/source.ts | 0 .../builders/src}/db/schema/ssh-key.ts | 6 +- .../builders/src}/db/schema/user.ts | 0 .../builders/src}/db/schema/utils.ts | 0 packages/builders/src/db/seed.ts | 35 + .../builders/src/db/validations/domain.ts | 46 + packages/builders/src/db/validations/index.ts | 37 + packages/builders/src/emails/.gitignore | 2 + .../src/emails/emails/build-failed.tsx | 113 + .../src/emails/emails/build-success.tsx | 106 + .../src/emails/emails/database-backup.tsx | 105 + .../src/emails/emails/docker-cleanup.tsx | 81 + .../src/emails/emails/dokploy-restart.tsx | 75 + .../builders/src/emails/emails/invitation.tsx | 98 + .../src/emails/emails/notion-magic-link.tsx | 150 + .../emails/emails/plaid-verify-identity.tsx | 158 + .../src/emails/emails/static/logo.png | Bin 0 -> 17567 bytes .../src/emails/emails/static/notion-logo.png | Bin 0 -> 1929 bytes .../src/emails/emails/static/plaid-logo.png | Bin 0 -> 3987 bytes .../src/emails/emails/static/plaid.png | Bin 0 -> 3987 bytes .../src/emails/emails/static/stripe-logo.png | Bin 0 -> 1813 bytes .../src/emails/emails/static/vercel-arrow.png | Bin 0 -> 426 bytes .../src/emails/emails/static/vercel-logo.png | Bin 0 -> 1178 bytes .../src/emails/emails/static/vercel-team.png | Bin 0 -> 3122 bytes .../src/emails/emails/static/vercel-user.png | Bin 0 -> 55726 bytes .../src/emails/emails/stripe-welcome.tsx | 152 + .../src/emails/emails/vercel-invite-user.tsx | 154 + packages/builders/src/emails/package.json | 20 + packages/builders/src/emails/pnpm-lock.yaml | 4209 +++++++++++++++++ packages/builders/src/emails/readme.md | 27 + packages/builders/src/index.ts | 137 + packages/builders/src/monitoring/utilts.ts | 198 + packages/builders/src/services/admin.ts | 151 + packages/builders/src/services/application.ts | 394 ++ packages/builders/src/services/auth.ts | 180 + packages/builders/src/services/backup.ts | 71 + packages/builders/src/services/bitbucket.ts | 88 + packages/builders/src/services/certificate.ts | 108 + packages/builders/src/services/cluster.ts | 41 + packages/builders/src/services/compose.ts | 467 ++ packages/builders/src/services/deployment.ts | 372 ++ packages/builders/src/services/destination.ts | 67 + packages/builders/src/services/docker.ts | 223 + packages/builders/src/services/domain.ts | 136 + .../builders/src/services/git-provider.ts | 29 + packages/builders/src/services/github.ts | 67 + packages/builders/src/services/gitlab.ts | 74 + packages/builders/src/services/mariadb.ts | 147 + packages/builders/src/services/mongo.ts | 140 + packages/builders/src/services/mount.ts | 280 ++ packages/builders/src/services/mysql.ts | 144 + .../builders/src/services/notification.ts | 409 ++ packages/builders/src/services/port.ts | 62 + packages/builders/src/services/postgres.ts | 142 + packages/builders/src/services/project.ts | 124 + packages/builders/src/services/redirect.ts | 123 + packages/builders/src/services/redis.ts | 117 + packages/builders/src/services/registry.ts | 124 + packages/builders/src/services/security.ts | 107 + packages/builders/src/services/server.ts | 120 + packages/builders/src/services/settings.ts | 148 + packages/builders/src/services/ssh-key.ts | 78 + packages/builders/src/services/user.ts | 207 + packages/builders/src/setup/config-paths.ts | 43 + packages/builders/src/setup/postgres-setup.ts | 61 + packages/builders/src/setup/redis-setup.ts | 57 + packages/builders/src/setup/registry-setup.ts | 91 + packages/builders/src/setup/server-setup.ts | 315 ++ packages/builders/src/setup/setup.ts | 47 + packages/builders/src/setup/traefik-setup.ts | 320 ++ .../src/templates/appsmith/docker-compose.yml | 6 + .../builders/src/templates/appsmith/index.ts | 23 + .../src/templates/aptabase/docker-compose.yml | 51 + .../builders/src/templates/aptabase/index.ts | 27 + .../src/templates/baserow/docker-compose.yml | 10 + .../builders/src/templates/baserow/index.ts | 24 + .../src/templates/calcom/docker-compose.yml | 26 + .../builders/src/templates/calcom/index.ts | 32 + .../src/templates/directus/docker-compose.yml | 47 + .../builders/src/templates/directus/index.ts | 20 + .../templates/documenso/docker-compose.yml | 43 + .../builders/src/templates/documenso/index.ts | 36 + .../templates/doublezero/docker-compose.yml | 19 + .../src/templates/doublezero/index.ts | 36 + .../templates/excalidraw/docker-compose.yml | 7 + .../src/templates/excalidraw/index.ts | 23 + .../src/templates/ghost/docker-compose.yml | 29 + .../builders/src/templates/ghost/index.ts | 25 + .../src/templates/gitea/docker-compose.yml | 37 + .../builders/src/templates/gitea/index.ts | 24 + .../templates/glitchtip/docker-compose.yml | 59 + .../builders/src/templates/glitchtip/index.ts | 30 + .../src/templates/grafana/docker-compose.yml | 9 + .../builders/src/templates/grafana/index.ts | 19 + .../src/templates/jellyfin/docker-compose.yml | 19 + .../builders/src/templates/jellyfin/index.ts | 25 + .../src/templates/listmonk/docker-compose.yml | 49 + .../builders/src/templates/listmonk/index.ts | 57 + .../templates/meilisearch/docker-compose.yml | 14 + .../src/templates/meilisearch/index.ts | 26 + .../src/templates/metabase/docker-compose.yml | 26 + .../builders/src/templates/metabase/index.ts | 22 + .../src/templates/minio/docker-compose.yml | 13 + .../builders/src/templates/minio/index.ts | 28 + .../src/templates/n8n/docker-compose.yml | 18 + packages/builders/src/templates/n8n/index.ts | 28 + .../src/templates/nocodb/docker-compose.yml | 32 + .../builders/src/templates/nocodb/index.ts | 28 + .../src/templates/odoo/docker-compose.yml | 29 + packages/builders/src/templates/odoo/index.ts | 22 + .../templates/open-webui/docker-compose.yml | 26 + .../src/templates/open-webui/index.ts | 24 + .../templates/phpmyadmin/docker-compose.yml | 28 + .../src/templates/phpmyadmin/index.ts | 32 + .../templates/plausible/docker-compose.yml | 44 + .../builders/src/templates/plausible/index.ts | 71 + .../templates/pocketbase/docker-compose.yml | 9 + .../src/templates/pocketbase/index.ts | 22 + .../templates/rocketchat/docker-compose.yml | 35 + .../src/templates/rocketchat/index.ts | 25 + .../src/templates/soketi/docker-compose.yml | 12 + .../builders/src/templates/soketi/index.ts | 28 + .../src/templates/supabase/docker-compose.yml | 460 ++ .../builders/src/templates/supabase/index.ts | 995 ++++ .../src/templates/teable/docker-compose.yml | 69 + .../builders/src/templates/teable/index.ts | 54 + packages/builders/src/templates/templates.ts | 500 ++ .../src/templates/typebot/docker-compose.yml | 49 + .../builders/src/templates/typebot/index.ts | 44 + .../templates/types/templates-data.type.ts | 67 + .../src/templates/umami/docker-compose.yml | 35 + .../builders/src/templates/umami/index.ts | 27 + .../templates/uptime-kuma/docker-compose.yml | 10 + .../src/templates/uptime-kuma/index.ts | 22 + .../builders/src/templates/utils/index.ts | 74 + .../templates/wordpress/docker-compose.yml | 27 + .../builders/src/templates/wordpress/index.ts | 22 + .../src/templates/zipline/docker-compose.yml | 37 + .../builders/src/templates/zipline/index.ts | 32 + packages/builders/src/types/with.ts | 45 + .../builders/src/utils/access-log/handler.ts | 114 + .../builders/src/utils/access-log/types.ts | 48 + .../builders/src/utils/access-log/utils.ts | 119 + packages/builders/src/utils/backups/index.ts | 157 + .../builders/src/utils/backups/mariadb.ts | 65 + packages/builders/src/utils/backups/mongo.ts | 63 + packages/builders/src/utils/backups/mysql.ts | 62 + .../builders/src/utils/backups/postgres.ts | 69 + packages/builders/src/utils/backups/utils.ts | 43 + .../builders/src/utils/builders/compose.ts | 216 + .../src/utils/builders/docker-file.ts | 116 + packages/builders/src/utils/builders/drop.ts | 124 + .../builders/src/utils/builders/heroku.ts | 73 + packages/builders/src/utils/builders/index.ts | 216 + .../builders/src/utils/builders/nixpacks.ts | 124 + .../builders/src/utils/builders/paketo.ts | 72 + .../builders/src/utils/builders/static.ts | 69 + packages/builders/src/utils/builders/utils.ts | 21 + packages/builders/src/utils/cluster/upload.ts | 65 + .../builders/src/utils/databases/mariadb.ts | 98 + .../builders/src/utils/databases/mongo.ts | 97 + .../builders/src/utils/databases/mysql.ts | 104 + .../builders/src/utils/databases/postgres.ts | 98 + .../builders/src/utils/databases/redis.ts | 95 + packages/builders/src/utils/docker/compose.ts | 56 + .../src/utils/docker/compose/configs.ts | 73 + .../src/utils/docker/compose/network.ts | 83 + .../src/utils/docker/compose/secrets.ts | 68 + .../src/utils/docker/compose/service.ts | 90 + .../src/utils/docker/compose/volume.ts | 78 + packages/builders/src/utils/docker/domain.ts | 327 ++ packages/builders/src/utils/docker/types.ts | 879 ++++ packages/builders/src/utils/docker/utils.ts | 525 ++ .../src/utils/filesystem/directory.ts | 142 + packages/builders/src/utils/filesystem/ssh.ts | 99 + .../src/utils/notifications/build-error.ts | 157 + .../src/utils/notifications/build-success.ts | 143 + .../utils/notifications/database-backup.ts | 177 + .../src/utils/notifications/docker-cleanup.ts | 94 + .../utils/notifications/dokploy-restart.ts | 83 + .../builders/src/utils/notifications/utils.ts | 84 + .../builders/src/utils/process/execAsync.ts | 73 + .../builders/src/utils/process/spawnAsync.ts | 58 + .../builders/src/utils/providers/bitbucket.ts | 361 ++ .../builders/src/utils/providers/docker.ts | 86 + packages/builders/src/utils/providers/git.ts | 347 ++ .../builders/src/utils/providers/github.ts | 336 ++ .../builders/src/utils/providers/gitlab.ts | 447 ++ packages/builders/src/utils/providers/raw.ts | 80 + .../src/utils/servers/remote-docker.ts | 23 + .../builders/src/utils/traefik/application.ts | 250 + packages/builders/src/utils/traefik/domain.ts | 145 + .../builders/src/utils/traefik/file-types.ts | 1274 +++++ .../builders/src/utils/traefik/middleware.ts | 107 + .../builders/src/utils/traefik/redirect.ts | 132 + .../builders/src/utils/traefik/registry.ts | 75 + .../builders/src/utils/traefik/security.ts | 129 + packages/builders/src/utils/traefik/types.ts | 574 +++ .../builders/src/utils/traefik/web-server.ts | 79 + .../builders/src/wss/docker-container-logs.ts | 136 + .../src/wss/docker-container-terminal.ts | 157 + packages/builders/src/wss/docker-stats.ts | 96 + .../builders/src/wss/listen-deployment.ts | 105 + packages/builders/src/wss/terminal.ts | 107 + packages/builders/src/wss/utils.ts | 12 + packages/builders/tsconfig.json | 43 + packages/builders/tsconfig.server.json | 19 + packages/builders/tsconfigtesting.json | 18 + packages/builders/tsup.ts | 55 + pnpm-lock.yaml | 1169 ++++- pnpm-workspace.yaml | 2 + 280 files changed, 28569 insertions(+), 196 deletions(-) create mode 100644 apps/api/docker-compose.yml create mode 100644 apps/api/src/test.ts delete mode 100644 apps/dokploy/server/utils/servers/connection.ts create mode 100644 packages/builders/esbuild.config.ts create mode 100644 packages/builders/package.json create mode 100644 packages/builders/src/auth/auth.ts create mode 100644 packages/builders/src/auth/random-password.ts create mode 100644 packages/builders/src/auth/token.ts create mode 100644 packages/builders/src/constants/index.ts create mode 100644 packages/builders/src/db/drizzle.config.ts create mode 100644 packages/builders/src/db/index.ts create mode 100644 packages/builders/src/db/migration.ts create mode 100644 packages/builders/src/db/reset.ts rename {apps/dokploy/server => packages/builders/src}/db/schema/admin.ts (100%) rename {apps/dokploy/server => packages/builders/src}/db/schema/application.ts (96%) rename {apps/dokploy/server => packages/builders/src}/db/schema/auth.ts (100%) rename {apps/dokploy/server => packages/builders/src}/db/schema/backups.ts (100%) rename {apps/dokploy/server => packages/builders/src}/db/schema/bitbucket.ts (100%) rename {apps/dokploy/server => packages/builders/src}/db/schema/certificate.ts (100%) rename {apps/dokploy/server => packages/builders/src}/db/schema/compose.ts (96%) rename {apps/dokploy/server => packages/builders/src}/db/schema/deployment.ts (100%) rename {apps/dokploy/server => packages/builders/src}/db/schema/destination.ts (100%) rename {apps/dokploy/server => packages/builders/src}/db/schema/domain.ts (97%) rename {apps/dokploy/server => packages/builders/src}/db/schema/git-provider.ts (100%) rename {apps/dokploy/server => packages/builders/src}/db/schema/github.ts (100%) rename {apps/dokploy/server => packages/builders/src}/db/schema/gitlab.ts (100%) create mode 100644 packages/builders/src/db/schema/index.ts rename {apps/dokploy/server => packages/builders/src}/db/schema/mariadb.ts (98%) rename {apps/dokploy/server => packages/builders/src}/db/schema/mongo.ts (100%) rename {apps/dokploy/server => packages/builders/src}/db/schema/mount.ts (100%) rename {apps/dokploy/server => packages/builders/src}/db/schema/mysql.ts (100%) rename {apps/dokploy/server => packages/builders/src}/db/schema/notification.ts (100%) rename {apps/dokploy/server => packages/builders/src}/db/schema/port.ts (100%) rename {apps/dokploy/server => packages/builders/src}/db/schema/postgres.ts (100%) rename {apps/dokploy/server => packages/builders/src}/db/schema/project.ts (100%) rename {apps/dokploy/server => packages/builders/src}/db/schema/redirects.ts (100%) rename {apps/dokploy/server => packages/builders/src}/db/schema/redis.ts (100%) rename {apps/dokploy/server => packages/builders/src}/db/schema/registry.ts (100%) rename {apps/dokploy/server => packages/builders/src}/db/schema/security.ts (100%) rename {apps/dokploy/server => packages/builders/src}/db/schema/server.ts (89%) rename {apps/dokploy/server => packages/builders/src}/db/schema/session.ts (100%) rename {apps/dokploy/server => packages/builders/src}/db/schema/shared.ts (100%) rename {apps/dokploy/server => packages/builders/src}/db/schema/source.ts (100%) rename {apps/dokploy/server => packages/builders/src}/db/schema/ssh-key.ts (88%) rename {apps/dokploy/server => packages/builders/src}/db/schema/user.ts (100%) rename {apps/dokploy/server => packages/builders/src}/db/schema/utils.ts (100%) create mode 100644 packages/builders/src/db/seed.ts create mode 100644 packages/builders/src/db/validations/domain.ts create mode 100644 packages/builders/src/db/validations/index.ts create mode 100644 packages/builders/src/emails/.gitignore create mode 100644 packages/builders/src/emails/emails/build-failed.tsx create mode 100644 packages/builders/src/emails/emails/build-success.tsx create mode 100644 packages/builders/src/emails/emails/database-backup.tsx create mode 100644 packages/builders/src/emails/emails/docker-cleanup.tsx create mode 100644 packages/builders/src/emails/emails/dokploy-restart.tsx create mode 100644 packages/builders/src/emails/emails/invitation.tsx create mode 100644 packages/builders/src/emails/emails/notion-magic-link.tsx create mode 100644 packages/builders/src/emails/emails/plaid-verify-identity.tsx create mode 100644 packages/builders/src/emails/emails/static/logo.png create mode 100644 packages/builders/src/emails/emails/static/notion-logo.png create mode 100644 packages/builders/src/emails/emails/static/plaid-logo.png create mode 100644 packages/builders/src/emails/emails/static/plaid.png create mode 100644 packages/builders/src/emails/emails/static/stripe-logo.png create mode 100644 packages/builders/src/emails/emails/static/vercel-arrow.png create mode 100644 packages/builders/src/emails/emails/static/vercel-logo.png create mode 100644 packages/builders/src/emails/emails/static/vercel-team.png create mode 100644 packages/builders/src/emails/emails/static/vercel-user.png create mode 100644 packages/builders/src/emails/emails/stripe-welcome.tsx create mode 100644 packages/builders/src/emails/emails/vercel-invite-user.tsx create mode 100644 packages/builders/src/emails/package.json create mode 100644 packages/builders/src/emails/pnpm-lock.yaml create mode 100644 packages/builders/src/emails/readme.md create mode 100644 packages/builders/src/index.ts create mode 100644 packages/builders/src/monitoring/utilts.ts create mode 100644 packages/builders/src/services/admin.ts create mode 100644 packages/builders/src/services/application.ts create mode 100644 packages/builders/src/services/auth.ts create mode 100644 packages/builders/src/services/backup.ts create mode 100644 packages/builders/src/services/bitbucket.ts create mode 100644 packages/builders/src/services/certificate.ts create mode 100644 packages/builders/src/services/cluster.ts create mode 100644 packages/builders/src/services/compose.ts create mode 100644 packages/builders/src/services/deployment.ts create mode 100644 packages/builders/src/services/destination.ts create mode 100644 packages/builders/src/services/docker.ts create mode 100644 packages/builders/src/services/domain.ts create mode 100644 packages/builders/src/services/git-provider.ts create mode 100644 packages/builders/src/services/github.ts create mode 100644 packages/builders/src/services/gitlab.ts create mode 100644 packages/builders/src/services/mariadb.ts create mode 100644 packages/builders/src/services/mongo.ts create mode 100644 packages/builders/src/services/mount.ts create mode 100644 packages/builders/src/services/mysql.ts create mode 100644 packages/builders/src/services/notification.ts create mode 100644 packages/builders/src/services/port.ts create mode 100644 packages/builders/src/services/postgres.ts create mode 100644 packages/builders/src/services/project.ts create mode 100644 packages/builders/src/services/redirect.ts create mode 100644 packages/builders/src/services/redis.ts create mode 100644 packages/builders/src/services/registry.ts create mode 100644 packages/builders/src/services/security.ts create mode 100644 packages/builders/src/services/server.ts create mode 100644 packages/builders/src/services/settings.ts create mode 100644 packages/builders/src/services/ssh-key.ts create mode 100644 packages/builders/src/services/user.ts create mode 100644 packages/builders/src/setup/config-paths.ts create mode 100644 packages/builders/src/setup/postgres-setup.ts create mode 100644 packages/builders/src/setup/redis-setup.ts create mode 100644 packages/builders/src/setup/registry-setup.ts create mode 100644 packages/builders/src/setup/server-setup.ts create mode 100644 packages/builders/src/setup/setup.ts create mode 100644 packages/builders/src/setup/traefik-setup.ts create mode 100644 packages/builders/src/templates/appsmith/docker-compose.yml create mode 100644 packages/builders/src/templates/appsmith/index.ts create mode 100644 packages/builders/src/templates/aptabase/docker-compose.yml create mode 100644 packages/builders/src/templates/aptabase/index.ts create mode 100644 packages/builders/src/templates/baserow/docker-compose.yml create mode 100644 packages/builders/src/templates/baserow/index.ts create mode 100644 packages/builders/src/templates/calcom/docker-compose.yml create mode 100644 packages/builders/src/templates/calcom/index.ts create mode 100644 packages/builders/src/templates/directus/docker-compose.yml create mode 100644 packages/builders/src/templates/directus/index.ts create mode 100644 packages/builders/src/templates/documenso/docker-compose.yml create mode 100644 packages/builders/src/templates/documenso/index.ts create mode 100644 packages/builders/src/templates/doublezero/docker-compose.yml create mode 100644 packages/builders/src/templates/doublezero/index.ts create mode 100644 packages/builders/src/templates/excalidraw/docker-compose.yml create mode 100644 packages/builders/src/templates/excalidraw/index.ts create mode 100644 packages/builders/src/templates/ghost/docker-compose.yml create mode 100644 packages/builders/src/templates/ghost/index.ts create mode 100644 packages/builders/src/templates/gitea/docker-compose.yml create mode 100644 packages/builders/src/templates/gitea/index.ts create mode 100644 packages/builders/src/templates/glitchtip/docker-compose.yml create mode 100644 packages/builders/src/templates/glitchtip/index.ts create mode 100644 packages/builders/src/templates/grafana/docker-compose.yml create mode 100644 packages/builders/src/templates/grafana/index.ts create mode 100644 packages/builders/src/templates/jellyfin/docker-compose.yml create mode 100644 packages/builders/src/templates/jellyfin/index.ts create mode 100644 packages/builders/src/templates/listmonk/docker-compose.yml create mode 100644 packages/builders/src/templates/listmonk/index.ts create mode 100644 packages/builders/src/templates/meilisearch/docker-compose.yml create mode 100644 packages/builders/src/templates/meilisearch/index.ts create mode 100644 packages/builders/src/templates/metabase/docker-compose.yml create mode 100644 packages/builders/src/templates/metabase/index.ts create mode 100644 packages/builders/src/templates/minio/docker-compose.yml create mode 100644 packages/builders/src/templates/minio/index.ts create mode 100644 packages/builders/src/templates/n8n/docker-compose.yml create mode 100644 packages/builders/src/templates/n8n/index.ts create mode 100644 packages/builders/src/templates/nocodb/docker-compose.yml create mode 100644 packages/builders/src/templates/nocodb/index.ts create mode 100644 packages/builders/src/templates/odoo/docker-compose.yml create mode 100644 packages/builders/src/templates/odoo/index.ts create mode 100644 packages/builders/src/templates/open-webui/docker-compose.yml create mode 100644 packages/builders/src/templates/open-webui/index.ts create mode 100644 packages/builders/src/templates/phpmyadmin/docker-compose.yml create mode 100644 packages/builders/src/templates/phpmyadmin/index.ts create mode 100644 packages/builders/src/templates/plausible/docker-compose.yml create mode 100644 packages/builders/src/templates/plausible/index.ts create mode 100644 packages/builders/src/templates/pocketbase/docker-compose.yml create mode 100644 packages/builders/src/templates/pocketbase/index.ts create mode 100644 packages/builders/src/templates/rocketchat/docker-compose.yml create mode 100644 packages/builders/src/templates/rocketchat/index.ts create mode 100644 packages/builders/src/templates/soketi/docker-compose.yml create mode 100644 packages/builders/src/templates/soketi/index.ts create mode 100644 packages/builders/src/templates/supabase/docker-compose.yml create mode 100644 packages/builders/src/templates/supabase/index.ts create mode 100644 packages/builders/src/templates/teable/docker-compose.yml create mode 100644 packages/builders/src/templates/teable/index.ts create mode 100644 packages/builders/src/templates/templates.ts create mode 100644 packages/builders/src/templates/typebot/docker-compose.yml create mode 100644 packages/builders/src/templates/typebot/index.ts create mode 100644 packages/builders/src/templates/types/templates-data.type.ts create mode 100644 packages/builders/src/templates/umami/docker-compose.yml create mode 100644 packages/builders/src/templates/umami/index.ts create mode 100644 packages/builders/src/templates/uptime-kuma/docker-compose.yml create mode 100644 packages/builders/src/templates/uptime-kuma/index.ts create mode 100644 packages/builders/src/templates/utils/index.ts create mode 100644 packages/builders/src/templates/wordpress/docker-compose.yml create mode 100644 packages/builders/src/templates/wordpress/index.ts create mode 100644 packages/builders/src/templates/zipline/docker-compose.yml create mode 100644 packages/builders/src/templates/zipline/index.ts create mode 100644 packages/builders/src/types/with.ts create mode 100644 packages/builders/src/utils/access-log/handler.ts create mode 100644 packages/builders/src/utils/access-log/types.ts create mode 100644 packages/builders/src/utils/access-log/utils.ts create mode 100644 packages/builders/src/utils/backups/index.ts create mode 100644 packages/builders/src/utils/backups/mariadb.ts create mode 100644 packages/builders/src/utils/backups/mongo.ts create mode 100644 packages/builders/src/utils/backups/mysql.ts create mode 100644 packages/builders/src/utils/backups/postgres.ts create mode 100644 packages/builders/src/utils/backups/utils.ts create mode 100644 packages/builders/src/utils/builders/compose.ts create mode 100644 packages/builders/src/utils/builders/docker-file.ts create mode 100644 packages/builders/src/utils/builders/drop.ts create mode 100644 packages/builders/src/utils/builders/heroku.ts create mode 100644 packages/builders/src/utils/builders/index.ts create mode 100644 packages/builders/src/utils/builders/nixpacks.ts create mode 100644 packages/builders/src/utils/builders/paketo.ts create mode 100644 packages/builders/src/utils/builders/static.ts create mode 100644 packages/builders/src/utils/builders/utils.ts create mode 100644 packages/builders/src/utils/cluster/upload.ts create mode 100644 packages/builders/src/utils/databases/mariadb.ts create mode 100644 packages/builders/src/utils/databases/mongo.ts create mode 100644 packages/builders/src/utils/databases/mysql.ts create mode 100644 packages/builders/src/utils/databases/postgres.ts create mode 100644 packages/builders/src/utils/databases/redis.ts create mode 100644 packages/builders/src/utils/docker/compose.ts create mode 100644 packages/builders/src/utils/docker/compose/configs.ts create mode 100644 packages/builders/src/utils/docker/compose/network.ts create mode 100644 packages/builders/src/utils/docker/compose/secrets.ts create mode 100644 packages/builders/src/utils/docker/compose/service.ts create mode 100644 packages/builders/src/utils/docker/compose/volume.ts create mode 100644 packages/builders/src/utils/docker/domain.ts create mode 100644 packages/builders/src/utils/docker/types.ts create mode 100644 packages/builders/src/utils/docker/utils.ts create mode 100644 packages/builders/src/utils/filesystem/directory.ts create mode 100644 packages/builders/src/utils/filesystem/ssh.ts create mode 100644 packages/builders/src/utils/notifications/build-error.ts create mode 100644 packages/builders/src/utils/notifications/build-success.ts create mode 100644 packages/builders/src/utils/notifications/database-backup.ts create mode 100644 packages/builders/src/utils/notifications/docker-cleanup.ts create mode 100644 packages/builders/src/utils/notifications/dokploy-restart.ts create mode 100644 packages/builders/src/utils/notifications/utils.ts create mode 100644 packages/builders/src/utils/process/execAsync.ts create mode 100644 packages/builders/src/utils/process/spawnAsync.ts create mode 100644 packages/builders/src/utils/providers/bitbucket.ts create mode 100644 packages/builders/src/utils/providers/docker.ts create mode 100644 packages/builders/src/utils/providers/git.ts create mode 100644 packages/builders/src/utils/providers/github.ts create mode 100644 packages/builders/src/utils/providers/gitlab.ts create mode 100644 packages/builders/src/utils/providers/raw.ts create mode 100644 packages/builders/src/utils/servers/remote-docker.ts create mode 100644 packages/builders/src/utils/traefik/application.ts create mode 100644 packages/builders/src/utils/traefik/domain.ts create mode 100644 packages/builders/src/utils/traefik/file-types.ts create mode 100644 packages/builders/src/utils/traefik/middleware.ts create mode 100644 packages/builders/src/utils/traefik/redirect.ts create mode 100644 packages/builders/src/utils/traefik/registry.ts create mode 100644 packages/builders/src/utils/traefik/security.ts create mode 100644 packages/builders/src/utils/traefik/types.ts create mode 100644 packages/builders/src/utils/traefik/web-server.ts create mode 100644 packages/builders/src/wss/docker-container-logs.ts create mode 100644 packages/builders/src/wss/docker-container-terminal.ts create mode 100644 packages/builders/src/wss/docker-stats.ts create mode 100644 packages/builders/src/wss/listen-deployment.ts create mode 100644 packages/builders/src/wss/terminal.ts create mode 100644 packages/builders/src/wss/utils.ts create mode 100644 packages/builders/tsconfig.json create mode 100644 packages/builders/tsconfig.server.json create mode 100644 packages/builders/tsconfigtesting.json create mode 100644 packages/builders/tsup.ts diff --git a/apps/api/docker-compose.yml b/apps/api/docker-compose.yml new file mode 100644 index 000000000..6e5df8871 --- /dev/null +++ b/apps/api/docker-compose.yml @@ -0,0 +1,21 @@ +version: "2" +services: + zookeeper: + image: "confluentinc/cp-zookeeper:latest" + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + ports: + - "2181:2181" + + kafka: + image: "confluentinc/cp-kafka:latest" + depends_on: + - zookeeper + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + ports: + - "9092:9092" diff --git a/apps/api/package.json b/apps/api/package.json index 5450ceab9..fc27422f7 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,14 +1,29 @@ { "name": "my-app", "scripts": { - "dev": "tsx watch src/index.ts" + "dev": "tsx watch src/index.ts", + "tsc": "tsc --project tsconfig.json" }, "dependencies": { + "react": "18.2.0", + "react-dom": "18.2.0", + "@dokploy/builders": "workspace:*", "@hono/node-server": "^1.12.1", "hono": "^4.5.8", - "dotenv": "^16.3.1" + "dotenv": "^16.3.1", + "@upstash/qstash": "2.7.9", + "ioredis": "5.4.1", + "nats": "2.28.2", + "bullmq": "5.13.2", + "@nerimity/mimiqueue": "1.2.3", + "timers": "0.1.1", + "redis": "4.7.0", + "date-fns": "4.1.0" }, "devDependencies": { + "typescript": "^5.4.2", + "@types/react": "^18.2.37", + "@types/react-dom": "^18.2.15", "@types/node": "^20.11.17", "tsx": "^4.7.1" } diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 866c1f3f6..816141b3b 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -1,66 +1,57 @@ import { serve } from "@hono/node-server"; -import { config } from "dotenv"; import { Hono } from "hono"; -import { cors } from "hono/cors"; -import { validateLemonSqueezyLicense } from "./utils"; - -config(); +import "dotenv/config"; +import { createClient } from "redis"; +import { Queue } from "@nerimity/mimiqueue"; +import { deployApplication } from "@dokploy/builders"; +// import { setTimeout } from "timers/promises"; const app = new Hono(); - -app.use( - "/*", - cors({ - origin: ["http://localhost:3000", "http://localhost:3001"], // Ajusta esto a los orígenes de tu aplicación Next.js - allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], - allowHeaders: ["Content-Type", "Authorization"], - exposeHeaders: ["Content-Length", "X-Kuma-Revision"], - maxAge: 600, - credentials: true, - }), -); - -export const LEMON_SQUEEZY_API_KEY = process.env.LEMON_SQUEEZY_API_KEY; -export const LEMON_SQUEEZY_STORE_ID = process.env.LEMON_SQUEEZY_STORE_ID; - -app.get("/v1/health", (c) => { - return c.text("Hello Hono!"); +const redisClient = createClient({ + socket: { + host: "localhost", + port: 6379, + }, + // password: "xlfvpQ0ma2BkkkPX", }); -app.post("/v1/validate-license", async (c) => { - const { licenseKey } = await c.req.json(); - - if (!licenseKey) { - return c.json({ error: "License key is required" }, 400); - } - - try { - const licenseValidation = await validateLemonSqueezyLicense(licenseKey); - - if (licenseValidation.valid) { - return c.json({ - valid: true, - message: "License is valid", - metadata: licenseValidation.meta, - }); - } - return c.json( +app.post("/publish", async (c) => { + const { userId, applicationId } = await c.req.json(); + queue + .add( { - valid: false, - message: licenseValidation.error || "Invalid license", + userId, + applicationId, }, - 400, - ); - } catch (error) { - console.error("Error during license validation:", error); - return c.json({ error: "Internal server error" }, 500); - } -}); + { groupName: userId }, + ) + .then((res) => { + console.log(res); + }); + return c.json({ message: `Despliegue encolado para el usuario ${userId}` }); +}); +// await redisClient.connect(); +// await redisClient.flushAll(); + +const queue = new Queue({ + name: "deployments", + process: async (data) => { + // await setTimeout(8000); + await deployApplication({ + applicationId: data.applicationId, + titleLog: "HHHHH", + descriptionLog: "", + }); + return { done: "lol", data }; + }, + redisClient, +}); const port = 4000; -console.log(`Server is running on port ${port}`); +(async () => { + await redisClient.connect(); + await redisClient.flushAll(); +})(); -serve({ - fetch: app.fetch, - port, -}); +console.log("Starting Server ✅"); +serve({ fetch: app.fetch, port }); diff --git a/apps/api/src/test.ts b/apps/api/src/test.ts new file mode 100644 index 000000000..9796eb60b --- /dev/null +++ b/apps/api/src/test.ts @@ -0,0 +1,82 @@ +import { Hono } from "hono"; +import { Client } from "@upstash/qstash"; +import { serve } from "@hono/node-server"; +import dotenv from "dotenv"; +import Redis from "ioredis"; + +dotenv.config(); + +const redis = new Redis({ + host: "localhost", + port: 7777, + password: "xlfvpQ0ma2BkkkPX", +}); + +// redis.set("test", "test"); +// console.log(await redis.get("test")); + +// console.log(await redis.get("user-1-processing")); +const app = new Hono(); +console.log("QStash Token:", process.env.PUBLIC_URL); + +const qstash = new Client({ + token: process.env.QSTASH_TOKEN as string, +}); + +const queue = qstash.queue({ + queueName: "deployments", +}); + +// Endpoint que publica un mensaje en QStash +app.post("/enqueue", async (c) => { + const { userId, deploymentId } = await c.req.json(); + const response = await qstash.publishJSON({ + url: `${process.env.PUBLIC_URL}/process`, // Endpoint para procesar la tarea + body: { userId, deploymentId }, // Datos del despliegue + + }); + + return c.json({ message: "Task enqueued", id: response.messageId }); +}); + +// Endpoint que recibe el mensaje procesado +app.post("/process", async (c) => { + const { userId, deploymentId } = await c.req.json(); + + const isProcessing = await redis.get(`user-${userId}-processing`); + console.log(`isProcessing for user ${userId}:`, isProcessing); + + if (isProcessing === "true") { + console.log( + `User ${userId} is already processing a deployment. Queuing the next one.`, + ); + return c.json( + { + status: "User is already processing a deployment, waiting...", + }, + { + status: 400, + }, + ); + } + redis.set(`user-${userId}-processing`, "true"); + + try { + await new Promise((resolve) => setTimeout(resolve, 5000)); + } catch (error) { + } finally { + await redis.del(`user-${userId}-processing`); + } + + return c.json({ status: "Processed", userId, deploymentId }); +}); + +// Inicia el servidor en el puerto 3000 +const port = 3000; +console.log(`Server is running on port http://localhost:${port}`); + +serve({ + fetch: app.fetch, + port, +}); +// 18 diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json index 68a9e8f04..55e7a5e15 100644 --- a/apps/api/tsconfig.json +++ b/apps/api/tsconfig.json @@ -1,12 +1,14 @@ { - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "Bundler", - "strict": true, - "skipLibCheck": true, - "types": ["node"], - "jsx": "react-jsx", - "jsxImportSource": "hono/jsx" - } + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Node", + "strict": true, + "skipLibCheck": true, + "types": ["node"], + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx", + "traceResolution": true, + "diagnostics": true + } } diff --git a/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx b/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx index bf1a298be..8bfcf4da2 100644 --- a/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/setup-server.tsx @@ -19,7 +19,6 @@ import { DialogTrigger, } from "@/components/ui/dialog"; import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; -import { Separator } from "@/components/ui/separator"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { api } from "@/utils/api"; import copy from "copy-to-clipboard"; diff --git a/apps/dokploy/next.config.mjs b/apps/dokploy/next.config.mjs index 46b4268c3..134cf19c2 100644 --- a/apps/dokploy/next.config.mjs +++ b/apps/dokploy/next.config.mjs @@ -11,45 +11,46 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** @type {import("next").NextConfig} */ const nextConfig = { - reactStrictMode: true, - eslint: { - ignoreDuringBuilds: true, - }, - typescript: { - ignoreBuildErrors: true, - }, - webpack: (config) => { - config.plugins.push( - new CopyWebpackPlugin({ - patterns: [ - { - from: path.resolve(__dirname, "templates/**/*.yml"), - to: ({ context, absoluteFilename }) => { - const relativePath = path.relative( - path.resolve(__dirname, "templates"), - absoluteFilename || context, - ); - return path.join(__dirname, ".next", "templates", relativePath); - }, - globOptions: { - ignore: ["**/node_modules/**"], - }, - }, - ], - }), - ); - return config; - }, + reactStrictMode: true, + eslint: { + ignoreDuringBuilds: true, + }, + typescript: { + ignoreBuildErrors: true, + }, + transpilePackages: ["@dokploy/builders"], + webpack: (config) => { + config.plugins.push( + new CopyWebpackPlugin({ + patterns: [ + { + from: path.resolve(__dirname, "templates/**/*.yml"), + to: ({ context, absoluteFilename }) => { + const relativePath = path.relative( + path.resolve(__dirname, "templates"), + absoluteFilename || context + ); + return path.join(__dirname, ".next", "templates", relativePath); + }, + globOptions: { + ignore: ["**/node_modules/**"], + }, + }, + ], + }) + ); + return config; + }, - /** - * If you are using `appDir` then you must comment the below `i18n` config out. - * - * @see https://github.com/vercel/next.js/issues/41980 - */ - i18n: { - locales: ["en"], - defaultLocale: "en", - }, + /** + * If you are using `appDir` then you must comment the below `i18n` config out. + * + * @see https://github.com/vercel/next.js/issues/41980 + */ + i18n: { + locales: ["en"], + defaultLocale: "en", + }, }; export default nextConfig; diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index 023218997..9f758cc10 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -34,6 +34,7 @@ "test": "vitest --config __test__/vitest.config.ts" }, "dependencies": { + "@dokploy/builders": "workspace:*", "rotating-file-stream": "3.2.3", "@codemirror/lang-json": "^6.0.1", "@codemirror/lang-yaml": "^6.1.1", diff --git a/apps/dokploy/pages/api/[...trpc].ts b/apps/dokploy/pages/api/[...trpc].ts index 4063d29f7..89c6616cd 100644 --- a/apps/dokploy/pages/api/[...trpc].ts +++ b/apps/dokploy/pages/api/[...trpc].ts @@ -18,6 +18,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { res.status(401).json({ message: "Unauthorized" }); return; } + + console.log(user); // @ts-ignore return createOpenApiNextHandler({ router: appRouter, diff --git a/apps/dokploy/server/api/routers/admin.ts b/apps/dokploy/server/api/routers/admin.ts index 2157073e6..acacc7769 100644 --- a/apps/dokploy/server/api/routers/admin.ts +++ b/apps/dokploy/server/api/routers/admin.ts @@ -6,14 +6,23 @@ import { apiRemoveUser, users, } from "@/server/db/schema"; + +// import { + +// } from "@dokploy/builders"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; +// import { +// createInvitation, +// getUserByToken, +// removeUserByAuthId, +// } from "../services/admin"; import { - createInvitation, findAdmin, + createInvitation, getUserByToken, removeUserByAuthId, -} from "../services/admin"; +} from "@dokploy/builders"; import { adminProcedure, createTRPCRouter, publicProcedure } from "../trpc"; export const adminRouter = createTRPCRouter({ diff --git a/apps/dokploy/server/api/routers/application.ts b/apps/dokploy/server/api/routers/application.ts index 4c43fe7ce..09e45da17 100644 --- a/apps/dokploy/server/api/routers/application.ts +++ b/apps/dokploy/server/api/routers/application.ts @@ -18,7 +18,7 @@ import { apiSaveGitlabProvider, apiUpdateApplication, applications, -} from "@/server/db/schema/application"; +} from "@/server/db/schema"; import { type DeploymentJob, cleanQueuesByApplication, @@ -55,7 +55,7 @@ import { getApplicationStats, updateApplication, updateApplicationStatus, -} from "../services/application"; +} from "@dokploy/builders"; import { removeDeployments } from "../services/deployment"; import { addNewService, checkServiceAccess } from "../services/user"; diff --git a/apps/dokploy/server/api/routers/mariadb.ts b/apps/dokploy/server/api/routers/mariadb.ts index e8893782b..d73215b2f 100644 --- a/apps/dokploy/server/api/routers/mariadb.ts +++ b/apps/dokploy/server/api/routers/mariadb.ts @@ -8,7 +8,7 @@ import { apiSaveEnvironmentVariablesMariaDB, apiSaveExternalPortMariaDB, apiUpdateMariaDB, -} from "@/server/db/schema/mariadb"; +} from "@/server/db/schema"; import { removeService, startService, diff --git a/apps/dokploy/server/api/routers/mongo.ts b/apps/dokploy/server/api/routers/mongo.ts index f51d0eb9e..89f3bc355 100644 --- a/apps/dokploy/server/api/routers/mongo.ts +++ b/apps/dokploy/server/api/routers/mongo.ts @@ -8,7 +8,7 @@ import { apiSaveEnvironmentVariablesMongo, apiSaveExternalPortMongo, apiUpdateMongo, -} from "@/server/db/schema/mongo"; +} from "@/server/db/schema"; import { removeService, startService, diff --git a/apps/dokploy/server/api/routers/mysql.ts b/apps/dokploy/server/api/routers/mysql.ts index 71f6d514b..0d4aaa6c6 100644 --- a/apps/dokploy/server/api/routers/mysql.ts +++ b/apps/dokploy/server/api/routers/mysql.ts @@ -8,7 +8,7 @@ import { apiSaveEnvironmentVariablesMySql, apiSaveExternalPortMySql, apiUpdateMySql, -} from "@/server/db/schema/mysql"; +} from "@/server/db/schema"; import { removeService, startService, diff --git a/apps/dokploy/server/api/routers/port.ts b/apps/dokploy/server/api/routers/port.ts index 36aba5de0..252977284 100644 --- a/apps/dokploy/server/api/routers/port.ts +++ b/apps/dokploy/server/api/routers/port.ts @@ -3,7 +3,7 @@ import { apiCreatePort, apiFindOnePort, apiUpdatePort, -} from "@/server/db/schema/port"; +} from "@/server/db/schema"; import { TRPCError } from "@trpc/server"; import { createPort, diff --git a/apps/dokploy/server/api/routers/postgres.ts b/apps/dokploy/server/api/routers/postgres.ts index b902ee517..1d2779efe 100644 --- a/apps/dokploy/server/api/routers/postgres.ts +++ b/apps/dokploy/server/api/routers/postgres.ts @@ -8,7 +8,7 @@ import { apiSaveEnvironmentVariablesPostgres, apiSaveExternalPortPostgres, apiUpdatePostgres, -} from "@/server/db/schema/postgres"; +} from "@/server/db/schema"; import { removeService, startService, diff --git a/apps/dokploy/server/api/routers/project.ts b/apps/dokploy/server/api/routers/project.ts index 4ad9be75f..62c250ba7 100644 --- a/apps/dokploy/server/api/routers/project.ts +++ b/apps/dokploy/server/api/routers/project.ts @@ -15,7 +15,7 @@ import { apiRemoveProject, apiUpdateProject, projects, -} from "@/server/db/schema/project"; +} from "@/server/db/schema"; import { TRPCError } from "@trpc/server"; import { desc, eq, sql } from "drizzle-orm"; import type { AnyPgColumn } from "drizzle-orm/pg-core"; diff --git a/apps/dokploy/server/api/routers/redis.ts b/apps/dokploy/server/api/routers/redis.ts index 19bfa7c20..2f904384d 100644 --- a/apps/dokploy/server/api/routers/redis.ts +++ b/apps/dokploy/server/api/routers/redis.ts @@ -8,7 +8,7 @@ import { apiSaveEnvironmentVariablesRedis, apiSaveExternalPortRedis, apiUpdateRedis, -} from "@/server/db/schema/redis"; +} from "@/server/db/schema"; import { removeService, startService, diff --git a/apps/dokploy/server/api/services/application.ts b/apps/dokploy/server/api/services/application.ts index 7f0403586..c56f39015 100644 --- a/apps/dokploy/server/api/services/application.ts +++ b/apps/dokploy/server/api/services/application.ts @@ -1,7 +1,7 @@ import { docker } from "@/server/constants"; import { db } from "@/server/db"; import { type apiCreateApplication, applications } from "@/server/db/schema"; -import { generateAppName } from "@/server/db/schema/utils"; +import { generateAppName } from "@/server/db/schema"; import { getAdvancedStats } from "@/server/monitoring/utilts"; import { buildApplication, diff --git a/apps/dokploy/server/api/services/compose.ts b/apps/dokploy/server/api/services/compose.ts index 13c728f5f..4df97bf20 100644 --- a/apps/dokploy/server/api/services/compose.ts +++ b/apps/dokploy/server/api/services/compose.ts @@ -2,7 +2,7 @@ import { join } from "node:path"; import { paths } from "@/server/constants"; import { db } from "@/server/db"; import { type apiCreateCompose, compose } from "@/server/db/schema"; -import { generateAppName } from "@/server/db/schema/utils"; +import { generateAppName } from "@/server/db/schema"; import { buildCompose, getBuildComposeCommand, diff --git a/apps/dokploy/server/api/services/mariadb.ts b/apps/dokploy/server/api/services/mariadb.ts index 2fdb3a7fe..13b521d56 100644 --- a/apps/dokploy/server/api/services/mariadb.ts +++ b/apps/dokploy/server/api/services/mariadb.ts @@ -1,7 +1,7 @@ import { generateRandomPassword } from "@/server/auth/random-password"; import { db } from "@/server/db"; import { type apiCreateMariaDB, backups, mariadb } from "@/server/db/schema"; -import { generateAppName } from "@/server/db/schema/utils"; +import { generateAppName } from "@/server/db/schema"; import { buildMariadb } from "@/server/utils/databases/mariadb"; import { pullImage } from "@/server/utils/docker/utils"; import { generatePassword } from "@/templates/utils"; diff --git a/apps/dokploy/server/api/services/mongo.ts b/apps/dokploy/server/api/services/mongo.ts index 9940768a7..30ed3f643 100644 --- a/apps/dokploy/server/api/services/mongo.ts +++ b/apps/dokploy/server/api/services/mongo.ts @@ -1,7 +1,7 @@ import { generateRandomPassword } from "@/server/auth/random-password"; import { db } from "@/server/db"; import { type apiCreateMongo, backups, mongo } from "@/server/db/schema"; -import { generateAppName } from "@/server/db/schema/utils"; +import { generateAppName } from "@/server/db/schema"; import { buildMongo } from "@/server/utils/databases/mongo"; import { pullImage } from "@/server/utils/docker/utils"; import { generatePassword } from "@/templates/utils"; diff --git a/apps/dokploy/server/api/services/mysql.ts b/apps/dokploy/server/api/services/mysql.ts index b4dacf81f..3a5c2884d 100644 --- a/apps/dokploy/server/api/services/mysql.ts +++ b/apps/dokploy/server/api/services/mysql.ts @@ -1,7 +1,7 @@ import { generateRandomPassword } from "@/server/auth/random-password"; import { db } from "@/server/db"; import { type apiCreateMySql, backups, mysql } from "@/server/db/schema"; -import { generateAppName } from "@/server/db/schema/utils"; +import { generateAppName } from "@/server/db/schema"; import { buildMysql } from "@/server/utils/databases/mysql"; import { pullImage } from "@/server/utils/docker/utils"; import { generatePassword } from "@/templates/utils"; diff --git a/apps/dokploy/server/api/services/postgres.ts b/apps/dokploy/server/api/services/postgres.ts index 56c91149c..03adaf9b1 100644 --- a/apps/dokploy/server/api/services/postgres.ts +++ b/apps/dokploy/server/api/services/postgres.ts @@ -1,7 +1,7 @@ import { generateRandomPassword } from "@/server/auth/random-password"; import { db } from "@/server/db"; import { type apiCreatePostgres, backups, postgres } from "@/server/db/schema"; -import { generateAppName } from "@/server/db/schema/utils"; +import { generateAppName } from "@/server/db/schema"; import { buildPostgres } from "@/server/utils/databases/postgres"; import { pullImage } from "@/server/utils/docker/utils"; import { generatePassword } from "@/templates/utils"; diff --git a/apps/dokploy/server/api/services/redis.ts b/apps/dokploy/server/api/services/redis.ts index a4d85857c..a06e979e5 100644 --- a/apps/dokploy/server/api/services/redis.ts +++ b/apps/dokploy/server/api/services/redis.ts @@ -1,7 +1,7 @@ import { generateRandomPassword } from "@/server/auth/random-password"; import { db } from "@/server/db"; import { type apiCreateRedis, redis } from "@/server/db/schema"; -import { generateAppName } from "@/server/db/schema/utils"; +import { generateAppName } from "@/server/db/schema"; import { buildRedis } from "@/server/utils/databases/redis"; import { pullImage } from "@/server/utils/docker/utils"; import { generatePassword } from "@/templates/utils"; diff --git a/apps/dokploy/server/api/services/server.ts b/apps/dokploy/server/api/services/server.ts index 4cd79cdcc..9f4c32701 100644 --- a/apps/dokploy/server/api/services/server.ts +++ b/apps/dokploy/server/api/services/server.ts @@ -1,4 +1,5 @@ import { db } from "@/server/db"; + import { type apiCreateServer, server } from "@/server/db/schema"; import { TRPCError } from "@trpc/server"; import { desc, eq } from "drizzle-orm"; diff --git a/apps/dokploy/server/db/index.ts b/apps/dokploy/server/db/index.ts index 00f68e6c8..79f2e03d2 100644 --- a/apps/dokploy/server/db/index.ts +++ b/apps/dokploy/server/db/index.ts @@ -1,7 +1,13 @@ import { type PostgresJsDatabase, drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; +// import { sc } from "@dokploy/schema"; +// import * as schema from "@dokploy/schema"; +// schema import * as schema from "./schema"; +// type Schema = typeof schema; + +// type Schema = typeof schema; declare global { // eslint-disable-next-line no-var -- only var works here var db: PostgresJsDatabase | undefined; diff --git a/apps/dokploy/server/db/schema/index.ts b/apps/dokploy/server/db/schema/index.ts index 3b24faff1..b93be6649 100644 --- a/apps/dokploy/server/db/schema/index.ts +++ b/apps/dokploy/server/db/schema/index.ts @@ -1,30 +1 @@ -export * from "./application"; -export * from "./postgres"; -export * from "./user"; -export * from "./admin"; -export * from "./auth"; -export * from "./project"; -export * from "./domain"; -export * from "./mariadb"; -export * from "./mongo"; -export * from "./mysql"; -export * from "./backups"; -export * from "./destination"; -export * from "./deployment"; -export * from "./mount"; -export * from "./certificate"; -export * from "./session"; -export * from "./redirects"; -export * from "./security"; -export * from "./port"; -export * from "./redis"; -export * from "./shared"; -export * from "./compose"; -export * from "./registry"; -export * from "./notification"; -export * from "./ssh-key"; -export * from "./git-provider"; -export * from "./bitbucket"; -export * from "./github"; -export * from "./gitlab"; -export * from "./server"; +export * from "@dokploy/builders"; diff --git a/apps/dokploy/server/utils/access-log/handler.ts b/apps/dokploy/server/utils/access-log/handler.ts index 24baa023b..d55882592 100644 --- a/apps/dokploy/server/utils/access-log/handler.ts +++ b/apps/dokploy/server/utils/access-log/handler.ts @@ -23,7 +23,6 @@ class LogRotationManager { if (isActive) { await this.activateStream(); } - console.log(`Log rotation initialized. Active: ${isActive}`); } private async getStateFromDB(): Promise { diff --git a/apps/dokploy/server/utils/servers/connection.ts b/apps/dokploy/server/utils/servers/connection.ts deleted file mode 100644 index 846c24554..000000000 --- a/apps/dokploy/server/utils/servers/connection.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { findServerById } from "@/server/api/services/server"; -import { Client } from "ssh2"; -import { readSSHKey } from "../filesystem/ssh"; diff --git a/packages/builders/esbuild.config.ts b/packages/builders/esbuild.config.ts new file mode 100644 index 000000000..878031046 --- /dev/null +++ b/packages/builders/esbuild.config.ts @@ -0,0 +1,25 @@ +import esbuild from "esbuild"; + +try { + esbuild + .build({ + entryPoints: ["./src/**/*.ts"], + bundle: true, + platform: "node", + format: "cjs", + target: "node18", + outExtension: { ".js": ".js" }, + minify: true, + outdir: "dist", + tsconfig: "tsconfig.server.json", + packages: "external", + alias: { + "@/server": "./src", + }, + }) + .catch(() => { + return process.exit(1); + }); +} catch (error) { + console.log(error); +} diff --git a/packages/builders/package.json b/packages/builders/package.json new file mode 100644 index 000000000..bb4ade65b --- /dev/null +++ b/packages/builders/package.json @@ -0,0 +1,156 @@ +{ + "name": "@dokploy/builders", + "version": "1.0.0", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "scripts": { + "dev": "tsup --config ./tsup.ts --watch", + "build": "tsc --project tsconfig.server.json && tsc-alias -p tsconfig.server.json", + "tsc": "tsc --project tsconfig.server.json", + "build:types": "tsc --emitDeclarationOnly --experimenta-dts" + }, + "dependencies": { + "tsc-alias": "1.8.10", + "esbuild": "0.20.2", + "esbuild-plugin-alias-path": "2.0.2", + "esbuild-plugin-alias": "0.2.1", + "tiny-glob": "^0.2.9", + "rotating-file-stream": "3.2.3", + "@codemirror/lang-json": "^6.0.1", + "@codemirror/lang-yaml": "^6.1.1", + "@codemirror/language": "^6.10.1", + "@codemirror/legacy-modes": "6.4.0", + "@codemirror/view": "6.29.0", + "@dokploy/trpc-openapi": "0.0.4", + "@faker-js/faker": "^8.4.1", + "@hookform/resolvers": "^3.3.4", + "@lucia-auth/adapter-drizzle": "1.0.7", + "@octokit/auth-app": "^6.0.4", + "@octokit/webhooks": "^13.2.7", + "@radix-ui/react-accordion": "1.1.2", + "@radix-ui/react-alert-dialog": "^1.0.5", + "@radix-ui/react-avatar": "^1.0.4", + "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-dropdown-menu": "^2.0.6", + "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-progress": "^1.0.3", + "@radix-ui/react-radio-group": "^1.1.3", + "@radix-ui/react-scroll-area": "^1.0.5", + "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-separator": "^1.0.3", + "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-switch": "^1.0.3", + "@radix-ui/react-tabs": "^1.0.4", + "@radix-ui/react-toggle": "^1.0.3", + "@radix-ui/react-tooltip": "^1.0.7", + "@react-email/components": "^0.0.21", + "@tanstack/react-query": "^4.36.1", + "@tanstack/react-table": "^8.16.0", + "@trpc/client": "^10.43.6", + "@trpc/next": "^10.43.6", + "@trpc/react-query": "^10.43.6", + "@trpc/server": "^10.43.6", + "@uiw/codemirror-theme-github": "^4.22.1", + "@uiw/react-codemirror": "^4.22.1", + "@xterm/addon-attach": "0.10.0", + "@xterm/xterm": "^5.4.0", + "adm-zip": "^0.5.14", + "bcrypt": "5.1.1", + "bl": "6.0.11", + "boxen": "^7.1.1", + "bullmq": "5.4.2", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.0", + "cmdk": "^0.2.0", + "copy-to-clipboard": "^3.3.3", + "copy-webpack-plugin": "^12.0.2", + "date-fns": "3.6.0", + "dockerode": "4.0.2", + "dockerode-compose": "^1.4.0", + "dockerstats": "2.4.2", + "dotenv": "16.4.5", + "drizzle-orm": "^0.30.8", + "drizzle-zod": "0.5.1", + "hi-base32": "^0.5.1", + "input-otp": "^1.2.4", + "js-yaml": "4.1.0", + "k6": "^0.0.0", + "lodash": "4.17.21", + "lucia": "^3.0.1", + "lucide-react": "^0.312.0", + "nanoid": "3", + "next": "^14.1.3", + "next-themes": "^0.2.1", + "node-os-utils": "1.3.7", + "node-pty": "1.0.0", + "node-schedule": "2.1.1", + "nodemailer": "6.9.14", + "octokit": "3.1.2", + "otpauth": "^9.2.3", + "postgres": "3.4.4", + "public-ip": "6.0.2", + "qrcode": "^1.5.3", + "react": "18.2.0", + "react-dom": "18.2.0", + "react-hook-form": "^7.49.3", + "recharts": "^2.12.7", + "slugify": "^1.6.6", + "sonner": "^1.4.0", + "superjson": "^2.2.1", + "swagger-ui-react": "^5.17.14", + "tailwind-merge": "^2.2.0", + "tailwindcss-animate": "^1.0.7", + "tar-fs": "3.0.5", + "undici": "^6.19.2", + "use-resize-observer": "9.1.0", + "ws": "8.16.0", + "xterm-addon-fit": "^0.8.0", + "zod": "^3.23.4", + "zod-form-data": "^2.0.2", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "ssh2": "1.15.0" + }, + "devDependencies": { + "@biomejs/biome": "1.8.3", + "@commitlint/cli": "^19.3.0", + "@commitlint/config-conventional": "^19.2.2", + "@types/adm-zip": "^0.5.5", + "@types/bcrypt": "5.0.2", + "@types/dockerode": "3.3.23", + "@types/js-yaml": "4.0.9", + "@types/lodash": "4.17.4", + "@types/node": "^18.17.0", + "@types/node-os-utils": "1.3.4", + "@types/node-schedule": "2.1.6", + "@types/nodemailer": "^6.4.15", + "@types/qrcode": "^1.5.5", + "@types/react": "^18.2.37", + "@types/react-dom": "^18.2.15", + "@types/swagger-ui-react": "^4.18.3", + "@types/tar-fs": "2.0.4", + "@types/ws": "8.5.10", + "autoprefixer": "^10.4.14", + "drizzle-kit": "^0.21.1", + "esbuild": "0.20.2", + "husky": "^9.0.11", + "lint-staged": "^15.2.7", + "localtunnel": "2.0.2", + "memfs": "^4.11.0", + "postcss": "^8.4.31", + "prettier": "^3.2.4", + "prettier-plugin-tailwindcss": "^0.5.11", + "tailwindcss": "^3.4.1", + "tsconfig-paths": "4.2.0", + "tsx": "^4.7.0", + "typescript": "^5.4.2", + "vite-tsconfig-paths": "4.3.2", + "vitest": "^1.6.0", + "xterm-readline": "1.1.1", + "@types/ssh2": "1.15.1", + "tsup": "6.4.0" + } +} diff --git a/packages/builders/src/auth/auth.ts b/packages/builders/src/auth/auth.ts new file mode 100644 index 000000000..a70989de5 --- /dev/null +++ b/packages/builders/src/auth/auth.ts @@ -0,0 +1,113 @@ +import { webcrypto } from "node:crypto"; +import type { IncomingMessage, ServerResponse } from "node:http"; +import { DrizzlePostgreSQLAdapter } from "@lucia-auth/adapter-drizzle"; +import { TimeSpan } from "lucia"; +import { Lucia } from "lucia/dist/core.js"; +import type { Session, User } from "lucia/dist/core.js"; +import { findAdminByAuthId } from "@/server/services/admin"; +import { findUserByAuthId } from "@/server/services/user"; +import { db } from "../db"; +import { type DatabaseUser, auth, sessionTable } from "../db/schema"; + +globalThis.crypto = webcrypto as Crypto; +export const adapter = new DrizzlePostgreSQLAdapter(db, sessionTable, auth); + +export const lucia = new Lucia(adapter, { + sessionCookie: { + attributes: { + secure: false, + }, + }, + sessionExpiresIn: new TimeSpan(1, "d"), + getUserAttributes: (attributes) => { + return { + email: attributes.email, + rol: attributes.rol, + secret: attributes.secret !== null, + adminId: attributes.adminId, + }; + }, +}); + +declare module "lucia" { + interface Register { + Lucia: typeof lucia; + DatabaseUserAttributes: Omit & { + authId: string; + adminId: string; + }; + } +} + +export type ReturnValidateToken = Promise<{ + user: (User & { authId: string; adminId: string }) | null; + session: Session | null; +}>; + +export async function validateRequest( + req: IncomingMessage, + res: ServerResponse, +): ReturnValidateToken { + const sessionId = lucia.readSessionCookie(req.headers.cookie ?? ""); + + if (!sessionId) { + return { + user: null, + session: null, + }; + } + const result = await lucia.validateSession(sessionId); + if (result?.session?.fresh) { + res.appendHeader( + "Set-Cookie", + lucia.createSessionCookie(result.session.id).serialize(), + ); + } + if (!result.session) { + res.appendHeader( + "Set-Cookie", + lucia.createBlankSessionCookie().serialize(), + ); + } + + if (result.user) { + if (result.user?.rol === "admin") { + const admin = await findAdminByAuthId(result.user.id); + result.user.adminId = admin.adminId; + } else if (result.user?.rol === "user") { + const userResult = await findUserByAuthId(result.user.id); + result.user.adminId = userResult.adminId; + } + } + + return { + session: result.session, + ...((result.user && { + user: { + authId: result.user.id, + email: result.user.email, + rol: result.user.rol, + id: result.user.id, + secret: result.user.secret, + adminId: result.user.adminId, + }, + }) || { + user: null, + }), + }; +} + +export async function validateWebSocketRequest( + req: IncomingMessage, +): Promise<{ user: User; session: Session } | { user: null; session: null }> { + const sessionId = lucia.readSessionCookie(req.headers.cookie ?? ""); + + if (!sessionId) { + return { + user: null, + session: null, + }; + } + const result = await lucia.validateSession(sessionId); + return result; +} diff --git a/packages/builders/src/auth/random-password.ts b/packages/builders/src/auth/random-password.ts new file mode 100644 index 000000000..150104b94 --- /dev/null +++ b/packages/builders/src/auth/random-password.ts @@ -0,0 +1,20 @@ +import bcrypt from "bcrypt"; + +export const generateRandomPassword = async () => { + const passwordLength = 16; + + const characters = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + let randomPassword = ""; + for (let i = 0; i < passwordLength; i++) { + randomPassword += characters.charAt( + Math.floor(Math.random() * characters.length), + ); + } + + const saltRounds = 10; + + const hashedPassword = await bcrypt.hash(randomPassword, saltRounds); + return { randomPassword, hashedPassword }; +}; diff --git a/packages/builders/src/auth/token.ts b/packages/builders/src/auth/token.ts new file mode 100644 index 000000000..54162fdcb --- /dev/null +++ b/packages/builders/src/auth/token.ts @@ -0,0 +1,49 @@ +import type { IncomingMessage } from "node:http"; +import { TimeSpan } from "lucia"; +import { Lucia } from "lucia/dist/core.js"; +import { type ReturnValidateToken, adapter } from "./auth"; + +export const luciaToken = new Lucia(adapter, { + sessionCookie: { + attributes: { + secure: false, + }, + }, + sessionExpiresIn: new TimeSpan(365, "d"), + getUserAttributes: (attributes) => { + return { + email: attributes.email, + rol: attributes.rol, + secret: attributes.secret !== null, + }; + }, +}); + +export const validateBearerToken = async ( + req: IncomingMessage, +): ReturnValidateToken => { + const authorizationHeader = req.headers.authorization; + const sessionId = luciaToken.readBearerToken(authorizationHeader ?? ""); + if (!sessionId) { + return { + user: null, + session: null, + }; + } + const result = await luciaToken.validateSession(sessionId); + return { + session: result.session, + ...((result.user && { + user: { + adminId: result.user.adminId, + authId: result.user.id, + email: result.user.email, + rol: result.user.rol, + id: result.user.id, + secret: result.user.secret, + }, + }) || { + user: null, + }), + }; +}; diff --git a/packages/builders/src/constants/index.ts b/packages/builders/src/constants/index.ts new file mode 100644 index 000000000..f2f1a4d88 --- /dev/null +++ b/packages/builders/src/constants/index.ts @@ -0,0 +1,39 @@ +import path from "node:path"; +import Docker from "dockerode"; + +export const IS_CLOUD = process.env.IS_CLOUD === "true"; +export const docker = new Docker(); + +export const paths = (isServer = false) => { + if (isServer) { + const BASE_PATH = "/etc/dokploy"; + return { + BASE_PATH, + MAIN_TRAEFIK_PATH: `${BASE_PATH}/traefik`, + DYNAMIC_TRAEFIK_PATH: `${BASE_PATH}/traefik/dynamic`, + LOGS_PATH: `${BASE_PATH}/logs`, + APPLICATIONS_PATH: `${BASE_PATH}/applications`, + COMPOSE_PATH: `${BASE_PATH}/compose`, + SSH_PATH: `${BASE_PATH}/ssh`, + CERTIFICATES_PATH: `${BASE_PATH}/certificates`, + MONITORING_PATH: `${BASE_PATH}/monitoring`, + REGISTRY_PATH: `${BASE_PATH}/registry`, + }; + } + const BASE_PATH = + process.env.NODE_ENV === "production" + ? "/etc/dokploy" + : path.join(process.cwd(), ".docker"); + return { + BASE_PATH, + MAIN_TRAEFIK_PATH: `${BASE_PATH}/traefik`, + DYNAMIC_TRAEFIK_PATH: `${BASE_PATH}/traefik/dynamic`, + LOGS_PATH: `${BASE_PATH}/logs`, + APPLICATIONS_PATH: `${BASE_PATH}/applications`, + COMPOSE_PATH: `${BASE_PATH}/compose`, + SSH_PATH: `${BASE_PATH}/ssh`, + CERTIFICATES_PATH: `${BASE_PATH}/certificates`, + MONITORING_PATH: `${BASE_PATH}/monitoring`, + REGISTRY_PATH: `${BASE_PATH}/registry`, + }; +}; diff --git a/packages/builders/src/db/drizzle.config.ts b/packages/builders/src/db/drizzle.config.ts new file mode 100644 index 000000000..f556649be --- /dev/null +++ b/packages/builders/src/db/drizzle.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + schema: "./server/db/schema/index.ts", + dialect: "postgresql", + dbCredentials: { + url: process.env.DATABASE_URL || "", + }, + out: "drizzle", + migrations: { + table: "migrations", + schema: "public", + }, +}); diff --git a/packages/builders/src/db/index.ts b/packages/builders/src/db/index.ts new file mode 100644 index 000000000..7c5f79bd2 --- /dev/null +++ b/packages/builders/src/db/index.ts @@ -0,0 +1,28 @@ +import { type PostgresJsDatabase, drizzle } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; +// import { sc } from "@dokploy/schema"; +import * as schema from "./schema"; +// schema +// import * as schema from "@dokploy/schema"; + +// type Schema = typeof schema; + +// type Schema = typeof schema; +declare global { + // eslint-disable-next-line no-var -- only var works here + var db: PostgresJsDatabase | undefined; +} + +export let db: PostgresJsDatabase; +if (process.env.NODE_ENV === "production") { + db = drizzle(postgres(process.env.DATABASE_URL || ""), { + schema, + }); +} else { + if (!global.db) + global.db = drizzle(postgres(process.env.DATABASE_URL || ""), { + schema, + }); + + db = global.db; +} diff --git a/packages/builders/src/db/migration.ts b/packages/builders/src/db/migration.ts new file mode 100644 index 000000000..d38d39434 --- /dev/null +++ b/packages/builders/src/db/migration.ts @@ -0,0 +1,21 @@ +import { drizzle } from "drizzle-orm/postgres-js"; +import { migrate } from "drizzle-orm/postgres-js/migrator"; +import postgres from "postgres"; + +const connectionString = process.env.DATABASE_URL || ""; + +const sql = postgres(connectionString, { max: 1 }); +const db = drizzle(sql); + +export const migration = async () => + await migrate(db, { migrationsFolder: "drizzle" }) + .then(() => { + console.log("Migration complete"); + sql.end(); + }) + .catch((error) => { + console.log("Migration failed", error); + }) + .finally(() => { + sql.end(); + }); diff --git a/packages/builders/src/db/reset.ts b/packages/builders/src/db/reset.ts new file mode 100644 index 000000000..4a4dbecba --- /dev/null +++ b/packages/builders/src/db/reset.ts @@ -0,0 +1,23 @@ +import { sql } from "drizzle-orm"; +// Credits to Louistiti from Drizzle Discord: https://discord.com/channels/1043890932593987624/1130802621750448160/1143083373535973406 +import { drizzle } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; + +const connectionString = process.env.DATABASE_URL || ""; + +const pg = postgres(connectionString, { max: 1 }); +const db = drizzle(pg); + +const clearDb = async (): Promise => { + try { + const tablesQuery = sql`DROP SCHEMA public CASCADE; CREATE SCHEMA public; DROP schema drizzle CASCADE;`; + const tables = await db.execute(tablesQuery); + console.log(tables); + await pg.end(); + } catch (error) { + console.error("Error to clean database", error); + } finally { + } +}; + +clearDb(); diff --git a/apps/dokploy/server/db/schema/admin.ts b/packages/builders/src/db/schema/admin.ts similarity index 100% rename from apps/dokploy/server/db/schema/admin.ts rename to packages/builders/src/db/schema/admin.ts diff --git a/apps/dokploy/server/db/schema/application.ts b/packages/builders/src/db/schema/application.ts similarity index 96% rename from apps/dokploy/server/db/schema/application.ts rename to packages/builders/src/db/schema/application.ts index 09175edc8..beeeece99 100644 --- a/apps/dokploy/server/db/schema/application.ts +++ b/packages/builders/src/db/schema/application.ts @@ -10,7 +10,6 @@ import { import { createInsertSchema } from "drizzle-zod"; import { nanoid } from "nanoid"; import { z } from "zod"; -import { bitbucket, github, gitlab, server } from "."; import { deployments } from "./deployment"; import { domains } from "./domain"; import { mounts } from "./mount"; @@ -22,6 +21,10 @@ import { security } from "./security"; import { applicationStatus } from "./shared"; import { sshKeys } from "./ssh-key"; import { generateAppName } from "./utils"; +import { github } from "./github"; +import { gitlab } from "./gitlab"; +import { bitbucket } from "./bitbucket"; +import { server } from "./server"; export const sourceType = pgEnum("sourceType", [ "docker", @@ -41,7 +44,7 @@ export const buildType = pgEnum("buildType", [ ]); // TODO: refactor this types -interface HealthCheckSwarm { +export interface HealthCheckSwarm { Test?: string[] | undefined; Interval?: number | undefined; Timeout?: number | undefined; @@ -49,14 +52,14 @@ interface HealthCheckSwarm { Retries?: number | undefined; } -interface RestartPolicySwarm { +export interface RestartPolicySwarm { Condition?: string | undefined; Delay?: number | undefined; MaxAttempts?: number | undefined; Window?: number | undefined; } -interface PlacementSwarm { +export interface PlacementSwarm { Constraints?: string[] | undefined; Preferences?: Array<{ Spread: { SpreadDescriptor: string } }> | undefined; MaxReplicas?: number | undefined; @@ -68,7 +71,7 @@ interface PlacementSwarm { | undefined; } -interface UpdateConfigSwarm { +export interface UpdateConfigSwarm { Parallelism: number; Delay?: number | undefined; FailureAction?: string | undefined; @@ -77,7 +80,7 @@ interface UpdateConfigSwarm { Order: string; } -interface ServiceModeSwarm { +export interface ServiceModeSwarm { Replicated?: { Replicas?: number | undefined } | undefined; Global?: {} | undefined; ReplicatedJob?: @@ -89,13 +92,13 @@ interface ServiceModeSwarm { GlobalJob?: {} | undefined; } -interface NetworkSwarm { +export interface NetworkSwarm { Target?: string | undefined; Aliases?: string[] | undefined; DriverOpts?: { [key: string]: string } | undefined; } -interface LabelsSwarm { +export interface LabelsSwarm { [name: string]: string; } diff --git a/apps/dokploy/server/db/schema/auth.ts b/packages/builders/src/db/schema/auth.ts similarity index 100% rename from apps/dokploy/server/db/schema/auth.ts rename to packages/builders/src/db/schema/auth.ts diff --git a/apps/dokploy/server/db/schema/backups.ts b/packages/builders/src/db/schema/backups.ts similarity index 100% rename from apps/dokploy/server/db/schema/backups.ts rename to packages/builders/src/db/schema/backups.ts diff --git a/apps/dokploy/server/db/schema/bitbucket.ts b/packages/builders/src/db/schema/bitbucket.ts similarity index 100% rename from apps/dokploy/server/db/schema/bitbucket.ts rename to packages/builders/src/db/schema/bitbucket.ts diff --git a/apps/dokploy/server/db/schema/certificate.ts b/packages/builders/src/db/schema/certificate.ts similarity index 100% rename from apps/dokploy/server/db/schema/certificate.ts rename to packages/builders/src/db/schema/certificate.ts diff --git a/apps/dokploy/server/db/schema/compose.ts b/packages/builders/src/db/schema/compose.ts similarity index 96% rename from apps/dokploy/server/db/schema/compose.ts rename to packages/builders/src/db/schema/compose.ts index b15171619..29d90edcd 100644 --- a/apps/dokploy/server/db/schema/compose.ts +++ b/packages/builders/src/db/schema/compose.ts @@ -1,16 +1,19 @@ -import { sshKeys } from "@/server/db/schema/ssh-key"; +import { sshKeys } from "./ssh-key"; import { relations } from "drizzle-orm"; import { boolean, integer, pgEnum, pgTable, text } from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; import { nanoid } from "nanoid"; import { z } from "zod"; -import { bitbucket, github, gitlab, server } from "."; import { deployments } from "./deployment"; import { domains } from "./domain"; import { mounts } from "./mount"; import { projects } from "./project"; import { applicationStatus } from "./shared"; import { generateAppName } from "./utils"; +import { github } from "./github"; +import { gitlab } from "./gitlab"; +import { bitbucket } from "./bitbucket"; +import { server } from "./server"; export const sourceTypeCompose = pgEnum("sourceTypeCompose", [ "git", diff --git a/apps/dokploy/server/db/schema/deployment.ts b/packages/builders/src/db/schema/deployment.ts similarity index 100% rename from apps/dokploy/server/db/schema/deployment.ts rename to packages/builders/src/db/schema/deployment.ts diff --git a/apps/dokploy/server/db/schema/destination.ts b/packages/builders/src/db/schema/destination.ts similarity index 100% rename from apps/dokploy/server/db/schema/destination.ts rename to packages/builders/src/db/schema/destination.ts diff --git a/apps/dokploy/server/db/schema/domain.ts b/packages/builders/src/db/schema/domain.ts similarity index 97% rename from apps/dokploy/server/db/schema/domain.ts rename to packages/builders/src/db/schema/domain.ts index 301175fc5..eee9a8198 100644 --- a/apps/dokploy/server/db/schema/domain.ts +++ b/packages/builders/src/db/schema/domain.ts @@ -1,4 +1,4 @@ -import { domain } from "@/server/db/validations/domain"; +import { domain } from "../validations/domain"; import { relations } from "drizzle-orm"; import { boolean, diff --git a/apps/dokploy/server/db/schema/git-provider.ts b/packages/builders/src/db/schema/git-provider.ts similarity index 100% rename from apps/dokploy/server/db/schema/git-provider.ts rename to packages/builders/src/db/schema/git-provider.ts diff --git a/apps/dokploy/server/db/schema/github.ts b/packages/builders/src/db/schema/github.ts similarity index 100% rename from apps/dokploy/server/db/schema/github.ts rename to packages/builders/src/db/schema/github.ts diff --git a/apps/dokploy/server/db/schema/gitlab.ts b/packages/builders/src/db/schema/gitlab.ts similarity index 100% rename from apps/dokploy/server/db/schema/gitlab.ts rename to packages/builders/src/db/schema/gitlab.ts diff --git a/packages/builders/src/db/schema/index.ts b/packages/builders/src/db/schema/index.ts new file mode 100644 index 000000000..4a6103688 --- /dev/null +++ b/packages/builders/src/db/schema/index.ts @@ -0,0 +1,31 @@ +export * from "./application"; +export * from "./postgres"; +export * from "./user"; +export * from "./admin"; +export * from "./auth"; +export * from "./project"; +export * from "./domain"; +export * from "./mariadb"; +export * from "./mongo"; +export * from "./mysql"; +export * from "./backups"; +export * from "./destination"; +export * from "./deployment"; +export * from "./mount"; +export * from "./certificate"; +export * from "./session"; +export * from "./redirects"; +export * from "./security"; +export * from "./port"; +export * from "./redis"; +export * from "./shared"; +export * from "./compose"; +export * from "./registry"; +export * from "./notification"; +export * from "./ssh-key"; +export * from "./git-provider"; +export * from "./bitbucket"; +export * from "./github"; +export * from "./gitlab"; +export * from "./server"; +export * from "./utils"; diff --git a/apps/dokploy/server/db/schema/mariadb.ts b/packages/builders/src/db/schema/mariadb.ts similarity index 98% rename from apps/dokploy/server/db/schema/mariadb.ts rename to packages/builders/src/db/schema/mariadb.ts index b5e13284f..dcae297b0 100644 --- a/apps/dokploy/server/db/schema/mariadb.ts +++ b/packages/builders/src/db/schema/mariadb.ts @@ -1,4 +1,3 @@ -import { generatePassword } from "@/templates/utils"; import { relations } from "drizzle-orm"; import { integer, pgTable, text } from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; diff --git a/apps/dokploy/server/db/schema/mongo.ts b/packages/builders/src/db/schema/mongo.ts similarity index 100% rename from apps/dokploy/server/db/schema/mongo.ts rename to packages/builders/src/db/schema/mongo.ts diff --git a/apps/dokploy/server/db/schema/mount.ts b/packages/builders/src/db/schema/mount.ts similarity index 100% rename from apps/dokploy/server/db/schema/mount.ts rename to packages/builders/src/db/schema/mount.ts diff --git a/apps/dokploy/server/db/schema/mysql.ts b/packages/builders/src/db/schema/mysql.ts similarity index 100% rename from apps/dokploy/server/db/schema/mysql.ts rename to packages/builders/src/db/schema/mysql.ts diff --git a/apps/dokploy/server/db/schema/notification.ts b/packages/builders/src/db/schema/notification.ts similarity index 100% rename from apps/dokploy/server/db/schema/notification.ts rename to packages/builders/src/db/schema/notification.ts diff --git a/apps/dokploy/server/db/schema/port.ts b/packages/builders/src/db/schema/port.ts similarity index 100% rename from apps/dokploy/server/db/schema/port.ts rename to packages/builders/src/db/schema/port.ts diff --git a/apps/dokploy/server/db/schema/postgres.ts b/packages/builders/src/db/schema/postgres.ts similarity index 100% rename from apps/dokploy/server/db/schema/postgres.ts rename to packages/builders/src/db/schema/postgres.ts diff --git a/apps/dokploy/server/db/schema/project.ts b/packages/builders/src/db/schema/project.ts similarity index 100% rename from apps/dokploy/server/db/schema/project.ts rename to packages/builders/src/db/schema/project.ts diff --git a/apps/dokploy/server/db/schema/redirects.ts b/packages/builders/src/db/schema/redirects.ts similarity index 100% rename from apps/dokploy/server/db/schema/redirects.ts rename to packages/builders/src/db/schema/redirects.ts diff --git a/apps/dokploy/server/db/schema/redis.ts b/packages/builders/src/db/schema/redis.ts similarity index 100% rename from apps/dokploy/server/db/schema/redis.ts rename to packages/builders/src/db/schema/redis.ts diff --git a/apps/dokploy/server/db/schema/registry.ts b/packages/builders/src/db/schema/registry.ts similarity index 100% rename from apps/dokploy/server/db/schema/registry.ts rename to packages/builders/src/db/schema/registry.ts diff --git a/apps/dokploy/server/db/schema/security.ts b/packages/builders/src/db/schema/security.ts similarity index 100% rename from apps/dokploy/server/db/schema/security.ts rename to packages/builders/src/db/schema/security.ts diff --git a/apps/dokploy/server/db/schema/server.ts b/packages/builders/src/db/schema/server.ts similarity index 89% rename from apps/dokploy/server/db/schema/server.ts rename to packages/builders/src/db/schema/server.ts index 02fa87cb7..6d435d88c 100644 --- a/apps/dokploy/server/db/schema/server.ts +++ b/packages/builders/src/db/schema/server.ts @@ -3,19 +3,18 @@ import { boolean, integer, pgTable, text } from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; import { nanoid } from "nanoid"; import { z } from "zod"; -import { - applications, - compose, - mariadb, - mongo, - mysql, - postgres, - redis, -} from "."; + import { admins } from "./admin"; import { deployments } from "./deployment"; import { sshKeys } from "./ssh-key"; import { generateAppName } from "./utils"; +import { applications } from "./application"; +import { compose } from "./compose"; +import { mariadb } from "./mariadb"; +import { mongo } from "./mongo"; +import { mysql } from "./mysql"; +import { postgres } from "./postgres"; +import { redis } from "./redis"; export const server = pgTable("server", { serverId: text("serverId") diff --git a/apps/dokploy/server/db/schema/session.ts b/packages/builders/src/db/schema/session.ts similarity index 100% rename from apps/dokploy/server/db/schema/session.ts rename to packages/builders/src/db/schema/session.ts diff --git a/apps/dokploy/server/db/schema/shared.ts b/packages/builders/src/db/schema/shared.ts similarity index 100% rename from apps/dokploy/server/db/schema/shared.ts rename to packages/builders/src/db/schema/shared.ts diff --git a/apps/dokploy/server/db/schema/source.ts b/packages/builders/src/db/schema/source.ts similarity index 100% rename from apps/dokploy/server/db/schema/source.ts rename to packages/builders/src/db/schema/source.ts diff --git a/apps/dokploy/server/db/schema/ssh-key.ts b/packages/builders/src/db/schema/ssh-key.ts similarity index 88% rename from apps/dokploy/server/db/schema/ssh-key.ts rename to packages/builders/src/db/schema/ssh-key.ts index 8ff6b0e21..34ce188de 100644 --- a/apps/dokploy/server/db/schema/ssh-key.ts +++ b/packages/builders/src/db/schema/ssh-key.ts @@ -1,6 +1,6 @@ -import { applications } from "@/server/db/schema/application"; -import { compose } from "@/server/db/schema/compose"; -import { sshKeyCreate, sshKeyType } from "@/server/db/validations"; +import { applications } from "./application"; +import { compose } from "./compose"; +import { sshKeyCreate, sshKeyType } from "../validations"; import { relations } from "drizzle-orm"; import { pgTable, text } from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; diff --git a/apps/dokploy/server/db/schema/user.ts b/packages/builders/src/db/schema/user.ts similarity index 100% rename from apps/dokploy/server/db/schema/user.ts rename to packages/builders/src/db/schema/user.ts diff --git a/apps/dokploy/server/db/schema/utils.ts b/packages/builders/src/db/schema/utils.ts similarity index 100% rename from apps/dokploy/server/db/schema/utils.ts rename to packages/builders/src/db/schema/utils.ts diff --git a/packages/builders/src/db/seed.ts b/packages/builders/src/db/seed.ts new file mode 100644 index 000000000..bbb3c3f27 --- /dev/null +++ b/packages/builders/src/db/seed.ts @@ -0,0 +1,35 @@ +import bc from "bcrypt"; +import { drizzle } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; +import { users } from "./schema"; + +const connectionString = process.env.DATABASE_URL || ""; + +const pg = postgres(connectionString, { max: 1 }); +const db = drizzle(pg); + +function password(txt: string) { + return bc.hashSync(txt, 10); +} + +async function seed() { + console.log("> Seed:", process.env.DATABASE_PATH, "\n"); + + // const authenticationR = await db + // .insert(users) + // .values([ + // { + // email: "user1@hotmail.com", + // password: password("12345671"), + // }, + // ]) + // .onConflictDoNothing() + // .returning(); + + // console.log("\nSemillas Update:", authenticationR.length); +} + +seed().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/packages/builders/src/db/validations/domain.ts b/packages/builders/src/db/validations/domain.ts new file mode 100644 index 000000000..b09b57b6c --- /dev/null +++ b/packages/builders/src/db/validations/domain.ts @@ -0,0 +1,46 @@ +import { z } from "zod"; + +export const domain = z + .object({ + host: z.string().min(1, { message: "Add a hostname" }), + path: z.string().min(1).optional(), + port: z + .number() + .min(1, { message: "Port must be at least 1" }) + .max(65535, { message: "Port must be 65535 or below" }) + .optional(), + https: z.boolean().optional(), + certificateType: z.enum(["letsencrypt", "none"]).optional(), + }) + .superRefine((input, ctx) => { + if (input.https && !input.certificateType) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["certificateType"], + message: "Required", + }); + } + }); + +export const domainCompose = z + .object({ + host: z.string().min(1, { message: "Host is required" }), + path: z.string().min(1).optional(), + port: z + .number() + .min(1, { message: "Port must be at least 1" }) + .max(65535, { message: "Port must be 65535 or below" }) + .optional(), + https: z.boolean().optional(), + certificateType: z.enum(["letsencrypt", "none"]).optional(), + serviceName: z.string().min(1, { message: "Service name is required" }), + }) + .superRefine((input, ctx) => { + if (input.https && !input.certificateType) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["certificateType"], + message: "Required", + }); + } + }); diff --git a/packages/builders/src/db/validations/index.ts b/packages/builders/src/db/validations/index.ts new file mode 100644 index 000000000..b3cb57852 --- /dev/null +++ b/packages/builders/src/db/validations/index.ts @@ -0,0 +1,37 @@ +import { z } from "zod"; + +export const sshKeyCreate = z.object({ + name: z.string().min(1), + description: z.string().optional(), + publicKey: z.string().refine( + (key) => { + const rsaPubPattern = /^ssh-rsa\s+([A-Za-z0-9+/=]+)\s*(.*)?\s*$/; + const ed25519PubPattern = /^ssh-ed25519\s+([A-Za-z0-9+/=]+)\s*(.*)?\s*$/; + return rsaPubPattern.test(key) || ed25519PubPattern.test(key); + }, + { + message: "Invalid public key format", + }, + ), + privateKey: z.string().refine( + (key) => { + const rsaPrivPattern = + /^-----BEGIN RSA PRIVATE KEY-----\n([A-Za-z0-9+/=\n]+)-----END RSA PRIVATE KEY-----\s*$/; + const ed25519PrivPattern = + /^-----BEGIN OPENSSH PRIVATE KEY-----\n([A-Za-z0-9+/=\n]+)-----END OPENSSH PRIVATE KEY-----\s*$/; + return rsaPrivPattern.test(key) || ed25519PrivPattern.test(key); + }, + { + message: "Invalid private key format", + }, + ), +}); + +export const sshKeyUpdate = sshKeyCreate.pick({ + name: true, + description: true, +}); + +export const sshKeyType = z.object({ + type: z.enum(["rsa", "ed25519"]).optional(), +}); diff --git a/packages/builders/src/emails/.gitignore b/packages/builders/src/emails/.gitignore new file mode 100644 index 000000000..b2d59d1f7 --- /dev/null +++ b/packages/builders/src/emails/.gitignore @@ -0,0 +1,2 @@ +/node_modules +/dist \ No newline at end of file diff --git a/packages/builders/src/emails/emails/build-failed.tsx b/packages/builders/src/emails/emails/build-failed.tsx new file mode 100644 index 000000000..87cd02a7c --- /dev/null +++ b/packages/builders/src/emails/emails/build-failed.tsx @@ -0,0 +1,113 @@ +import { + Body, + Button, + Container, + Head, + Heading, + Html, + Img, + Link, + Preview, + Section, + Tailwind, + Text, +} from "@react-email/components"; +import * as React from "react"; + +export type TemplateProps = { + projectName: string; + applicationName: string; + applicationType: string; + errorMessage: string; + buildLink: string; + date: string; +}; + +export const BuildFailedEmail = ({ + projectName = "dokploy", + applicationName = "frontend", + applicationType = "application", + errorMessage = "Error array.length is not a function", + buildLink = "https://dokploy.com/projects/dokploy-test/applications/dokploy-test", + date = "2023-05-01T00:00:00.000Z", +}: TemplateProps) => { + const previewText = `Build failed for ${applicationName}`; + return ( + + + {previewText} + + + +
+ Dokploy +
+ + Build failed for {applicationName} + + + Hello, + + + Your build for {applicationName} failed. Please + check the error message below. + +
+ Details: + + Project Name: {projectName} + + + Application Name: {applicationName} + + + Application Type: {applicationType} + + + Date: {date} + +
+
+ Reason: + {errorMessage} +
+
+ +
+ + or copy and paste this URL into your browser:{" "} + + {buildLink} + + +
+ +
+ + ); +}; + +export default BuildFailedEmail; diff --git a/packages/builders/src/emails/emails/build-success.tsx b/packages/builders/src/emails/emails/build-success.tsx new file mode 100644 index 000000000..321a65c92 --- /dev/null +++ b/packages/builders/src/emails/emails/build-success.tsx @@ -0,0 +1,106 @@ +import { + Body, + Button, + Container, + Head, + Heading, + Html, + Img, + Link, + Preview, + Section, + Tailwind, + Text, +} from "@react-email/components"; +import * as React from "react"; + +export type TemplateProps = { + projectName: string; + applicationName: string; + applicationType: string; + buildLink: string; + date: string; +}; + +export const BuildSuccessEmail = ({ + projectName = "dokploy", + applicationName = "frontend", + applicationType = "application", + buildLink = "https://dokploy.com/projects/dokploy-test/applications/dokploy-test", + date = "2023-05-01T00:00:00.000Z", +}: TemplateProps) => { + const previewText = `Build success for ${applicationName}`; + return ( + + + {previewText} + + + +
+ Dokploy +
+ + Build success for {applicationName} + + + Hello, + + + Your build for {applicationName} was successful + +
+ Details: + + Project Name: {projectName} + + + Application Name: {applicationName} + + + Application Type: {applicationType} + + + Date: {date} + +
+
+ +
+ + or copy and paste this URL into your browser:{" "} + + {buildLink} + + +
+ +
+ + ); +}; + +export default BuildSuccessEmail; diff --git a/packages/builders/src/emails/emails/database-backup.tsx b/packages/builders/src/emails/emails/database-backup.tsx new file mode 100644 index 000000000..a539246e4 --- /dev/null +++ b/packages/builders/src/emails/emails/database-backup.tsx @@ -0,0 +1,105 @@ +import { + Body, + Container, + Head, + Heading, + Html, + Img, + Preview, + Section, + Tailwind, + Text, +} from "@react-email/components"; +import * as React from "react"; + +export type TemplateProps = { + projectName: string; + applicationName: string; + databaseType: "postgres" | "mysql" | "mongodb" | "mariadb"; + type: "error" | "success"; + errorMessage?: string; + date: string; +}; + +export const DatabaseBackupEmail = ({ + projectName = "dokploy", + applicationName = "frontend", + databaseType = "postgres", + type = "success", + errorMessage, + date = "2023-05-01T00:00:00.000Z", +}: TemplateProps) => { + const previewText = `Database backup for ${applicationName} was ${type === "success" ? "successful ✅" : "failed ❌"}`; + return ( + + {previewText} + + + + + +
+ Dokploy +
+ + Database backup for {applicationName} + + + Hello, + + + Your database backup for {applicationName} was{" "} + {type === "success" + ? "successful ✅" + : "failed Please check the error message below. ❌"} + . + +
+ Details: + + Project Name: {projectName} + + + Application Name: {applicationName} + + + Database Type: {databaseType} + + + Date: {date} + +
+ {type === "error" && errorMessage ? ( +
+ Reason: + + {errorMessage || "Error message not provided"} + +
+ ) : null} +
+ +
+ + ); +}; + +export default DatabaseBackupEmail; diff --git a/packages/builders/src/emails/emails/docker-cleanup.tsx b/packages/builders/src/emails/emails/docker-cleanup.tsx new file mode 100644 index 000000000..7ff5212cd --- /dev/null +++ b/packages/builders/src/emails/emails/docker-cleanup.tsx @@ -0,0 +1,81 @@ +import { + Body, + Button, + Container, + Head, + Heading, + Html, + Img, + Preview, + Section, + Tailwind, + Text, +} from "@react-email/components"; +import * as React from "react"; + +export type TemplateProps = { + message: string; + date: string; +}; + +export const DockerCleanupEmail = ({ + message = "Docker cleanup for dokploy", + date = "2023-05-01T00:00:00.000Z", +}: TemplateProps) => { + const previewText = "Docker cleanup for dokploy"; + return ( + + {previewText} + + + + + +
+ Dokploy +
+ + Docker cleanup for dokploy + + + Hello, + + + The docker cleanup for dokploy was successful ✅ + + +
+ Details: + + Message: {message} + + + Date: {date} + +
+
+ +
+ + ); +}; + +export default DockerCleanupEmail; diff --git a/packages/builders/src/emails/emails/dokploy-restart.tsx b/packages/builders/src/emails/emails/dokploy-restart.tsx new file mode 100644 index 000000000..c8ed665a7 --- /dev/null +++ b/packages/builders/src/emails/emails/dokploy-restart.tsx @@ -0,0 +1,75 @@ +import { + Body, + Container, + Head, + Heading, + Html, + Img, + Preview, + Section, + Tailwind, + Text, +} from "@react-email/components"; +import * as React from "react"; + +export type TemplateProps = { + date: string; +}; + +export const DokployRestartEmail = ({ + date = "2023-05-01T00:00:00.000Z", +}: TemplateProps) => { + const previewText = "Your dokploy server was restarted"; + return ( + + {previewText} + + + + + +
+ Dokploy +
+ + Dokploy Server Restart + + + Hello, + + + Your dokploy server was restarted ✅ + + +
+ Details: + + Date: {date} + +
+
+ +
+ + ); +}; + +export default DokployRestartEmail; diff --git a/packages/builders/src/emails/emails/invitation.tsx b/packages/builders/src/emails/emails/invitation.tsx new file mode 100644 index 000000000..170f0e4b0 --- /dev/null +++ b/packages/builders/src/emails/emails/invitation.tsx @@ -0,0 +1,98 @@ +import { + Body, + Button, + Container, + Head, + Heading, + Hr, + Html, + Img, + Link, + Preview, + Section, + Tailwind, + Text, +} from "@react-email/components"; + +export type TemplateProps = { + email: string; + name: string; +}; + +interface VercelInviteUserEmailProps { + inviteLink: string; + toEmail: string; +} + +export const InvitationEmail = ({ + inviteLink, + toEmail, +}: VercelInviteUserEmailProps) => { + const previewText = "Join to Dokploy"; + return ( + + + {previewText} + + + +
+ Dokploy +
+ + Join to Dokploy + + + Hello, + + + You have been invited to join Dokploy, a platform + that helps for deploying your apps to the cloud. + +
+ +
+ + or copy and paste this URL into your browser:{" "} + + https://dokploy.com + + +
+ + This invitation was intended for {toEmail}. This invite was sent + from dokploy.com. If you + were not expecting this invitation, you can ignore this email. If + you are concerned about your account's safety, please reply to + +
+ +
+ + ); +}; + +export default InvitationEmail; diff --git a/packages/builders/src/emails/emails/notion-magic-link.tsx b/packages/builders/src/emails/emails/notion-magic-link.tsx new file mode 100644 index 000000000..89cc3444c --- /dev/null +++ b/packages/builders/src/emails/emails/notion-magic-link.tsx @@ -0,0 +1,150 @@ +import { + Body, + Container, + Head, + Heading, + Html, + Img, + Link, + Preview, + Text, +} from "@react-email/components"; +import * as React from "react"; + +interface NotionMagicLinkEmailProps { + loginCode?: string; +} + +const baseUrl = process.env.VERCEL_URL + ? `https://${process.env.VERCEL_URL}` + : ""; + +export const NotionMagicLinkEmail = ({ + loginCode, +}: NotionMagicLinkEmailProps) => ( + + + Log in with this magic link + + + Login + + Click here to log in with this magic link + + + Or, copy and paste this temporary login code: + + {loginCode} + + If you didn't try to login, you can safely ignore this email. + + + Hint: You can set a permanent password in Settings & members → My + account. + + Notion's Logo + + + Notion.so + + , the all-in-one-workspace +
+ for your notes, tasks, wikis, and databases. +
+
+ + +); + +NotionMagicLinkEmail.PreviewProps = { + loginCode: "sparo-ndigo-amurt-secan", +} as NotionMagicLinkEmailProps; + +export default NotionMagicLinkEmail; + +const main = { + backgroundColor: "#ffffff", +}; + +const container = { + paddingLeft: "12px", + paddingRight: "12px", + margin: "0 auto", +}; + +const h1 = { + color: "#333", + fontFamily: + "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif", + fontSize: "24px", + fontWeight: "bold", + margin: "40px 0", + padding: "0", +}; + +const link = { + color: "#2754C5", + fontFamily: + "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif", + fontSize: "14px", + textDecoration: "underline", +}; + +const text = { + color: "#333", + fontFamily: + "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif", + fontSize: "14px", + margin: "24px 0", +}; + +const footer = { + color: "#898989", + fontFamily: + "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif", + fontSize: "12px", + lineHeight: "22px", + marginTop: "12px", + marginBottom: "24px", +}; + +const code = { + display: "inline-block", + padding: "16px 4.5%", + width: "90.5%", + backgroundColor: "#f4f4f4", + borderRadius: "5px", + border: "1px solid #eee", + color: "#333", +}; diff --git a/packages/builders/src/emails/emails/plaid-verify-identity.tsx b/packages/builders/src/emails/emails/plaid-verify-identity.tsx new file mode 100644 index 000000000..2fce846a8 --- /dev/null +++ b/packages/builders/src/emails/emails/plaid-verify-identity.tsx @@ -0,0 +1,158 @@ +import { + Body, + Container, + Head, + Heading, + Html, + Img, + Link, + Section, + Text, +} from "@react-email/components"; +import * as React from "react"; + +interface PlaidVerifyIdentityEmailProps { + validationCode?: string; +} + +const baseUrl = process.env.VERCEL_URL + ? `https://${process.env.VERCEL_URL}` + : ""; + +export const PlaidVerifyIdentityEmail = ({ + validationCode, +}: PlaidVerifyIdentityEmailProps) => ( + + + + + Plaid + Verify Your Identity + + Enter the following code to finish linking Venmo. + +
+ {validationCode} +
+ Not expecting this email? + + Contact{" "} + + login@plaid.com + {" "} + if you did not request this code. + +
+ Securely powered by Plaid. + + +); + +PlaidVerifyIdentityEmail.PreviewProps = { + validationCode: "144833", +} as PlaidVerifyIdentityEmailProps; + +export default PlaidVerifyIdentityEmail; + +const main = { + backgroundColor: "#ffffff", + fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif", +}; + +const container = { + backgroundColor: "#ffffff", + border: "1px solid #eee", + borderRadius: "5px", + boxShadow: "0 5px 10px rgba(20,50,70,.2)", + marginTop: "20px", + maxWidth: "360px", + margin: "0 auto", + padding: "68px 0 130px", +}; + +const logo = { + margin: "0 auto", +}; + +const tertiary = { + color: "#0a85ea", + fontSize: "11px", + fontWeight: 700, + fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif", + height: "16px", + letterSpacing: "0", + lineHeight: "16px", + margin: "16px 8px 8px 8px", + textTransform: "uppercase" as const, + textAlign: "center" as const, +}; + +const secondary = { + color: "#000", + display: "inline-block", + fontFamily: "HelveticaNeue-Medium,Helvetica,Arial,sans-serif", + fontSize: "20px", + fontWeight: 500, + lineHeight: "24px", + marginBottom: "0", + marginTop: "0", + textAlign: "center" as const, +}; + +const codeContainer = { + background: "rgba(0,0,0,.05)", + borderRadius: "4px", + margin: "16px auto 14px", + verticalAlign: "middle", + width: "280px", +}; + +const code = { + color: "#000", + display: "inline-block", + fontFamily: "HelveticaNeue-Bold", + fontSize: "32px", + fontWeight: 700, + letterSpacing: "6px", + lineHeight: "40px", + paddingBottom: "8px", + paddingTop: "8px", + margin: "0 auto", + width: "100%", + textAlign: "center" as const, +}; + +const paragraph = { + color: "#444", + fontSize: "15px", + fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif", + letterSpacing: "0", + lineHeight: "23px", + padding: "0 40px", + margin: "0", + textAlign: "center" as const, +}; + +const link = { + color: "#444", + textDecoration: "underline", +}; + +const footer = { + color: "#000", + fontSize: "12px", + fontWeight: 800, + letterSpacing: "0", + lineHeight: "23px", + margin: "0", + marginTop: "20px", + fontFamily: "HelveticaNeue,Helvetica,Arial,sans-serif", + textAlign: "center" as const, + textTransform: "uppercase" as const, +}; diff --git a/packages/builders/src/emails/emails/static/logo.png b/packages/builders/src/emails/emails/static/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..e2e1413dfd7605b9b10cf424710bffdc54041e63 GIT binary patch literal 17567 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 z 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 literal 0 HcmV?d00001 diff --git a/packages/builders/src/emails/emails/static/notion-logo.png b/packages/builders/src/emails/emails/static/notion-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..57cddee9b737055f418bd95f126093204bb09e3f GIT binary patch literal 1929 zcmV;42X^?0P)hmX z+?la#F$UPeFocWj!A>VJ$i_%Io=9I`NJdtFt_Ztqq}li>{sjSA#zgT>*+a+D*CSz2 zo8V-a`#^*c37{HJHteNjyeeMl8k=m)vT=x&0Fx5W>np}R=|co~%ElBkF@n#MA)j8jkR*I=aKiH3a_e>Qx-l3VxCQ+!437_eqQi@6wMPggqDTZ zoy#VO2l>R6Nca{N36lspxU)HLi9xpdA`-h6#mXv6!d<4Z+S>)3@ijQBc( zl?wuJIA8W;F;ka~GgQj43+34L61A1E!zY}#aN_(nC_lL`H)B*|6d7l%F?fjm1*Vdq zjR>Gh!UI7xm}rc1sqxO3F_Yz>Z34)$@T=r|{R-X*G@JXIHYK1;7EmR)lfVCQhWUsa zAVB0e#}ovKqUCK95FlLRZIxqnrPQA37pC$mXKk7=g8*hw4RI)_X6hJ^>e}X_iUR>^ z8ZXrt(|4redIw!2fNF>?8jo_WCZP}@75bNIj0tqG3i#BL1mfxg>|p%33GfqBA2$I2 z5C8xH01yBG0f0#R05M>q9IH#$sw^Dk{rv|JARKTb@`o~}grA29(7^Lfy|*y`S^lDi zl*m{Kqg}(e2LX;JnS0mw6QR`<{Cz#&So51lx5UQh*RufuJ{vl*5Ei)(FNEGU!I;qN zdAr=|t6)N`0&G5ge9PRv=O7hQ^j9{%vXebJ%|DY#-E5256_M_&+2CkSONrY?N;DH6Cl!q#`-z;_2q^-rq!1}*{bLK-ol5}-;1T4|nkscX zH32xmmMnsI8$bfev4}wmKmffIfB+F=)O8sE0jf#C5Cnh}fB+F=BKiX$0Hgo}h!uk_ zN&yHExfH}ifEA=5ZUX#Zbj!Z(L4Ytp^~&1Lyyh-80_?N#NZ1<~fB@lP$!L~Zb)qVW zwF;OKeNOOF5Gw(A1fY7_CTJ;$xesv1Xdb!J6 z1!x7+^#+El+9uE-fQM2rC;BYcPZxmzHL+rJPvrz91t37=QUC&cfR}_+ZQ@{_E&>5Q zW69{A$bA9D@mjV!=yf;881q%=$Jn+5%RhxJM z0>s@LfCL0A1#=J}Zmd{`_b>thB9{UXpc;5d_y_{TjU{X0#f>0<|55+~L=4I{F$fSS zDBHwd5?~7(YkLE;E)!q}8*9P)0-LT9AccM7=?$Efgt!&#TS*G$T_ixB=m2`GLXVhb z1lV`2++pu~4B1$n6SNeZY08~0^}qQ_-REq)vG>gzmhAmLebY7k+*uyGNPrN^9q!Tp z)c8Ga_=OJT8$2LOM2BUTBJCpt6CwRaerCd*A$1a&sa2^hu+ zdebFgbA|;ZK42gc>#5B$uq+>#iOg~UJ6H+nL&eat#`neXsP6nz>>Zrv18ald=|_@` z4Ne%dUy!g(+P=sh;b-x-X4U<4cp3TC<8V6C81q8onLZ=U#ua4*kn@32MHchPd$LeT zs9sj&RhJsi>^io#B0%m5F9v$VBf(=yB3a~BV~uA5dhj03#ufo`k4rGmUgAi4zgRlS zpKSb%`SuQ6a{q1XWE3}zFzCj#ZMNn404Cqoub#`xlB5RZ9z4L z>V2wrl6qfk1ju^?_l%}(T4ao^#yh?8z8DE0Cx;VFa@cwA0|4jvPk;dc68z~C(;byw P00000NkvXXu0mjfWmII? literal 0 HcmV?d00001 diff --git a/packages/builders/src/emails/emails/static/plaid-logo.png b/packages/builders/src/emails/emails/static/plaid-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ce9d08cb23a9d033c9be3c5a28cefae06214bf52 GIT binary patch literal 3987 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 Q`MOc)X&Y$~G#!!u13Bs91poj5 literal 0 HcmV?d00001 diff --git a/packages/builders/src/emails/emails/static/vercel-user.png b/packages/builders/src/emails/emails/static/vercel-user.png new file mode 100644 index 0000000000000000000000000000000000000000..81beac6945a486f6c03edaad5e035a1c6a67026b GIT binary patch literal 55726 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

b_4NZ02x!e-&bvaid>@c6qBDycI@ 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

N10A(43%#Xj0Fa8KtC0cBko)!Tx5rY*_)wkZy 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 literal 0 HcmV?d00001 diff --git a/packages/builders/src/emails/emails/stripe-welcome.tsx b/packages/builders/src/emails/emails/stripe-welcome.tsx new file mode 100644 index 000000000..232f4a2ce --- /dev/null +++ b/packages/builders/src/emails/emails/stripe-welcome.tsx @@ -0,0 +1,152 @@ +import { + Body, + Button, + Container, + Head, + Hr, + Html, + Img, + Link, + Preview, + Section, + Text, +} from "@react-email/components"; +import * as React from "react"; + +const baseUrl = process.env.VERCEL_URL + ? `https://${process.env.VERCEL_URL}` + : ""; + +export const StripeWelcomeEmail = () => ( + + + You're now ready to make live transactions with Stripe! + + +

+ Stripe +
+ + Thanks for submitting your account information. You're now ready to + make live transactions with Stripe! + + + You can view your payments and a variety of other information about + your account right from your dashboard. + + +
+ + If you haven't finished your integration, you might find our{" "} + + docs + {" "} + handy. + + + Once you're ready to start accepting payments, you'll just need to + use your live{" "} + + API keys + {" "} + instead of your test API keys. Your account can simultaneously be + used for both test and live requests, so you can continue testing + while accepting live payments. Check out our{" "} + + tutorial about account basics + + . + + + Finally, we've put together a{" "} + + quick checklist + {" "} + to ensure your website conforms to card network standards. + + + We'll be here to help you with any step along the way. You can find + answers to most questions and get in touch with us on our{" "} + + support site + + . + + — The Stripe team +
+ + Stripe, 354 Oyster Point Blvd, South San Francisco, CA 94080 + +
+ + + +); + +export default StripeWelcomeEmail; + +const main = { + backgroundColor: "#f6f9fc", + fontFamily: + '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif', +}; + +const container = { + backgroundColor: "#ffffff", + margin: "0 auto", + padding: "20px 0 48px", + marginBottom: "64px", +}; + +const box = { + padding: "0 48px", +}; + +const hr = { + borderColor: "#e6ebf1", + margin: "20px 0", +}; + +const paragraph = { + color: "#525f7f", + + fontSize: "16px", + lineHeight: "24px", + textAlign: "left" as const, +}; + +const anchor = { + color: "#556cd6", +}; + +const button = { + backgroundColor: "#656ee8", + borderRadius: "5px", + color: "#fff", + fontSize: "16px", + fontWeight: "bold", + textDecoration: "none", + textAlign: "center" as const, + display: "block", + width: "100%", + padding: "10px", +}; + +const footer = { + color: "#8898aa", + fontSize: "12px", + lineHeight: "16px", +}; diff --git a/packages/builders/src/emails/emails/vercel-invite-user.tsx b/packages/builders/src/emails/emails/vercel-invite-user.tsx new file mode 100644 index 000000000..bd7404ab9 --- /dev/null +++ b/packages/builders/src/emails/emails/vercel-invite-user.tsx @@ -0,0 +1,154 @@ +import { + Body, + Button, + Column, + Container, + Head, + Heading, + Hr, + Html, + Img, + Link, + Preview, + Row, + Section, + Tailwind, + Text, +} from "@react-email/components"; +import * as React from "react"; + +interface VercelInviteUserEmailProps { + username?: string; + userImage?: string; + invitedByUsername?: string; + invitedByEmail?: string; + teamName?: string; + teamImage?: string; + inviteLink?: string; + inviteFromIp?: string; + inviteFromLocation?: string; +} + +const baseUrl = process.env.VERCEL_URL + ? `https://${process.env.VERCEL_URL}` + : ""; + +export const VercelInviteUserEmail = ({ + username, + userImage, + invitedByUsername, + invitedByEmail, + teamName, + teamImage, + inviteLink, + inviteFromIp, + inviteFromLocation, +}: VercelInviteUserEmailProps) => { + const previewText = `Join ${invitedByUsername} on Vercel`; + + return ( + + + {previewText} + + + +
+ Vercel +
+ + Join {teamName} on Vercel + + + Hello {username}, + + + {invitedByUsername} ( + + {invitedByEmail} + + ) has invited you to the {teamName} team on{" "} + Vercel. + +
+ + + + + + invited you to + + + + + +
+
+ +
+ + or copy and paste this URL into your browser:{" "} + + {inviteLink} + + +
+ + This invitation was intended for{" "} + {username}. This invite was + sent from {inviteFromIp}{" "} + located in{" "} + {inviteFromLocation}. If you + were not expecting this invitation, you can ignore this email. If + you are concerned about your account's safety, please reply to + this email to get in touch with us. + +
+ +
+ + ); +}; + +VercelInviteUserEmail.PreviewProps = { + username: "alanturing", + userImage: `${baseUrl}/static/vercel-user.png`, + invitedByUsername: "Alan", + invitedByEmail: "alan.turing@example.com", + teamName: "Enigma", + teamImage: `${baseUrl}/static/vercel-team.png`, + inviteLink: "https://vercel.com/teams/invite/foo", + inviteFromIp: "204.13.186.218", + inviteFromLocation: "São Paulo, Brazil", +} as VercelInviteUserEmailProps; + +export default VercelInviteUserEmail; diff --git a/packages/builders/src/emails/package.json b/packages/builders/src/emails/package.json new file mode 100644 index 000000000..c7aad5b58 --- /dev/null +++ b/packages/builders/src/emails/package.json @@ -0,0 +1,20 @@ +{ + "name": "emails", + "version": "0.0.19", + "private": true, + "type": "module", + "scripts": { + "build": "email build", + "dev": "email dev", + "export": "email export" + }, + "dependencies": { + "@react-email/components": "0.0.21", + "react-email": "2.1.5", + "react": "^18.2.0" + }, + "devDependencies": { + "@types/react": "18.2.33", + "@types/react-dom": "18.2.14" + } +} diff --git a/packages/builders/src/emails/pnpm-lock.yaml b/packages/builders/src/emails/pnpm-lock.yaml new file mode 100644 index 000000000..270dd6cb5 --- /dev/null +++ b/packages/builders/src/emails/pnpm-lock.yaml @@ -0,0 +1,4209 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + '@react-email/components': + specifier: 0.0.21 + version: 0.0.21(@types/react@18.2.33)(react-dom@18.3.1)(react@18.3.1) + react: + specifier: ^18.2.0 + version: 18.3.1 + react-email: + specifier: 2.1.5 + version: 2.1.5(eslint@9.7.0) + +devDependencies: + '@types/react': + specifier: 18.2.33 + version: 18.2.33 + '@types/react-dom': + specifier: 18.2.14 + version: 18.2.14 + +packages: + + /@alloc/quick-lru@5.2.0: + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + dev: false + + /@ampproject/remapping@2.3.0: + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + dev: false + + /@babel/code-frame@7.24.7: + resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.24.7 + picocolors: 1.0.1 + dev: false + + /@babel/compat-data@7.24.8: + resolution: {integrity: sha512-c4IM7OTg6k1Q+AJ153e2mc2QVTezTwnb4VzquwcyiEzGnW0Kedv4do/TrkU98qPeC5LNiMt/QXwIjzYXLBpyZg==} + engines: {node: '>=6.9.0'} + dev: false + + /@babel/core@7.24.5: + resolution: {integrity: sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.24.8 + '@babel/helper-compilation-targets': 7.24.8 + '@babel/helper-module-transforms': 7.24.8(@babel/core@7.24.5) + '@babel/helpers': 7.24.8 + '@babel/parser': 7.24.5 + '@babel/template': 7.24.7 + '@babel/traverse': 7.24.8 + '@babel/types': 7.24.8 + convert-source-map: 2.0.0 + debug: 4.3.5 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: false + + /@babel/generator@7.24.8: + resolution: {integrity: sha512-47DG+6F5SzOi0uEvK4wMShmn5yY0mVjVJoWTphdY2B4Rx9wHgjK7Yhtr0ru6nE+sn0v38mzrWOlah0p/YlHHOQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.8 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 2.5.2 + dev: false + + /@babel/helper-compilation-targets@7.24.8: + resolution: {integrity: sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/compat-data': 7.24.8 + '@babel/helper-validator-option': 7.24.8 + browserslist: 4.23.2 + lru-cache: 5.1.1 + semver: 6.3.1 + dev: false + + /@babel/helper-environment-visitor@7.24.7: + resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.8 + dev: false + + /@babel/helper-function-name@7.24.7: + resolution: {integrity: sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.24.7 + '@babel/types': 7.24.8 + dev: false + + /@babel/helper-hoist-variables@7.24.7: + resolution: {integrity: sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.8 + dev: false + + /@babel/helper-module-imports@7.24.7: + resolution: {integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/traverse': 7.24.8 + '@babel/types': 7.24.8 + transitivePeerDependencies: + - supports-color + dev: false + + /@babel/helper-module-transforms@7.24.8(@babel/core@7.24.5): + resolution: {integrity: sha512-m4vWKVqvkVAWLXfHCCfff2luJj86U+J0/x+0N3ArG/tP0Fq7zky2dYwMbtPmkc/oulkkbjdL3uWzuoBwQ8R00Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.5 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-module-imports': 7.24.7 + '@babel/helper-simple-access': 7.24.7 + '@babel/helper-split-export-declaration': 7.24.7 + '@babel/helper-validator-identifier': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: false + + /@babel/helper-simple-access@7.24.7: + resolution: {integrity: sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/traverse': 7.24.8 + '@babel/types': 7.24.8 + transitivePeerDependencies: + - supports-color + dev: false + + /@babel/helper-split-export-declaration@7.24.7: + resolution: {integrity: sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.8 + dev: false + + /@babel/helper-string-parser@7.24.8: + resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==} + engines: {node: '>=6.9.0'} + dev: false + + /@babel/helper-validator-identifier@7.24.7: + resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} + engines: {node: '>=6.9.0'} + dev: false + + /@babel/helper-validator-option@7.24.8: + resolution: {integrity: sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==} + engines: {node: '>=6.9.0'} + dev: false + + /@babel/helpers@7.24.8: + resolution: {integrity: sha512-gV2265Nkcz7weJJfvDoAEVzC1e2OTDpkGbEsebse8koXUJUXPsCMi7sRo/+SPMuMZ9MtUPnGwITTnQnU5YjyaQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.24.7 + '@babel/types': 7.24.8 + dev: false + + /@babel/highlight@7.24.7: + resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.24.7 + chalk: 2.4.2 + js-tokens: 4.0.0 + picocolors: 1.0.1 + dev: false + + /@babel/parser@7.24.5: + resolution: {integrity: sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.24.8 + dev: false + + /@babel/parser@7.24.8: + resolution: {integrity: sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.24.8 + dev: false + + /@babel/runtime@7.24.8: + resolution: {integrity: sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.1 + dev: false + + /@babel/template@7.24.7: + resolution: {integrity: sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/parser': 7.24.8 + '@babel/types': 7.24.8 + dev: false + + /@babel/traverse@7.24.8: + resolution: {integrity: sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.24.8 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-function-name': 7.24.7 + '@babel/helper-hoist-variables': 7.24.7 + '@babel/helper-split-export-declaration': 7.24.7 + '@babel/parser': 7.24.8 + '@babel/types': 7.24.8 + debug: 4.3.5 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@babel/types@7.24.8: + resolution: {integrity: sha512-SkSBEHwwJRU52QEVZBmMBnE5Ux2/6WU1grdYyOhpbCNxbmJrDuDCphBzKZSO3taf0zztp+qkWlymE5tVL5l0TA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.24.8 + '@babel/helper-validator-identifier': 7.24.7 + to-fast-properties: 2.0.0 + dev: false + + /@emotion/is-prop-valid@0.8.8: + resolution: {integrity: sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==} + requiresBuild: true + dependencies: + '@emotion/memoize': 0.7.4 + dev: false + optional: true + + /@emotion/memoize@0.7.4: + resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==} + requiresBuild: true + dev: false + optional: true + + /@esbuild/aix-ppc64@0.19.11: + resolution: {integrity: sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + dev: false + optional: true + + /@esbuild/android-arm64@0.19.11: + resolution: {integrity: sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: false + optional: true + + /@esbuild/android-arm@0.19.11: + resolution: {integrity: sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: false + optional: true + + /@esbuild/android-x64@0.19.11: + resolution: {integrity: sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: false + optional: true + + /@esbuild/darwin-arm64@0.19.11: + resolution: {integrity: sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@esbuild/darwin-x64@0.19.11: + resolution: {integrity: sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@esbuild/freebsd-arm64@0.19.11: + resolution: {integrity: sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: false + optional: true + + /@esbuild/freebsd-x64@0.19.11: + resolution: {integrity: sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-arm64@0.19.11: + resolution: {integrity: sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-arm@0.19.11: + resolution: {integrity: sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-ia32@0.19.11: + resolution: {integrity: sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-loong64@0.19.11: + resolution: {integrity: sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-mips64el@0.19.11: + resolution: {integrity: sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-ppc64@0.19.11: + resolution: {integrity: sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-riscv64@0.19.11: + resolution: {integrity: sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-s390x@0.19.11: + resolution: {integrity: sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/linux-x64@0.19.11: + resolution: {integrity: sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@esbuild/netbsd-x64@0.19.11: + resolution: {integrity: sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: false + optional: true + + /@esbuild/openbsd-x64@0.19.11: + resolution: {integrity: sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: false + optional: true + + /@esbuild/sunos-x64@0.19.11: + resolution: {integrity: sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: false + optional: true + + /@esbuild/win32-arm64@0.19.11: + resolution: {integrity: sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@esbuild/win32-ia32@0.19.11: + resolution: {integrity: sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@esbuild/win32-x64@0.19.11: + resolution: {integrity: sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@eslint-community/eslint-utils@4.4.0(eslint@9.7.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 9.7.0 + eslint-visitor-keys: 3.4.3 + dev: false + + /@eslint-community/regexpp@4.11.0: + resolution: {integrity: sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dev: false + + /@eslint/config-array@0.17.0: + resolution: {integrity: sha512-A68TBu6/1mHHuc5YJL0U0VVeGNiklLAL6rRmhTCP2B5XjWLMnrX+HkO+IAXyHvks5cyyY1jjK5ITPQ1HGS2EVA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dependencies: + '@eslint/object-schema': 2.1.4 + debug: 4.3.5 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: false + + /@eslint/eslintrc@3.1.0: + resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.5 + espree: 10.1.0 + globals: 14.0.0 + ignore: 5.3.1 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: false + + /@eslint/js@9.7.0: + resolution: {integrity: sha512-ChuWDQenef8OSFnvuxv0TCVxEwmu3+hPNKvM9B34qpM0rDRbjL8t5QkQeHHeAfsKQjuH9wS82WeCi1J/owatng==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dev: false + + /@eslint/object-schema@2.1.4: + resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dev: false + + /@floating-ui/core@1.6.4: + resolution: {integrity: sha512-a4IowK4QkXl4SCWTGUR0INAfEOX3wtsYw3rKK5InQEHMGObkR8Xk44qYQD9P4r6HHw0iIfK6GUKECmY8sTkqRA==} + dependencies: + '@floating-ui/utils': 0.2.4 + dev: false + + /@floating-ui/dom@1.6.7: + resolution: {integrity: sha512-wmVfPG5o2xnKDU4jx/m4w5qva9FWHcnZ8BvzEe90D/RpwsJaTAVYPEPdQ8sbr/N8zZTAHlZUTQdqg8ZUbzHmng==} + dependencies: + '@floating-ui/core': 1.6.4 + '@floating-ui/utils': 0.2.4 + dev: false + + /@floating-ui/react-dom@2.1.1(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + '@floating-ui/dom': 1.6.7 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@floating-ui/utils@0.2.4: + resolution: {integrity: sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA==} + dev: false + + /@humanwhocodes/module-importer@1.0.1: + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + dev: false + + /@humanwhocodes/retry@0.3.0: + resolution: {integrity: sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==} + engines: {node: '>=18.18'} + dev: false + + /@isaacs/cliui@8.0.2: + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + string-width-cjs: /string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: /strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: /wrap-ansi@7.0.0 + dev: false + + /@jridgewell/gen-mapping@0.3.5: + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + dev: false + + /@jridgewell/resolve-uri@3.1.2: + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + dev: false + + /@jridgewell/set-array@1.2.1: + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + dev: false + + /@jridgewell/source-map@0.3.6: + resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + dev: false + + /@jridgewell/sourcemap-codec@1.5.0: + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + dev: false + + /@jridgewell/trace-mapping@0.3.25: + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + dev: false + + /@next/env@14.1.4: + resolution: {integrity: sha512-e7X7bbn3Z6DWnDi75UWn+REgAbLEqxI8Tq2pkFOFAMpWAWApz/YCUhtWMWn410h8Q2fYiYL7Yg5OlxMOCfFjJQ==} + dev: false + + /@next/swc-darwin-arm64@14.1.4: + resolution: {integrity: sha512-ubmUkbmW65nIAOmoxT1IROZdmmJMmdYvXIe8211send9ZYJu+SqxSnJM4TrPj9wmL6g9Atvj0S/2cFmMSS99jg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@next/swc-darwin-x64@14.1.4: + resolution: {integrity: sha512-b0Xo1ELj3u7IkZWAKcJPJEhBop117U78l70nfoQGo4xUSvv0PJSTaV4U9xQBLvZlnjsYkc8RwQN1HoH/oQmLlQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@next/swc-linux-arm64-gnu@14.1.4: + resolution: {integrity: sha512-457G0hcLrdYA/u1O2XkRMsDKId5VKe3uKPvrKVOyuARa6nXrdhJOOYU9hkKKyQTMru1B8qEP78IAhf/1XnVqKA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@next/swc-linux-arm64-musl@14.1.4: + resolution: {integrity: sha512-l/kMG+z6MB+fKA9KdtyprkTQ1ihlJcBh66cf0HvqGP+rXBbOXX0dpJatjZbHeunvEHoBBS69GYQG5ry78JMy3g==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@next/swc-linux-x64-gnu@14.1.4: + resolution: {integrity: sha512-BapIFZ3ZRnvQ1uWbmqEGJuPT9cgLwvKtxhK/L2t4QYO7l+/DxXuIGjvp1x8rvfa/x1FFSsipERZK70pewbtJtw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@next/swc-linux-x64-musl@14.1.4: + resolution: {integrity: sha512-mqVxTwk4XuBl49qn2A5UmzFImoL1iLm0KQQwtdRJRKl21ylQwwGCxJtIYo2rbfkZHoSKlh/YgztY0qH3wG1xIg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@next/swc-win32-arm64-msvc@14.1.4: + resolution: {integrity: sha512-xzxF4ErcumXjO2Pvg/wVGrtr9QQJLk3IyQX1ddAC/fi6/5jZCZ9xpuL9Tzc4KPWMFq8GGWFVDMshZOdHGdkvag==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@next/swc-win32-ia32-msvc@14.1.4: + resolution: {integrity: sha512-WZiz8OdbkpRw6/IU/lredZWKKZopUMhcI2F+XiMAcPja0uZYdMTZQRoQ0WZcvinn9xZAidimE7tN9W5v9Yyfyw==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@next/swc-win32-x64-msvc@14.1.4: + resolution: {integrity: sha512-4Rto21sPfw555sZ/XNLqfxDUNeLhNYGO2dlPqsnuCg8N8a2a9u1ltqBOPQ4vj1Gf7eJC0W2hHG2eYUHuiXgY2w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: false + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: false + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + dev: false + + /@one-ini/wasm@0.1.1: + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + dev: false + + /@pkgjs/parseargs@0.11.0: + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + requiresBuild: true + dev: false + optional: true + + /@radix-ui/colors@1.0.1: + resolution: {integrity: sha512-xySw8f0ZVsAEP+e7iLl3EvcBXX7gsIlC1Zso/sPBW9gIWerBTgz6axrjU+MZ39wD+WFi5h5zdWpsg3+hwt2Qsg==} + dev: false + + /@radix-ui/primitive@1.1.0: + resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==} + dev: false + + /@radix-ui/react-arrow@1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1) + '@types/react': 18.2.47 + '@types/react-dom': 18.2.14 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-collapsible@1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-zQY7Epa8sTL0mq4ajSJpjgn2YmCgyrG7RsQgLp3C0LQVkG7+Tf6Pv1CeNWZLyqMjhdPkBa5Lx7wYBeSu7uCSTA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@types/react': 18.2.47 + '@types/react-dom': 18.2.14 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-collection@1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@types/react': 18.2.47 + '@types/react-dom': 18.2.14 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.33)(react@18.3.1): + resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.24.8 + '@types/react': 18.2.33 + react: 18.3.1 + dev: false + + /@radix-ui/react-compose-refs@1.1.0(@types/react@18.2.47)(react@18.3.1): + resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.47 + react: 18.3.1 + dev: false + + /@radix-ui/react-context@1.1.0(@types/react@18.2.47)(react@18.3.1): + resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.47 + react: 18.3.1 + dev: false + + /@radix-ui/react-direction@1.1.0(@types/react@18.2.47)(react@18.3.1): + resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.47 + react: 18.3.1 + dev: false + + /@radix-ui/react-dismissable-layer@1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@types/react': 18.2.47 + '@types/react-dom': 18.2.14 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-focus-guards@1.1.0(@types/react@18.2.47)(react@18.3.1): + resolution: {integrity: sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.47 + react: 18.3.1 + dev: false + + /@radix-ui/react-focus-scope@1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@types/react': 18.2.47 + '@types/react-dom': 18.2.14 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-id@1.1.0(@types/react@18.2.47)(react@18.3.1): + resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@types/react': 18.2.47 + react: 18.3.1 + dev: false + + /@radix-ui/react-popover@1.1.1(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-3y1A3isulwnWhvTTwmIreiB8CF4L+qRjZnK1wYLO7pplddzXKby/GnZ2M7OZY3qgnl6p9AodUIHRYGXNah8Y7g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@types/react': 18.2.47 + '@types/react-dom': 18.2.14 + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.5.7(@types/react@18.2.47)(react@18.3.1) + dev: false + + /@radix-ui/react-popper@1.2.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@floating-ui/react-dom': 2.1.1(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-arrow': 1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/rect': 1.1.0 + '@types/react': 18.2.47 + '@types/react-dom': 18.2.14 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-portal@1.1.1(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@types/react': 18.2.47 + '@types/react-dom': 18.2.14 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-presence@1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@types/react': 18.2.47 + '@types/react-dom': 18.2.14 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-primitive@2.0.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-slot': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@types/react': 18.2.47 + '@types/react-dom': 18.2.14 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-roving-focus@1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@types/react': 18.2.47 + '@types/react-dom': 18.2.14 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-slot@1.0.2(@types/react@18.2.33)(react@18.3.1): + resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.24.8 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.33)(react@18.3.1) + '@types/react': 18.2.33 + react: 18.3.1 + dev: false + + /@radix-ui/react-slot@1.1.0(@types/react@18.2.47)(react@18.3.1): + resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@types/react': 18.2.47 + react: 18.3.1 + dev: false + + /@radix-ui/react-toggle-group@1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-PpTJV68dZU2oqqgq75Uzto5o/XfOVgkrJ9rulVmfTKxWp3HfUjHE6CP/WLRR4AzPX9HWxw7vFow2me85Yu+Naw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-context': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-toggle': 1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@types/react': 18.2.47 + '@types/react-dom': 18.2.14 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-toggle@1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-gwoxaKZ0oJ4vIgzsfESBuSgJNdc0rv12VhHgcqN0TEJmmZixXG/2XpsLK8kzNWYcnaoRIEEQc0bEi3dIvdUpjw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@types/react': 18.2.47 + '@types/react-dom': 18.2.14 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-tooltip@1.1.1(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-LLE8nzNE4MzPMw3O2zlVlkLFid3y9hMUs7uCbSHyKSo+tCN4yMCf+ZCCcfrYgsOC0TiHBPQ1mtpJ2liY3ZT3SQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1) + '@types/react': 18.2.47 + '@types/react-dom': 18.2.14 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.2.47)(react@18.3.1): + resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.47 + react: 18.3.1 + dev: false + + /@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.2.47)(react@18.3.1): + resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@types/react': 18.2.47 + react: 18.3.1 + dev: false + + /@radix-ui/react-use-escape-keydown@1.1.0(@types/react@18.2.47)(react@18.3.1): + resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@types/react': 18.2.47 + react: 18.3.1 + dev: false + + /@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.2.47)(react@18.3.1): + resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.47 + react: 18.3.1 + dev: false + + /@radix-ui/react-use-rect@1.1.0(@types/react@18.2.47)(react@18.3.1): + resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/rect': 1.1.0 + '@types/react': 18.2.47 + react: 18.3.1 + dev: false + + /@radix-ui/react-use-size@1.1.0(@types/react@18.2.47)(react@18.3.1): + resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@types/react': 18.2.47 + react: 18.3.1 + dev: false + + /@radix-ui/react-visually-hidden@1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1) + '@types/react': 18.2.47 + '@types/react-dom': 18.2.14 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/rect@1.1.0: + resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} + dev: false + + /@react-email/body@0.0.8(react@18.3.1): + resolution: {integrity: sha512-gqdkNYlIaIw0OdpWu8KjIcQSIFvx7t2bZpXVxMMvBS859Ia1+1X3b5RNbjI3S1ZqLddUf7owOHkO4MiXGE+nxg==} + peerDependencies: + react: ^18.2.0 + dependencies: + react: 18.3.1 + dev: false + + /@react-email/button@0.0.15(react@18.3.1): + resolution: {integrity: sha512-9Zi6SO3E8PoHYDfcJTecImiHLyitYWmIRs0HE3Ogra60ZzlWP2EXu+AZqwQnhXuq+9pbgwBWNWxB5YPetNPTNA==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + dependencies: + react: 18.3.1 + dev: false + + /@react-email/code-block@0.0.5(react@18.3.1): + resolution: {integrity: sha512-mmInpZsSIkNaYC1y40/S0XXrIqbTzrpllP6J1JMJuDOBG8l5T7pNl4V+gwfsSTvy9hVsuzQFmhHK8kVb1UXv3A==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + dependencies: + prismjs: 1.29.0 + react: 18.3.1 + dev: false + + /@react-email/code-inline@0.0.2(react@18.3.1): + resolution: {integrity: sha512-0cmgbbibFeOJl0q04K9jJlPDuJ+SEiX/OG6m3Ko7UOkG3TqjRD8Dtvkij6jNDVfUh/zESpqJCP2CxrCLLMUjdA==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + dependencies: + react: 18.3.1 + dev: false + + /@react-email/column@0.0.10(react@18.3.1): + resolution: {integrity: sha512-MnP8Mnwipr0X3XtdD6jMLckb0sI5/IlS6Kl/2F6/rsSWBJy5Gg6nizlekTdkwDmy0kNSe3/1nGU0Zqo98pl63Q==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + dependencies: + react: 18.3.1 + dev: false + + /@react-email/components@0.0.21(@types/react@18.2.33)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-fwGfH7FF+iuq+IdPcbEO5HoF0Pakk9big+fFW9+3kiyvbSNuo8Io1rhPTMLd8q41XomN4g7mgWovdAeS/8PHrA==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + dependencies: + '@react-email/body': 0.0.8(react@18.3.1) + '@react-email/button': 0.0.15(react@18.3.1) + '@react-email/code-block': 0.0.5(react@18.3.1) + '@react-email/code-inline': 0.0.2(react@18.3.1) + '@react-email/column': 0.0.10(react@18.3.1) + '@react-email/container': 0.0.12(react@18.3.1) + '@react-email/font': 0.0.6(react@18.3.1) + '@react-email/head': 0.0.9(react@18.3.1) + '@react-email/heading': 0.0.12(@types/react@18.2.33)(react@18.3.1) + '@react-email/hr': 0.0.8(react@18.3.1) + '@react-email/html': 0.0.8(react@18.3.1) + '@react-email/img': 0.0.8(react@18.3.1) + '@react-email/link': 0.0.8(react@18.3.1) + '@react-email/markdown': 0.0.10(react@18.3.1) + '@react-email/preview': 0.0.9(react@18.3.1) + '@react-email/render': 0.0.16(react-dom@18.3.1)(react@18.3.1) + '@react-email/row': 0.0.8(react@18.3.1) + '@react-email/section': 0.0.12(react@18.3.1) + '@react-email/tailwind': 0.0.18(react@18.3.1) + '@react-email/text': 0.0.8(react@18.3.1) + react: 18.3.1 + transitivePeerDependencies: + - '@types/react' + - react-dom + dev: false + + /@react-email/container@0.0.12(react@18.3.1): + resolution: {integrity: sha512-HFu8Pu5COPFfeZxSL+wKv/TV5uO/sp4zQ0XkRCdnGkj/xoq0lqOHVDL4yC2Pu6fxXF/9C3PHDA++5uEYV5WVJw==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + dependencies: + react: 18.3.1 + dev: false + + /@react-email/font@0.0.6(react@18.3.1): + resolution: {integrity: sha512-sZZFvEZ4U3vNCAZ8wXqIO3DuGJR2qE/8m2fEH+tdqwa532zGO3zW+UlCTg0b9455wkJSzEBeaWik0IkNvjXzxw==} + peerDependencies: + react: ^18.2.0 + dependencies: + react: 18.3.1 + dev: false + + /@react-email/head@0.0.9(react@18.3.1): + resolution: {integrity: sha512-dF3Uv1qy3oh+IU2atXdv5Xk0hk2udOlMb1A/MNGngC0eHyoEV9ThA0XvhN7mm5x9dDLkVamoWUKXDtmkiuSRqQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + dependencies: + react: 18.3.1 + dev: false + + /@react-email/heading@0.0.12(@types/react@18.2.33)(react@18.3.1): + resolution: {integrity: sha512-eB7mpnAvDmwvQLoPuwEiPRH4fPXWe6ltz6Ptbry2BlI88F0a2k11Ghb4+sZHBqg7vVw/MKbqEgtLqr3QJ/KfCQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + dependencies: + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.33)(react@18.3.1) + react: 18.3.1 + transitivePeerDependencies: + - '@types/react' + dev: false + + /@react-email/hr@0.0.8(react@18.3.1): + resolution: {integrity: sha512-JLVvpCg2wYKEB+n/PGCggWG9fRU5e4lxsGdpK5SDLsCL0ic3OLKSpHMfeE+ZSuw0GixAVVQN7F64PVJHQkd4MQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + dependencies: + react: 18.3.1 + dev: false + + /@react-email/html@0.0.8(react@18.3.1): + resolution: {integrity: sha512-arII3wBNLpeJtwyIJXPaILm5BPKhA+nvdC1F9QkuKcOBJv2zXctn8XzPqyGqDfdplV692ulNJP7XY55YqbKp6w==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + dependencies: + react: 18.3.1 + dev: false + + /@react-email/img@0.0.8(react@18.3.1): + resolution: {integrity: sha512-jx/rPuKo31tV18fu7P5rRqelaH5wkhg83Dq7uLwJpfqhbi4KFBGeBfD0Y3PiLPPoh+WvYf+Adv9W2ghNW8nOMQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + dependencies: + react: 18.3.1 + dev: false + + /@react-email/link@0.0.8(react@18.3.1): + resolution: {integrity: sha512-nVikuTi8WJHa6Baad4VuRUbUCa/7EtZ1Qy73TRejaCHn+vhetc39XGqHzKLNh+Z/JFL8Hv9g+4AgG16o2R0ogQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + dependencies: + react: 18.3.1 + dev: false + + /@react-email/markdown@0.0.10(react@18.3.1): + resolution: {integrity: sha512-MH0xO+NJ4IuJcx9nyxbgGKAMXyudFjCZ0A2GQvuWajemW9qy2hgnJ3mW3/z5lwcenG+JPn7JyO/iZpizQ7u1tA==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + dependencies: + md-to-react-email: 5.0.2(react@18.3.1) + react: 18.3.1 + dev: false + + /@react-email/preview@0.0.9(react@18.3.1): + resolution: {integrity: sha512-2fyAA/zzZYfYmxfyn3p2YOIU30klyA6Dq4ytyWq4nfzQWWglt5hNDE0cMhObvRtfjM9ghMSVtoELAb0MWiF/kw==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + dependencies: + react: 18.3.1 + dev: false + + /@react-email/render@0.0.16(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-wDaMy27xAq1cJHtSFptp0DTKPuV2GYhloqia95ub/DH9Dea1aWYsbdM918MOc/b/HvVS3w1z8DWzfAk13bGStQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + react-dom: ^18.2.0 + dependencies: + html-to-text: 9.0.5 + js-beautify: 1.15.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-promise-suspense: 0.3.4 + dev: false + + /@react-email/row@0.0.8(react@18.3.1): + resolution: {integrity: sha512-JsB6pxs/ZyjYpEML3nbwJRGAerjcN/Pa/QG48XUwnT/MioDWrUuyQuefw+CwCrSUZ2P1IDrv2tUD3/E3xzcoKw==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + dependencies: + react: 18.3.1 + dev: false + + /@react-email/section@0.0.12(react@18.3.1): + resolution: {integrity: sha512-UCD/N/BeOTN4h3VZBUaFdiSem6HnpuxD1Q51TdBFnqeNqS5hBomp8LWJJ9s4gzwHWk1XPdNfLA3I/fJwulJshg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + dependencies: + react: 18.3.1 + dev: false + + /@react-email/tailwind@0.0.18(react@18.3.1): + resolution: {integrity: sha512-ob8CXX/Pqq1U8YfL5OJTL48WJkixizyoXMMRYTiDLDN9LVLU7lSLtcK9kOD9CgFbO2yUPQr7/5+7gnQJ+cXa8Q==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + dependencies: + react: 18.3.1 + dev: false + + /@react-email/text@0.0.8(react@18.3.1): + resolution: {integrity: sha512-uvN2TNWMrfC9wv/LLmMLbbEN1GrMWZb9dBK14eYxHHAEHCeyvGb5ePZZ2MPyzO7Y5yTC+vFEnCEr76V+hWMxCQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.2.0 + dependencies: + react: 18.3.1 + dev: false + + /@selderee/plugin-htmlparser2@0.11.0: + resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + dependencies: + domhandler: 5.0.3 + selderee: 0.11.0 + dev: false + + /@socket.io/component-emitter@3.1.2: + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + dev: false + + /@swc/core-darwin-arm64@1.3.101: + resolution: {integrity: sha512-mNFK+uHNPRXSnfTOG34zJOeMl2waM4hF4a2NY7dkMXrPqw9CoJn4MwTXJcyMiSz1/BnNjjTCHF3Yhj0jPxmkzQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@swc/core-darwin-x64@1.3.101: + resolution: {integrity: sha512-B085j8XOx73Fg15KsHvzYWG262bRweGr3JooO1aW5ec5pYbz5Ew9VS5JKYS03w2UBSxf2maWdbPz2UFAxg0whw==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@swc/core-linux-arm-gnueabihf@1.3.101: + resolution: {integrity: sha512-9xLKRb6zSzRGPqdz52Hy5GuB1lSjmLqa0lST6MTFads3apmx4Vgs8Y5NuGhx/h2I8QM4jXdLbpqQlifpzTlSSw==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@swc/core-linux-arm64-gnu@1.3.101: + resolution: {integrity: sha512-oE+r1lo7g/vs96Weh2R5l971dt+ZLuhaUX+n3BfDdPxNHfObXgKMjO7E+QS5RbGjv/AwiPCxQmbdCp/xN5ICJA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@swc/core-linux-arm64-musl@1.3.101: + resolution: {integrity: sha512-OGjYG3H4BMOTnJWJyBIovCez6KiHF30zMIu4+lGJTCrxRI2fAjGLml3PEXj8tC3FMcud7U2WUn6TdG0/te2k6g==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@swc/core-linux-x64-gnu@1.3.101: + resolution: {integrity: sha512-/kBMcoF12PRO/lwa8Z7w4YyiKDcXQEiLvM+S3G9EvkoKYGgkkz4Q6PSNhF5rwg/E3+Hq5/9D2R+6nrkF287ihg==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@swc/core-linux-x64-musl@1.3.101: + resolution: {integrity: sha512-kDN8lm4Eew0u1p+h1l3JzoeGgZPQ05qDE0czngnjmfpsH2sOZxVj1hdiCwS5lArpy7ktaLu5JdRnx70MkUzhXw==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@swc/core-win32-arm64-msvc@1.3.101: + resolution: {integrity: sha512-9Wn8TTLWwJKw63K/S+jjrZb9yoJfJwCE2RV5vPCCWmlMf3U1AXj5XuWOLUX+Rp2sGKau7wZKsvywhheWm+qndQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@swc/core-win32-ia32-msvc@1.3.101: + resolution: {integrity: sha512-onO5KvICRVlu2xmr4//V2je9O2XgS1SGKpbX206KmmjcJhXN5EYLSxW9qgg+kgV5mip+sKTHTAu7IkzkAtElYA==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@swc/core-win32-x64-msvc@1.3.101: + resolution: {integrity: sha512-T3GeJtNQV00YmiVw/88/nxJ/H43CJvFnpvBHCVn17xbahiVUOPOduh3rc9LgAkKiNt/aV8vU3OJR+6PhfMR7UQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@swc/core@1.3.101: + resolution: {integrity: sha512-w5aQ9qYsd/IYmXADAnkXPGDMTqkQalIi+kfFf/MHRKTpaOL7DHjMXwPp/n8hJ0qNjRvchzmPtOqtPBiER50d8A==} + engines: {node: '>=10'} + requiresBuild: true + peerDependencies: + '@swc/helpers': ^0.5.0 + peerDependenciesMeta: + '@swc/helpers': + optional: true + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.9 + optionalDependencies: + '@swc/core-darwin-arm64': 1.3.101 + '@swc/core-darwin-x64': 1.3.101 + '@swc/core-linux-arm-gnueabihf': 1.3.101 + '@swc/core-linux-arm64-gnu': 1.3.101 + '@swc/core-linux-arm64-musl': 1.3.101 + '@swc/core-linux-x64-gnu': 1.3.101 + '@swc/core-linux-x64-musl': 1.3.101 + '@swc/core-win32-arm64-msvc': 1.3.101 + '@swc/core-win32-ia32-msvc': 1.3.101 + '@swc/core-win32-x64-msvc': 1.3.101 + dev: false + + /@swc/counter@0.1.3: + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + dev: false + + /@swc/helpers@0.5.2: + resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==} + dependencies: + tslib: 2.6.3 + dev: false + + /@swc/types@0.1.9: + resolution: {integrity: sha512-qKnCno++jzcJ4lM4NTfYifm1EFSCeIfKiAHAfkENZAV5Kl9PjJIyd2yeeVv6c/2CckuLyv2NmRC5pv6pm2WQBg==} + dependencies: + '@swc/counter': 0.1.3 + dev: false + + /@types/cookie@0.4.1: + resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} + dev: false + + /@types/cors@2.8.17: + resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} + dependencies: + '@types/node': 20.14.10 + dev: false + + /@types/eslint-scope@3.7.7: + resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} + dependencies: + '@types/eslint': 8.56.10 + '@types/estree': 1.0.5 + dev: false + + /@types/eslint@8.56.10: + resolution: {integrity: sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==} + dependencies: + '@types/estree': 1.0.5 + '@types/json-schema': 7.0.15 + dev: false + + /@types/estree@1.0.5: + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + dev: false + + /@types/json-schema@7.0.15: + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + dev: false + + /@types/node@20.14.10: + resolution: {integrity: sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==} + dependencies: + undici-types: 5.26.5 + dev: false + + /@types/prismjs@1.26.4: + resolution: {integrity: sha512-rlAnzkW2sZOjbqZ743IHUhFcvzaGbqijwOu8QZnZCjfQzBqFE3s4lOTJEsxikImav9uzz/42I+O7YUs1mWgMlg==} + dev: false + + /@types/prop-types@15.7.12: + resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} + + /@types/react-dom@18.2.14: + resolution: {integrity: sha512-V835xgdSVmyQmI1KLV2BEIUgqEuinxp9O4G6g3FqO/SqLac049E53aysv0oEFD2kHfejeKU+ZqL2bcFWj9gLAQ==} + dependencies: + '@types/react': 18.2.33 + + /@types/react@18.2.33: + resolution: {integrity: sha512-v+I7S+hu3PIBoVkKGpSYYpiBT1ijqEzWpzQD62/jm4K74hPpSP7FF9BnKG6+fg2+62weJYkkBWDJlZt5JO/9hg==} + dependencies: + '@types/prop-types': 15.7.12 + '@types/scheduler': 0.23.0 + csstype: 3.1.3 + + /@types/react@18.2.47: + resolution: {integrity: sha512-xquNkkOirwyCgoClNk85BjP+aqnIS+ckAJ8i37gAbDs14jfW/J23f2GItAf33oiUPQnqNMALiFeoM9Y5mbjpVQ==} + dependencies: + '@types/prop-types': 15.7.12 + '@types/scheduler': 0.23.0 + csstype: 3.1.3 + dev: false + + /@types/scheduler@0.23.0: + resolution: {integrity: sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw==} + + /@types/webpack@5.28.5(@swc/core@1.3.101)(esbuild@0.19.11): + resolution: {integrity: sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==} + dependencies: + '@types/node': 20.14.10 + tapable: 2.2.1 + webpack: 5.93.0(@swc/core@1.3.101)(esbuild@0.19.11) + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + - webpack-cli + dev: false + + /@webassemblyjs/ast@1.12.1: + resolution: {integrity: sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==} + dependencies: + '@webassemblyjs/helper-numbers': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + dev: false + + /@webassemblyjs/floating-point-hex-parser@1.11.6: + resolution: {integrity: sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==} + dev: false + + /@webassemblyjs/helper-api-error@1.11.6: + resolution: {integrity: sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==} + dev: false + + /@webassemblyjs/helper-buffer@1.12.1: + resolution: {integrity: sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==} + dev: false + + /@webassemblyjs/helper-numbers@1.11.6: + resolution: {integrity: sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==} + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.11.6 + '@webassemblyjs/helper-api-error': 1.11.6 + '@xtuc/long': 4.2.2 + dev: false + + /@webassemblyjs/helper-wasm-bytecode@1.11.6: + resolution: {integrity: sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==} + dev: false + + /@webassemblyjs/helper-wasm-section@1.12.1: + resolution: {integrity: sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==} + dependencies: + '@webassemblyjs/ast': 1.12.1 + '@webassemblyjs/helper-buffer': 1.12.1 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/wasm-gen': 1.12.1 + dev: false + + /@webassemblyjs/ieee754@1.11.6: + resolution: {integrity: sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==} + dependencies: + '@xtuc/ieee754': 1.2.0 + dev: false + + /@webassemblyjs/leb128@1.11.6: + resolution: {integrity: sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==} + dependencies: + '@xtuc/long': 4.2.2 + dev: false + + /@webassemblyjs/utf8@1.11.6: + resolution: {integrity: sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==} + dev: false + + /@webassemblyjs/wasm-edit@1.12.1: + resolution: {integrity: sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==} + dependencies: + '@webassemblyjs/ast': 1.12.1 + '@webassemblyjs/helper-buffer': 1.12.1 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/helper-wasm-section': 1.12.1 + '@webassemblyjs/wasm-gen': 1.12.1 + '@webassemblyjs/wasm-opt': 1.12.1 + '@webassemblyjs/wasm-parser': 1.12.1 + '@webassemblyjs/wast-printer': 1.12.1 + dev: false + + /@webassemblyjs/wasm-gen@1.12.1: + resolution: {integrity: sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==} + dependencies: + '@webassemblyjs/ast': 1.12.1 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/ieee754': 1.11.6 + '@webassemblyjs/leb128': 1.11.6 + '@webassemblyjs/utf8': 1.11.6 + dev: false + + /@webassemblyjs/wasm-opt@1.12.1: + resolution: {integrity: sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==} + dependencies: + '@webassemblyjs/ast': 1.12.1 + '@webassemblyjs/helper-buffer': 1.12.1 + '@webassemblyjs/wasm-gen': 1.12.1 + '@webassemblyjs/wasm-parser': 1.12.1 + dev: false + + /@webassemblyjs/wasm-parser@1.12.1: + resolution: {integrity: sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==} + dependencies: + '@webassemblyjs/ast': 1.12.1 + '@webassemblyjs/helper-api-error': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/ieee754': 1.11.6 + '@webassemblyjs/leb128': 1.11.6 + '@webassemblyjs/utf8': 1.11.6 + dev: false + + /@webassemblyjs/wast-printer@1.12.1: + resolution: {integrity: sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==} + dependencies: + '@webassemblyjs/ast': 1.12.1 + '@xtuc/long': 4.2.2 + dev: false + + /@xtuc/ieee754@1.2.0: + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + dev: false + + /@xtuc/long@4.2.2: + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + dev: false + + /abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: false + + /accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + dev: false + + /acorn-import-attributes@1.9.5(acorn@8.12.1): + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + dependencies: + acorn: 8.12.1 + dev: false + + /acorn-jsx@5.3.2(acorn@8.12.1): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.12.1 + dev: false + + /acorn@8.12.1: + resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: false + + /ajv-keywords@3.5.2(ajv@6.12.6): + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + dependencies: + ajv: 6.12.6 + dev: false + + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: false + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: false + + /ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + dev: false + + /ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + dev: false + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: false + + /ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + dev: false + + /any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + dev: false + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: false + + /arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + dev: false + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: false + + /aria-hidden@1.2.4: + resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==} + engines: {node: '>=10'} + dependencies: + tslib: 2.6.3 + dev: false + + /autoprefixer@10.4.14(postcss@8.4.38): + resolution: {integrity: sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + dependencies: + browserslist: 4.23.2 + caniuse-lite: 1.0.30001642 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.0.1 + postcss: 8.4.38 + postcss-value-parser: 4.2.0 + dev: false + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: false + + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: false + + /base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + dev: false + + /binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + dev: false + + /bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: false + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: false + + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: false + + /braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.1.1 + dev: false + + /browserslist@4.23.2: + resolution: {integrity: sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001642 + electron-to-chromium: 1.4.827 + node-releases: 2.0.14 + update-browserslist-db: 1.1.0(browserslist@4.23.2) + dev: false + + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: false + + /buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: false + + /busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + dependencies: + streamsearch: 1.1.0 + dev: false + + /callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + dev: false + + /camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + dev: false + + /caniuse-lite@1.0.30001642: + resolution: {integrity: sha512-3XQ0DoRgLijXJErLSl+bLnJ+Et4KqV1PY6JJBGAFlsNsz31zeAIncyeZfLCabHK/jtSh+671RM9YMldxjUPZtA==} + dev: false + + /chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + dev: false + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: false + + /chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + dev: false + + /chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + dev: false + + /cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + dependencies: + restore-cursor: 3.1.0 + dev: false + + /cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + dev: false + + /client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + dev: false + + /clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + dev: false + + /clsx@1.2.1: + resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} + engines: {node: '>=6'} + dev: false + + /clsx@2.1.0: + resolution: {integrity: sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==} + engines: {node: '>=6'} + dev: false + + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + dev: false + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: false + + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + dev: false + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: false + + /commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + dev: false + + /commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + dev: false + + /commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + dev: false + + /commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + dev: false + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: false + + /config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + dev: false + + /convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + dev: false + + /cookie@0.4.2: + resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} + engines: {node: '>= 0.6'} + dev: false + + /cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + dev: false + + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dev: false + + /cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + dev: false + + /csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + /debounce@2.0.0: + resolution: {integrity: sha512-xRetU6gL1VJbs85Mc4FoEGSjQxzpdxRyFhe3lmWFyy2EzydIcD4xzUvRJMD+NPDfMwKNhxa3PvsIOU32luIWeA==} + engines: {node: '>=18'} + dev: false + + /debug@4.3.5: + resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: false + + /deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: false + + /deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + dev: false + + /defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + dependencies: + clone: 1.0.4 + dev: false + + /detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + dev: false + + /didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + dev: false + + /dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dev: false + + /dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + dev: false + + /domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + dev: false + + /domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + dependencies: + domelementtype: 2.3.0 + dev: false + + /domutils@3.1.0: + resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dev: false + + /dotenv@16.0.3: + resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} + engines: {node: '>=12'} + dev: false + + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: false + + /editorconfig@1.0.4: + resolution: {integrity: sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==} + engines: {node: '>=14'} + hasBin: true + dependencies: + '@one-ini/wasm': 0.1.1 + commander: 10.0.1 + minimatch: 9.0.1 + semver: 7.6.2 + dev: false + + /electron-to-chromium@1.4.827: + resolution: {integrity: sha512-VY+J0e4SFcNfQy19MEoMdaIcZLmDCprqvBtkii1WTCTQHpRvf5N8+3kTYCgL/PcntvwQvmMJWTuDPsq+IlhWKQ==} + dev: false + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: false + + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: false + + /engine.io-client@6.5.4: + resolution: {integrity: sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==} + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.5 + engine.io-parser: 5.2.3 + ws: 8.17.1 + xmlhttprequest-ssl: 2.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + + /engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + dev: false + + /engine.io@6.5.5: + resolution: {integrity: sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==} + engines: {node: '>=10.2.0'} + dependencies: + '@types/cookie': 0.4.1 + '@types/cors': 2.8.17 + '@types/node': 20.14.10 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.4.2 + cors: 2.8.5 + debug: 4.3.5 + engine.io-parser: 5.2.3 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + + /enhanced-resolve@5.17.0: + resolution: {integrity: sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==} + engines: {node: '>=10.13.0'} + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + dev: false + + /entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + dev: false + + /es-module-lexer@1.5.4: + resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} + dev: false + + /esbuild@0.19.11: + resolution: {integrity: sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.19.11 + '@esbuild/android-arm': 0.19.11 + '@esbuild/android-arm64': 0.19.11 + '@esbuild/android-x64': 0.19.11 + '@esbuild/darwin-arm64': 0.19.11 + '@esbuild/darwin-x64': 0.19.11 + '@esbuild/freebsd-arm64': 0.19.11 + '@esbuild/freebsd-x64': 0.19.11 + '@esbuild/linux-arm': 0.19.11 + '@esbuild/linux-arm64': 0.19.11 + '@esbuild/linux-ia32': 0.19.11 + '@esbuild/linux-loong64': 0.19.11 + '@esbuild/linux-mips64el': 0.19.11 + '@esbuild/linux-ppc64': 0.19.11 + '@esbuild/linux-riscv64': 0.19.11 + '@esbuild/linux-s390x': 0.19.11 + '@esbuild/linux-x64': 0.19.11 + '@esbuild/netbsd-x64': 0.19.11 + '@esbuild/openbsd-x64': 0.19.11 + '@esbuild/sunos-x64': 0.19.11 + '@esbuild/win32-arm64': 0.19.11 + '@esbuild/win32-ia32': 0.19.11 + '@esbuild/win32-x64': 0.19.11 + dev: false + + /escalade@3.1.2: + resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} + engines: {node: '>=6'} + dev: false + + /escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + dev: false + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: false + + /eslint-config-prettier@9.0.0(eslint@9.7.0): + resolution: {integrity: sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + dependencies: + eslint: 9.7.0 + dev: false + + /eslint-config-turbo@1.10.12(eslint@9.7.0): + resolution: {integrity: sha512-z3jfh+D7UGYlzMWGh+Kqz++hf8LOE96q3o5R8X4HTjmxaBWlLAWG+0Ounr38h+JLR2TJno0hU9zfzoPNkR9BdA==} + peerDependencies: + eslint: '>6.6.0' + dependencies: + eslint: 9.7.0 + eslint-plugin-turbo: 1.10.12(eslint@9.7.0) + dev: false + + /eslint-plugin-turbo@1.10.12(eslint@9.7.0): + resolution: {integrity: sha512-uNbdj+ohZaYo4tFJ6dStRXu2FZigwulR1b3URPXe0Q8YaE7thuekKNP+54CHtZPH9Zey9dmDx5btAQl9mfzGOw==} + peerDependencies: + eslint: '>6.6.0' + dependencies: + dotenv: 16.0.3 + eslint: 9.7.0 + dev: false + + /eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + dev: false + + /eslint-scope@8.0.2: + resolution: {integrity: sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: false + + /eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: false + + /eslint-visitor-keys@4.0.0: + resolution: {integrity: sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dev: false + + /eslint@9.7.0: + resolution: {integrity: sha512-FzJ9D/0nGiCGBf8UXO/IGLTgLVzIxze1zpfA8Ton2mjLovXdAPlYDv+MQDcqj3TmrhAGYfOpz9RfR+ent0AgAw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@9.7.0) + '@eslint-community/regexpp': 4.11.0 + '@eslint/config-array': 0.17.0 + '@eslint/eslintrc': 3.1.0 + '@eslint/js': 9.7.0 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.3.0 + '@nodelib/fs.walk': 1.2.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.5 + escape-string-regexp: 4.0.0 + eslint-scope: 8.0.2 + eslint-visitor-keys: 4.0.0 + espree: 10.1.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.1 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: false + + /espree@10.1.0: + resolution: {integrity: sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dependencies: + acorn: 8.12.1 + acorn-jsx: 5.3.2(acorn@8.12.1) + eslint-visitor-keys: 4.0.0 + dev: false + + /esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + dev: false + + /esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + dev: false + + /estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + dev: false + + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + dev: false + + /esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: false + + /events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + dev: false + + /fast-deep-equal@2.0.1: + resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} + dev: false + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: false + + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.7 + dev: false + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: false + + /fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: false + + /fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + dependencies: + reusify: 1.0.4 + dev: false + + /file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + dependencies: + flat-cache: 4.0.1 + dev: false + + /fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: false + + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: false + + /flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + dependencies: + flatted: 3.3.1 + keyv: 4.5.4 + dev: false + + /flatted@3.3.1: + resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + dev: false + + /foreground-child@3.2.1: + resolution: {integrity: sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==} + engines: {node: '>=14'} + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + dev: false + + /fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + dev: false + + /framer-motion@10.17.4(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-CYBSs6cWfzcasAX8aofgKFZootmkQtR4qxbfTOksBLny/lbUfkGbQAFOS3qnl6Uau1N9y8tUpI7mVIrHgkFjLQ==} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.6.3 + optionalDependencies: + '@emotion/is-prop-valid': 0.8.8 + dev: false + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + dev: false + + /gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + dev: false + + /get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + dev: false + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: false + + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: false + + /glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + dev: false + + /glob@10.3.4: + resolution: {integrity: sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + foreground-child: 3.2.1 + jackspeak: 2.3.6 + minimatch: 9.0.5 + minipass: 7.1.2 + path-scurry: 1.11.1 + dev: false + + /glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + dependencies: + foreground-child: 3.2.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.0 + path-scurry: 1.11.1 + dev: false + + /globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + dev: false + + /globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + dev: false + + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + dev: false + + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + dev: false + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: false + + /hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: false + + /html-to-text@9.0.5: + resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} + engines: {node: '>=14'} + dependencies: + '@selderee/plugin-htmlparser2': 0.11.0 + deepmerge: 4.3.1 + dom-serializer: 2.0.0 + htmlparser2: 8.0.2 + selderee: 0.11.0 + dev: false + + /htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + entities: 4.5.0 + dev: false + + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: false + + /ignore@5.3.1: + resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} + engines: {node: '>= 4'} + dev: false + + /import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + dev: false + + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + dev: false + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: false + + /ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + dev: false + + /invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + dependencies: + loose-envify: 1.4.0 + dev: false + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.3.0 + dev: false + + /is-core-module@2.14.0: + resolution: {integrity: sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==} + engines: {node: '>= 0.4'} + dependencies: + hasown: 2.0.2 + dev: false + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: false + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: false + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: false + + /is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + dev: false + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: false + + /is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + dev: false + + /is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + dev: false + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: false + + /jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} + engines: {node: '>=14'} + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + dev: false + + /jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + dev: false + + /jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + dependencies: + '@types/node': 20.14.10 + merge-stream: 2.0.0 + supports-color: 8.1.1 + dev: false + + /jiti@1.21.6: + resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} + hasBin: true + dev: false + + /js-beautify@1.15.1: + resolution: {integrity: sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==} + engines: {node: '>=14'} + hasBin: true + dependencies: + config-chain: 1.1.13 + editorconfig: 1.0.4 + glob: 10.4.5 + js-cookie: 3.0.5 + nopt: 7.2.1 + dev: false + + /js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + dev: false + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: false + + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: false + + /jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + dev: false + + /json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + dev: false + + /json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + dev: false + + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: false + + /json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + dev: false + + /json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + dev: false + + /keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + dependencies: + json-buffer: 3.0.1 + dev: false + + /leac@0.6.0: + resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} + dev: false + + /levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: false + + /lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + dev: false + + /lilconfig@3.1.2: + resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==} + engines: {node: '>=14'} + dev: false + + /lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + dev: false + + /loader-runner@4.3.0: + resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} + engines: {node: '>=6.11.5'} + dev: false + + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: false + + /lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + dev: false + + /log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + dev: false + + /loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + dependencies: + js-tokens: 4.0.0 + dev: false + + /lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + dev: false + + /lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + dependencies: + yallist: 3.1.1 + dev: false + + /marked@7.0.4: + resolution: {integrity: sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ==} + engines: {node: '>= 16'} + hasBin: true + dev: false + + /md-to-react-email@5.0.2(react@18.3.1): + resolution: {integrity: sha512-x6kkpdzIzUhecda/yahltfEl53mH26QdWu4abUF9+S0Jgam8P//Ciro8cdhyMHnT5MQUJYrIbO6ORM2UxPiNNA==} + peerDependencies: + react: 18.x + dependencies: + marked: 7.0.4 + react: 18.3.1 + dev: false + + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: false + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: false + + /micromatch@4.0.7: + resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + dev: false + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: false + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: false + + /mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + dev: false + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: false + + /minimatch@9.0.1: + resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: false + + /minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: false + + /minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + dev: false + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: false + + /mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + dev: false + + /nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: false + + /natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + dev: false + + /negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + dev: false + + /neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + dev: false + + /next@14.1.4(@babel/core@7.24.5)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-1WTaXeSrUwlz/XcnhGTY7+8eiaFvdet5z9u3V2jb+Ek1vFo0VhHKSAIJvDWfQpttWjnyw14kBeq28TPq7bTeEQ==} + engines: {node: '>=18.17.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + sass: + optional: true + dependencies: + '@next/env': 14.1.4 + '@swc/helpers': 0.5.2 + busboy: 1.6.0 + caniuse-lite: 1.0.30001642 + graceful-fs: 4.2.11 + postcss: 8.4.31 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + styled-jsx: 5.1.1(@babel/core@7.24.5)(react@18.3.1) + optionalDependencies: + '@next/swc-darwin-arm64': 14.1.4 + '@next/swc-darwin-x64': 14.1.4 + '@next/swc-linux-arm64-gnu': 14.1.4 + '@next/swc-linux-arm64-musl': 14.1.4 + '@next/swc-linux-x64-gnu': 14.1.4 + '@next/swc-linux-x64-musl': 14.1.4 + '@next/swc-win32-arm64-msvc': 14.1.4 + '@next/swc-win32-ia32-msvc': 14.1.4 + '@next/swc-win32-x64-msvc': 14.1.4 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + dev: false + + /node-releases@2.0.14: + resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} + dev: false + + /nopt@7.2.1: + resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + dependencies: + abbrev: 2.0.0 + dev: false + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: false + + /normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + dev: false + + /object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + dev: false + + /object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + dev: false + + /onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + dependencies: + mimic-fn: 2.1.0 + dev: false + + /optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + dev: false + + /ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + dev: false + + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + dev: false + + /p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: false + + /package-json-from-dist@1.0.0: + resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==} + dev: false + + /parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + dev: false + + /parseley@0.12.1: + resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} + dependencies: + leac: 0.6.0 + peberminta: 0.9.0 + dev: false + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: false + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + dev: false + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: false + + /path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + dev: false + + /peberminta@0.9.0: + resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} + dev: false + + /picocolors@1.0.1: + resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + dev: false + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: false + + /pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + dev: false + + /pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + dev: false + + /postcss-import@15.1.0(postcss@8.4.38): + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + dependencies: + postcss: 8.4.38 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.8 + dev: false + + /postcss-js@4.0.1(postcss@8.4.38): + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + dependencies: + camelcase-css: 2.0.1 + postcss: 8.4.38 + dev: false + + /postcss-load-config@4.0.2(postcss@8.4.38): + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 3.1.2 + postcss: 8.4.38 + yaml: 2.4.5 + dev: false + + /postcss-nested@6.0.1(postcss@8.4.38): + resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + dependencies: + postcss: 8.4.38 + postcss-selector-parser: 6.1.1 + dev: false + + /postcss-selector-parser@6.1.1: + resolution: {integrity: sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==} + engines: {node: '>=4'} + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + dev: false + + /postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + dev: false + + /postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.1 + source-map-js: 1.0.2 + dev: false + + /postcss@8.4.38: + resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.1 + source-map-js: 1.2.0 + dev: false + + /prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + dev: false + + /prism-react-renderer@2.1.0(react@18.3.1): + resolution: {integrity: sha512-I5cvXHjA1PVGbGm1MsWCpvBCRrYyxEri0MC7/JbfIfYfcXAxHyO5PaUjs3A8H5GW6kJcLhTHxxMaOZZpRZD2iQ==} + peerDependencies: + react: '>=16.0.0' + dependencies: + '@types/prismjs': 1.26.4 + clsx: 1.2.1 + react: 18.3.1 + dev: false + + /prismjs@1.29.0: + resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} + engines: {node: '>=6'} + dev: false + + /proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + dev: false + + /punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + dev: false + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: false + + /randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /react-dom@18.3.1(react@18.3.1): + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + dev: false + + /react-email@2.1.5(eslint@9.7.0): + resolution: {integrity: sha512-SjGt5XiqNwrC6FT0rAxERj0MC9binUOVZDzspAxcRHpxjZavvePAHvV29uROWNQ1Ha7ssg1sfy4dTQi7bjCXrg==} + engines: {node: '>=18.0.0'} + hasBin: true + dependencies: + '@babel/core': 7.24.5 + '@babel/parser': 7.24.5 + '@radix-ui/colors': 1.0.1 + '@radix-ui/react-collapsible': 1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-popover': 1.1.1(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.2.47)(react@18.3.1) + '@radix-ui/react-toggle-group': 1.1.0(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-tooltip': 1.1.1(@types/react-dom@18.2.14)(@types/react@18.2.47)(react-dom@18.3.1)(react@18.3.1) + '@swc/core': 1.3.101 + '@types/react': 18.2.47 + '@types/react-dom': 18.2.14 + '@types/webpack': 5.28.5(@swc/core@1.3.101)(esbuild@0.19.11) + autoprefixer: 10.4.14(postcss@8.4.38) + chalk: 4.1.2 + chokidar: 3.5.3 + clsx: 2.1.0 + commander: 11.1.0 + debounce: 2.0.0 + esbuild: 0.19.11 + eslint-config-prettier: 9.0.0(eslint@9.7.0) + eslint-config-turbo: 1.10.12(eslint@9.7.0) + framer-motion: 10.17.4(react-dom@18.3.1)(react@18.3.1) + glob: 10.3.4 + log-symbols: 4.1.0 + mime-types: 2.1.35 + next: 14.1.4(@babel/core@7.24.5)(react-dom@18.3.1)(react@18.3.1) + normalize-path: 3.0.0 + ora: 5.4.1 + postcss: 8.4.38 + prism-react-renderer: 2.1.0(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + socket.io: 4.7.3 + socket.io-client: 4.7.3 + sonner: 1.3.1(react-dom@18.3.1)(react@18.3.1) + source-map-js: 1.0.2 + stacktrace-parser: 0.1.10 + tailwind-merge: 2.2.0 + tailwindcss: 3.4.0 + typescript: 5.1.6 + transitivePeerDependencies: + - '@opentelemetry/api' + - '@swc/helpers' + - babel-plugin-macros + - bufferutil + - eslint + - sass + - supports-color + - ts-node + - uglify-js + - utf-8-validate + - webpack-cli + dev: false + + /react-promise-suspense@0.3.4: + resolution: {integrity: sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==} + dependencies: + fast-deep-equal: 2.0.1 + dev: false + + /react-remove-scroll-bar@2.3.6(@types/react@18.2.47)(react@18.3.1): + resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.47 + react: 18.3.1 + react-style-singleton: 2.2.1(@types/react@18.2.47)(react@18.3.1) + tslib: 2.6.3 + dev: false + + /react-remove-scroll@2.5.7(@types/react@18.2.47)(react@18.3.1): + resolution: {integrity: sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.47 + react: 18.3.1 + react-remove-scroll-bar: 2.3.6(@types/react@18.2.47)(react@18.3.1) + react-style-singleton: 2.2.1(@types/react@18.2.47)(react@18.3.1) + tslib: 2.6.3 + use-callback-ref: 1.3.2(@types/react@18.2.47)(react@18.3.1) + use-sidecar: 1.1.2(@types/react@18.2.47)(react@18.3.1) + dev: false + + /react-style-singleton@2.2.1(@types/react@18.2.47)(react@18.3.1): + resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.47 + get-nonce: 1.0.1 + invariant: 2.2.4 + react: 18.3.1 + tslib: 2.6.3 + dev: false + + /react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + dependencies: + loose-envify: 1.4.0 + dev: false + + /read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + dependencies: + pify: 2.3.0 + dev: false + + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: false + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: false + + /regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + dev: false + + /resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + dev: false + + /resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + dependencies: + is-core-module: 2.14.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: false + + /restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + dev: false + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: false + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: false + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: false + + /scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + dependencies: + loose-envify: 1.4.0 + dev: false + + /schema-utils@3.3.0: + resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} + engines: {node: '>= 10.13.0'} + dependencies: + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + dev: false + + /selderee@0.11.0: + resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} + dependencies: + parseley: 0.12.1 + dev: false + + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + dev: false + + /semver@7.6.2: + resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==} + engines: {node: '>=10'} + hasBin: true + dev: false + + /serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + dependencies: + randombytes: 2.1.0 + dev: false + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + dev: false + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + dev: false + + /signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + dev: false + + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: false + + /socket.io-adapter@2.5.5: + resolution: {integrity: sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==} + dependencies: + debug: 4.3.5 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + + /socket.io-client@4.7.3: + resolution: {integrity: sha512-nU+ywttCyBitXIl9Xe0RSEfek4LneYkJxCeNnKCuhwoH4jGXO1ipIUw/VA/+Vvv2G1MTym11fzFC0SxkrcfXDw==} + engines: {node: '>=10.0.0'} + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.5 + engine.io-client: 6.5.4 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + + /socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.5 + transitivePeerDependencies: + - supports-color + dev: false + + /socket.io@4.7.3: + resolution: {integrity: sha512-SE+UIQXBQE+GPG2oszWMlsEmWtHVqw/h1VrYJGK5/MC7CH5p58N448HwIrtREcvR4jfdOJAY4ieQfxMr55qbbw==} + engines: {node: '>=10.2.0'} + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.5 + debug: 4.3.5 + engine.io: 6.5.5 + socket.io-adapter: 2.5.5 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + + /sonner@1.3.1(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-+rOAO56b2eI3q5BtgljERSn2umRk63KFIvgb2ohbZ5X+Eb5u+a/7/0ZgswYqgBMg8dyl7n6OXd9KasA8QF9ToA==} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /source-map-js@1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + dev: false + + /source-map-js@1.2.0: + resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} + engines: {node: '>=0.10.0'} + dev: false + + /source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: false + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: false + + /stacktrace-parser@0.1.10: + resolution: {integrity: sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==} + engines: {node: '>=6'} + dependencies: + type-fest: 0.7.1 + dev: false + + /streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + dev: false + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: false + + /string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + dev: false + + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: false + + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + dev: false + + /strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: false + + /styled-jsx@5.1.1(@babel/core@7.24.5)(react@18.3.1): + resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + dependencies: + '@babel/core': 7.24.5 + client-only: 0.0.1 + react: 18.3.1 + dev: false + + /sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + commander: 4.1.1 + glob: 10.4.5 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.6 + ts-interface-checker: 0.1.13 + dev: false + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: false + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: false + + /supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 + dev: false + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: false + + /tailwind-merge@2.2.0: + resolution: {integrity: sha512-SqqhhaL0T06SW59+JVNfAqKdqLs0497esifRrZ7jOaefP3o64fdFNDMrAQWZFMxTLJPiHVjRLUywT8uFz1xNWQ==} + dependencies: + '@babel/runtime': 7.24.8 + dev: false + + /tailwindcss@3.4.0: + resolution: {integrity: sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.5.3 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.2 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.6 + lilconfig: 2.1.0 + micromatch: 4.0.7 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.0.1 + postcss: 8.4.38 + postcss-import: 15.1.0(postcss@8.4.38) + postcss-js: 4.0.1(postcss@8.4.38) + postcss-load-config: 4.0.2(postcss@8.4.38) + postcss-nested: 6.0.1(postcss@8.4.38) + postcss-selector-parser: 6.1.1 + resolve: 1.22.8 + sucrase: 3.35.0 + transitivePeerDependencies: + - ts-node + dev: false + + /tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + dev: false + + /terser-webpack-plugin@5.3.10(@swc/core@1.3.101)(esbuild@0.19.11)(webpack@5.93.0): + resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + '@swc/core': 1.3.101 + esbuild: 0.19.11 + jest-worker: 27.5.1 + schema-utils: 3.3.0 + serialize-javascript: 6.0.2 + terser: 5.31.2 + webpack: 5.93.0(@swc/core@1.3.101)(esbuild@0.19.11) + dev: false + + /terser@5.31.2: + resolution: {integrity: sha512-LGyRZVFm/QElZHy/CPr/O4eNZOZIzsrQ92y4v9UJe/pFJjypje2yI3C2FmPtvUEnhadlSbmG2nXtdcjHOjCfxw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + '@jridgewell/source-map': 0.3.6 + acorn: 8.12.1 + commander: 2.20.3 + source-map-support: 0.5.21 + dev: false + + /text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + dev: false + + /thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + dependencies: + thenify: 3.3.1 + dev: false + + /thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + dependencies: + any-promise: 1.3.0 + dev: false + + /to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + dev: false + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: false + + /ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + dev: false + + /tslib@2.6.3: + resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} + dev: false + + /type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + dev: false + + /type-fest@0.7.1: + resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} + engines: {node: '>=8'} + dev: false + + /typescript@5.1.6: + resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==} + engines: {node: '>=14.17'} + hasBin: true + dev: false + + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + dev: false + + /update-browserslist-db@1.1.0(browserslist@4.23.2): + resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.23.2 + escalade: 3.1.2 + picocolors: 1.0.1 + dev: false + + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.1 + dev: false + + /use-callback-ref@1.3.2(@types/react@18.2.47)(react@18.3.1): + resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.47 + react: 18.3.1 + tslib: 2.6.3 + dev: false + + /use-sidecar@1.1.2(@types/react@18.2.47)(react@18.3.1): + resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.9.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.47 + detect-node-es: 1.1.0 + react: 18.3.1 + tslib: 2.6.3 + dev: false + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: false + + /vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + dev: false + + /watchpack@2.4.1: + resolution: {integrity: sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==} + engines: {node: '>=10.13.0'} + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + dev: false + + /wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + dependencies: + defaults: 1.0.4 + dev: false + + /webpack-sources@3.2.3: + resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} + engines: {node: '>=10.13.0'} + dev: false + + /webpack@5.93.0(@swc/core@1.3.101)(esbuild@0.19.11): + resolution: {integrity: sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.5 + '@webassemblyjs/ast': 1.12.1 + '@webassemblyjs/wasm-edit': 1.12.1 + '@webassemblyjs/wasm-parser': 1.12.1 + acorn: 8.12.1 + acorn-import-attributes: 1.9.5(acorn@8.12.1) + browserslist: 4.23.2 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.17.0 + es-module-lexer: 1.5.4 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.0 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 3.3.0 + tapable: 2.2.1 + terser-webpack-plugin: 5.3.10(@swc/core@1.3.101)(esbuild@0.19.11)(webpack@5.93.0) + watchpack: 2.4.1 + webpack-sources: 3.2.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + dev: false + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: false + + /word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + dev: false + + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: false + + /wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + dev: false + + /ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + + /xmlhttprequest-ssl@2.0.0: + resolution: {integrity: sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==} + engines: {node: '>=0.4.0'} + dev: false + + /yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + dev: false + + /yaml@2.4.5: + resolution: {integrity: sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==} + engines: {node: '>= 14'} + hasBin: true + dev: false + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + dev: false diff --git a/packages/builders/src/emails/readme.md b/packages/builders/src/emails/readme.md new file mode 100644 index 000000000..fc875bfa3 --- /dev/null +++ b/packages/builders/src/emails/readme.md @@ -0,0 +1,27 @@ +# React Email Starter + +A live preview right in your browser so you don't need to keep sending real emails during development. + +## Getting Started + +First, install the dependencies: + +```sh +npm install +# or +yarn +``` + +Then, run the development server: + +```sh +npm run dev +# or +yarn dev +``` + +Open [localhost:3000](http://localhost:3000) with your browser to see the result. + +## License + +MIT License diff --git a/packages/builders/src/index.ts b/packages/builders/src/index.ts new file mode 100644 index 000000000..f1583be5e --- /dev/null +++ b/packages/builders/src/index.ts @@ -0,0 +1,137 @@ +export * from "./auth/auth"; +// export * from "./db"; +export * from "./services/admin"; +export * from "./services/user"; +export * from "./services/project"; +export * from "./services/domain"; +export * from "./services/mariadb"; +export * from "./services/mongo"; +export * from "./services/mysql"; +export * from "./services/backup"; +export * from "./services/destination"; +export * from "./services/deployment"; +export * from "./services/mount"; +export * from "./services/certificate"; +export * from "./services/redirect"; +export * from "./services/security"; +export * from "./services/port"; +export * from "./services/redis"; +export * from "./services/compose"; +export * from "./services/registry"; +export * from "./services/notification"; +export * from "./services/ssh-key"; +export * from "./services/git-provider"; +export * from "./services/bitbucket"; +export * from "./services/github"; +export * from "./services/gitlab"; +export * from "./services/server"; +export * from "./services/application"; +export * from "./db/schema/application"; +export * from "./db/schema/postgres"; +export * from "./db/schema/user"; +export * from "./db/schema/admin"; +export * from "./db/schema/auth"; +export * from "./db/schema/project"; +export * from "./db/schema/domain"; +export * from "./db/schema/mariadb"; +export * from "./db/schema/mongo"; +export * from "./db/schema/mysql"; +export * from "./db/schema/backups"; +export * from "./db/schema/destination"; +export * from "./db/schema/deployment"; +export * from "./db/schema/mount"; +export * from "./db/schema/certificate"; +export * from "./db/schema/session"; +export * from "./db/schema/redirects"; +export * from "./db/schema/security"; +export * from "./db/schema/port"; +export * from "./db/schema/redis"; +export * from "./db/schema/shared"; +export * from "./db/schema/compose"; +export * from "./db/schema/registry"; +export * from "./db/schema/notification"; +export * from "./db/schema/ssh-key"; +export * from "./db/schema/git-provider"; +export * from "./db/schema/bitbucket"; +export * from "./db/schema/github"; +export * from "./db/schema/gitlab"; +export * from "./db/schema/server"; +export * from "./db/schema/utils"; + +export * from "./setup/config-paths"; +export * from "./setup/postgres-setup"; +export * from "./setup/redis-setup"; +export * from "./setup/registry-setup"; +export * from "./setup/server-setup"; +export * from "./setup/setup"; +export * from "./setup/traefik-setup"; + +export * from "./utils/backups/index"; +export * from "./utils/backups/mariadb"; +export * from "./utils/backups/mongo"; +export * from "./utils/backups/mysql"; +export * from "./utils/backups/postgres"; +export * from "./utils/backups/utils"; + +export * from "./utils/notifications/build-error"; +export * from "./utils/notifications/build-success"; +export * from "./utils/notifications/database-backup"; +export * from "./utils/notifications/dokploy-restart"; +export * from "./utils/notifications/utils"; +export * from "./utils/notifications/docker-cleanup"; + +export * from "./utils/builders/index"; +export * from "./utils/builders/compose"; +export * from "./utils/builders/docker-file"; +export * from "./utils/builders/drop"; +export * from "./utils/builders/heroku"; +export * from "./utils/builders/nixpacks"; +export * from "./utils/builders/paketo"; +export * from "./utils/builders/static"; +export * from "./utils/builders/utils"; + +export * from "./utils/cluster/upload"; + +export * from "./utils/docker/compose"; +export * from "./utils/docker/domain"; +export * from "./utils/docker/utils"; +export * from "./utils/docker/compose/configs"; +export * from "./utils/docker/compose/network"; +export * from "./utils/docker/compose/secrets"; +export * from "./utils/docker/compose/service"; +export * from "./utils/docker/compose/volume"; + +export * from "./utils/filesystem/directory"; +export * from "./utils/filesystem/ssh"; + +export * from "./utils/process/execAsync"; +export * from "./utils/process/spawnAsync"; +export * from "./utils/providers/bitbucket"; +export * from "./utils/providers/docker"; +export * from "./utils/providers/git"; +export * from "./utils/providers/github"; +export * from "./utils/providers/gitlab"; +export * from "./utils/providers/raw"; + +export * from "./utils/servers/remote-docker"; + +export * from "./utils/traefik/application"; +export * from "./utils/traefik/domain"; +export * from "./utils/traefik/file-types"; +export * from "./utils/traefik/middleware"; +export * from "./utils/traefik/redirect"; +export * from "./utils/traefik/registry"; +export * from "./utils/traefik/security"; +export * from "./utils/traefik/types"; +export * from "./utils/traefik/web-server"; + +export * from "./wss/docker-container-logs"; +export * from "./wss/docker-container-terminal"; +export * from "./wss/docker-stats"; +export * from "./wss/listen-deployment"; +export * from "./wss/terminal"; +export * from "./wss/utils"; + +export * from "./utils/access-log/handler"; +export * from "./utils/access-log/types"; +export * from "./utils/access-log/utils"; diff --git a/packages/builders/src/monitoring/utilts.ts b/packages/builders/src/monitoring/utilts.ts new file mode 100644 index 000000000..f67d57050 --- /dev/null +++ b/packages/builders/src/monitoring/utilts.ts @@ -0,0 +1,198 @@ +import { promises } from "node:fs"; +import type Dockerode from "dockerode"; +import osUtils from "node-os-utils"; +import { paths } from "../constants"; + +export const recordAdvancedStats = async ( + stats: Dockerode.ContainerStats, + appName: string, +) => { + const { MONITORING_PATH } = paths(); + const path = `${MONITORING_PATH}/${appName}`; + + await promises.mkdir(path, { recursive: true }); + + const cpuPercent = calculateCpuUsagePercent( + stats.cpu_stats, + stats.precpu_stats, + ); + const memoryStats = calculateMemoryStats(stats.memory_stats); + const blockIO = calculateBlockIO(stats.blkio_stats); + const networkUsage = calculateNetworkUsage(stats.networks); + + await updateStatsFile(appName, "cpu", cpuPercent); + await updateStatsFile(appName, "memory", { + used: memoryStats.used, + free: memoryStats.free, + usedPercentage: memoryStats.usedPercentage, + total: memoryStats.total, + }); + await updateStatsFile(appName, "block", { + readMb: blockIO.readMb, + writeMb: blockIO.writeMb, + }); + + await updateStatsFile(appName, "network", { + inputMb: networkUsage.inputMb, + outputMb: networkUsage.outputMb, + }); + + if (appName === "dokploy") { + const disk = await osUtils.drive.info("/"); + + const diskUsage = disk.usedGb; + const diskTotal = disk.totalGb; + const diskUsedPercentage = disk.usedPercentage; + const diskFree = disk.freeGb; + + await updateStatsFile(appName, "disk", { + diskTotal: +diskTotal, + diskUsedPercentage: +diskUsedPercentage, + diskUsage: +diskUsage, + diskFree: +diskFree, + }); + } +}; + +export const getAdvancedStats = async (appName: string) => { + return { + cpu: await readStatsFile(appName, "cpu"), + memory: await readStatsFile(appName, "memory"), + disk: await readStatsFile(appName, "disk"), + network: await readStatsFile(appName, "network"), + block: await readStatsFile(appName, "block"), + }; +}; + +export const readStatsFile = async ( + appName: string, + statType: "cpu" | "memory" | "disk" | "network" | "block", +) => { + try { + const { MONITORING_PATH } = paths(); + const filePath = `${MONITORING_PATH}/${appName}/${statType}.json`; + const data = await promises.readFile(filePath, "utf-8"); + return JSON.parse(data); + } catch (error) { + return []; + } +}; + +export const updateStatsFile = async ( + appName: string, + statType: "cpu" | "memory" | "disk" | "network" | "block", + value: number | string | unknown, +) => { + const { MONITORING_PATH } = paths(); + const stats = await readStatsFile(appName, statType); + stats.push({ value, time: new Date() }); + + if (stats.length > 288) { + stats.shift(); + } + + const content = JSON.stringify(stats); + await promises.writeFile( + `${MONITORING_PATH}/${appName}/${statType}.json`, + content, + ); +}; + +export const readLastValueStatsFile = async ( + appName: string, + statType: "cpu" | "memory" | "disk" | "network" | "block", +) => { + try { + const { MONITORING_PATH } = paths(); + const filePath = `${MONITORING_PATH}/${appName}/${statType}.json`; + const data = await promises.readFile(filePath, "utf-8"); + const stats = JSON.parse(data); + return stats[stats.length - 1] || null; + } catch (error) { + return null; + } +}; + +export const getLastAdvancedStatsFile = async (appName: string) => { + return { + cpu: await readLastValueStatsFile(appName, "cpu"), + memory: await readLastValueStatsFile(appName, "memory"), + disk: await readLastValueStatsFile(appName, "disk"), + network: await readLastValueStatsFile(appName, "network"), + block: await readLastValueStatsFile(appName, "block"), + }; +}; + +const calculateCpuUsagePercent = ( + cpu_stats: Dockerode.ContainerStats["cpu_stats"], + precpu_stats: Dockerode.ContainerStats["precpu_stats"], +) => { + const cpuDelta = + cpu_stats.cpu_usage.total_usage - precpu_stats.cpu_usage.total_usage; + const systemDelta = + cpu_stats.system_cpu_usage - precpu_stats.system_cpu_usage; + + const numberCpus = + cpu_stats.online_cpus || + (cpu_stats.cpu_usage.percpu_usage + ? cpu_stats.cpu_usage.percpu_usage.length + : 1); + + if (systemDelta > 0 && cpuDelta > 0) { + return (cpuDelta / systemDelta) * numberCpus * 100.0; + } + return 0; +}; + +const calculateMemoryStats = ( + memory_stats: Dockerode.ContainerStats["memory_stats"], +) => { + const usedMemory = memory_stats.usage - (memory_stats.stats.cache || 0); + const availableMemory = memory_stats.limit; + const memoryUsedPercentage = (usedMemory / availableMemory) * 100.0; + + return { + used: usedMemory, + free: availableMemory - usedMemory, + usedPercentage: memoryUsedPercentage, + total: availableMemory, + }; +}; +const calculateBlockIO = ( + blkio_stats: Dockerode.ContainerStats["blkio_stats"], +) => { + let readIO = 0; + let writeIO = 0; + if (blkio_stats?.io_service_bytes_recursive) { + for (const io of blkio_stats.io_service_bytes_recursive) { + if (io.op === "read") { + readIO += io.value; + } else if (io.op === "write") { + writeIO += io.value; + } + } + } + return { + readMb: readIO / (1024 * 1024), + writeMb: writeIO / (1024 * 1024), + }; +}; + +const calculateNetworkUsage = ( + networks: Dockerode.ContainerStats["networks"], +) => { + let totalRx = 0; + let totalTx = 0; + + const stats = Object.keys(networks); + + for (const interfaceName of stats) { + const net = networks[interfaceName]; + totalRx += net?.rx_bytes || 0; + totalTx += net?.tx_bytes || 0; + } + return { + inputMb: totalRx / (1024 * 1024), + outputMb: totalTx / (1024 * 1024), + }; +}; diff --git a/packages/builders/src/services/admin.ts b/packages/builders/src/services/admin.ts new file mode 100644 index 000000000..b9394bc53 --- /dev/null +++ b/packages/builders/src/services/admin.ts @@ -0,0 +1,151 @@ +import { randomBytes } from "node:crypto"; +import { db } from "@/server/db"; +import { + admins, + type apiCreateUserInvitation, + auth, + users, +} from "@/server/db/schema"; +import { TRPCError } from "@trpc/server"; +import * as bcrypt from "bcrypt"; +import { eq } from "drizzle-orm"; + +export type Admin = typeof admins.$inferSelect; +export const createInvitation = async ( + input: typeof apiCreateUserInvitation._type, +) => { + await db.transaction(async (tx) => { + const admin = await findAdmin(); + + const result = await tx + .insert(auth) + .values({ + email: input.email, + rol: "user", + password: bcrypt.hashSync("01231203012312", 10), + }) + .returning() + .then((res) => res[0]); + + if (!result) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the user", + }); + } + const expiresIn24Hours = new Date(); + expiresIn24Hours.setDate(expiresIn24Hours.getDate() + 1); + const token = randomBytes(32).toString("hex"); + await tx + .insert(users) + .values({ + adminId: admin.adminId, + authId: result.id, + token, + expirationDate: expiresIn24Hours.toISOString(), + }) + .returning(); + }); +}; + +export const findAdminById = async (adminId: string) => { + const admin = await db.query.admins.findFirst({ + where: eq(admins.adminId, adminId), + }); + if (!admin) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Admin not found", + }); + } + return admin; +}; + +export const updateAdmin = async ( + authId: string, + adminData: Partial, +) => { + const admin = await db + .update(admins) + .set({ + ...adminData, + }) + .where(eq(admins.authId, authId)) + .returning() + .then((res) => res[0]); + + return admin; +}; + +export const isAdminPresent = async () => { + const admin = await db.query.admins.findFirst(); + if (!admin) { + return false; + } + return true; +}; + +export const findAdminByAuthId = async (authId: string) => { + const admin = await db.query.admins.findFirst({ + where: eq(admins.authId, authId), + }); + if (!admin) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Admin not found", + }); + } + return admin; +}; + +export const findAdmin = async () => { + const admin = await db.query.admins.findFirst({}); + if (!admin) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Admin not found", + }); + } + return admin; +}; + +export const getUserByToken = async (token: string) => { + const user = await db.query.users.findFirst({ + where: eq(users.token, token), + with: { + auth: { + columns: { + password: false, + }, + }, + }, + }); + + if (!user) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Invitation not found", + }); + } + return { + ...user, + isExpired: user.isRegistered, + }; +}; + +export const removeUserByAuthId = async (authId: string) => { + await db + .delete(auth) + .where(eq(auth.id, authId)) + .returning() + .then((res) => res[0]); +}; + +export const getDokployUrl = async () => { + const admin = await findAdmin(); + + if (admin.host) { + return `https://${admin.host}`; + } + return `http://${admin.serverIp}:${process.env.PORT}`; +}; diff --git a/packages/builders/src/services/application.ts b/packages/builders/src/services/application.ts new file mode 100644 index 000000000..45a7ad653 --- /dev/null +++ b/packages/builders/src/services/application.ts @@ -0,0 +1,394 @@ +import { docker } from "@/server/constants"; +import { db } from "@/server/db"; +import { type apiCreateApplication, applications } from "@/server/db/schema"; +import { generateAppName } from "@/server/db/schema"; +import { getAdvancedStats } from "@/server/monitoring/utilts"; +import { + buildApplication, + getBuildCommand, + mechanizeDockerContainer, +} from "@/server/utils/builders"; +import { sendBuildErrorNotifications } from "@/server/utils/notifications/build-error"; +import { sendBuildSuccessNotifications } from "@/server/utils/notifications/build-success"; +import { execAsyncRemote } from "@/server/utils/process/execAsync"; +import { + cloneBitbucketRepository, + getBitbucketCloneCommand, +} from "@/server/utils/providers/bitbucket"; +import { + buildDocker, + buildRemoteDocker, +} from "@/server/utils/providers/docker"; +import { + cloneGitRepository, + getCustomGitCloneCommand, +} from "@/server/utils/providers/git"; +import { + cloneGithubRepository, + getGithubCloneCommand, +} from "@/server/utils/providers/github"; +import { + cloneGitlabRepository, + getGitlabCloneCommand, +} from "@/server/utils/providers/gitlab"; +import { createTraefikConfig } from "@/server/utils/traefik/application"; +import { generatePassword } from "@/server/templates/utils"; +import { TRPCError } from "@trpc/server"; +import { eq } from "drizzle-orm"; +import { getDokployUrl } from "./admin"; +import { createDeployment, updateDeploymentStatus } from "./deployment"; +import { validUniqueServerAppName } from "./project"; +export type Application = typeof applications.$inferSelect; + +export const createApplication = async ( + input: typeof apiCreateApplication._type, +) => { + input.appName = + `${input.appName}-${generatePassword(6)}` || generateAppName("app"); + if (input.appName) { + const valid = await validUniqueServerAppName(input.appName); + + if (!valid) { + throw new TRPCError({ + code: "CONFLICT", + message: "Application with this 'AppName' already exists", + }); + } + } + + return await db.transaction(async (tx) => { + const newApplication = await tx + .insert(applications) + .values({ + ...input, + }) + .returning() + .then((value) => value[0]); + + if (!newApplication) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the application", + }); + } + + if (process.env.NODE_ENV === "development") { + createTraefikConfig(newApplication.appName); + } + + return newApplication; + }); +}; + +export const findApplicationById = async (applicationId: string) => { + const application = await db.query.applications.findFirst({ + where: eq(applications.applicationId, applicationId), + with: { + project: true, + domains: true, + deployments: true, + mounts: true, + redirects: true, + security: true, + ports: true, + registry: true, + gitlab: true, + github: true, + bitbucket: true, + server: true, + }, + }); + if (!application) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Application not found", + }); + } + return application; +}; + +export const findApplicationByName = async (appName: string) => { + const application = await db.query.applications.findFirst({ + where: eq(applications.appName, appName), + }); + + return application; +}; + +export const updateApplication = async ( + applicationId: string, + applicationData: Partial, +) => { + const application = await db + .update(applications) + .set({ + ...applicationData, + }) + .where(eq(applications.applicationId, applicationId)) + .returning(); + + return application[0]; +}; + +export const updateApplicationStatus = async ( + applicationId: string, + applicationStatus: Application["applicationStatus"], +) => { + const application = await db + .update(applications) + .set({ + applicationStatus: applicationStatus, + }) + .where(eq(applications.applicationId, applicationId)) + .returning(); + + return application; +}; + +export const deployApplication = async ({ + applicationId, + titleLog = "Manual deployment", + descriptionLog = "", +}: { + applicationId: string; + titleLog: string; + descriptionLog: string; +}) => { + const application = await findApplicationById(applicationId); + const buildLink = `${await getDokployUrl()}/dashboard/project/${application.projectId}/services/application/${application.applicationId}?tab=deployments`; + const deployment = await createDeployment({ + applicationId: applicationId, + title: titleLog, + description: descriptionLog, + }); + + try { + if (application.sourceType === "github") { + await cloneGithubRepository(application, deployment.logPath); + await buildApplication(application, deployment.logPath); + } else if (application.sourceType === "gitlab") { + await cloneGitlabRepository(application, deployment.logPath); + await buildApplication(application, deployment.logPath); + } else if (application.sourceType === "bitbucket") { + await cloneBitbucketRepository(application, deployment.logPath); + await buildApplication(application, deployment.logPath); + } else if (application.sourceType === "docker") { + await buildDocker(application, deployment.logPath); + } else if (application.sourceType === "git") { + await cloneGitRepository(application, deployment.logPath); + await buildApplication(application, deployment.logPath); + } else if (application.sourceType === "drop") { + await buildApplication(application, deployment.logPath); + } + + await updateDeploymentStatus(deployment.deploymentId, "done"); + await updateApplicationStatus(applicationId, "done"); + + await sendBuildSuccessNotifications({ + projectName: application.project.name, + applicationName: application.name, + applicationType: "application", + buildLink, + }); + } catch (error) { + await updateDeploymentStatus(deployment.deploymentId, "error"); + await updateApplicationStatus(applicationId, "error"); + await sendBuildErrorNotifications({ + projectName: application.project.name, + applicationName: application.name, + applicationType: "application", + // @ts-ignore + errorMessage: error?.message || "Error to build", + buildLink, + }); + + console.log( + "Error on ", + application.buildType, + "/", + application.sourceType, + error, + ); + + throw error; + } + + return true; +}; + +export const rebuildApplication = async ({ + applicationId, + titleLog = "Rebuild deployment", + descriptionLog = "", +}: { + applicationId: string; + titleLog: string; + descriptionLog: string; +}) => { + const application = await findApplicationById(applicationId); + const deployment = await createDeployment({ + applicationId: applicationId, + title: titleLog, + description: descriptionLog, + }); + + try { + if (application.sourceType === "github") { + await buildApplication(application, deployment.logPath); + } else if (application.sourceType === "gitlab") { + await buildApplication(application, deployment.logPath); + } else if (application.sourceType === "bitbucket") { + await buildApplication(application, deployment.logPath); + } else if (application.sourceType === "docker") { + await buildDocker(application, deployment.logPath); + } else if (application.sourceType === "git") { + await buildApplication(application, deployment.logPath); + } else if (application.sourceType === "drop") { + await buildApplication(application, deployment.logPath); + } + await updateDeploymentStatus(deployment.deploymentId, "done"); + await updateApplicationStatus(applicationId, "done"); + } catch (error) { + await updateDeploymentStatus(deployment.deploymentId, "error"); + await updateApplicationStatus(applicationId, "error"); + throw error; + } + + return true; +}; + +export const deployRemoteApplication = async ({ + applicationId, + titleLog = "Manual deployment", + descriptionLog = "", +}: { + applicationId: string; + titleLog: string; + descriptionLog: string; +}) => { + const application = await findApplicationById(applicationId); + const buildLink = `${await getDokployUrl()}/dashboard/project/${application.projectId}/services/application/${application.applicationId}?tab=deployments`; + const deployment = await createDeployment({ + applicationId: applicationId, + title: titleLog, + description: descriptionLog, + }); + + try { + if (application.serverId) { + let command = "set -e;"; + if (application.sourceType === "github") { + command += await getGithubCloneCommand(application, deployment.logPath); + } else if (application.sourceType === "gitlab") { + command += await getGitlabCloneCommand(application, deployment.logPath); + } else if (application.sourceType === "bitbucket") { + command += await getBitbucketCloneCommand( + application, + deployment.logPath, + ); + } else if (application.sourceType === "git") { + command += await getCustomGitCloneCommand( + application, + deployment.logPath, + ); + } else if (application.sourceType === "docker") { + command += await buildRemoteDocker(application, deployment.logPath); + } + + if (application.sourceType !== "docker") { + command += getBuildCommand(application, deployment.logPath); + } + await execAsyncRemote(application.serverId, command); + await mechanizeDockerContainer(application); + } + + await updateDeploymentStatus(deployment.deploymentId, "done"); + await updateApplicationStatus(applicationId, "done"); + + await sendBuildSuccessNotifications({ + projectName: application.project.name, + applicationName: application.name, + applicationType: "application", + buildLink, + }); + } catch (error) { + await updateDeploymentStatus(deployment.deploymentId, "error"); + await updateApplicationStatus(applicationId, "error"); + await sendBuildErrorNotifications({ + projectName: application.project.name, + applicationName: application.name, + applicationType: "application", + // @ts-ignore + errorMessage: error?.message || "Error to build", + buildLink, + }); + + console.log( + "Error on ", + application.buildType, + "/", + application.sourceType, + error, + ); + + throw error; + } + + return true; +}; + +export const rebuildRemoteApplication = async ({ + applicationId, + titleLog = "Rebuild deployment", + descriptionLog = "", +}: { + applicationId: string; + titleLog: string; + descriptionLog: string; +}) => { + const application = await findApplicationById(applicationId); + const deployment = await createDeployment({ + applicationId: applicationId, + title: titleLog, + description: descriptionLog, + }); + + try { + if (application.serverId) { + if (application.sourceType !== "docker") { + let command = "set -e;"; + command += getBuildCommand(application, deployment.logPath); + await execAsyncRemote(application.serverId, command); + } + await mechanizeDockerContainer(application); + } + await updateDeploymentStatus(deployment.deploymentId, "done"); + await updateApplicationStatus(applicationId, "done"); + } catch (error) { + await updateDeploymentStatus(deployment.deploymentId, "error"); + await updateApplicationStatus(applicationId, "error"); + throw error; + } + + return true; +}; + +export const getApplicationStats = async (appName: string) => { + const filter = { + status: ["running"], + label: [`com.docker.swarm.service.name=${appName}`], + }; + + const containers = await docker.listContainers({ + filters: JSON.stringify(filter), + }); + + const container = containers[0]; + if (!container || container?.State !== "running") { + return null; + } + + const data = await getAdvancedStats(appName); + + return data; +}; diff --git a/packages/builders/src/services/auth.ts b/packages/builders/src/services/auth.ts new file mode 100644 index 000000000..6bbf325c8 --- /dev/null +++ b/packages/builders/src/services/auth.ts @@ -0,0 +1,180 @@ +import { randomBytes } from "node:crypto"; +import { db } from "@/server/db"; +import { + admins, + type apiCreateAdmin, + type apiCreateUser, + auth, + users, +} from "@/server/db/schema"; +import { getPublicIpWithFallback } from "@/server/wss/terminal"; +import { TRPCError } from "@trpc/server"; +import * as bcrypt from "bcrypt"; +import { eq } from "drizzle-orm"; +import encode from "hi-base32"; +import { TOTP } from "otpauth"; +import QRCode from "qrcode"; + +export type Auth = typeof auth.$inferSelect; + +export const createAdmin = async (input: typeof apiCreateAdmin._type) => { + return await db.transaction(async (tx) => { + const hashedPassword = bcrypt.hashSync(input.password, 10); + const newAuth = await tx + .insert(auth) + .values({ + email: input.email, + password: hashedPassword, + rol: "admin", + }) + .returning() + .then((res) => res[0]); + + if (!newAuth) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the user", + }); + } + + await tx + .insert(admins) + .values({ + authId: newAuth.id, + serverIp: await getPublicIpWithFallback(), + }) + .returning(); + + return newAuth; + }); +}; + +export const createUser = async (input: typeof apiCreateUser._type) => { + return await db.transaction(async (tx) => { + const hashedPassword = bcrypt.hashSync(input.password, 10); + const res = await tx + .update(auth) + .set({ + password: hashedPassword, + }) + .where(eq(auth.id, input.id)) + .returning() + .then((res) => res[0]); + + if (!res) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the user", + }); + } + + const user = await tx + .update(users) + .set({ + isRegistered: true, + expirationDate: undefined, + }) + .where(eq(users.token, input.token)) + .returning() + .then((res) => res[0]); + + return user; + }); +}; + +export const findAuthByEmail = async (email: string) => { + const result = await db.query.auth.findFirst({ + where: eq(auth.email, email), + }); + if (!result) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Auth not found", + }); + } + return result; +}; + +export const findAuthById = async (authId: string) => { + const result = await db.query.auth.findFirst({ + where: eq(auth.id, authId), + columns: { + password: false, + }, + }); + if (!result) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Auth not found", + }); + } + return result; +}; + +export const updateAuthById = async ( + authId: string, + authData: Partial, +) => { + const result = await db + .update(auth) + .set({ + ...authData, + }) + .where(eq(auth.id, authId)) + .returning(); + + return result[0]; +}; + +export const generate2FASecret = async (authId: string) => { + const auth = await findAuthById(authId); + + const base32_secret = generateBase32Secret(); + + const totp = new TOTP({ + issuer: "Dokploy", + label: `${auth?.email}`, + algorithm: "SHA1", + digits: 6, + secret: base32_secret, + }); + + const otpauth_url = totp.toString(); + + const qrUrl = await QRCode.toDataURL(otpauth_url); + + return { + qrCodeUrl: qrUrl, + secret: base32_secret, + }; +}; + +export const verify2FA = async ( + auth: Omit, + secret: string, + pin: string, +) => { + const totp = new TOTP({ + issuer: "Dokploy", + label: `${auth?.email}`, + algorithm: "SHA1", + digits: 6, + secret: secret, + }); + + const delta = totp.validate({ token: pin }); + + if (delta === null) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Invalid 2FA code", + }); + } + return auth; +}; + +const generateBase32Secret = () => { + const buffer = randomBytes(15); + const base32 = encode.encode(buffer).replace(/=/g, "").substring(0, 24); + return base32; +}; diff --git a/packages/builders/src/services/backup.ts b/packages/builders/src/services/backup.ts new file mode 100644 index 000000000..55b38c225 --- /dev/null +++ b/packages/builders/src/services/backup.ts @@ -0,0 +1,71 @@ +import { db } from "@/server/db"; +import { type apiCreateBackup, backups } from "@/server/db/schema"; +import { TRPCError } from "@trpc/server"; +import { eq } from "drizzle-orm"; + +export type Backup = typeof backups.$inferSelect; + +export type BackupSchedule = Awaited>; + +export const createBackup = async (input: typeof apiCreateBackup._type) => { + const newBackup = await db + .insert(backups) + .values({ + ...input, + }) + .returning() + .then((value) => value[0]); + + if (!newBackup) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the Backup", + }); + } + + return newBackup; +}; + +export const findBackupById = async (backupId: string) => { + const backup = await db.query.backups.findFirst({ + where: eq(backups.backupId, backupId), + with: { + postgres: true, + mysql: true, + mariadb: true, + mongo: true, + destination: true, + }, + }); + if (!backup) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Backup not found", + }); + } + return backup; +}; + +export const updateBackupById = async ( + backupId: string, + backupData: Partial, +) => { + const result = await db + .update(backups) + .set({ + ...backupData, + }) + .where(eq(backups.backupId, backupId)) + .returning(); + + return result[0]; +}; + +export const removeBackupById = async (backupId: string) => { + const result = await db + .delete(backups) + .where(eq(backups.backupId, backupId)) + .returning(); + + return result[0]; +}; diff --git a/packages/builders/src/services/bitbucket.ts b/packages/builders/src/services/bitbucket.ts new file mode 100644 index 000000000..11ae0903d --- /dev/null +++ b/packages/builders/src/services/bitbucket.ts @@ -0,0 +1,88 @@ +import { db } from "@/server/db"; +import { + type apiCreateBitbucket, + type apiUpdateBitbucket, + bitbucket, + gitProvider, +} from "@/server/db/schema"; +import { TRPCError } from "@trpc/server"; +import { eq } from "drizzle-orm"; + +export type Bitbucket = typeof bitbucket.$inferSelect; + +export const createBitbucket = async ( + input: typeof apiCreateBitbucket._type, +) => { + return await db.transaction(async (tx) => { + const newGitProvider = await tx + .insert(gitProvider) + .values({ + providerType: "bitbucket", + authId: input.authId, + name: input.name, + }) + .returning() + .then((response) => response[0]); + + if (!newGitProvider) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the git provider", + }); + } + + await tx + .insert(bitbucket) + .values({ + ...input, + gitProviderId: newGitProvider?.gitProviderId, + }) + .returning() + .then((response) => response[0]); + }); +}; + +export const findBitbucketById = async (bitbucketId: string) => { + const bitbucketProviderResult = await db.query.bitbucket.findFirst({ + where: eq(bitbucket.bitbucketId, bitbucketId), + with: { + gitProvider: true, + }, + }); + + if (!bitbucketProviderResult) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Bitbucket Provider not found", + }); + } + + return bitbucketProviderResult; +}; + +export const updateBitbucket = async ( + bitbucketId: string, + input: typeof apiUpdateBitbucket._type, +) => { + return await db.transaction(async (tx) => { + const result = await tx + .update(bitbucket) + .set({ + ...input, + }) + .where(eq(bitbucket.bitbucketId, bitbucketId)) + .returning(); + + if (input.name) { + await tx + .update(gitProvider) + .set({ + name: input.name, + }) + .where(eq(gitProvider.gitProviderId, input.gitProviderId)) + .returning(); + } + + return result[0]; + }); +}; diff --git a/packages/builders/src/services/certificate.ts b/packages/builders/src/services/certificate.ts new file mode 100644 index 000000000..3ed1dcd19 --- /dev/null +++ b/packages/builders/src/services/certificate.ts @@ -0,0 +1,108 @@ +import fs from "node:fs"; +import path from "node:path"; +import { paths } from "@/server/constants"; +import { db } from "@/server/db"; +import { type apiCreateCertificate, certificates } from "@/server/db/schema"; +import { removeDirectoryIfExistsContent } from "@/server/utils/filesystem/directory"; +import { TRPCError } from "@trpc/server"; +import { eq } from "drizzle-orm"; +import { dump } from "js-yaml"; +import type { z } from "zod"; + +export type Certificate = typeof certificates.$inferSelect; + +export const findCertificateById = async (certificateId: string) => { + const certificate = await db.query.certificates.findFirst({ + where: eq(certificates.certificateId, certificateId), + }); + + if (!certificate) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Certificate not found", + }); + } + + return certificate; +}; + +export const createCertificate = async ( + certificateData: z.infer, +) => { + const certificate = await db + .insert(certificates) + .values({ + ...certificateData, + }) + .returning(); + + if (!certificate || certificate[0] === undefined) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Failed to create the certificate", + }); + } + + const cer = certificate[0]; + + createCertificateFiles(cer); + return cer; +}; + +export const removeCertificateById = async (certificateId: string) => { + const { CERTIFICATES_PATH } = paths(); + const certificate = await findCertificateById(certificateId); + const certDir = path.join(CERTIFICATES_PATH, certificate.certificatePath); + + await removeDirectoryIfExistsContent(certDir); + const result = await db + .delete(certificates) + .where(eq(certificates.certificateId, certificateId)) + .returning(); + + if (!result) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Failed to delete the certificate", + }); + } + + return result; +}; + +export const findCertificates = async () => { + return await db.query.certificates.findMany(); +}; + +const createCertificateFiles = (certificate: Certificate) => { + const { CERTIFICATES_PATH } = paths(); + const dockerPath = "/etc/traefik"; + const certDir = path.join(CERTIFICATES_PATH, certificate.certificatePath); + const crtPath = path.join(certDir, "chain.crt"); + const keyPath = path.join(certDir, "privkey.key"); + + const chainPath = path.join(dockerPath, certDir, "chain.crt"); + const keyPathDocker = path.join(dockerPath, certDir, "privkey.key"); + + if (!fs.existsSync(certDir)) { + fs.mkdirSync(certDir, { recursive: true }); + } + + fs.writeFileSync(crtPath, certificate.certificateData); + fs.writeFileSync(keyPath, certificate.privateKey); + + const traefikConfig = { + tls: { + certificates: [ + { + certFile: chainPath, + keyFile: keyPathDocker, + }, + ], + }, + }; + + const yamlConfig = dump(traefikConfig); + const configFile = path.join(certDir, "certificate.yml"); + fs.writeFileSync(configFile, yamlConfig); +}; diff --git a/packages/builders/src/services/cluster.ts b/packages/builders/src/services/cluster.ts new file mode 100644 index 000000000..ea71d1aee --- /dev/null +++ b/packages/builders/src/services/cluster.ts @@ -0,0 +1,41 @@ +export interface DockerNode { + ID: string; + Version: { + Index: number; + }; + CreatedAt: string; + UpdatedAt: string; + Spec: { + Name: string; + Labels: Record; + Role: "worker" | "manager"; + Availability: "active" | "pause" | "drain"; + }; + Description: { + Hostname: string; + Platform: { + Architecture: string; + OS: string; + }; + Resources: { + NanoCPUs: number; + MemoryBytes: number; + }; + Engine: { + EngineVersion: string; + Plugins: Array<{ + Type: string; + Name: string; + }>; + }; + }; + Status: { + State: "unknown" | "down" | "ready" | "disconnected"; + Message: string; + Addr: string; + }; + ManagerStatus?: { + Leader: boolean; + Addr: string; + }; +} diff --git a/packages/builders/src/services/compose.ts b/packages/builders/src/services/compose.ts new file mode 100644 index 000000000..bacee42dc --- /dev/null +++ b/packages/builders/src/services/compose.ts @@ -0,0 +1,467 @@ +import { join } from "node:path"; +import { paths } from "@/server/constants"; +import { db } from "@/server/db"; +import { type apiCreateCompose, compose } from "@/server/db/schema"; +import { generateAppName } from "@/server/db/schema"; +import { + buildCompose, + getBuildComposeCommand, +} from "@/server/utils/builders/compose"; +import { randomizeSpecificationFile } from "@/server/utils/docker/compose"; +import { + cloneCompose, + cloneComposeRemote, + loadDockerCompose, + loadDockerComposeRemote, +} from "@/server/utils/docker/domain"; +import type { ComposeSpecification } from "@/server/utils/docker/types"; +import { sendBuildErrorNotifications } from "@/server/utils/notifications/build-error"; +import { sendBuildSuccessNotifications } from "@/server/utils/notifications/build-success"; +import { execAsync, execAsyncRemote } from "@/server/utils/process/execAsync"; +import { + cloneBitbucketRepository, + getBitbucketCloneCommand, +} from "@/server/utils/providers/bitbucket"; +import { + cloneGitRepository, + getCustomGitCloneCommand, +} from "@/server/utils/providers/git"; +import { + cloneGithubRepository, + getGithubCloneCommand, +} from "@/server/utils/providers/github"; +import { + cloneGitlabRepository, + getGitlabCloneCommand, +} from "@/server/utils/providers/gitlab"; +import { + createComposeFile, + getCreateComposeFileCommand, +} from "@/server/utils/providers/raw"; +import { generatePassword } from "@/server/templates/utils"; +import { TRPCError } from "@trpc/server"; +import { eq } from "drizzle-orm"; +import { getDokployUrl } from "./admin"; +import { createDeploymentCompose, updateDeploymentStatus } from "./deployment"; +import { validUniqueServerAppName } from "./project"; + +export type Compose = typeof compose.$inferSelect; + +export const createCompose = async (input: typeof apiCreateCompose._type) => { + input.appName = + `${input.appName}-${generatePassword(6)}` || generateAppName("compose"); + if (input.appName) { + const valid = await validUniqueServerAppName(input.appName); + + if (!valid) { + throw new TRPCError({ + code: "CONFLICT", + message: "Service with this 'AppName' already exists", + }); + } + } + + const newDestination = await db + .insert(compose) + .values({ + ...input, + composeFile: "", + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting compose", + }); + } + + return newDestination; +}; + +export const createComposeByTemplate = async ( + input: typeof compose.$inferInsert, +) => { + if (input.appName) { + const valid = await validUniqueServerAppName(input.appName); + + if (!valid) { + throw new TRPCError({ + code: "CONFLICT", + message: "Service with this 'AppName' already exists", + }); + } + } + const newDestination = await db + .insert(compose) + .values({ + ...input, + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting compose", + }); + } + + return newDestination; +}; + +export const findComposeById = async (composeId: string) => { + const result = await db.query.compose.findFirst({ + where: eq(compose.composeId, composeId), + with: { + project: true, + deployments: true, + mounts: true, + domains: true, + github: true, + gitlab: true, + bitbucket: true, + server: true, + }, + }); + if (!result) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Compose not found", + }); + } + return result; +}; + +export const loadServices = async ( + composeId: string, + type: "fetch" | "cache" = "fetch", +) => { + const compose = await findComposeById(composeId); + + if (type === "fetch") { + if (compose.serverId) { + await cloneComposeRemote(compose); + } else { + await cloneCompose(compose); + } + } + + let composeData: ComposeSpecification | null; + + if (compose.serverId) { + composeData = await loadDockerComposeRemote(compose); + } else { + composeData = await loadDockerCompose(compose); + } + + if (compose.randomize && composeData) { + const randomizedCompose = randomizeSpecificationFile( + composeData, + compose.suffix, + ); + composeData = randomizedCompose; + } + + if (!composeData?.services) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Services not found", + }); + } + + const services = Object.keys(composeData.services); + + return [...services]; +}; + +export const updateCompose = async ( + composeId: string, + composeData: Partial, +) => { + const composeResult = await db + .update(compose) + .set({ + ...composeData, + }) + .where(eq(compose.composeId, composeId)) + .returning(); + + return composeResult[0]; +}; + +export const deployCompose = async ({ + composeId, + titleLog = "Manual deployment", + descriptionLog = "", +}: { + composeId: string; + titleLog: string; + descriptionLog: string; +}) => { + const compose = await findComposeById(composeId); + const buildLink = `${await getDokployUrl()}/dashboard/project/${compose.projectId}/services/compose/${compose.composeId}?tab=deployments`; + const deployment = await createDeploymentCompose({ + composeId: composeId, + title: titleLog, + description: descriptionLog, + }); + + try { + if (compose.sourceType === "github") { + await cloneGithubRepository(compose, deployment.logPath, true); + } else if (compose.sourceType === "gitlab") { + await cloneGitlabRepository(compose, deployment.logPath, true); + } else if (compose.sourceType === "bitbucket") { + await cloneBitbucketRepository(compose, deployment.logPath, true); + } else if (compose.sourceType === "git") { + await cloneGitRepository(compose, deployment.logPath, true); + } else if (compose.sourceType === "raw") { + await createComposeFile(compose, deployment.logPath); + } + await buildCompose(compose, deployment.logPath); + await updateDeploymentStatus(deployment.deploymentId, "done"); + await updateCompose(composeId, { + composeStatus: "done", + }); + + await sendBuildSuccessNotifications({ + projectName: compose.project.name, + applicationName: compose.name, + applicationType: "compose", + buildLink, + }); + } catch (error) { + await updateDeploymentStatus(deployment.deploymentId, "error"); + await updateCompose(composeId, { + composeStatus: "error", + }); + await sendBuildErrorNotifications({ + projectName: compose.project.name, + applicationName: compose.name, + applicationType: "compose", + // @ts-ignore + errorMessage: error?.message || "Error to build", + buildLink, + }); + throw error; + } +}; + +export const rebuildCompose = async ({ + composeId, + titleLog = "Rebuild deployment", + descriptionLog = "", +}: { + composeId: string; + titleLog: string; + descriptionLog: string; +}) => { + const compose = await findComposeById(composeId); + const deployment = await createDeploymentCompose({ + composeId: composeId, + title: titleLog, + description: descriptionLog, + }); + + try { + if (compose.serverId) { + await getBuildComposeCommand(compose, deployment.logPath); + } else { + await buildCompose(compose, deployment.logPath); + } + + await updateDeploymentStatus(deployment.deploymentId, "done"); + await updateCompose(composeId, { + composeStatus: "done", + }); + } catch (error) { + await updateDeploymentStatus(deployment.deploymentId, "error"); + await updateCompose(composeId, { + composeStatus: "error", + }); + throw error; + } + + return true; +}; + +export const deployRemoteCompose = async ({ + composeId, + titleLog = "Manual deployment", + descriptionLog = "", +}: { + composeId: string; + titleLog: string; + descriptionLog: string; +}) => { + const compose = await findComposeById(composeId); + const buildLink = `${await getDokployUrl()}/dashboard/project/${compose.projectId}/services/compose/${compose.composeId}?tab=deployments`; + const deployment = await createDeploymentCompose({ + composeId: composeId, + title: titleLog, + description: descriptionLog, + }); + try { + if (compose.serverId) { + let command = "set -e;"; + + if (compose.sourceType === "github") { + command += await getGithubCloneCommand( + compose, + deployment.logPath, + true, + ); + } else if (compose.sourceType === "gitlab") { + command += await getGitlabCloneCommand( + compose, + deployment.logPath, + true, + ); + } else if (compose.sourceType === "bitbucket") { + command += await getBitbucketCloneCommand( + compose, + deployment.logPath, + true, + ); + } else if (compose.sourceType === "git") { + command += await getCustomGitCloneCommand( + compose, + deployment.logPath, + true, + ); + } else if (compose.sourceType === "raw") { + command += getCreateComposeFileCommand(compose, deployment.logPath); + } + + await execAsyncRemote(compose.serverId, command); + await getBuildComposeCommand(compose, deployment.logPath); + } + + await updateDeploymentStatus(deployment.deploymentId, "done"); + await updateCompose(composeId, { + composeStatus: "done", + }); + + await sendBuildSuccessNotifications({ + projectName: compose.project.name, + applicationName: compose.name, + applicationType: "compose", + buildLink, + }); + } catch (error) { + console.log(error); + await updateDeploymentStatus(deployment.deploymentId, "error"); + await updateCompose(composeId, { + composeStatus: "error", + }); + await sendBuildErrorNotifications({ + projectName: compose.project.name, + applicationName: compose.name, + applicationType: "compose", + // @ts-ignore + errorMessage: error?.message || "Error to build", + buildLink, + }); + throw error; + } +}; + +export const rebuildRemoteCompose = async ({ + composeId, + titleLog = "Rebuild deployment", + descriptionLog = "", +}: { + composeId: string; + titleLog: string; + descriptionLog: string; +}) => { + const compose = await findComposeById(composeId); + const deployment = await createDeploymentCompose({ + composeId: composeId, + title: titleLog, + description: descriptionLog, + }); + + try { + if (compose.serverId) { + await getBuildComposeCommand(compose, deployment.logPath); + } + + await updateDeploymentStatus(deployment.deploymentId, "done"); + await updateCompose(composeId, { + composeStatus: "done", + }); + } catch (error) { + await updateDeploymentStatus(deployment.deploymentId, "error"); + await updateCompose(composeId, { + composeStatus: "error", + }); + throw error; + } + + return true; +}; + +export const removeCompose = async (compose: Compose) => { + try { + const { COMPOSE_PATH } = paths(!!compose.serverId); + const projectPath = join(COMPOSE_PATH, compose.appName); + + if (compose.composeType === "stack") { + const command = `cd ${projectPath} && docker stack rm ${compose.appName} && rm -rf ${projectPath}`; + if (compose.serverId) { + await execAsyncRemote(compose.serverId, command); + } else { + await execAsync(command); + } + await execAsync(command, { + cwd: projectPath, + }); + } else { + const command = `cd ${projectPath} && docker compose -p ${compose.appName} down && rm -rf ${projectPath}`; + if (compose.serverId) { + await execAsyncRemote(compose.serverId, command); + } else { + await execAsync(command, { + cwd: projectPath, + }); + } + } + } catch (error) { + throw error; + } + + return true; +}; + +export const stopCompose = async (composeId: string) => { + const compose = await findComposeById(composeId); + try { + const { COMPOSE_PATH } = paths(!!compose.serverId); + if (compose.composeType === "docker-compose") { + if (compose.serverId) { + await execAsyncRemote( + compose.serverId, + `cd ${join(COMPOSE_PATH, compose.appName)} && docker compose -p ${compose.appName} stop`, + ); + } else { + await execAsync(`docker compose -p ${compose.appName} stop`, { + cwd: join(COMPOSE_PATH, compose.appName), + }); + } + } + + await updateCompose(composeId, { + composeStatus: "idle", + }); + } catch (error) { + await updateCompose(composeId, { + composeStatus: "error", + }); + throw error; + } + + return true; +}; diff --git a/packages/builders/src/services/deployment.ts b/packages/builders/src/services/deployment.ts new file mode 100644 index 000000000..5ea39c577 --- /dev/null +++ b/packages/builders/src/services/deployment.ts @@ -0,0 +1,372 @@ +import { existsSync, promises as fsPromises } from "node:fs"; +import path from "node:path"; +import { paths } from "@/server/constants"; +import { db } from "@/server/db"; +import { + type apiCreateDeployment, + type apiCreateDeploymentCompose, + type apiCreateDeploymentServer, + deployments, +} from "@/server/db/schema"; +import { removeDirectoryIfExistsContent } from "@/server/utils/filesystem/directory"; +import { TRPCError } from "@trpc/server"; +import { format } from "date-fns"; +import { desc, eq } from "drizzle-orm"; +import { + type Application, + findApplicationById, + updateApplicationStatus, +} from "./application"; +import { type Compose, findComposeById, updateCompose } from "./compose"; +import { type Server, findServerById } from "./server"; + +import { execAsyncRemote } from "@/server/utils/process/execAsync"; + +export type Deployment = typeof deployments.$inferSelect; + +export const findDeploymentById = async (applicationId: string) => { + const application = await db.query.deployments.findFirst({ + where: eq(deployments.applicationId, applicationId), + with: { + application: true, + }, + }); + if (!application) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Deployment not found", + }); + } + return application; +}; + +export const createDeployment = async ( + deployment: Omit< + typeof apiCreateDeployment._type, + "deploymentId" | "createdAt" | "status" | "logPath" + >, +) => { + const application = await findApplicationById(deployment.applicationId); + + try { + // await removeLastTenDeployments(deployment.applicationId); + const { LOGS_PATH } = paths(!!application.serverId); + const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss"); + const fileName = `${application.appName}-${formattedDateTime}.log`; + const logFilePath = path.join(LOGS_PATH, application.appName, fileName); + + if (application.serverId) { + const server = await findServerById(application.serverId); + + const command = ` + mkdir -p ${LOGS_PATH}/${application.appName}; + echo "Initializing deployment" >> ${logFilePath}; + `; + + await execAsyncRemote(server.serverId, command); + } else { + await fsPromises.mkdir(path.join(LOGS_PATH, application.appName), { + recursive: true, + }); + await fsPromises.writeFile(logFilePath, "Initializing deployment"); + } + + const deploymentCreate = await db + .insert(deployments) + .values({ + applicationId: deployment.applicationId, + title: deployment.title || "Deployment", + status: "running", + logPath: logFilePath, + description: deployment.description || "", + }) + .returning(); + if (deploymentCreate.length === 0 || !deploymentCreate[0]) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the deployment", + }); + } + return deploymentCreate[0]; + } catch (error) { + await updateApplicationStatus(application.applicationId, "error"); + console.log(error); + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the deployment", + }); + } +}; + +export const createDeploymentCompose = async ( + deployment: Omit< + typeof apiCreateDeploymentCompose._type, + "deploymentId" | "createdAt" | "status" | "logPath" + >, +) => { + const compose = await findComposeById(deployment.composeId); + try { + // await removeLastTenComposeDeployments(deployment.composeId); + const { LOGS_PATH } = paths(!!compose.serverId); + const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss"); + const fileName = `${compose.appName}-${formattedDateTime}.log`; + const logFilePath = path.join(LOGS_PATH, compose.appName, fileName); + + if (compose.serverId) { + const server = await findServerById(compose.serverId); + + const command = ` +mkdir -p ${LOGS_PATH}/${compose.appName}; +echo "Initializing deployment" >> ${logFilePath}; +`; + + await execAsyncRemote(server.serverId, command); + } else { + await fsPromises.mkdir(path.join(LOGS_PATH, compose.appName), { + recursive: true, + }); + await fsPromises.writeFile(logFilePath, "Initializing deployment"); + } + + const deploymentCreate = await db + .insert(deployments) + .values({ + composeId: deployment.composeId, + title: deployment.title || "Deployment", + description: deployment.description || "", + status: "running", + logPath: logFilePath, + }) + .returning(); + if (deploymentCreate.length === 0 || !deploymentCreate[0]) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the deployment", + }); + } + return deploymentCreate[0]; + } catch (error) { + await updateCompose(compose.composeId, { + composeStatus: "error", + }); + console.log(error); + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the deployment", + }); + } +}; + +export const removeDeployment = async (deploymentId: string) => { + try { + const deployment = await db + .delete(deployments) + .where(eq(deployments.deploymentId, deploymentId)) + .returning(); + return deployment[0]; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to delete this deployment", + }); + } +}; + +export const removeDeploymentsByApplicationId = async ( + applicationId: string, +) => { + await db + .delete(deployments) + .where(eq(deployments.applicationId, applicationId)) + .returning(); +}; + +const removeLastTenDeployments = async (applicationId: string) => { + const deploymentList = await db.query.deployments.findMany({ + where: eq(deployments.applicationId, applicationId), + orderBy: desc(deployments.createdAt), + }); + if (deploymentList.length > 10) { + const deploymentsToDelete = deploymentList.slice(10); + for (const oldDeployment of deploymentsToDelete) { + const logPath = path.join(oldDeployment.logPath); + if (existsSync(logPath)) { + await fsPromises.unlink(logPath); + } + await removeDeployment(oldDeployment.deploymentId); + } + } +}; + +const removeLastTenComposeDeployments = async (composeId: string) => { + const deploymentList = await db.query.deployments.findMany({ + where: eq(deployments.composeId, composeId), + orderBy: desc(deployments.createdAt), + }); + if (deploymentList.length > 10) { + const deploymentsToDelete = deploymentList.slice(10); + for (const oldDeployment of deploymentsToDelete) { + const logPath = path.join(oldDeployment.logPath); + if (existsSync(logPath)) { + await fsPromises.unlink(logPath); + } + await removeDeployment(oldDeployment.deploymentId); + } + } +}; + +export const removeDeployments = async (application: Application) => { + const { appName, applicationId } = application; + const { LOGS_PATH } = paths(!!application.serverId); + const logsPath = path.join(LOGS_PATH, appName); + if (application.serverId) { + await execAsyncRemote(application.serverId, `rm -rf ${logsPath}`); + } else { + await removeDirectoryIfExistsContent(logsPath); + } + await removeDeploymentsByApplicationId(applicationId); +}; + +export const removeDeploymentsByComposeId = async (compose: Compose) => { + const { appName } = compose; + const { LOGS_PATH } = paths(!!compose.serverId); + const logsPath = path.join(LOGS_PATH, appName); + if (compose.serverId) { + await execAsyncRemote(compose.serverId, `rm -rf ${logsPath}`); + } else { + await removeDirectoryIfExistsContent(logsPath); + } + + await db + .delete(deployments) + .where(eq(deployments.composeId, compose.composeId)) + .returning(); +}; + +export const findAllDeploymentsByApplicationId = async ( + applicationId: string, +) => { + const deploymentsList = await db.query.deployments.findMany({ + where: eq(deployments.applicationId, applicationId), + orderBy: desc(deployments.createdAt), + }); + return deploymentsList; +}; + +export const findAllDeploymentsByComposeId = async (composeId: string) => { + const deploymentsList = await db.query.deployments.findMany({ + where: eq(deployments.composeId, composeId), + orderBy: desc(deployments.createdAt), + }); + return deploymentsList; +}; + +export const updateDeployment = async ( + deploymentId: string, + deploymentData: Partial, +) => { + const application = await db + .update(deployments) + .set({ + ...deploymentData, + }) + .where(eq(deployments.deploymentId, deploymentId)) + .returning(); + + return application; +}; + +export const updateDeploymentStatus = async ( + deploymentId: string, + deploymentStatus: Deployment["status"], +) => { + const application = await db + .update(deployments) + .set({ + status: deploymentStatus, + }) + .where(eq(deployments.deploymentId, deploymentId)) + .returning(); + + return application; +}; + +export const createServerDeployment = async ( + deployment: Omit< + typeof apiCreateDeploymentServer._type, + "deploymentId" | "createdAt" | "status" | "logPath" + >, +) => { + try { + const { LOGS_PATH } = paths(); + + const server = await findServerById(deployment.serverId); + await removeLastFiveDeployments(deployment.serverId); + const formattedDateTime = format(new Date(), "yyyy-MM-dd:HH:mm:ss"); + const fileName = `${server.appName}-${formattedDateTime}.log`; + const logFilePath = path.join(LOGS_PATH, server.appName, fileName); + await fsPromises.mkdir(path.join(LOGS_PATH, server.appName), { + recursive: true, + }); + await fsPromises.writeFile(logFilePath, "Initializing Setup Server"); + const deploymentCreate = await db + .insert(deployments) + .values({ + serverId: server.serverId, + title: deployment.title || "Deployment", + description: deployment.description || "", + status: "running", + logPath: logFilePath, + }) + .returning(); + if (deploymentCreate.length === 0 || !deploymentCreate[0]) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the deployment", + }); + } + return deploymentCreate[0]; + } catch (error) { + console.log(error); + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the deployment", + }); + } +}; + +export const removeLastFiveDeployments = async (serverId: string) => { + const deploymentList = await db.query.deployments.findMany({ + where: eq(deployments.serverId, serverId), + orderBy: desc(deployments.createdAt), + }); + if (deploymentList.length >= 5) { + const deploymentsToDelete = deploymentList.slice(4); + for (const oldDeployment of deploymentsToDelete) { + const logPath = path.join(oldDeployment.logPath); + if (existsSync(logPath)) { + await fsPromises.unlink(logPath); + } + await removeDeployment(oldDeployment.deploymentId); + } + } +}; + +export const removeDeploymentsByServerId = async (server: Server) => { + const { LOGS_PATH } = paths(); + const { appName } = server; + const logsPath = path.join(LOGS_PATH, appName); + await removeDirectoryIfExistsContent(logsPath); + await db + .delete(deployments) + .where(eq(deployments.serverId, server.serverId)) + .returning(); +}; + +export const findAllDeploymentsByServerId = async (serverId: string) => { + const deploymentsList = await db.query.deployments.findMany({ + where: eq(deployments.serverId, serverId), + orderBy: desc(deployments.createdAt), + }); + return deploymentsList; +}; diff --git a/packages/builders/src/services/destination.ts b/packages/builders/src/services/destination.ts new file mode 100644 index 000000000..dfc15649b --- /dev/null +++ b/packages/builders/src/services/destination.ts @@ -0,0 +1,67 @@ +import { db } from "@/server/db"; +import { type apiCreateDestination, destinations } from "@/server/db/schema"; +import { TRPCError } from "@trpc/server"; +import { eq } from "drizzle-orm"; +import { findAdmin } from "./admin"; + +export type Destination = typeof destinations.$inferSelect; + +export const createDestintation = async ( + input: typeof apiCreateDestination._type, +) => { + const adminResponse = await findAdmin(); + const newDestination = await db + .insert(destinations) + .values({ + ...input, + adminId: adminResponse.adminId, + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting destination", + }); + } + + return newDestination; +}; + +export const findDestinationById = async (destinationId: string) => { + const destination = await db.query.destinations.findFirst({ + where: eq(destinations.destinationId, destinationId), + }); + if (!destination) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Destination not found", + }); + } + return destination; +}; + +export const removeDestinationById = async (destinationId: string) => { + const result = await db + .delete(destinations) + .where(eq(destinations.destinationId, destinationId)) + .returning(); + + return result[0]; +}; + +export const updateDestinationById = async ( + destinationId: string, + destinationData: Partial, +) => { + const result = await db + .update(destinations) + .set({ + ...destinationData, + }) + .where(eq(destinations.destinationId, destinationId)) + .returning(); + + return result[0]; +}; diff --git a/packages/builders/src/services/docker.ts b/packages/builders/src/services/docker.ts new file mode 100644 index 000000000..d611a11d1 --- /dev/null +++ b/packages/builders/src/services/docker.ts @@ -0,0 +1,223 @@ +import { execAsync, execAsyncRemote } from "@/server/utils/process/execAsync"; + +export const getContainers = async (serverId?: string | null) => { + try { + const command = + "docker ps -a --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | Image: {{.Image}} | Ports: {{.Ports}} | State: {{.State}} | Status: {{.Status}}'"; + let stdout = ""; + let stderr = ""; + + if (serverId) { + const result = await execAsyncRemote(serverId, command); + + stdout = result.stdout; + stderr = result.stderr; + } else { + const result = await execAsync(command); + stdout = result.stdout; + stderr = result.stderr; + } + if (stderr) { + console.error(`Error: ${stderr}`); + return; + } + + const lines = stdout.trim().split("\n"); + + const containers = lines + .map((line) => { + const parts = line.split(" | "); + const containerId = parts[0] + ? parts[0].replace("CONTAINER ID : ", "").trim() + : "No container id"; + const name = parts[1] + ? parts[1].replace("Name: ", "").trim() + : "No container name"; + const image = parts[2] + ? parts[2].replace("Image: ", "").trim() + : "No image"; + const ports = parts[3] + ? parts[3].replace("Ports: ", "").trim() + : "No ports"; + const state = parts[4] + ? parts[4].replace("State: ", "").trim() + : "No state"; + const status = parts[5] + ? parts[5].replace("Status: ", "").trim() + : "No status"; + return { + containerId, + name, + image, + ports, + state, + status, + serverId, + }; + }) + .filter((container) => !container.name.includes("dokploy")); + + return containers; + } catch (error) { + console.error(error); + + return []; + } +}; + +export const getConfig = async ( + containerId: string, + serverId?: string | null, +) => { + try { + const command = `docker inspect ${containerId} --format='{{json .}}'`; + let stdout = ""; + let stderr = ""; + if (serverId) { + const result = await execAsyncRemote(serverId, command); + stdout = result.stdout; + stderr = result.stderr; + } else { + const result = await execAsync(command); + stdout = result.stdout; + stderr = result.stderr; + } + + if (stderr) { + console.error(`Error: ${stderr}`); + return; + } + + const config = JSON.parse(stdout); + + return config; + } catch (error) {} +}; + +export const getContainersByAppNameMatch = async ( + appName: string, + appType?: "stack" | "docker-compose", + serverId?: string, +) => { + try { + let result: string[] = []; + const cmd = + "docker ps -a --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}}'"; + + const command = + appType === "docker-compose" + ? `${cmd} --filter='label=com.docker.compose.project=${appName}'` + : `${cmd} | grep ${appName}`; + if (serverId) { + const { stdout, stderr } = await execAsyncRemote(serverId, command); + + if (stderr) { + return []; + } + + if (!stdout) return []; + result = stdout.trim().split("\n"); + } else { + const { stdout, stderr } = await execAsync(command); + + if (stderr) { + return []; + } + + if (!stdout) return []; + + result = stdout.trim().split("\n"); + } + + const containers = result.map((line) => { + const parts = line.split(" | "); + const containerId = parts[0] + ? parts[0].replace("CONTAINER ID : ", "").trim() + : "No container id"; + const name = parts[1] + ? parts[1].replace("Name: ", "").trim() + : "No container name"; + + const state = parts[2] + ? parts[2].replace("State: ", "").trim() + : "No state"; + return { + containerId, + name, + state, + }; + }); + + return containers || []; + } catch (error) {} + + return []; +}; + +export const getContainersByAppLabel = async ( + appName: string, + serverId?: string, +) => { + try { + let stdout = ""; + let stderr = ""; + + const command = `docker ps --filter "label=com.docker.swarm.service.name=${appName}" --format 'CONTAINER ID : {{.ID}} | Name: {{.Names}} | State: {{.State}}'`; + if (serverId) { + const result = await execAsyncRemote(serverId, command); + stdout = result.stdout; + stderr = result.stderr; + } else { + const result = await execAsync(command); + stdout = result.stdout; + stderr = result.stderr; + } + if (stderr) { + console.error(`Error: ${stderr}`); + return; + } + + if (!stdout) return []; + + const lines = stdout.trim().split("\n"); + + const containers = lines.map((line) => { + const parts = line.split(" | "); + const containerId = parts[0] + ? parts[0].replace("CONTAINER ID : ", "").trim() + : "No container id"; + const name = parts[1] + ? parts[1].replace("Name: ", "").trim() + : "No container name"; + const state = parts[2] + ? parts[2].replace("State: ", "").trim() + : "No state"; + return { + containerId, + name, + state, + }; + }); + + return containers || []; + } catch (error) {} + + return []; +}; + +export const containerRestart = async (containerId: string) => { + try { + const { stdout, stderr } = await execAsync( + `docker container restart ${containerId}`, + ); + + if (stderr) { + console.error(`Error: ${stderr}`); + return; + } + + const config = JSON.parse(stdout); + + return config; + } catch (error) {} +}; diff --git a/packages/builders/src/services/domain.ts b/packages/builders/src/services/domain.ts new file mode 100644 index 000000000..2ff663c17 --- /dev/null +++ b/packages/builders/src/services/domain.ts @@ -0,0 +1,136 @@ +import { db } from "@/server/db"; +import { type apiCreateDomain, domains } from "../db/schema"; +import { manageDomain } from "@/server/utils/traefik/domain"; +import { generateRandomDomain } from "@/server/templates/utils"; +import { TRPCError } from "@trpc/server"; +import { eq } from "drizzle-orm"; +import { findAdmin, findAdminById } from "./admin"; +import { findApplicationById } from "./application"; +import { findServerById } from "./server"; + +export type Domain = typeof domains.$inferSelect; + +export const createDomain = async (input: typeof apiCreateDomain._type) => { + const result = await db.transaction(async (tx) => { + const domain = await tx + .insert(domains) + .values({ + ...input, + }) + .returning() + .then((response) => response[0]); + + if (!domain) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error creating domain", + }); + } + + if (domain.applicationId) { + const application = await findApplicationById(domain.applicationId); + await manageDomain(application, domain); + } + + return domain; + }); + + return result; +}; + +export const generateTraefikMeDomain = async ( + appName: string, + adminId: string, + serverId?: string, +) => { + if (serverId) { + const server = await findServerById(serverId); + return generateRandomDomain({ + serverIp: server.ipAddress, + projectName: appName, + }); + } + + if (process.env.NODE_ENV === "development") { + return generateRandomDomain({ + serverIp: "", + projectName: appName, + }); + } + const admin = await findAdminById(adminId); + return generateRandomDomain({ + serverIp: admin?.serverIp || "", + projectName: appName, + }); +}; + +export const generateWildcardDomain = ( + appName: string, + serverDomain: string, +) => { + return `${appName}-${serverDomain}`; +}; + +export const findDomainById = async (domainId: string) => { + const domain = await db.query.domains.findFirst({ + where: eq(domains.domainId, domainId), + with: { + application: true, + }, + }); + if (!domain) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Domain not found", + }); + } + return domain; +}; + +export const findDomainsByApplicationId = async (applicationId: string) => { + const domainsArray = await db.query.domains.findMany({ + where: eq(domains.applicationId, applicationId), + with: { + application: true, + }, + }); + + return domainsArray; +}; + +export const findDomainsByComposeId = async (composeId: string) => { + const domainsArray = await db.query.domains.findMany({ + where: eq(domains.composeId, composeId), + with: { + compose: true, + }, + }); + + return domainsArray; +}; + +export const updateDomainById = async ( + domainId: string, + domainData: Partial, +) => { + const domain = await db + .update(domains) + .set({ + ...domainData, + }) + .where(eq(domains.domainId, domainId)) + .returning(); + + return domain[0]; +}; + +export const removeDomainById = async (domainId: string) => { + await findDomainById(domainId); + // TODO: fix order + const result = await db + .delete(domains) + .where(eq(domains.domainId, domainId)) + .returning(); + + return result[0]; +}; diff --git a/packages/builders/src/services/git-provider.ts b/packages/builders/src/services/git-provider.ts new file mode 100644 index 000000000..6846de4dd --- /dev/null +++ b/packages/builders/src/services/git-provider.ts @@ -0,0 +1,29 @@ +import { db } from "@/server/db"; +import { type apiCreateGithub, gitProvider, github } from "@/server/db/schema"; +import { TRPCError } from "@trpc/server"; +import { eq } from "drizzle-orm"; + +export type GitProvider = typeof gitProvider.$inferSelect; + +export const removeGitProvider = async (gitProviderId: string) => { + const result = await db + .delete(gitProvider) + .where(eq(gitProvider.gitProviderId, gitProviderId)) + .returning(); + + return result[0]; +}; + +export const updateGitProvider = async ( + gitProviderId: string, + input: Partial, +) => { + return await db + .update(gitProvider) + .set({ + ...input, + }) + .where(eq(gitProvider.gitProviderId, gitProviderId)) + .returning() + .then((response) => response[0]); +}; diff --git a/packages/builders/src/services/github.ts b/packages/builders/src/services/github.ts new file mode 100644 index 000000000..64f8ec939 --- /dev/null +++ b/packages/builders/src/services/github.ts @@ -0,0 +1,67 @@ +import { db } from "@/server/db"; +import { type apiCreateGithub, gitProvider, github } from "@/server/db/schema"; +import { TRPCError } from "@trpc/server"; +import { eq } from "drizzle-orm"; + +export type Github = typeof github.$inferSelect; +export const createGithub = async (input: typeof apiCreateGithub._type) => { + return await db.transaction(async (tx) => { + const newGitProvider = await tx + .insert(gitProvider) + .values({ + providerType: "github", + authId: input.authId, + name: input.name, + }) + .returning() + .then((response) => response[0]); + + if (!newGitProvider) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the git provider", + }); + } + + return await tx + .insert(github) + .values({ + ...input, + gitProviderId: newGitProvider?.gitProviderId, + }) + .returning() + .then((response) => response[0]); + }); +}; + +export const findGithubById = async (githubId: string) => { + const githubProviderResult = await db.query.github.findFirst({ + where: eq(github.githubId, githubId), + with: { + gitProvider: true, + }, + }); + + if (!githubProviderResult) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Github Provider not found", + }); + } + + return githubProviderResult; +}; + +export const updateGithub = async ( + githubId: string, + input: Partial, +) => { + return await db + .update(github) + .set({ + ...input, + }) + .where(eq(github.githubId, githubId)) + .returning() + .then((response) => response[0]); +}; diff --git a/packages/builders/src/services/gitlab.ts b/packages/builders/src/services/gitlab.ts new file mode 100644 index 000000000..528fb1b6a --- /dev/null +++ b/packages/builders/src/services/gitlab.ts @@ -0,0 +1,74 @@ +import { db } from "@/server/db"; +import { + type apiCreateGitlab, + type bitbucket, + gitProvider, + type github, + gitlab, +} from "@/server/db/schema"; +import { TRPCError } from "@trpc/server"; +import { eq } from "drizzle-orm"; + +export type Gitlab = typeof gitlab.$inferSelect; + +export const createGitlab = async (input: typeof apiCreateGitlab._type) => { + return await db.transaction(async (tx) => { + const newGitProvider = await tx + .insert(gitProvider) + .values({ + providerType: "gitlab", + authId: input.authId, + name: input.name, + }) + .returning() + .then((response) => response[0]); + + if (!newGitProvider) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the git provider", + }); + } + + await tx + .insert(gitlab) + .values({ + ...input, + gitProviderId: newGitProvider?.gitProviderId, + }) + .returning() + .then((response) => response[0]); + }); +}; + +export const findGitlabById = async (gitlabId: string) => { + const gitlabProviderResult = await db.query.gitlab.findFirst({ + where: eq(gitlab.gitlabId, gitlabId), + with: { + gitProvider: true, + }, + }); + + if (!gitlabProviderResult) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Gitlab Provider not found", + }); + } + + return gitlabProviderResult; +}; + +export const updateGitlab = async ( + gitlabId: string, + input: Partial, +) => { + return await db + .update(gitlab) + .set({ + ...input, + }) + .where(eq(gitlab.gitlabId, gitlabId)) + .returning() + .then((response) => response[0]); +}; diff --git a/packages/builders/src/services/mariadb.ts b/packages/builders/src/services/mariadb.ts new file mode 100644 index 000000000..237d00e95 --- /dev/null +++ b/packages/builders/src/services/mariadb.ts @@ -0,0 +1,147 @@ +import { db } from "@/server/db"; +import { type apiCreateMariaDB, backups, mariadb } from "@/server/db/schema"; +import { generateAppName } from "@/server/db/schema"; +import { buildMariadb } from "@/server/utils/databases/mariadb"; +import { pullImage } from "@/server/utils/docker/utils"; +import { generatePassword } from "@/server/templates/utils"; +import { TRPCError } from "@trpc/server"; +import { eq, getTableColumns } from "drizzle-orm"; +import { validUniqueServerAppName } from "./project"; + +import { execAsyncRemote } from "@/server/utils/process/execAsync"; + +export type Mariadb = typeof mariadb.$inferSelect; + +export const createMariadb = async (input: typeof apiCreateMariaDB._type) => { + input.appName = + `${input.appName}-${generatePassword(6)}` || generateAppName("mariadb"); + if (input.appName) { + const valid = await validUniqueServerAppName(input.appName); + + if (!valid) { + throw new TRPCError({ + code: "CONFLICT", + message: "Service with this 'AppName' already exists", + }); + } + } + + const newMariadb = await db + .insert(mariadb) + .values({ + ...input, + databasePassword: input.databasePassword + ? input.databasePassword + : generatePassword(), + databaseRootPassword: input.databaseRootPassword + ? input.databaseRootPassword + : generatePassword(), + }) + .returning() + .then((value) => value[0]); + + if (!newMariadb) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting mariadb database", + }); + } + + return newMariadb; +}; + +// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881 +export const findMariadbById = async (mariadbId: string) => { + const result = await db.query.mariadb.findFirst({ + where: eq(mariadb.mariadbId, mariadbId), + with: { + project: true, + mounts: true, + server: true, + backups: { + with: { + destination: true, + }, + }, + }, + }); + if (!result) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Mariadb not found", + }); + } + return result; +}; + +export const updateMariadbById = async ( + mariadbId: string, + mariadbData: Partial, +) => { + const result = await db + .update(mariadb) + .set({ + ...mariadbData, + }) + .where(eq(mariadb.mariadbId, mariadbId)) + .returning(); + + return result[0]; +}; + +export const removeMariadbById = async (mariadbId: string) => { + const result = await db + .delete(mariadb) + .where(eq(mariadb.mariadbId, mariadbId)) + .returning(); + + return result[0]; +}; + +export const findMariadbByBackupId = async (backupId: string) => { + const result = await db + .select({ + ...getTableColumns(mariadb), + }) + .from(mariadb) + .innerJoin(backups, eq(mariadb.mariadbId, backups.mariadbId)) + .where(eq(backups.backupId, backupId)) + .limit(1); + + if (!result || !result[0]) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "MariaDB not found", + }); + } + return result[0]; +}; + +export const deployMariadb = async (mariadbId: string) => { + const mariadb = await findMariadbById(mariadbId); + try { + if (mariadb.serverId) { + await execAsyncRemote( + mariadb.serverId, + `docker pull ${mariadb.dockerImage}`, + ); + } else { + await pullImage(mariadb.dockerImage); + } + + await buildMariadb(mariadb); + await updateMariadbById(mariadbId, { + applicationStatus: "done", + }); + } catch (error) { + await updateMariadbById(mariadbId, { + applicationStatus: "error", + }); + + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: `Error on deploy mariadb${error}`, + }); + } + return mariadb; +}; diff --git a/packages/builders/src/services/mongo.ts b/packages/builders/src/services/mongo.ts new file mode 100644 index 000000000..41c44f09b --- /dev/null +++ b/packages/builders/src/services/mongo.ts @@ -0,0 +1,140 @@ +import { db } from "@/server/db"; +import { type apiCreateMongo, backups, mongo } from "@/server/db/schema"; +import { generateAppName } from "@/server/db/schema"; +import { buildMongo } from "@/server/utils/databases/mongo"; +import { pullImage } from "@/server/utils/docker/utils"; +import { generatePassword } from "@/server/templates/utils"; +import { TRPCError } from "@trpc/server"; +import { eq, getTableColumns } from "drizzle-orm"; +import { validUniqueServerAppName } from "./project"; + +import { execAsyncRemote } from "@/server/utils/process/execAsync"; + +export type Mongo = typeof mongo.$inferSelect; + +export const createMongo = async (input: typeof apiCreateMongo._type) => { + input.appName = + `${input.appName}-${generatePassword(6)}` || generateAppName("postgres"); + if (input.appName) { + const valid = await validUniqueServerAppName(input.appName); + + if (!valid) { + throw new TRPCError({ + code: "CONFLICT", + message: "Service with this 'AppName' already exists", + }); + } + } + + const newMongo = await db + .insert(mongo) + .values({ + ...input, + databasePassword: input.databasePassword + ? input.databasePassword + : generatePassword(), + }) + .returning() + .then((value) => value[0]); + + if (!newMongo) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting mongo database", + }); + } + + return newMongo; +}; + +export const findMongoById = async (mongoId: string) => { + const result = await db.query.mongo.findFirst({ + where: eq(mongo.mongoId, mongoId), + with: { + project: true, + mounts: true, + server: true, + backups: { + with: { + destination: true, + }, + }, + }, + }); + if (!result) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Mongo not found", + }); + } + return result; +}; + +export const updateMongoById = async ( + mongoId: string, + postgresData: Partial, +) => { + const result = await db + .update(mongo) + .set({ + ...postgresData, + }) + .where(eq(mongo.mongoId, mongoId)) + .returning(); + + return result[0]; +}; + +export const findMongoByBackupId = async (backupId: string) => { + const result = await db + .select({ + ...getTableColumns(mongo), + }) + .from(mongo) + .innerJoin(backups, eq(mongo.mongoId, backups.mongoId)) + .where(eq(backups.backupId, backupId)) + .limit(1); + + if (!result || !result[0]) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Mongo not found", + }); + } + return result[0]; +}; + +export const removeMongoById = async (mongoId: string) => { + const result = await db + .delete(mongo) + .where(eq(mongo.mongoId, mongoId)) + .returning(); + + return result[0]; +}; + +export const deployMongo = async (mongoId: string) => { + const mongo = await findMongoById(mongoId); + try { + if (mongo.serverId) { + await execAsyncRemote(mongo.serverId, `docker pull ${mongo.dockerImage}`); + } else { + await pullImage(mongo.dockerImage); + } + + await buildMongo(mongo); + await updateMongoById(mongoId, { + applicationStatus: "done", + }); + } catch (error) { + await updateMongoById(mongoId, { + applicationStatus: "error", + }); + + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: `Error on deploy mongo${error}`, + }); + } + return mongo; +}; diff --git a/packages/builders/src/services/mount.ts b/packages/builders/src/services/mount.ts new file mode 100644 index 000000000..97528aa5f --- /dev/null +++ b/packages/builders/src/services/mount.ts @@ -0,0 +1,280 @@ +import path from "node:path"; +import { paths } from "@/server/constants"; +import { db } from "@/server/db"; +import { + type ServiceType, + type apiCreateMount, + mounts, +} from "@/server/db/schema"; +import { createFile, getCreateFileCommand } from "@/server/utils/docker/utils"; +import { removeFileOrDirectory } from "@/server/utils/filesystem/directory"; +import { execAsyncRemote } from "@/server/utils/process/execAsync"; +import { TRPCError } from "@trpc/server"; +import { type SQL, eq, sql } from "drizzle-orm"; + +export type Mount = typeof mounts.$inferSelect; + +export const createMount = async (input: typeof apiCreateMount._type) => { + try { + const { serviceId, ...rest } = input; + const value = await db + .insert(mounts) + .values({ + ...rest, + ...(input.serviceType === "application" && { + applicationId: serviceId, + }), + ...(input.serviceType === "postgres" && { + postgresId: serviceId, + }), + ...(input.serviceType === "mariadb" && { + mariadbId: serviceId, + }), + ...(input.serviceType === "mongo" && { + mongoId: serviceId, + }), + ...(input.serviceType === "mysql" && { + mysqlId: serviceId, + }), + ...(input.serviceType === "redis" && { + redisId: serviceId, + }), + ...(input.serviceType === "compose" && { + composeId: serviceId, + }), + }) + .returning() + .then((value) => value[0]); + + if (!value) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting mount", + }); + } + + if (value.type === "file") { + await createFileMount(value.mountId); + } + return value; + } catch (error) { + console.log(error); + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the mount", + cause: error, + }); + } +}; + +export const createFileMount = async (mountId: string) => { + try { + const mount = await findMountById(mountId); + const baseFilePath = await getBaseFilesPath(mountId); + + const serverId = await getServerId(mount); + + if (serverId) { + const command = getCreateFileCommand( + baseFilePath, + mount.filePath || "", + mount.content || "", + ); + await execAsyncRemote(serverId, command); + } else { + await createFile(baseFilePath, mount.filePath || "", mount.content || ""); + } + } catch (error) { + console.log(`Error to create the file mount: ${error}`); + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the mount", + cause: error, + }); + } +}; + +export const findMountById = async (mountId: string) => { + const mount = await db.query.mounts.findFirst({ + where: eq(mounts.mountId, mountId), + with: { + application: true, + postgres: true, + mariadb: true, + mongo: true, + mysql: true, + redis: true, + compose: true, + }, + }); + if (!mount) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Mount not found", + }); + } + return mount; +}; + +export const updateMount = async ( + mountId: string, + mountData: Partial, +) => { + return await db.transaction(async (transaction) => { + const mount = await db + .update(mounts) + .set({ + ...mountData, + }) + .where(eq(mounts.mountId, mountId)) + .returning() + .then((value) => value[0]); + + if (!mount) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Mount not found", + }); + } + + if (mount.type === "file") { + await deleteFileMount(mountId); + await createFileMount(mountId); + } + return mount; + }); +}; + +export const findMountsByApplicationId = async ( + serviceId: string, + serviceType: ServiceType, +) => { + const sqlChunks: SQL[] = []; + + switch (serviceType) { + case "application": + sqlChunks.push(eq(mounts.applicationId, serviceId)); + break; + case "postgres": + sqlChunks.push(eq(mounts.postgresId, serviceId)); + break; + case "mariadb": + sqlChunks.push(eq(mounts.mariadbId, serviceId)); + break; + case "mongo": + sqlChunks.push(eq(mounts.mongoId, serviceId)); + break; + case "mysql": + sqlChunks.push(eq(mounts.mysqlId, serviceId)); + break; + case "redis": + sqlChunks.push(eq(mounts.redisId, serviceId)); + break; + default: + throw new Error(`Unknown service type: ${serviceType}`); + } + const mount = await db.query.mounts.findMany({ + where: sql.join(sqlChunks, sql.raw(" ")), + }); + + return mount; +}; + +export const deleteMount = async (mountId: string) => { + const { type } = await findMountById(mountId); + + if (type === "file") { + await deleteFileMount(mountId); + } + + const deletedMount = await db + .delete(mounts) + .where(eq(mounts.mountId, mountId)) + .returning(); + return deletedMount[0]; +}; + +export const deleteFileMount = async (mountId: string) => { + const mount = await findMountById(mountId); + if (!mount.filePath) return; + const basePath = await getBaseFilesPath(mountId); + + const fullPath = path.join(basePath, mount.filePath); + try { + const serverId = await getServerId(mount); + if (serverId) { + const command = `rm -rf ${fullPath}`; + await execAsyncRemote(serverId, command); + } else { + await removeFileOrDirectory(fullPath); + } + } catch (error) {} +}; + +export const getBaseFilesPath = async (mountId: string) => { + const mount = await findMountById(mountId); + + let absoluteBasePath = ""; + let appName = ""; + let directoryPath = ""; + + if (mount.serviceType === "application" && mount.application) { + const { APPLICATIONS_PATH } = paths(!!mount.application.serverId); + absoluteBasePath = path.resolve(APPLICATIONS_PATH); + appName = mount.application.appName; + } else if (mount.serviceType === "postgres" && mount.postgres) { + const { APPLICATIONS_PATH } = paths(!!mount.postgres.serverId); + absoluteBasePath = path.resolve(APPLICATIONS_PATH); + appName = mount.postgres.appName; + } else if (mount.serviceType === "mariadb" && mount.mariadb) { + const { APPLICATIONS_PATH } = paths(!!mount.mariadb.serverId); + absoluteBasePath = path.resolve(APPLICATIONS_PATH); + appName = mount.mariadb.appName; + } else if (mount.serviceType === "mongo" && mount.mongo) { + const { APPLICATIONS_PATH } = paths(!!mount.mongo.serverId); + absoluteBasePath = path.resolve(APPLICATIONS_PATH); + appName = mount.mongo.appName; + } else if (mount.serviceType === "mysql" && mount.mysql) { + const { APPLICATIONS_PATH } = paths(!!mount.mysql.serverId); + absoluteBasePath = path.resolve(APPLICATIONS_PATH); + appName = mount.mysql.appName; + } else if (mount.serviceType === "redis" && mount.redis) { + const { APPLICATIONS_PATH } = paths(!!mount.redis.serverId); + absoluteBasePath = path.resolve(APPLICATIONS_PATH); + appName = mount.redis.appName; + } else if (mount.serviceType === "compose" && mount.compose) { + const { COMPOSE_PATH } = paths(!!mount.compose.serverId); + appName = mount.compose.appName; + absoluteBasePath = path.resolve(COMPOSE_PATH); + } + directoryPath = path.join(absoluteBasePath, appName, "files"); + + return directoryPath; +}; + +type MountNested = Awaited>; +export const getServerId = async (mount: MountNested) => { + if (mount.serviceType === "application" && mount?.application?.serverId) { + return mount.application.serverId; + } + if (mount.serviceType === "postgres" && mount?.postgres?.serverId) { + return mount.postgres.serverId; + } + if (mount.serviceType === "mariadb" && mount?.mariadb?.serverId) { + return mount.mariadb.serverId; + } + if (mount.serviceType === "mongo" && mount?.mongo?.serverId) { + return mount.mongo.serverId; + } + if (mount.serviceType === "mysql" && mount?.mysql?.serverId) { + return mount.mysql.serverId; + } + if (mount.serviceType === "redis" && mount?.redis?.serverId) { + return mount.redis.serverId; + } + if (mount.serviceType === "compose" && mount?.compose?.serverId) { + return mount.compose.serverId; + } + + return null; +}; diff --git a/packages/builders/src/services/mysql.ts b/packages/builders/src/services/mysql.ts new file mode 100644 index 000000000..e221c9b75 --- /dev/null +++ b/packages/builders/src/services/mysql.ts @@ -0,0 +1,144 @@ +import { db } from "@/server/db"; +import { type apiCreateMySql, backups, mysql } from "@/server/db/schema"; +import { generateAppName } from "@/server/db/schema"; +import { buildMysql } from "@/server/utils/databases/mysql"; +import { pullImage } from "@/server/utils/docker/utils"; +import { generatePassword } from "@/server/templates/utils"; +import { TRPCError } from "@trpc/server"; +import { eq, getTableColumns } from "drizzle-orm"; +import { validUniqueServerAppName } from "./project"; + +import { execAsyncRemote } from "@/server/utils/process/execAsync"; + +export type MySql = typeof mysql.$inferSelect; + +export const createMysql = async (input: typeof apiCreateMySql._type) => { + input.appName = + `${input.appName}-${generatePassword(6)}` || generateAppName("mysql"); + + if (input.appName) { + const valid = await validUniqueServerAppName(input.appName); + + if (!valid) { + throw new TRPCError({ + code: "CONFLICT", + message: "Service with this 'AppName' already exists", + }); + } + } + + const newMysql = await db + .insert(mysql) + .values({ + ...input, + databasePassword: input.databasePassword + ? input.databasePassword + : generatePassword(), + databaseRootPassword: input.databaseRootPassword + ? input.databaseRootPassword + : generatePassword(), + }) + .returning() + .then((value) => value[0]); + + if (!newMysql) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting mysql database", + }); + } + + return newMysql; +}; + +// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881 +export const findMySqlById = async (mysqlId: string) => { + const result = await db.query.mysql.findFirst({ + where: eq(mysql.mysqlId, mysqlId), + with: { + project: true, + mounts: true, + server: true, + backups: { + with: { + destination: true, + }, + }, + }, + }); + if (!result) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "MySql not found", + }); + } + return result; +}; + +export const updateMySqlById = async ( + mysqlId: string, + mysqlData: Partial, +) => { + const result = await db + .update(mysql) + .set({ + ...mysqlData, + }) + .where(eq(mysql.mysqlId, mysqlId)) + .returning(); + + return result[0]; +}; + +export const findMySqlByBackupId = async (backupId: string) => { + const result = await db + .select({ + ...getTableColumns(mysql), + }) + .from(mysql) + .innerJoin(backups, eq(mysql.mysqlId, backups.mysqlId)) + .where(eq(backups.backupId, backupId)) + .limit(1); + + if (!result || !result[0]) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Mysql not found", + }); + } + return result[0]; +}; + +export const removeMySqlById = async (mysqlId: string) => { + const result = await db + .delete(mysql) + .where(eq(mysql.mysqlId, mysqlId)) + .returning(); + + return result[0]; +}; + +export const deployMySql = async (mysqlId: string) => { + const mysql = await findMySqlById(mysqlId); + try { + if (mysql.serverId) { + await execAsyncRemote(mysql.serverId, `docker pull ${mysql.dockerImage}`); + } else { + await pullImage(mysql.dockerImage); + } + + await buildMysql(mysql); + await updateMySqlById(mysqlId, { + applicationStatus: "done", + }); + } catch (error) { + await updateMySqlById(mysqlId, { + applicationStatus: "error", + }); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: `Error on deploy mysql${error}`, + }); + } + return mysql; +}; diff --git a/packages/builders/src/services/notification.ts b/packages/builders/src/services/notification.ts new file mode 100644 index 000000000..15d8f9d7c --- /dev/null +++ b/packages/builders/src/services/notification.ts @@ -0,0 +1,409 @@ +import { db } from "@/server/db"; +import { + type apiCreateDiscord, + type apiCreateEmail, + type apiCreateSlack, + type apiCreateTelegram, + type apiUpdateDiscord, + type apiUpdateEmail, + type apiUpdateSlack, + type apiUpdateTelegram, + discord, + email, + notifications, + slack, + telegram, +} from "@/server/db/schema"; +import { TRPCError } from "@trpc/server"; +import { eq } from "drizzle-orm"; + +export type Notification = typeof notifications.$inferSelect; + +export const createSlackNotification = async ( + input: typeof apiCreateSlack._type, +) => { + await db.transaction(async (tx) => { + const newSlack = await tx + .insert(slack) + .values({ + channel: input.channel, + webhookUrl: input.webhookUrl, + }) + .returning() + .then((value) => value[0]); + + if (!newSlack) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting slack", + }); + } + + const newDestination = await tx + .insert(notifications) + .values({ + slackId: newSlack.slackId, + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + notificationType: "slack", + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting notification", + }); + } + + return newDestination; + }); +}; + +export const updateSlackNotification = async ( + input: typeof apiUpdateSlack._type, +) => { + await db.transaction(async (tx) => { + const newDestination = await tx + .update(notifications) + .set({ + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + }) + .where(eq(notifications.notificationId, input.notificationId)) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error Updating notification", + }); + } + + await tx + .update(slack) + .set({ + channel: input.channel, + webhookUrl: input.webhookUrl, + }) + .where(eq(slack.slackId, input.slackId)) + .returning() + .then((value) => value[0]); + + return newDestination; + }); +}; + +export const createTelegramNotification = async ( + input: typeof apiCreateTelegram._type, +) => { + await db.transaction(async (tx) => { + const newTelegram = await tx + .insert(telegram) + .values({ + botToken: input.botToken, + chatId: input.chatId, + }) + .returning() + .then((value) => value[0]); + + if (!newTelegram) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting telegram", + }); + } + + const newDestination = await tx + .insert(notifications) + .values({ + telegramId: newTelegram.telegramId, + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + notificationType: "telegram", + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting notification", + }); + } + + return newDestination; + }); +}; + +export const updateTelegramNotification = async ( + input: typeof apiUpdateTelegram._type, +) => { + await db.transaction(async (tx) => { + const newDestination = await tx + .update(notifications) + .set({ + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + }) + .where(eq(notifications.notificationId, input.notificationId)) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error Updating notification", + }); + } + + await tx + .update(telegram) + .set({ + botToken: input.botToken, + chatId: input.chatId, + }) + .where(eq(telegram.telegramId, input.telegramId)) + .returning() + .then((value) => value[0]); + + return newDestination; + }); +}; + +export const createDiscordNotification = async ( + input: typeof apiCreateDiscord._type, +) => { + await db.transaction(async (tx) => { + const newDiscord = await tx + .insert(discord) + .values({ + webhookUrl: input.webhookUrl, + }) + .returning() + .then((value) => value[0]); + + if (!newDiscord) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting discord", + }); + } + + const newDestination = await tx + .insert(notifications) + .values({ + discordId: newDiscord.discordId, + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + notificationType: "discord", + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting notification", + }); + } + + return newDestination; + }); +}; + +export const updateDiscordNotification = async ( + input: typeof apiUpdateDiscord._type, +) => { + await db.transaction(async (tx) => { + const newDestination = await tx + .update(notifications) + .set({ + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + }) + .where(eq(notifications.notificationId, input.notificationId)) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error Updating notification", + }); + } + + await tx + .update(discord) + .set({ + webhookUrl: input.webhookUrl, + }) + .where(eq(discord.discordId, input.discordId)) + .returning() + .then((value) => value[0]); + + return newDestination; + }); +}; + +export const createEmailNotification = async ( + input: typeof apiCreateEmail._type, +) => { + await db.transaction(async (tx) => { + const newEmail = await tx + .insert(email) + .values({ + smtpServer: input.smtpServer, + smtpPort: input.smtpPort, + username: input.username, + password: input.password, + fromAddress: input.fromAddress, + toAddresses: input.toAddresses, + }) + .returning() + .then((value) => value[0]); + + if (!newEmail) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting email", + }); + } + + const newDestination = await tx + .insert(notifications) + .values({ + emailId: newEmail.emailId, + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + notificationType: "email", + }) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting notification", + }); + } + + return newDestination; + }); +}; + +export const updateEmailNotification = async ( + input: typeof apiUpdateEmail._type, +) => { + await db.transaction(async (tx) => { + const newDestination = await tx + .update(notifications) + .set({ + name: input.name, + appDeploy: input.appDeploy, + appBuildError: input.appBuildError, + databaseBackup: input.databaseBackup, + dokployRestart: input.dokployRestart, + dockerCleanup: input.dockerCleanup, + }) + .where(eq(notifications.notificationId, input.notificationId)) + .returning() + .then((value) => value[0]); + + if (!newDestination) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error Updating notification", + }); + } + + await tx + .update(email) + .set({ + smtpServer: input.smtpServer, + smtpPort: input.smtpPort, + username: input.username, + password: input.password, + fromAddress: input.fromAddress, + toAddresses: input.toAddresses, + }) + .where(eq(email.emailId, input.emailId)) + .returning() + .then((value) => value[0]); + + return newDestination; + }); +}; + +export const findNotificationById = async (notificationId: string) => { + const notification = await db.query.notifications.findFirst({ + where: eq(notifications.notificationId, notificationId), + with: { + slack: true, + telegram: true, + discord: true, + email: true, + }, + }); + if (!notification) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Notification not found", + }); + } + return notification; +}; + +export const removeNotificationById = async (notificationId: string) => { + const result = await db + .delete(notifications) + .where(eq(notifications.notificationId, notificationId)) + .returning(); + + return result[0]; +}; + +export const updateNotificationById = async ( + notificationId: string, + notificationData: Partial, +) => { + const result = await db + .update(notifications) + .set({ + ...notificationData, + }) + .where(eq(notifications.notificationId, notificationId)) + .returning(); + + return result[0]; +}; diff --git a/packages/builders/src/services/port.ts b/packages/builders/src/services/port.ts new file mode 100644 index 000000000..19229a9c9 --- /dev/null +++ b/packages/builders/src/services/port.ts @@ -0,0 +1,62 @@ +import { db } from "@/server/db"; +import { type apiCreatePort, ports } from "@/server/db/schema"; +import { TRPCError } from "@trpc/server"; +import { eq } from "drizzle-orm"; + +export type Port = typeof ports.$inferSelect; + +export const createPort = async (input: typeof apiCreatePort._type) => { + const newPort = await db + .insert(ports) + .values({ + ...input, + }) + .returning() + .then((value) => value[0]); + + if (!newPort) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting port", + }); + } + + return newPort; +}; + +export const finPortById = async (portId: string) => { + const result = await db.query.ports.findFirst({ + where: eq(ports.portId, portId), + }); + if (!result) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Port not found", + }); + } + return result; +}; + +export const removePortById = async (portId: string) => { + const result = await db + .delete(ports) + .where(eq(ports.portId, portId)) + .returning(); + + return result[0]; +}; + +export const updatePortById = async ( + portId: string, + portData: Partial, +) => { + const result = await db + .update(ports) + .set({ + ...portData, + }) + .where(eq(ports.portId, portId)) + .returning(); + + return result[0]; +}; diff --git a/packages/builders/src/services/postgres.ts b/packages/builders/src/services/postgres.ts new file mode 100644 index 000000000..857df630b --- /dev/null +++ b/packages/builders/src/services/postgres.ts @@ -0,0 +1,142 @@ +import { db } from "@/server/db"; +import { type apiCreatePostgres, backups, postgres } from "@/server/db/schema"; +import { generateAppName } from "@/server/db/schema"; +import { buildPostgres } from "@/server/utils/databases/postgres"; +import { pullImage } from "@/server/utils/docker/utils"; +import { generatePassword } from "@/server/templates/utils"; +import { TRPCError } from "@trpc/server"; +import { eq, getTableColumns } from "drizzle-orm"; +import { validUniqueServerAppName } from "./project"; + +import { execAsyncRemote } from "@/server/utils/process/execAsync"; + +export type Postgres = typeof postgres.$inferSelect; + +export const createPostgres = async (input: typeof apiCreatePostgres._type) => { + input.appName = + `${input.appName}-${generatePassword(6)}` || generateAppName("postgres"); + if (input.appName) { + const valid = await validUniqueServerAppName(input.appName); + + if (!valid) { + throw new TRPCError({ + code: "CONFLICT", + message: "Service with this 'AppName' already exists", + }); + } + } + + const newPostgres = await db + .insert(postgres) + .values({ + ...input, + databasePassword: input.databasePassword + ? input.databasePassword + : generatePassword(), + }) + .returning() + .then((value) => value[0]); + + if (!newPostgres) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting postgresql database", + }); + } + + return newPostgres; +}; +export const findPostgresById = async (postgresId: string) => { + const result = await db.query.postgres.findFirst({ + where: eq(postgres.postgresId, postgresId), + with: { + project: true, + mounts: true, + server: true, + backups: { + with: { + destination: true, + }, + }, + }, + }); + if (!result) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Postgres not found", + }); + } + return result; +}; + +export const findPostgresByBackupId = async (backupId: string) => { + const result = await db + .select({ + ...getTableColumns(postgres), + }) + .from(postgres) + .innerJoin(backups, eq(postgres.postgresId, backups.postgresId)) + .where(eq(backups.backupId, backupId)) + .limit(1); + + if (!result || !result[0]) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Postgres not found", + }); + } + return result[0]; +}; + +export const updatePostgresById = async ( + postgresId: string, + postgresData: Partial, +) => { + const result = await db + .update(postgres) + .set({ + ...postgresData, + }) + .where(eq(postgres.postgresId, postgresId)) + .returning(); + + return result[0]; +}; + +export const removePostgresById = async (postgresId: string) => { + const result = await db + .delete(postgres) + .where(eq(postgres.postgresId, postgresId)) + .returning(); + + return result[0]; +}; + +export const deployPostgres = async (postgresId: string) => { + const postgres = await findPostgresById(postgresId); + try { + const promises = []; + if (postgres.serverId) { + const result = await execAsyncRemote( + postgres.serverId, + `docker pull ${postgres.dockerImage}`, + ); + } else { + await pullImage(postgres.dockerImage); + } + + await buildPostgres(postgres); + await updatePostgresById(postgresId, { + applicationStatus: "done", + }); + } catch (error) { + await updatePostgresById(postgresId, { + applicationStatus: "error", + }); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: `Error on deploy postgres${error}`, + }); + } + return postgres; +}; diff --git a/packages/builders/src/services/project.ts b/packages/builders/src/services/project.ts new file mode 100644 index 000000000..902dd06b3 --- /dev/null +++ b/packages/builders/src/services/project.ts @@ -0,0 +1,124 @@ +import { db } from "@/server/db"; +import { + type apiCreateProject, + applications, + mariadb, + mongo, + mysql, + postgres, + projects, + redis, +} from "@/server/db/schema"; +import { TRPCError } from "@trpc/server"; +import { eq } from "drizzle-orm"; + +export type Project = typeof projects.$inferSelect; + +export const createProject = async ( + input: typeof apiCreateProject._type, + adminId: string, +) => { + const newProject = await db + .insert(projects) + .values({ + ...input, + adminId: adminId, + }) + .returning() + .then((value) => value[0]); + + if (!newProject) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the project", + }); + } + + return newProject; +}; + +export const findProjectById = async (projectId: string) => { + const project = await db.query.projects.findFirst({ + where: eq(projects.projectId, projectId), + with: { + applications: true, + mariadb: true, + mongo: true, + mysql: true, + postgres: true, + redis: true, + compose: true, + }, + }); + if (!project) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Project not found", + }); + } + return project; +}; + +export const deleteProject = async (projectId: string) => { + const project = await db + .delete(projects) + .where(eq(projects.projectId, projectId)) + .returning() + .then((value) => value[0]); + + return project; +}; + +export const updateProjectById = async ( + projectId: string, + projectData: Partial, +) => { + const result = await db + .update(projects) + .set({ + ...projectData, + }) + .where(eq(projects.projectId, projectId)) + .returning() + .then((res) => res[0]); + + return result; +}; + +export const validUniqueServerAppName = async (appName: string) => { + const query = await db.query.projects.findMany({ + with: { + applications: { + where: eq(applications.appName, appName), + }, + mariadb: { + where: eq(mariadb.appName, appName), + }, + mongo: { + where: eq(mongo.appName, appName), + }, + mysql: { + where: eq(mysql.appName, appName), + }, + postgres: { + where: eq(postgres.appName, appName), + }, + redis: { + where: eq(redis.appName, appName), + }, + }, + }); + + // Filter out items with non-empty fields + const nonEmptyProjects = query.filter( + (project) => + project.applications.length > 0 || + project.mariadb.length > 0 || + project.mongo.length > 0 || + project.mysql.length > 0 || + project.postgres.length > 0 || + project.redis.length > 0, + ); + + return nonEmptyProjects.length === 0; +}; diff --git a/packages/builders/src/services/redirect.ts b/packages/builders/src/services/redirect.ts new file mode 100644 index 000000000..972603f2e --- /dev/null +++ b/packages/builders/src/services/redirect.ts @@ -0,0 +1,123 @@ +import { db } from "@/server/db"; +import { type apiCreateRedirect, redirects } from "@/server/db/schema"; +import { + createRedirectMiddleware, + removeRedirectMiddleware, + updateRedirectMiddleware, +} from "@/server/utils/traefik/redirect"; +import { TRPCError } from "@trpc/server"; +import { desc, eq } from "drizzle-orm"; +import type { z } from "zod"; +import { findApplicationById } from "./application"; +export type Redirect = typeof redirects.$inferSelect; + +export const findRedirectById = async (redirectId: string) => { + const application = await db.query.redirects.findFirst({ + where: eq(redirects.redirectId, redirectId), + }); + if (!application) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Redirect not found", + }); + } + return application; +}; + +export const createRedirect = async ( + redirectData: z.infer, +) => { + try { + await db.transaction(async (tx) => { + const redirect = await tx + .insert(redirects) + .values({ + ...redirectData, + }) + .returning() + .then((res) => res[0]); + + if (!redirect) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the redirect", + }); + } + + const application = await findApplicationById(redirect.applicationId); + + createRedirectMiddleware(application, redirect); + }); + + return true; + } catch (error) { + console.log(error); + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create this redirect", + cause: error, + }); + } +}; + +export const removeRedirectById = async (redirectId: string) => { + try { + const response = await db + .delete(redirects) + .where(eq(redirects.redirectId, redirectId)) + .returning() + .then((res) => res[0]); + + if (!response) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Redirect not found", + }); + } + + const application = await findApplicationById(response.applicationId); + + await removeRedirectMiddleware(application, response); + + return response; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to remove this redirect", + cause: error, + }); + } +}; + +export const updateRedirectById = async ( + redirectId: string, + redirectData: Partial, +) => { + try { + const redirect = await db + .update(redirects) + .set({ + ...redirectData, + }) + .where(eq(redirects.redirectId, redirectId)) + .returning() + .then((res) => res[0]); + + if (!redirect) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Redirect not found", + }); + } + const application = await findApplicationById(redirect.applicationId); + + await updateRedirectMiddleware(application, redirect); + + return redirect; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to update this redirect", + }); + } +}; diff --git a/packages/builders/src/services/redis.ts b/packages/builders/src/services/redis.ts new file mode 100644 index 000000000..498c5f4cf --- /dev/null +++ b/packages/builders/src/services/redis.ts @@ -0,0 +1,117 @@ +import { db } from "@/server/db"; +import { type apiCreateRedis, redis } from "@/server/db/schema"; +import { generateAppName } from "@/server/db/schema"; +import { buildRedis } from "@/server/utils/databases/redis"; +import { pullImage } from "@/server/utils/docker/utils"; +import { generatePassword } from "@/server/templates/utils"; +import { TRPCError } from "@trpc/server"; +import { eq } from "drizzle-orm"; +import { validUniqueServerAppName } from "./project"; + +import { execAsyncRemote } from "@/server/utils/process/execAsync"; + +export type Redis = typeof redis.$inferSelect; + +// https://github.com/drizzle-team/drizzle-orm/discussions/1483#discussioncomment-7523881 +export const createRedis = async (input: typeof apiCreateRedis._type) => { + input.appName = + `${input.appName}-${generatePassword(6)}` || generateAppName("redis"); + if (input.appName) { + const valid = await validUniqueServerAppName(input.appName); + + if (!valid) { + throw new TRPCError({ + code: "CONFLICT", + message: "Service with this 'AppName' already exists", + }); + } + } + + const newRedis = await db + .insert(redis) + .values({ + ...input, + databasePassword: input.databasePassword + ? input.databasePassword + : generatePassword(), + }) + .returning() + .then((value) => value[0]); + + if (!newRedis) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting redis database", + }); + } + + return newRedis; +}; + +export const findRedisById = async (redisId: string) => { + const result = await db.query.redis.findFirst({ + where: eq(redis.redisId, redisId), + with: { + project: true, + mounts: true, + server: true, + }, + }); + if (!result) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Redis not found", + }); + } + return result; +}; + +export const updateRedisById = async ( + redisId: string, + redisData: Partial, +) => { + const result = await db + .update(redis) + .set({ + ...redisData, + }) + .where(eq(redis.redisId, redisId)) + .returning(); + + return result[0]; +}; + +export const removeRedisById = async (redisId: string) => { + const result = await db + .delete(redis) + .where(eq(redis.redisId, redisId)) + .returning(); + + return result[0]; +}; + +export const deployRedis = async (redisId: string) => { + const redis = await findRedisById(redisId); + try { + if (redis.serverId) { + await execAsyncRemote(redis.serverId, `docker pull ${redis.dockerImage}`); + } else { + await pullImage(redis.dockerImage); + } + + await buildRedis(redis); + await updateRedisById(redisId, { + applicationStatus: "done", + }); + } catch (error) { + await updateRedisById(redisId, { + applicationStatus: "error", + }); + + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: `Error on deploy redis${error}`, + }); + } + return redis; +}; diff --git a/packages/builders/src/services/registry.ts b/packages/builders/src/services/registry.ts new file mode 100644 index 000000000..83dcc2a21 --- /dev/null +++ b/packages/builders/src/services/registry.ts @@ -0,0 +1,124 @@ +import { db } from "@/server/db"; +import { type apiCreateRegistry, registry } from "@/server/db/schema"; +import { initializeRegistry } from "@/server/setup/registry-setup"; +import { removeService } from "@/server/utils/docker/utils"; +import { execAsync } from "@/server/utils/process/execAsync"; +import { + manageRegistry, + removeSelfHostedRegistry, +} from "@/server/utils/traefik/registry"; +import { TRPCError } from "@trpc/server"; +import { eq } from "drizzle-orm"; +import { findAdmin } from "./admin"; + +export type Registry = typeof registry.$inferSelect; + +export const createRegistry = async (input: typeof apiCreateRegistry._type) => { + const admin = await findAdmin(); + + return await db.transaction(async (tx) => { + const newRegistry = await tx + .insert(registry) + .values({ + ...input, + adminId: admin.adminId, + }) + .returning() + .then((value) => value[0]); + + if (!newRegistry) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error input: Inserting registry", + }); + } + + if (newRegistry.registryType === "cloud") { + const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`; + await execAsync(loginCommand); + } + + return newRegistry; + }); +}; + +export const removeRegistry = async (registryId: string) => { + try { + const response = await db + .delete(registry) + .where(eq(registry.registryId, registryId)) + .returning() + .then((res) => res[0]); + + if (!response) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Registry not found", + }); + } + + if (response.registryType === "selfHosted") { + await removeSelfHostedRegistry(); + await removeService("dokploy-registry"); + } + + await execAsync(`docker logout ${response.registryUrl}`); + + return response; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to remove this registry", + cause: error, + }); + } +}; + +export const updateRegistry = async ( + registryId: string, + registryData: Partial, +) => { + try { + const response = await db + .update(registry) + .set({ + ...registryData, + }) + .where(eq(registry.registryId, registryId)) + .returning() + .then((res) => res[0]); + + if (response?.registryType === "selfHosted") { + await manageRegistry(response); + await initializeRegistry(response.username, response.password); + } + + return response; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to update this registry", + }); + } +}; + +export const findRegistryById = async (registryId: string) => { + const registryResponse = await db.query.registry.findFirst({ + where: eq(registry.registryId, registryId), + columns: { + password: false, + }, + }); + if (!registryResponse) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Registry not found", + }); + } + return registryResponse; +}; + +export const findAllRegistry = async () => { + const registryResponse = await db.query.registry.findMany(); + return registryResponse; +}; diff --git a/packages/builders/src/services/security.ts b/packages/builders/src/services/security.ts new file mode 100644 index 000000000..2da78e266 --- /dev/null +++ b/packages/builders/src/services/security.ts @@ -0,0 +1,107 @@ +import { db } from "@/server/db"; +import { type apiCreateSecurity, security } from "@/server/db/schema"; +import { + createSecurityMiddleware, + removeSecurityMiddleware, +} from "@/server/utils/traefik/security"; +import { TRPCError } from "@trpc/server"; +import { eq } from "drizzle-orm"; +import type { z } from "zod"; +import { findApplicationById } from "./application"; +export type Security = typeof security.$inferSelect; + +export const findSecurityById = async (securityId: string) => { + const application = await db.query.security.findFirst({ + where: eq(security.securityId, securityId), + }); + if (!application) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Security not found", + }); + } + return application; +}; + +export const createSecurity = async ( + data: z.infer, +) => { + try { + await db.transaction(async (tx) => { + const application = await findApplicationById(data.applicationId); + + const securityResponse = await tx + .insert(security) + .values({ + ...data, + }) + .returning() + .then((res) => res[0]); + + if (!securityResponse) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the security", + }); + } + await createSecurityMiddleware(application, securityResponse); + return true; + }); + } catch (error) { + console.log(error); + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create this security", + cause: error, + }); + } +}; + +export const deleteSecurityById = async (securityId: string) => { + try { + const result = await db + .delete(security) + .where(eq(security.securityId, securityId)) + .returning() + .then((res) => res[0]); + + if (!result) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Security not found", + }); + } + + const application = await findApplicationById(result.applicationId); + + await removeSecurityMiddleware(application, result); + return result; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to remove this security", + }); + } +}; + +export const updateSecurityById = async ( + securityId: string, + data: Partial, +) => { + try { + const response = await db + .update(security) + .set({ + ...data, + }) + .where(eq(security.securityId, securityId)) + .returning(); + + return response[0]; + } catch (error) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to update this security", + }); + } +}; diff --git a/packages/builders/src/services/server.ts b/packages/builders/src/services/server.ts new file mode 100644 index 000000000..4cd79cdcc --- /dev/null +++ b/packages/builders/src/services/server.ts @@ -0,0 +1,120 @@ +import { db } from "@/server/db"; +import { type apiCreateServer, server } from "@/server/db/schema"; +import { TRPCError } from "@trpc/server"; +import { desc, eq } from "drizzle-orm"; + +export type Server = typeof server.$inferSelect; + +export const createServer = async ( + input: typeof apiCreateServer._type, + adminId: string, +) => { + const newServer = await db + .insert(server) + .values({ + ...input, + adminId: adminId, + }) + .returning() + .then((value) => value[0]); + + if (!newServer) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the server", + }); + } + + return newServer; +}; + +export const findServerById = async (serverId: string) => { + const currentServer = await db.query.server.findFirst({ + where: eq(server.serverId, serverId), + with: { + deployments: true, + sshKey: true, + }, + }); + if (!currentServer) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Server not found", + }); + } + return currentServer; +}; + +export const findServersByAdminId = async (adminId: string) => { + const servers = await db.query.server.findMany({ + where: eq(server.adminId, adminId), + orderBy: desc(server.createdAt), + }); + + return servers; +}; + +export const deleteServer = async (serverId: string) => { + const currentServer = await db + .delete(server) + .where(eq(server.serverId, serverId)) + .returning() + .then((value) => value[0]); + + return currentServer; +}; + +export const haveActiveServices = async (serverId: string) => { + const currentServer = await db.query.server.findFirst({ + where: eq(server.serverId, serverId), + with: { + applications: true, + compose: true, + redis: true, + mariadb: true, + mongo: true, + mysql: true, + postgres: true, + }, + }); + + if (!currentServer) { + return false; + } + + const total = + currentServer?.applications?.length + + currentServer?.compose?.length + + currentServer?.redis?.length + + currentServer?.mariadb?.length + + currentServer?.mongo?.length + + currentServer?.mysql?.length + + currentServer?.postgres?.length; + + if (total === 0) { + return false; + } + + return true; +}; + +export const updateServerById = async ( + serverId: string, + serverData: Partial, +) => { + const result = await db + .update(server) + .set({ + ...serverData, + }) + .where(eq(server.serverId, serverId)) + .returning() + .then((res) => res[0]); + + return result; +}; + +export const getAllServers = async () => { + const servers = await db.query.server.findMany(); + return servers; +}; diff --git a/packages/builders/src/services/settings.ts b/packages/builders/src/services/settings.ts new file mode 100644 index 000000000..eacfd8caf --- /dev/null +++ b/packages/builders/src/services/settings.ts @@ -0,0 +1,148 @@ +import { readdirSync } from "node:fs"; +import { join } from "node:path"; +import { docker } from "@/server/constants"; +import { getServiceContainer } from "@/server/utils/docker/utils"; +import { execAsyncRemote } from "@/server/utils/process/execAsync"; +// import packageInfo from "../../../package.json"; + +const updateIsAvailable = async () => { + try { + const service = await getServiceContainer("dokploy"); + + const localImage = await docker.getImage(getDokployImage()).inspect(); + return localImage.Id !== service?.ImageID; + } catch (error) { + return false; + } +}; + +export const getDokployImage = () => { + return `dokploy/dokploy:${process.env.RELEASE_TAG || "latest"}`; +}; + +export const pullLatestRelease = async () => { + try { + const stream = await docker.pull(getDokployImage(), {}); + await new Promise((resolve, reject) => { + docker.modem.followProgress(stream, (err, res) => + err ? reject(err) : resolve(res), + ); + }); + const newUpdateIsAvailable = await updateIsAvailable(); + return newUpdateIsAvailable; + } catch (error) {} + + return false; +}; +export const getDokployVersion = () => { + // return packageInfo.version; +}; + +interface TreeDataItem { + id: string; + name: string; + type: "file" | "directory"; + children?: TreeDataItem[]; +} + +export const readDirectory = async ( + dirPath: string, + serverId?: string, +): Promise => { + if (serverId) { + const { stdout } = await execAsyncRemote( + serverId, + ` +process_items() { + local parent_dir="$1" + local __resultvar=$2 + + local items_json="" + local first=true + for item in "$parent_dir"/*; do + [ -e "$item" ] || continue + process_item "$item" item_json + if [ "$first" = true ]; then + first=false + items_json="$item_json" + else + items_json="$items_json,$item_json" + fi + done + + eval $__resultvar="'[$items_json]'" +} + +process_item() { + local item_path="$1" + local __resultvar=$2 + + local item_name=$(basename "$item_path") + local escaped_name=$(echo "$item_name" | sed 's/"/\\"/g') + local escaped_path=$(echo "$item_path" | sed 's/"/\\"/g') + + if [ -d "$item_path" ]; then + # Is directory + process_items "$item_path" children_json + local json='{"id":"'"$escaped_path"'","name":"'"$escaped_name"'","type":"directory","children":'"$children_json"'}' + else + # Is file + local json='{"id":"'"$escaped_path"'","name":"'"$escaped_name"'","type":"file"}' + fi + + eval $__resultvar="'$json'" +} + +root_dir=${dirPath} + +process_items "$root_dir" json_output + +echo "$json_output" + `, + ); + const result = JSON.parse(stdout); + return result; + } + const items = readdirSync(dirPath, { withFileTypes: true }); + + const stack = [dirPath]; + const result: TreeDataItem[] = []; + const parentMap: Record = {}; + + while (stack.length > 0) { + const currentPath = stack.pop(); + if (!currentPath) continue; + + const items = readdirSync(currentPath, { withFileTypes: true }); + const currentDirectoryResult: TreeDataItem[] = []; + + for (const item of items) { + const fullPath = join(currentPath, item.name); + if (item.isDirectory()) { + stack.push(fullPath); + const directoryItem: TreeDataItem = { + id: fullPath, + name: item.name, + type: "directory", + children: [], + }; + currentDirectoryResult.push(directoryItem); + parentMap[fullPath] = directoryItem.children as TreeDataItem[]; + } else { + const fileItem: TreeDataItem = { + id: fullPath, + name: item.name, + type: "file", + }; + currentDirectoryResult.push(fileItem); + } + } + + if (parentMap[currentPath]) { + parentMap[currentPath].push(...currentDirectoryResult); + } else { + result.push(...currentDirectoryResult); + } + } + return result; +}; diff --git a/packages/builders/src/services/ssh-key.ts b/packages/builders/src/services/ssh-key.ts new file mode 100644 index 000000000..9d6f0070e --- /dev/null +++ b/packages/builders/src/services/ssh-key.ts @@ -0,0 +1,78 @@ +import { db } from "@/server/db"; +import { + type apiCreateSshKey, + type apiFindOneSshKey, + type apiRemoveSshKey, + type apiUpdateSshKey, + sshKeys, +} from "@/server/db/schema"; +import { removeSSHKey, saveSSHKey } from "@/server/utils/filesystem/ssh"; +import { TRPCError } from "@trpc/server"; +import { eq } from "drizzle-orm"; + +export const createSshKey = async ({ + privateKey, + ...input +}: typeof apiCreateSshKey._type) => { + await db.transaction(async (tx) => { + const sshKey = await tx + .insert(sshKeys) + .values(input) + .returning() + .then((response) => response[0]) + .catch((e) => console.error(e)); + + if (sshKey) { + saveSSHKey(sshKey.sshKeyId, sshKey.publicKey, privateKey); + } + + if (!sshKey) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error to create the ssh key", + }); + } + return sshKey; + }); +}; + +export const removeSSHKeyById = async ( + sshKeyId: (typeof apiRemoveSshKey._type)["sshKeyId"], +) => { + const result = await db + .delete(sshKeys) + .where(eq(sshKeys.sshKeyId, sshKeyId)) + .returning(); + + removeSSHKey(sshKeyId); + + return result[0]; +}; + +export const updateSSHKeyById = async ({ + sshKeyId, + ...input +}: typeof apiUpdateSshKey._type) => { + const result = await db + .update(sshKeys) + .set(input) + .where(eq(sshKeys.sshKeyId, sshKeyId)) + .returning(); + + return result[0]; +}; + +export const findSSHKeyById = async ( + sshKeyId: (typeof apiFindOneSshKey._type)["sshKeyId"], +) => { + const sshKey = await db.query.sshKeys.findFirst({ + where: eq(sshKeys.sshKeyId, sshKeyId), + }); + if (!sshKey) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "SSH Key not found", + }); + } + return sshKey; +}; diff --git a/packages/builders/src/services/user.ts b/packages/builders/src/services/user.ts new file mode 100644 index 000000000..044bc3cb4 --- /dev/null +++ b/packages/builders/src/services/user.ts @@ -0,0 +1,207 @@ +import { db } from "@/server/db"; +import { users } from "@/server/db/schema"; +import { TRPCError } from "@trpc/server"; +import { eq } from "drizzle-orm"; + +export type User = typeof users.$inferSelect; + +export const findUserById = async (userId: string) => { + const user = await db.query.users.findFirst({ + where: eq(users.userId, userId), + }); + if (!user) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "User not found", + }); + } + return user; +}; + +export const findUserByAuthId = async (authId: string) => { + const user = await db.query.users.findFirst({ + where: eq(users.authId, authId), + with: { + auth: true, + }, + }); + if (!user) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "User not found", + }); + } + return user; +}; + +export const findUsers = async () => { + const users = await db.query.users.findMany({ + with: { + auth: { + columns: { + secret: false, + }, + }, + }, + }); + return users; +}; + +export const addNewProject = async (authId: string, projectId: string) => { + const user = await findUserByAuthId(authId); + + await db + .update(users) + .set({ + accesedProjects: [...user.accesedProjects, projectId], + }) + .where(eq(users.authId, authId)); +}; + +export const addNewService = async (authId: string, serviceId: string) => { + const user = await findUserByAuthId(authId); + await db + .update(users) + .set({ + accesedServices: [...user.accesedServices, serviceId], + }) + .where(eq(users.authId, authId)); +}; + +export const canPerformCreationService = async ( + userId: string, + projectId: string, +) => { + const { accesedProjects, canCreateServices } = await findUserByAuthId(userId); + const haveAccessToProject = accesedProjects.includes(projectId); + + if (canCreateServices && haveAccessToProject) { + return true; + } + + return false; +}; + +export const canPerformAccessService = async ( + userId: string, + serviceId: string, +) => { + const { accesedServices } = await findUserByAuthId(userId); + const haveAccessToService = accesedServices.includes(serviceId); + + if (haveAccessToService) { + return true; + } + + return false; +}; + +export const canPeformDeleteService = async ( + authId: string, + serviceId: string, +) => { + const { accesedServices, canDeleteServices } = await findUserByAuthId(authId); + const haveAccessToService = accesedServices.includes(serviceId); + + if (canDeleteServices && haveAccessToService) { + return true; + } + + return false; +}; + +export const canPerformCreationProject = async (authId: string) => { + const { canCreateProjects } = await findUserByAuthId(authId); + + if (canCreateProjects) { + return true; + } + + return false; +}; + +export const canPerformDeleteProject = async (authId: string) => { + const { canDeleteProjects } = await findUserByAuthId(authId); + + if (canDeleteProjects) { + return true; + } + + return false; +}; + +export const canPerformAccessProject = async ( + authId: string, + projectId: string, +) => { + const { accesedProjects } = await findUserByAuthId(authId); + + const haveAccessToProject = accesedProjects.includes(projectId); + + if (haveAccessToProject) { + return true; + } + return false; +}; + +export const canAccessToTraefikFiles = async (authId: string) => { + const { canAccessToTraefikFiles } = await findUserByAuthId(authId); + return canAccessToTraefikFiles; +}; + +export const checkServiceAccess = async ( + authId: string, + serviceId: string, + action = "access" as "access" | "create" | "delete", +) => { + let hasPermission = false; + switch (action) { + case "create": + hasPermission = await canPerformCreationService(authId, serviceId); + break; + case "access": + hasPermission = await canPerformAccessService(authId, serviceId); + break; + case "delete": + hasPermission = await canPeformDeleteService(authId, serviceId); + break; + default: + hasPermission = false; + } + if (!hasPermission) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Permission denied", + }); + } +}; + +export const checkProjectAccess = async ( + authId: string, + action: "create" | "delete" | "access", + projectId?: string, +) => { + let hasPermission = false; + switch (action) { + case "access": + hasPermission = await canPerformAccessProject( + authId, + projectId as string, + ); + break; + case "create": + hasPermission = await canPerformCreationProject(authId); + break; + case "delete": + hasPermission = await canPerformDeleteProject(authId); + break; + default: + hasPermission = false; + } + if (!hasPermission) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Permission denied", + }); + } +}; diff --git a/packages/builders/src/setup/config-paths.ts b/packages/builders/src/setup/config-paths.ts new file mode 100644 index 000000000..190e438b0 --- /dev/null +++ b/packages/builders/src/setup/config-paths.ts @@ -0,0 +1,43 @@ +import { chmodSync, existsSync, mkdirSync } from "node:fs"; +import { paths } from "../constants"; + +const createDirectoryIfNotExist = (dirPath: string) => { + if (!existsSync(dirPath)) { + mkdirSync(dirPath, { recursive: true }); + console.log(`Directory created: ${dirPath}`); + } +}; + +export const setupDirectories = () => { + const { + APPLICATIONS_PATH, + BASE_PATH, + CERTIFICATES_PATH, + DYNAMIC_TRAEFIK_PATH, + LOGS_PATH, + MAIN_TRAEFIK_PATH, + MONITORING_PATH, + SSH_PATH, + } = paths(); + const directories = [ + BASE_PATH, + MAIN_TRAEFIK_PATH, + DYNAMIC_TRAEFIK_PATH, + LOGS_PATH, + APPLICATIONS_PATH, + SSH_PATH, + CERTIFICATES_PATH, + MONITORING_PATH, + ]; + + for (const dir of directories) { + try { + createDirectoryIfNotExist(dir); + if (dir === SSH_PATH) { + chmodSync(SSH_PATH, "700"); + } + } catch (error) { + console.log(error, " On path: ", dir); + } + } +}; diff --git a/packages/builders/src/setup/postgres-setup.ts b/packages/builders/src/setup/postgres-setup.ts new file mode 100644 index 000000000..ca606c2d1 --- /dev/null +++ b/packages/builders/src/setup/postgres-setup.ts @@ -0,0 +1,61 @@ +import type { CreateServiceOptions } from "dockerode"; +import { docker } from "../constants"; +import { pullImage } from "../utils/docker/utils"; +export const initializePostgres = async () => { + const imageName = "postgres:16"; + const containerName = "dokploy-postgres"; + const settings: CreateServiceOptions = { + Name: containerName, + TaskTemplate: { + ContainerSpec: { + Image: imageName, + Env: [ + "POSTGRES_USER=dokploy", + "POSTGRES_DB=dokploy", + "POSTGRES_PASSWORD=amukds4wi9001583845717ad2", + ], + Mounts: [ + { + Type: "volume", + Source: "dokploy-postgres-database", + Target: "/var/lib/postgresql/data", + }, + ], + }, + Networks: [{ Target: "dokploy-network" }], + Placement: { + Constraints: ["node.role==manager"], + }, + }, + Mode: { + Replicated: { + Replicas: 1, + }, + }, + EndpointSpec: { + Ports: [ + { + TargetPort: 5432, + PublishedPort: process.env.NODE_ENV === "development" ? 5432 : 0, + Protocol: "tcp", + PublishMode: "host", + }, + ], + }, + }; + try { + await pullImage(imageName); + + const service = docker.getService(containerName); + const inspect = await service.inspect(); + await service.update({ + version: Number.parseInt(inspect.Version.Index), + ...settings, + }); + + console.log("Postgres Started ✅"); + } catch (error) { + await docker.createService(settings); + console.log("Postgres Not Found: Starting ✅"); + } +}; diff --git a/packages/builders/src/setup/redis-setup.ts b/packages/builders/src/setup/redis-setup.ts new file mode 100644 index 000000000..abbacdd83 --- /dev/null +++ b/packages/builders/src/setup/redis-setup.ts @@ -0,0 +1,57 @@ +import type { CreateServiceOptions } from "dockerode"; +import { docker } from "../constants"; +import { pullImage } from "../utils/docker/utils"; + +export const initializeRedis = async () => { + const imageName = "redis:7"; + const containerName = "dokploy-redis"; + + const settings: CreateServiceOptions = { + Name: containerName, + TaskTemplate: { + ContainerSpec: { + Image: imageName, + Mounts: [ + { + Type: "volume", + Source: "redis-data-volume", + Target: "/data", + }, + ], + }, + Networks: [{ Target: "dokploy-network" }], + Placement: { + Constraints: ["node.role==manager"], + }, + }, + Mode: { + Replicated: { + Replicas: 1, + }, + }, + EndpointSpec: { + Ports: [ + { + TargetPort: 6379, + PublishedPort: process.env.NODE_ENV === "development" ? 6379 : 0, + Protocol: "tcp", + PublishMode: "host", + }, + ], + }, + }; + try { + await pullImage(imageName); + + const service = docker.getService(containerName); + const inspect = await service.inspect(); + await service.update({ + version: Number.parseInt(inspect.Version.Index), + ...settings, + }); + console.log("Redis Started ✅"); + } catch (error) { + await docker.createService(settings); + console.log("Redis Not Found: Starting ✅"); + } +}; diff --git a/packages/builders/src/setup/registry-setup.ts b/packages/builders/src/setup/registry-setup.ts new file mode 100644 index 000000000..02c1d3d01 --- /dev/null +++ b/packages/builders/src/setup/registry-setup.ts @@ -0,0 +1,91 @@ +import type { CreateServiceOptions } from "dockerode"; +import { docker, paths } from "../constants"; +import { pullImage } from "../utils/docker/utils"; +import { execAsync } from "../utils/process/execAsync"; +import { generatePassword } from "../templates/utils"; + +export const initializeRegistry = async ( + username: string, + password: string, +) => { + const { REGISTRY_PATH } = paths(); + const imageName = "registry:2.8.3"; + const containerName = "dokploy-registry"; + await generateRegistryPassword(username, password); + const randomPass = generatePassword(); + const settings: CreateServiceOptions = { + Name: containerName, + TaskTemplate: { + ContainerSpec: { + Image: imageName, + Env: [ + "REGISTRY_STORAGE_DELETE_ENABLED=true", + "REGISTRY_AUTH=htpasswd", + "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm", + "REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd", + `REGISTRY_HTTP_SECRET=${randomPass}`, + ], + Mounts: [ + { + Type: "bind", + Source: `${REGISTRY_PATH}/htpasswd`, + Target: "/auth/htpasswd", + ReadOnly: true, + }, + { + Type: "volume", + Source: "registry-data", + Target: "/var/lib/registry", + ReadOnly: false, + }, + ], + }, + Networks: [{ Target: "dokploy-network" }], + Placement: { + Constraints: ["node.role==manager"], + }, + }, + Mode: { + Replicated: { + Replicas: 1, + }, + }, + EndpointSpec: { + Ports: [ + { + TargetPort: 5000, + PublishedPort: 5000, + Protocol: "tcp", + PublishMode: "host", + }, + ], + }, + }; + try { + await pullImage(imageName); + + const service = docker.getService(containerName); + const inspect = await service.inspect(); + await service.update({ + version: Number.parseInt(inspect.Version.Index), + ...settings, + }); + console.log("Registry Started ✅"); + } catch (error) { + await docker.createService(settings); + console.log("Registry Not Found: Starting ✅"); + } +}; + +const generateRegistryPassword = async (username: string, password: string) => { + try { + const { REGISTRY_PATH } = paths(); + const command = `htpasswd -nbB ${username} "${password}" > ${REGISTRY_PATH}/htpasswd`; + const result = await execAsync(command); + console.log("Password generated ✅"); + return result.stdout.trim(); + } catch (error) { + console.error("Error generating password:", error); + return null; + } +}; diff --git a/packages/builders/src/setup/server-setup.ts b/packages/builders/src/setup/server-setup.ts new file mode 100644 index 000000000..ffc29db1f --- /dev/null +++ b/packages/builders/src/setup/server-setup.ts @@ -0,0 +1,315 @@ +import { createWriteStream } from "node:fs"; +import path from "node:path"; +import { + createServerDeployment, + updateDeploymentStatus, +} from "@/server/services/deployment"; +import { findServerById } from "@/server/services/server"; +import { paths } from "@/server/constants"; +import { + getDefaultMiddlewares, + getDefaultServerTraefikConfig, +} from "@/server/setup/traefik-setup"; +import { Client } from "ssh2"; +import { recreateDirectory } from "../utils/filesystem/directory"; +import { readSSHKey } from "../utils/filesystem/ssh"; + +import slug from "slugify"; + +export const slugify = (text: string | undefined) => { + if (!text) { + return ""; + } + + const cleanedText = text.trim().replace(/[^a-zA-Z0-9\s]/g, ""); + + return slug(cleanedText, { + lower: true, + trim: true, + strict: true, + }); +}; + +export const serverSetup = async (serverId: string) => { + const server = await findServerById(serverId); + const { LOGS_PATH } = paths(); + + const slugifyName = slugify(`server ${server.name}`); + + const fullPath = path.join(LOGS_PATH, slugifyName); + + await recreateDirectory(fullPath); + + const deployment = await createServerDeployment({ + serverId: server.serverId, + title: "Setup Server", + description: "Setup Server", + }); + + const writeStream = createWriteStream(deployment.logPath, { flags: "a" }); + try { + writeStream.write("\nInstalling Server Dependencies: ✅\n"); + await installRequirements(serverId, deployment.logPath); + writeStream.close(); + + await updateDeploymentStatus(deployment.deploymentId, "done"); + } catch (err) { + console.log(err); + await updateDeploymentStatus(deployment.deploymentId, "error"); + writeStream.write(err); + writeStream.close(); + } +}; + +const installRequirements = async (serverId: string, logPath: string) => { + const writeStream = createWriteStream(logPath, { flags: "a" }); + const client = new Client(); + const server = await findServerById(serverId); + if (!server.sshKeyId) { + writeStream.write("❌ No SSH Key found"); + writeStream.close(); + throw new Error("No SSH Key found"); + } + const keys = await readSSHKey(server.sshKeyId); + + if (!keys.privateKey) { + writeStream.write("❌ No SSH Key found"); + writeStream.close(); + throw new Error("No SSH Key found"); + } + return new Promise((resolve, reject) => { + client + .once("ready", () => { + const bashCommand = ` + + ${validatePorts()} + + command_exists() { + command -v "$@" > /dev/null 2>&1 + } + ${installRClone()} + ${installDocker()} + ${setupSwarm()} + ${setupNetwork()} + ${setupMainDirectory()} + ${setupDirectories()} + ${createTraefikConfig()} + ${createDefaultMiddlewares()} + ${createTraefikInstance()} + ${installNixpacks()} + ${installBuildpacks()} + `; + + client.exec(bashCommand, (err, stream) => { + if (err) { + writeStream.write(err); + reject(err); + return; + } + stream + .on("close", () => { + writeStream.write("Connection closed ✅"); + client.end(); + resolve(); + }) + .on("data", (data: string) => { + writeStream.write(data.toString()); + }) + .stderr.on("data", (data) => { + writeStream.write(data.toString()); + }); + }); + }) + .on("error", (err) => { + client.end(); + if (err.level === "client-authentication") { + writeStream.write( + `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`, + ); + reject( + new Error( + `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`, + ), + ); + } else { + writeStream.write( + `SSH connection error: ${err.message} ${err.level}`, + ); + reject(new Error(`SSH connection error: ${err.message}`)); + } + }) + .connect({ + host: server.ipAddress, + port: server.port, + username: server.username, + privateKey: keys.privateKey, + timeout: 99999, + }); + }); +}; + +const setupDirectories = () => { + const { SSH_PATH } = paths(true); + const directories = Object.values(paths(true)); + + const createDirsCommand = directories + .map((dir) => `mkdir -p "${dir}"`) + .join(" && "); + const chmodCommand = `chmod 700 "${SSH_PATH}"`; + + const command = ` + ${createDirsCommand} + ${chmodCommand} + `; + + return command; +}; + +const setupMainDirectory = () => ` + # Check if the /etc/dokploy directory exists + if [ -d /etc/dokploy ]; then + echo "/etc/dokploy already exists ✅" + else + # Create the /etc/dokploy directory + mkdir -p /etc/dokploy + chmod 777 /etc/dokploy + + echo "Directory /etc/dokploy created ✅" + fi +`; + +export const setupSwarm = () => ` + # Check if the node is already part of a Docker Swarm + if docker info | grep -q 'Swarm: active'; then + echo "Already part of a Docker Swarm ✅" + else + # Get IP address + get_ip() { + # Try to get IPv4 + local ipv4=\$(curl -4s https://ifconfig.io 2>/dev/null) + + if [ -n "\$ipv4" ]; then + echo "\$ipv4" + else + # Try to get IPv6 + local ipv6=\$(curl -6s https://ifconfig.io 2>/dev/null) + if [ -n "\$ipv6" ]; then + echo "\$ipv6" + fi + fi + } + advertise_addr=\$(get_ip) + + # Initialize Docker Swarm + docker swarm init --advertise-addr \$advertise_addr + echo "Swarm initialized ✅" + fi + `; + +const setupNetwork = () => ` + # Check if the dokploy-network already exists + if docker network ls | grep -q 'dokploy-network'; then + echo "Network dokploy-network already exists ✅" + else + # Create the dokploy-network if it doesn't exist + docker network create --driver overlay --attachable dokploy-network + echo "Network created ✅" + fi +`; + +const installDocker = () => ` + if command_exists docker; then + echo "Docker already installed ✅" + else + echo "Installing Docker ✅" + curl -sSL https://get.docker.com | sh -s -- --version 27.2.0 + fi +`; + +const validatePorts = () => ` + # check if something is running on port 80 + if ss -tulnp | grep ':80 ' >/dev/null; then + echo "Something is already running on port 80" >&2 + fi + + # check if something is running on port 443 + if ss -tulnp | grep ':443 ' >/dev/null; then + echo "Something is already running on port 443" >&2 + fi +`; + +const createTraefikConfig = () => { + const config = getDefaultServerTraefikConfig(); + + const command = ` + if [ -f "/etc/dokploy/traefik/dynamic/acme.json" ]; then + chmod 600 "/etc/dokploy/traefik/dynamic/acme.json" + fi + if [ -f "/etc/dokploy/traefik/traefik.yml" ]; then + echo "Traefik config already exists ✅" + else + echo "${config}" > /etc/dokploy/traefik/traefik.yml + fi + `; + + return command; +}; + +const createDefaultMiddlewares = () => { + const config = getDefaultMiddlewares(); + const command = ` + if [ -f "/etc/dokploy/traefik/dynamic/middlewares.yml" ]; then + echo "Middlewares config already exists ✅" + else + echo "${config}" > /etc/dokploy/traefik/dynamic/middlewares.yml + fi + `; + return command; +}; + +export const installRClone = () => ` +curl https://rclone.org/install.sh | sudo bash +`; + +export const createTraefikInstance = () => { + const command = ` + # Check if dokpyloy-traefik exists + if docker service ls | grep -q 'dokploy-traefik'; then + echo "Traefik already exists ✅" + else + # Create the dokploy-traefik service + docker service create \ + --name dokploy-traefik \ + --replicas 1 \ + --constraint 'node.role==manager' \ + --network dokploy-network \ + --mount type=bind,src=/etc/dokploy/traefik/traefik.yml,dst=/etc/traefik/traefik.yml \ + --mount type=bind,src=/etc/dokploy/traefik/dynamic,dst=/etc/dokploy/traefik/dynamic \ + --mount type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \ + --label traefik.enable=true \ + --publish mode=host,target=443,published=443 \ + --publish mode=host,target=80,published=80 \ + traefik:v3.1.2 + fi + `; + + return command; +}; + +const installNixpacks = () => ` + if command_exists nixpacks; then + echo "Nixpacks already installed ✅" + else + VERSION=1.28.1 bash -c "$(curl -fsSL https://nixpacks.com/install.sh)" + echo "Nixpacks version 1.28.1 installed ✅" + fi +`; + +const installBuildpacks = () => ` + if command_exists pack; then + echo "Buildpacks already installed ✅" + else + curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.35.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack + echo "Buildpacks version 0.35.0 installed ✅" + fi +`; diff --git a/packages/builders/src/setup/setup.ts b/packages/builders/src/setup/setup.ts new file mode 100644 index 000000000..c59877022 --- /dev/null +++ b/packages/builders/src/setup/setup.ts @@ -0,0 +1,47 @@ +import { docker } from "../constants"; + +export const initializeSwarm = async () => { + const swarmInitialized = await dockerSwarmInitialized(); + if (swarmInitialized) { + console.log("Swarm is already initilized"); + } else { + await docker.swarmInit({ + AdvertiseAddr: "127.0.0.1", + ListenAddr: "0.0.0.0", + }); + console.log("Swarm was initilized"); + } +}; + +export const dockerSwarmInitialized = async () => { + try { + await docker.swarmInspect(); + + return true; + } catch (e) { + return false; + } +}; + +export const initializeNetwork = async () => { + const networkInitialized = await dockerNetworkInitialized(); + if (networkInitialized) { + console.log("Network is already initilized"); + } else { + docker.createNetwork({ + Attachable: true, + Name: "dokploy-network", + Driver: "overlay", + }); + console.log("Network was initilized"); + } +}; + +export const dockerNetworkInitialized = async () => { + try { + await docker.getNetwork("dokploy-network").inspect(); + return true; + } catch (e) { + return false; + } +}; diff --git a/packages/builders/src/setup/traefik-setup.ts b/packages/builders/src/setup/traefik-setup.ts new file mode 100644 index 000000000..27bc99a4f --- /dev/null +++ b/packages/builders/src/setup/traefik-setup.ts @@ -0,0 +1,320 @@ +import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import type { ContainerTaskSpec, CreateServiceOptions } from "dockerode"; +import { dump } from "js-yaml"; +import { paths } from "../constants"; +import { pullImage, pullRemoteImage } from "../utils/docker/utils"; +import { getRemoteDocker } from "../utils/servers/remote-docker"; +import type { FileConfig } from "../utils/traefik/file-types"; +import type { MainTraefikConfig } from "../utils/traefik/types"; + +const TRAEFIK_SSL_PORT = + Number.parseInt(process.env.TRAEFIK_SSL_PORT ?? "", 10) || 443; +const TRAEFIK_PORT = Number.parseInt(process.env.TRAEFIK_PORT ?? "", 10) || 80; + +interface TraefikOptions { + enableDashboard?: boolean; + env?: string[]; + serverId?: string; +} + +export const initializeTraefik = async ({ + enableDashboard = false, + env, + serverId, +}: TraefikOptions = {}) => { + const { MAIN_TRAEFIK_PATH, DYNAMIC_TRAEFIK_PATH } = paths(!!serverId); + const imageName = "traefik:v3.1.2"; + const containerName = "dokploy-traefik"; + const settings: CreateServiceOptions = { + Name: containerName, + TaskTemplate: { + ContainerSpec: { + Image: imageName, + Env: env, + Mounts: [ + { + Type: "bind", + Source: `${MAIN_TRAEFIK_PATH}/traefik.yml`, + Target: "/etc/traefik/traefik.yml", + }, + { + Type: "bind", + Source: DYNAMIC_TRAEFIK_PATH, + Target: "/etc/dokploy/traefik/dynamic", + }, + { + Type: "bind", + Source: "/var/run/docker.sock", + Target: "/var/run/docker.sock", + }, + ], + }, + Networks: [{ Target: "dokploy-network" }], + Placement: { + Constraints: ["node.role==manager"], + }, + }, + Mode: { + Replicated: { + Replicas: 1, + }, + }, + Labels: { + "traefik.enable": "true", + }, + EndpointSpec: { + Ports: [ + { + TargetPort: 443, + PublishedPort: TRAEFIK_SSL_PORT, + PublishMode: "host", + }, + { + TargetPort: 80, + PublishedPort: TRAEFIK_PORT, + PublishMode: "host", + }, + ...(enableDashboard + ? [ + { + TargetPort: 8080, + PublishedPort: 8080, + PublishMode: "host" as const, + }, + ] + : []), + ], + }, + }; + const docker = await getRemoteDocker(serverId); + try { + if (serverId) { + await pullRemoteImage(imageName, serverId); + } else { + await pullImage(imageName); + } + + const service = docker.getService(containerName); + const inspect = await service.inspect(); + + const existingEnv = inspect.Spec.TaskTemplate.ContainerSpec.Env || []; + const updatedEnv = !env ? existingEnv : env; + + const updatedSettings = { + ...settings, + TaskTemplate: { + ...settings.TaskTemplate, + ContainerSpec: { + ...(settings?.TaskTemplate as ContainerTaskSpec).ContainerSpec, + Env: updatedEnv, + }, + }, + }; + await service.update({ + version: Number.parseInt(inspect.Version.Index), + ...updatedSettings, + }); + + console.log("Traefik Started ✅"); + } catch (error) { + await docker.createService(settings); + console.log("Traefik Not Found: Starting ✅"); + } +}; + +export const createDefaultServerTraefikConfig = () => { + const { DYNAMIC_TRAEFIK_PATH } = paths(); + const configFilePath = path.join(DYNAMIC_TRAEFIK_PATH, "dokploy.yml"); + + if (existsSync(configFilePath)) { + console.log("Default traefik config already exists"); + return; + } + + const appName = "dokploy"; + const serviceURLDefault = `http://${appName}:${process.env.PORT || 3000}`; + const config: FileConfig = { + http: { + routers: { + [`${appName}-router-app`]: { + rule: `Host(\`${appName}.docker.localhost\`) && PathPrefix(\`/\`)`, + service: `${appName}-service-app`, + entryPoints: ["web"], + }, + }, + services: { + [`${appName}-service-app`]: { + loadBalancer: { + servers: [{ url: serviceURLDefault }], + passHostHeader: true, + }, + }, + }, + }, + }; + + const yamlStr = dump(config); + mkdirSync(DYNAMIC_TRAEFIK_PATH, { recursive: true }); + writeFileSync( + path.join(DYNAMIC_TRAEFIK_PATH, `${appName}.yml`), + yamlStr, + "utf8", + ); +}; + +export const getDefaultTraefikConfig = () => { + const configObject: MainTraefikConfig = { + providers: { + ...(process.env.NODE_ENV === "development" + ? { + docker: { + defaultRule: + "Host(`{{ trimPrefix `/` .Name }}.docker.localhost`)", + }, + } + : { + swarm: { + exposedByDefault: false, + watch: false, + }, + docker: { + exposedByDefault: false, + }, + }), + file: { + directory: "/etc/dokploy/traefik/dynamic", + watch: true, + }, + }, + entryPoints: { + web: { + address: `:${TRAEFIK_PORT}`, + }, + websecure: { + address: `:${TRAEFIK_SSL_PORT}`, + ...(process.env.NODE_ENV === "production" && { + http: { + tls: { + certResolver: "letsencrypt", + }, + }, + }), + }, + }, + api: { + insecure: true, + }, + ...(process.env.NODE_ENV === "production" && { + certificatesResolvers: { + letsencrypt: { + acme: { + email: "test@localhost.com", + storage: "/etc/dokploy/traefik/dynamic/acme.json", + httpChallenge: { + entryPoint: "web", + }, + }, + }, + }, + }), + }; + + const yamlStr = dump(configObject); + + return yamlStr; +}; + +export const getDefaultServerTraefikConfig = () => { + const configObject: MainTraefikConfig = { + providers: { + swarm: { + exposedByDefault: false, + watch: false, + }, + docker: { + exposedByDefault: false, + }, + file: { + directory: "/etc/dokploy/traefik/dynamic", + watch: true, + }, + }, + entryPoints: { + web: { + address: `:${TRAEFIK_PORT}`, + }, + websecure: { + address: `:${TRAEFIK_SSL_PORT}`, + http: { + tls: { + certResolver: "letsencrypt", + }, + }, + }, + }, + api: { + insecure: true, + }, + certificatesResolvers: { + letsencrypt: { + acme: { + email: "test@localhost.com", + storage: "/etc/dokploy/traefik/dynamic/acme.json", + httpChallenge: { + entryPoint: "web", + }, + }, + }, + }, + }; + + const yamlStr = dump(configObject); + + return yamlStr; +}; + +export const createDefaultTraefikConfig = () => { + const { MAIN_TRAEFIK_PATH, DYNAMIC_TRAEFIK_PATH } = paths(); + const mainConfig = path.join(MAIN_TRAEFIK_PATH, "traefik.yml"); + const acmeJsonPath = path.join(DYNAMIC_TRAEFIK_PATH, "acme.json"); + + if (existsSync(acmeJsonPath)) { + chmodSync(acmeJsonPath, "600"); + } + if (existsSync(mainConfig)) { + console.log("Main config already exists"); + return; + } + const yamlStr = getDefaultTraefikConfig(); + mkdirSync(MAIN_TRAEFIK_PATH, { recursive: true }); + writeFileSync(mainConfig, yamlStr, "utf8"); +}; + +export const getDefaultMiddlewares = () => { + const defaultMiddlewares = { + http: { + middlewares: { + "redirect-to-https": { + redirectScheme: { + scheme: "https", + permanent: true, + }, + }, + }, + }, + }; + const yamlStr = dump(defaultMiddlewares); + return yamlStr; +}; +export const createDefaultMiddlewares = () => { + const { DYNAMIC_TRAEFIK_PATH } = paths(); + const middlewaresPath = path.join(DYNAMIC_TRAEFIK_PATH, "middlewares.yml"); + if (existsSync(middlewaresPath)) { + console.log("Default middlewares already exists"); + return; + } + const yamlStr = getDefaultMiddlewares(); + mkdirSync(DYNAMIC_TRAEFIK_PATH, { recursive: true }); + writeFileSync(middlewaresPath, yamlStr, "utf8"); +}; diff --git a/packages/builders/src/templates/appsmith/docker-compose.yml b/packages/builders/src/templates/appsmith/docker-compose.yml new file mode 100644 index 000000000..f520ee362 --- /dev/null +++ b/packages/builders/src/templates/appsmith/docker-compose.yml @@ -0,0 +1,6 @@ +version: "3.8" +services: + appsmith: + image: index.docker.io/appsmith/appsmith-ee:v1.29 + volumes: + - ../files/stacks:/appsmith-stacks diff --git a/packages/builders/src/templates/appsmith/index.ts b/packages/builders/src/templates/appsmith/index.ts new file mode 100644 index 000000000..ff744a249 --- /dev/null +++ b/packages/builders/src/templates/appsmith/index.ts @@ -0,0 +1,23 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateHash, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainServiceHash = generateHash(schema.projectName); + + const domains: DomainSchema[] = [ + { + host: generateRandomDomain(schema), + port: 80, + serviceName: "appsmith", + }, + ]; + + return { + domains, + }; +} diff --git a/packages/builders/src/templates/aptabase/docker-compose.yml b/packages/builders/src/templates/aptabase/docker-compose.yml new file mode 100644 index 000000000..934fd1eea --- /dev/null +++ b/packages/builders/src/templates/aptabase/docker-compose.yml @@ -0,0 +1,51 @@ +services: + aptabase_db: + image: postgres:15-alpine + restart: always + volumes: + - db-data:/var/lib/postgresql/data + environment: + POSTGRES_USER: aptabase + POSTGRES_PASSWORD: sTr0NGp4ssw0rd + networks: + - dokploy-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U aptabase"] + interval: 10s + timeout: 5s + retries: 5 + + aptabase_events_db: + image: clickhouse/clickhouse-server:23.8.16.16-alpine + restart: always + volumes: + - events-db-data:/var/lib/clickhouse + environment: + CLICKHOUSE_USER: aptabase + CLICKHOUSE_PASSWORD: sTr0NGp4ssw0rd + ulimits: + nofile: + soft: 262144 + hard: 262144 + networks: + - dokploy-network + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8123 || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + + aptabase: + image: ghcr.io/aptabase/aptabase:main + restart: always + environment: + BASE_URL: http://${APTABASE_HOST} + AUTH_SECRET: ${AUTH_SECRET} + DATABASE_URL: Server=aptabase_db;Port=5432;User Id=aptabase;Password=sTr0NGp4ssw0rd;Database=aptabase + CLICKHOUSE_URL: Host=aptabase_events_db;Port=8123;Username=aptabase;Password=sTr0NGp4ssw0rd + +volumes: + db-data: + driver: local + events-db-data: + driver: local diff --git a/packages/builders/src/templates/aptabase/index.ts b/packages/builders/src/templates/aptabase/index.ts new file mode 100644 index 000000000..38b077ae8 --- /dev/null +++ b/packages/builders/src/templates/aptabase/index.ts @@ -0,0 +1,27 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateBase64, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + const authSecret = generateBase64(32); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 8080, + serviceName: "aptabase", + }, + ]; + + const envs = [`APTABASE_HOST=${mainDomain}`, `AUTH_SECRET=${authSecret}`]; + + return { + envs, + domains, + }; +} diff --git a/packages/builders/src/templates/baserow/docker-compose.yml b/packages/builders/src/templates/baserow/docker-compose.yml new file mode 100644 index 000000000..db588e83a --- /dev/null +++ b/packages/builders/src/templates/baserow/docker-compose.yml @@ -0,0 +1,10 @@ +version: "3.8" +services: + baserow: + image: baserow/baserow:1.25.2 + environment: + BASEROW_PUBLIC_URL: "http://${BASEROW_HOST}" + volumes: + - baserow_data:/baserow/data +volumes: + baserow_data: diff --git a/packages/builders/src/templates/baserow/index.ts b/packages/builders/src/templates/baserow/index.ts new file mode 100644 index 000000000..fa57417cc --- /dev/null +++ b/packages/builders/src/templates/baserow/index.ts @@ -0,0 +1,24 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainHost = generateRandomDomain(schema); + + const domains: DomainSchema[] = [ + { + host: mainHost, + port: 80, + serviceName: "baserow", + }, + ]; + const envs = [`BASEROW_HOST=${mainHost}`]; + + return { + envs, + domains, + }; +} diff --git a/packages/builders/src/templates/calcom/docker-compose.yml b/packages/builders/src/templates/calcom/docker-compose.yml new file mode 100644 index 000000000..7a1d8c92a --- /dev/null +++ b/packages/builders/src/templates/calcom/docker-compose.yml @@ -0,0 +1,26 @@ +services: + postgres: + image: postgres:16-alpine + networks: + - dokploy-network + volumes: + - calcom-data:/var/lib/postgresql/data + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=password + - POSTGRES_DB=db + - DATABASE_URL=postgres://postgres:password@postgres:5432/db + + calcom: + image: calcom/cal.com:v2.7.6 + depends_on: + - postgres + environment: + - NEXTAUTH_SECRET=${NEXTAUTH_SECRET} + - CALENDSO_ENCRYPTION_KEY=${CALENDSO_ENCRYPTION_KEY} + - DATABASE_URL=postgres://postgres:password@postgres:5432/db + - NEXT_PUBLIC_WEBAPP_URL=http://${CALCOM_HOST} + - NEXTAUTH_URL=http://${CALCOM_HOST}/api/auth + +volumes: + calcom-data: diff --git a/packages/builders/src/templates/calcom/index.ts b/packages/builders/src/templates/calcom/index.ts new file mode 100644 index 000000000..d359e9c7c --- /dev/null +++ b/packages/builders/src/templates/calcom/index.ts @@ -0,0 +1,32 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateBase64, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + const calcomEncryptionKey = generateBase64(32); + const nextAuthSecret = generateBase64(32); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 3000, + serviceName: "calcom", + }, + ]; + + const envs = [ + `CALCOM_HOST=${mainDomain}`, + `NEXTAUTH_SECRET=${nextAuthSecret}`, + `CALENDSO_ENCRYPTION_KEY=${calcomEncryptionKey}`, + ]; + + return { + envs, + domains, + }; +} diff --git a/packages/builders/src/templates/directus/docker-compose.yml b/packages/builders/src/templates/directus/docker-compose.yml new file mode 100644 index 000000000..6f058ba61 --- /dev/null +++ b/packages/builders/src/templates/directus/docker-compose.yml @@ -0,0 +1,47 @@ +version: "3.8" +services: + database: + image: postgis/postgis:13-master + volumes: + - directus:/var/lib/postgresql/data + networks: + - dokploy-network + environment: + POSTGRES_USER: "directus" + POSTGRES_PASSWORD: "directus" + POSTGRES_DB: "directus" + + cache: + image: redis:6 + networks: + - dokploy-network + + directus: + image: directus/directus:10.12.1 + ports: + - 8055 + volumes: + - ../files/uploads:/directus/uploads + - ../files/extensions:/directus/extensions + depends_on: + - cache + - database + environment: + SECRET: "replace-with-secure-random-value" + + DB_CLIENT: "pg" + DB_HOST: "database" + DB_PORT: "5432" + DB_DATABASE: "directus" + DB_USER: "directus" + DB_PASSWORD: "directus" + + CACHE_ENABLED: "true" + CACHE_AUTO_PURGE: "true" + CACHE_STORE: "redis" + REDIS: "redis://cache:6379" + + ADMIN_EMAIL: "admin@example.com" + ADMIN_PASSWORD: "d1r3ctu5" +volumes: + directus: diff --git a/packages/builders/src/templates/directus/index.ts b/packages/builders/src/templates/directus/index.ts new file mode 100644 index 000000000..42a05aee0 --- /dev/null +++ b/packages/builders/src/templates/directus/index.ts @@ -0,0 +1,20 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const domains: DomainSchema[] = [ + { + host: generateRandomDomain(schema), + port: 8055, + serviceName: "directus", + }, + ]; + + return { + domains, + }; +} diff --git a/packages/builders/src/templates/documenso/docker-compose.yml b/packages/builders/src/templates/documenso/docker-compose.yml new file mode 100644 index 000000000..562fe4987 --- /dev/null +++ b/packages/builders/src/templates/documenso/docker-compose.yml @@ -0,0 +1,43 @@ +version: "3.8" +services: + postgres: + image: postgres:16 + networks: + - dokploy-network + volumes: + - documenso-data:/var/lib/postgresql/data + environment: + - POSTGRES_USER=documenso + - POSTGRES_PASSWORD=password + - POSTGRES_DB=documenso + healthcheck: + test: ["CMD-SHELL", "pg_isready -U documenso"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + + documenso: + image: documenso/documenso:v1.5.6-rc.2 + depends_on: + postgres: + condition: service_healthy + environment: + - PORT=${DOCUMENSO_PORT} + - NEXTAUTH_URL=http://${DOCUMENSO_HOST} + - NEXTAUTH_SECRET=${NEXTAUTH_SECRET} + - NEXT_PRIVATE_ENCRYPTION_KEY=${NEXT_PRIVATE_ENCRYPTION_KEY} + - NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY=${NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY} + - NEXT_PUBLIC_WEBAPP_URL=http://${DOCUMENSO_HOST} + - NEXT_PRIVATE_DATABASE_URL=postgres://documenso:password@postgres:5432/documenso + - NEXT_PRIVATE_DIRECT_DATABASE_URL=postgres://documenso:password@postgres:5432/documenso + - NEXT_PUBLIC_UPLOAD_TRANSPORT=database + - NEXT_PRIVATE_SMTP_TRANSPORT=smtp-auth + - NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH=/opt/documenso/cert.p12 + ports: + - ${DOCUMENSO_PORT} + volumes: + - /opt/documenso/cert.p12:/opt/documenso/cert.p12 + +volumes: + documenso-data: diff --git a/packages/builders/src/templates/documenso/index.ts b/packages/builders/src/templates/documenso/index.ts new file mode 100644 index 000000000..c70d1db4a --- /dev/null +++ b/packages/builders/src/templates/documenso/index.ts @@ -0,0 +1,36 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateBase64, + generatePassword, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + const nextAuthSecret = generateBase64(32); + const documensoEncryptionKey = generatePassword(32); + const documensoSecondaryEncryptionKey = generatePassword(64); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 3000, + serviceName: "documenso", + }, + ]; + + const envs = [ + `DOCUMENSO_HOST=${mainDomain}`, + "DOCUMENSO_PORT=3000", + `NEXTAUTH_SECRET=${nextAuthSecret}`, + `NEXT_PRIVATE_ENCRYPTION_KEY=${documensoEncryptionKey}`, + `NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY=${documensoSecondaryEncryptionKey}`, + ]; + + return { + envs, + domains, + }; +} diff --git a/packages/builders/src/templates/doublezero/docker-compose.yml b/packages/builders/src/templates/doublezero/docker-compose.yml new file mode 100644 index 000000000..352470334 --- /dev/null +++ b/packages/builders/src/templates/doublezero/docker-compose.yml @@ -0,0 +1,19 @@ +services: + doublezero: + restart: always + image: liltechnomancer/double-zero:0.2.1 + volumes: + - db-data:/var/lib/doublezero/data + environment: + AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} + AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} + AWS_REGION: ${AWS_REGION} + SQS_URL: ${SQS_URL} + SYSTEM_EMAIL: ${SYSTEM_EMAIL} + SECRET_KEY_BASE: ${SECRET_KEY_BASE} + PHX_HOST: ${DOUBLEZERO_HOST} + DATABASE_PATH: ./00.db + +volumes: + db-data: + driver: local diff --git a/packages/builders/src/templates/doublezero/index.ts b/packages/builders/src/templates/doublezero/index.ts new file mode 100644 index 000000000..fa774e9dc --- /dev/null +++ b/packages/builders/src/templates/doublezero/index.ts @@ -0,0 +1,36 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateBase64, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + const secretKeyBase = generateBase64(64); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 4000, + serviceName: "doublezero", + }, + ]; + + const envs = [ + `DOUBLEZERO_HOST=${mainDomain}`, + "DOUBLEZERO_PORT=4000", + `SECRET_KEY_BASE=${secretKeyBase}`, + "AWS_ACCESS_KEY_ID=your-aws-access-key", + "AWS_SECRET_ACCESS_KEY=your-aws-secret-key", + "AWS_REGION=your-aws-region", + "SQS_URL=your-aws-sqs-url", + "SYSTEM_EMAIL=", + ]; + + return { + envs, + domains, + }; +} diff --git a/packages/builders/src/templates/excalidraw/docker-compose.yml b/packages/builders/src/templates/excalidraw/docker-compose.yml new file mode 100644 index 000000000..8743434b6 --- /dev/null +++ b/packages/builders/src/templates/excalidraw/docker-compose.yml @@ -0,0 +1,7 @@ +version: "3.8" + +services: + excalidraw: + networks: + - dokploy-network + image: excalidraw/excalidraw:latest diff --git a/packages/builders/src/templates/excalidraw/index.ts b/packages/builders/src/templates/excalidraw/index.ts new file mode 100644 index 000000000..13a43c440 --- /dev/null +++ b/packages/builders/src/templates/excalidraw/index.ts @@ -0,0 +1,23 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateHash, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 80, + serviceName: "excalidraw", + }, + ]; + + return { + domains, + }; +} diff --git a/packages/builders/src/templates/ghost/docker-compose.yml b/packages/builders/src/templates/ghost/docker-compose.yml new file mode 100644 index 000000000..288c59e54 --- /dev/null +++ b/packages/builders/src/templates/ghost/docker-compose.yml @@ -0,0 +1,29 @@ +version: "3.8" +services: + ghost: + image: ghost:5-alpine + restart: always + environment: + database__client: mysql + database__connection__host: db + database__connection__user: root + database__connection__password: example + database__connection__database: ghost + url: http://${GHOST_HOST} + + volumes: + - ghost:/var/lib/ghost/content + + db: + image: mysql:8.0 + restart: always + networks: + - dokploy-network + environment: + MYSQL_ROOT_PASSWORD: example + volumes: + - db:/var/lib/mysql + +volumes: + ghost: + db: diff --git a/packages/builders/src/templates/ghost/index.ts b/packages/builders/src/templates/ghost/index.ts new file mode 100644 index 000000000..1a88c3629 --- /dev/null +++ b/packages/builders/src/templates/ghost/index.ts @@ -0,0 +1,25 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateHash, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 2368, + serviceName: "ghost", + }, + ]; + const envs = [`GHOST_HOST=${mainDomain}`]; + + return { + envs, + domains, + }; +} diff --git a/packages/builders/src/templates/gitea/docker-compose.yml b/packages/builders/src/templates/gitea/docker-compose.yml new file mode 100644 index 000000000..679936fb7 --- /dev/null +++ b/packages/builders/src/templates/gitea/docker-compose.yml @@ -0,0 +1,37 @@ +version: "3.8" +services: + gitea: + image: gitea/gitea:1.22.2 + environment: + - USER_UID=${USER_UID} + - USER_GID=${USER_GID} + - GITEA__database__DB_TYPE=postgres + - GITEA__database__HOST=db:5432 + - GITEA__database__NAME=gitea + - GITEA__database__USER=gitea + - GITEA__database__PASSWD=gitea + restart: always + networks: + - dokploy-network + volumes: + - gitea_server:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + depends_on: + - db + + db: + image: postgres:16 + restart: always + environment: + - POSTGRES_USER=gitea + - POSTGRES_PASSWORD=gitea + - POSTGRES_DB=gitea + networks: + - dokploy-network + volumes: + - gitea_db:/var/lib/postgresql/data + +volumes: + gitea_db: + gitea_server: \ No newline at end of file diff --git a/packages/builders/src/templates/gitea/index.ts b/packages/builders/src/templates/gitea/index.ts new file mode 100644 index 000000000..92a20df64 --- /dev/null +++ b/packages/builders/src/templates/gitea/index.ts @@ -0,0 +1,24 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 3000, + serviceName: "gitea", + }, + ]; + const envs = ["USER_UID=1000", "USER_GID=1000"]; + + return { + envs, + domains, + }; +} diff --git a/packages/builders/src/templates/glitchtip/docker-compose.yml b/packages/builders/src/templates/glitchtip/docker-compose.yml new file mode 100644 index 000000000..e45c76627 --- /dev/null +++ b/packages/builders/src/templates/glitchtip/docker-compose.yml @@ -0,0 +1,59 @@ +x-environment: &default-environment + DATABASE_URL: postgres://postgres:postgres@postgres:5432/postgres + SECRET_KEY: ${SECRET_KEY} + PORT: ${GLITCHTIP_PORT} + EMAIL_URL: consolemail:// + GLITCHTIP_DOMAIN: http://${GLITCHTIP_HOST} + DEFAULT_FROM_EMAIL: email@glitchtip.com + CELERY_WORKER_AUTOSCALE: "1,3" + CELERY_WORKER_MAX_TASKS_PER_CHILD: "10000" + +x-depends_on: &default-depends_on + - postgres + - redis + +services: + postgres: + image: postgres:16 + environment: + POSTGRES_HOST_AUTH_METHOD: "trust" + restart: unless-stopped + volumes: + - pg-data:/var/lib/postgresql/data + networks: + - dokploy-network + redis: + image: redis + restart: unless-stopped + networks: + - dokploy-network + web: + image: glitchtip/glitchtip:v4.0 + depends_on: *default-depends_on + ports: + - ${GLITCHTIP_PORT} + environment: *default-environment + restart: unless-stopped + volumes: + - uploads:/code/uploads + worker: + image: glitchtip/glitchtip:v4.0 + command: ./bin/run-celery-with-beat.sh + depends_on: *default-depends_on + environment: *default-environment + restart: unless-stopped + volumes: + - uploads:/code/uploads + networks: + - dokploy-network + migrate: + image: glitchtip/glitchtip:v4.0 + depends_on: *default-depends_on + command: "./manage.py migrate" + environment: *default-environment + networks: + - dokploy-network + +volumes: + pg-data: + uploads: diff --git a/packages/builders/src/templates/glitchtip/index.ts b/packages/builders/src/templates/glitchtip/index.ts new file mode 100644 index 000000000..093d752e1 --- /dev/null +++ b/packages/builders/src/templates/glitchtip/index.ts @@ -0,0 +1,30 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateBase64, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + const secretKey = generateBase64(32); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 8000, + serviceName: "web", + }, + ]; + const envs = [ + `GLITCHTIP_HOST=${mainDomain}`, + "GLITCHTIP_PORT=8000", + `SECRET_KEY=${secretKey}`, + ]; + + return { + envs, + domains, + }; +} diff --git a/packages/builders/src/templates/grafana/docker-compose.yml b/packages/builders/src/templates/grafana/docker-compose.yml new file mode 100644 index 000000000..9d913c17f --- /dev/null +++ b/packages/builders/src/templates/grafana/docker-compose.yml @@ -0,0 +1,9 @@ +version: "3.8" +services: + grafana: + image: grafana/grafana-enterprise:9.5.20 + restart: unless-stopped + volumes: + - grafana-storage:/var/lib/grafana +volumes: + grafana-storage: {} diff --git a/packages/builders/src/templates/grafana/index.ts b/packages/builders/src/templates/grafana/index.ts new file mode 100644 index 000000000..fb614ef32 --- /dev/null +++ b/packages/builders/src/templates/grafana/index.ts @@ -0,0 +1,19 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const domains: DomainSchema[] = [ + { + host: generateRandomDomain(schema), + port: 3000, + serviceName: "grafana", + }, + ]; + return { + domains, + }; +} diff --git a/packages/builders/src/templates/jellyfin/docker-compose.yml b/packages/builders/src/templates/jellyfin/docker-compose.yml new file mode 100644 index 000000000..cb61476ab --- /dev/null +++ b/packages/builders/src/templates/jellyfin/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.8" +services: + jellyfin: + image: jellyfin/jellyfin:10 + volumes: + - config:/config + - cache:/cache + - media:/media + restart: "unless-stopped" + # Optional - alternative address used for autodiscovery + environment: + - JELLYFIN_PublishedServerUrl=http://${JELLYFIN_HOST} + # Optional - may be necessary for docker healthcheck to pass if running in host network mode + extra_hosts: + - "host.docker.internal:host-gateway" +volumes: + config: + cache: + media: diff --git a/packages/builders/src/templates/jellyfin/index.ts b/packages/builders/src/templates/jellyfin/index.ts new file mode 100644 index 000000000..61c9c9b7c --- /dev/null +++ b/packages/builders/src/templates/jellyfin/index.ts @@ -0,0 +1,25 @@ +// EXAMPLE +import { + type DomainSchema, + type Schema, + type Template, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const domain = generateRandomDomain(schema); + const domains: DomainSchema[] = [ + { + host: domain, + port: 8096, + serviceName: "jellyfin", + }, + ]; + + const envs = [`JELLYFIN_HOST=${domain}`]; + + return { + envs, + domains, + }; +} diff --git a/packages/builders/src/templates/listmonk/docker-compose.yml b/packages/builders/src/templates/listmonk/docker-compose.yml new file mode 100644 index 000000000..725d0a098 --- /dev/null +++ b/packages/builders/src/templates/listmonk/docker-compose.yml @@ -0,0 +1,49 @@ +services: + db: + image: postgres:13 + ports: + - 5432 + networks: + - dokploy-network + environment: + - POSTGRES_PASSWORD=listmonk + - POSTGRES_USER=listmonk + - POSTGRES_DB=listmonk + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U listmonk"] + interval: 10s + timeout: 5s + retries: 6 + volumes: + - listmonk-data:/var/lib/postgresql/data + + setup: + image: listmonk/listmonk:v3.0.0 + networks: + - dokploy-network + volumes: + - ../files/config.toml:/listmonk/config.toml + depends_on: + - db + command: + [ + sh, + -c, + "sleep 3 && ./listmonk --install --idempotent --yes --config config.toml", + ] + + app: + restart: unless-stopped + image: listmonk/listmonk:v3.0.0 + environment: + - TZ=Etc/UTC + depends_on: + - db + - setup + volumes: + - ../files/config.toml:/listmonk/config.toml + +volumes: + listmonk-data: + driver: local diff --git a/packages/builders/src/templates/listmonk/index.ts b/packages/builders/src/templates/listmonk/index.ts new file mode 100644 index 000000000..725659ca4 --- /dev/null +++ b/packages/builders/src/templates/listmonk/index.ts @@ -0,0 +1,57 @@ +import { + type DomainSchema, + type Schema, + type Template, + generatePassword, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const randomDomain = generateRandomDomain(schema); + const adminPassword = generatePassword(32); + + const domains: DomainSchema[] = [ + { + host: randomDomain, + port: 9000, + serviceName: "app", + }, + ]; + + const envs = [ + `# login with admin:${adminPassword}`, + "# check config.toml in Advanced / Volumes for more options", + ]; + + const mounts: Template["mounts"] = [ + { + filePath: "config.toml", + content: `[app] +address = "0.0.0.0:9000" + +admin_username = "admin" +admin_password = "${adminPassword}" + +[db] +host = "db" +port = 5432 +user = "listmonk" +password = "listmonk" +database = "listmonk" + +ssl_mode = "disable" +max_open = 25 +max_idle = 25 +max_lifetime = "300s" + +params = "" +`, + }, + ]; + + return { + envs, + mounts, + domains, + }; +} diff --git a/packages/builders/src/templates/meilisearch/docker-compose.yml b/packages/builders/src/templates/meilisearch/docker-compose.yml new file mode 100644 index 000000000..ae5ebcb1a --- /dev/null +++ b/packages/builders/src/templates/meilisearch/docker-compose.yml @@ -0,0 +1,14 @@ +version: "3.8" + +services: + meilisearch: + image: getmeili/meilisearch:v1.8.3 + volumes: + - meili_data:/meili_data + environment: + MEILI_MASTER_KEY: ${MEILI_MASTER_KEY} + MEILI_ENV: ${MEILI_ENV} + +volumes: + meili_data: + driver: local diff --git a/packages/builders/src/templates/meilisearch/index.ts b/packages/builders/src/templates/meilisearch/index.ts new file mode 100644 index 000000000..cfb8a9a4b --- /dev/null +++ b/packages/builders/src/templates/meilisearch/index.ts @@ -0,0 +1,26 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateBase64, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + const masterKey = generateBase64(32); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 7700, + serviceName: "meilisearch", + }, + ]; + const envs = ["MEILI_ENV=development", `MEILI_MASTER_KEY=${masterKey}`]; + + return { + envs, + domains, + }; +} diff --git a/packages/builders/src/templates/metabase/docker-compose.yml b/packages/builders/src/templates/metabase/docker-compose.yml new file mode 100644 index 000000000..4dca4d015 --- /dev/null +++ b/packages/builders/src/templates/metabase/docker-compose.yml @@ -0,0 +1,26 @@ +version: "3.8" +services: + metabase: + image: metabase/metabase:v0.50.8 + volumes: + - /dev/urandom:/dev/random:ro + environment: + MB_DB_TYPE: postgres + MB_DB_DBNAME: metabaseappdb + MB_DB_PORT: 5432 + MB_DB_USER: metabase + MB_DB_PASS: mysecretpassword + MB_DB_HOST: postgres + healthcheck: + test: curl --fail -I http://localhost:3000/api/health || exit 1 + interval: 15s + timeout: 5s + retries: 5 + postgres: + image: postgres:14 + environment: + POSTGRES_USER: metabase + POSTGRES_DB: metabaseappdb + POSTGRES_PASSWORD: mysecretpassword + networks: + - dokploy-network diff --git a/packages/builders/src/templates/metabase/index.ts b/packages/builders/src/templates/metabase/index.ts new file mode 100644 index 000000000..0a08916e6 --- /dev/null +++ b/packages/builders/src/templates/metabase/index.ts @@ -0,0 +1,22 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const randomDomain = generateRandomDomain(schema); + + const domains: DomainSchema[] = [ + { + host: randomDomain, + port: 3000, + serviceName: "metabase", + }, + ]; + + return { + domains, + }; +} diff --git a/packages/builders/src/templates/minio/docker-compose.yml b/packages/builders/src/templates/minio/docker-compose.yml new file mode 100644 index 000000000..4b24bbcce --- /dev/null +++ b/packages/builders/src/templates/minio/docker-compose.yml @@ -0,0 +1,13 @@ +version: "3.8" +services: + minio: + image: minio/minio + volumes: + - minio-data:/data + environment: + - MINIO_ROOT_USER=minioadmin + - MINIO_ROOT_PASSWORD=minioadmin123 + command: server /data --console-address ":9001" + +volumes: + minio-data: diff --git a/packages/builders/src/templates/minio/index.ts b/packages/builders/src/templates/minio/index.ts new file mode 100644 index 000000000..1345aafdc --- /dev/null +++ b/packages/builders/src/templates/minio/index.ts @@ -0,0 +1,28 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + const apiDomain = generateRandomDomain(schema); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 9001, + serviceName: "minio", + }, + { + host: apiDomain, + port: 9000, + serviceName: "minio", + }, + ]; + + return { + domains, + }; +} diff --git a/packages/builders/src/templates/n8n/docker-compose.yml b/packages/builders/src/templates/n8n/docker-compose.yml new file mode 100644 index 000000000..f8fb1f169 --- /dev/null +++ b/packages/builders/src/templates/n8n/docker-compose.yml @@ -0,0 +1,18 @@ +version: "3.8" +services: + n8n: + image: docker.n8n.io/n8nio/n8n:1.48.1 + restart: always + environment: + - N8N_HOST=${N8N_HOST} + - N8N_PORT=${N8N_PORT} + - N8N_PROTOCOL=http + - NODE_ENV=production + - WEBHOOK_URL=https://${N8N_HOST}/ + - GENERIC_TIMEZONE=${GENERIC_TIMEZONE} + - N8N_SECURE_COOKIE=false + volumes: + - n8n_data:/home/node/.n8n + +volumes: + n8n_data: diff --git a/packages/builders/src/templates/n8n/index.ts b/packages/builders/src/templates/n8n/index.ts new file mode 100644 index 000000000..da93c025f --- /dev/null +++ b/packages/builders/src/templates/n8n/index.ts @@ -0,0 +1,28 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 5678, + serviceName: "n8n", + }, + ]; + const envs = [ + `N8N_HOST=${mainDomain}`, + "N8N_PORT=5678", + "GENERIC_TIMEZONE=Europe/Berlin", + ]; + + return { + envs, + domains, + }; +} diff --git a/packages/builders/src/templates/nocodb/docker-compose.yml b/packages/builders/src/templates/nocodb/docker-compose.yml new file mode 100644 index 000000000..726cf5e61 --- /dev/null +++ b/packages/builders/src/templates/nocodb/docker-compose.yml @@ -0,0 +1,32 @@ +version: "3.8" +services: + nocodb: + image: nocodb/nocodb:0.251.1 + restart: always + environment: + NC_DB: "pg://root_db?u=postgres&p=password&d=root_db" + PORT: ${NOCODB_PORT} + NC_REDIS_URL: ${NC_REDIS_URL} + volumes: + - nc_data:/usr/app/data + + root_db: + image: postgres:14.7 + restart: always + networks: + - dokploy-network + environment: + POSTGRES_DB: root_db + POSTGRES_PASSWORD: password + POSTGRES_USER: postgres + healthcheck: + interval: 10s + retries: 10 + test: 'pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB"' + timeout: 2s + volumes: + - "db_data:/var/lib/postgresql/data" + +volumes: + db_data: {} + nc_data: {} diff --git a/packages/builders/src/templates/nocodb/index.ts b/packages/builders/src/templates/nocodb/index.ts new file mode 100644 index 000000000..60620dbd4 --- /dev/null +++ b/packages/builders/src/templates/nocodb/index.ts @@ -0,0 +1,28 @@ +// EXAMPLE +import { + type DomainSchema, + type Schema, + type Template, + generateBase64, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const randomDomain = generateRandomDomain(schema); + const secretBase = generateBase64(64); + + const domains: DomainSchema[] = [ + { + host: randomDomain, + port: 8000, + serviceName: "nocodb", + }, + ]; + + const envs = ["NOCODB_PORT=8000", `NC_AUTH_JWT_SECRET=${secretBase}`]; + + return { + envs, + domains, + }; +} diff --git a/packages/builders/src/templates/odoo/docker-compose.yml b/packages/builders/src/templates/odoo/docker-compose.yml new file mode 100644 index 000000000..80b34f0c0 --- /dev/null +++ b/packages/builders/src/templates/odoo/docker-compose.yml @@ -0,0 +1,29 @@ +version: "3.8" +services: + web: + image: odoo:16.0 + depends_on: + - db + environment: + - HOST=db + - USER=odoo + - PASSWORD=odoo + volumes: + - odoo-web-data:/var/lib/odoo + - ../files/config:/etc/odoo + - ../files/addons:/mnt/extra-addons + + db: + image: postgres:13 + networks: + - dokploy-network + environment: + - POSTGRES_DB=postgres + - POSTGRES_USER=odoo + - POSTGRES_PASSWORD=odoo + volumes: + - odoo-db-data:/var/lib/postgresql/data + +volumes: + odoo-web-data: + odoo-db-data: diff --git a/packages/builders/src/templates/odoo/index.ts b/packages/builders/src/templates/odoo/index.ts new file mode 100644 index 000000000..904293c11 --- /dev/null +++ b/packages/builders/src/templates/odoo/index.ts @@ -0,0 +1,22 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const randomDomain = generateRandomDomain(schema); + + const domains: DomainSchema[] = [ + { + host: randomDomain, + port: 8069, + serviceName: "web", + }, + ]; + + return { + domains, + }; +} diff --git a/packages/builders/src/templates/open-webui/docker-compose.yml b/packages/builders/src/templates/open-webui/docker-compose.yml new file mode 100644 index 000000000..d396dacc0 --- /dev/null +++ b/packages/builders/src/templates/open-webui/docker-compose.yml @@ -0,0 +1,26 @@ +version: "3.8" +services: + ollama: + volumes: + - ollama:/root/.ollama + networks: + - dokploy-network + pull_policy: always + tty: true + restart: unless-stopped + image: ollama/ollama:${OLLAMA_DOCKER_TAG-latest} + + open-webui: + image: ghcr.io/open-webui/open-webui:${WEBUI_DOCKER_TAG-main} + volumes: + - open-webui:/app/backend/data + depends_on: + - ollama + environment: + - "OLLAMA_BASE_URL=http://ollama:11434" + - "WEBUI_SECRET_KEY=" + restart: unless-stopped + +volumes: + ollama: {} + open-webui: {} diff --git a/packages/builders/src/templates/open-webui/index.ts b/packages/builders/src/templates/open-webui/index.ts new file mode 100644 index 000000000..0431c2a11 --- /dev/null +++ b/packages/builders/src/templates/open-webui/index.ts @@ -0,0 +1,24 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const randomDomain = generateRandomDomain(schema); + + const domains: DomainSchema[] = [ + { + host: randomDomain, + port: 8080, + serviceName: "open-webui", + }, + ]; + const envs = ["OLLAMA_DOCKER_TAG=0.1.47", "WEBUI_DOCKER_TAG=0.3.7"]; + + return { + envs, + domains, + }; +} diff --git a/packages/builders/src/templates/phpmyadmin/docker-compose.yml b/packages/builders/src/templates/phpmyadmin/docker-compose.yml new file mode 100644 index 000000000..1f775f09a --- /dev/null +++ b/packages/builders/src/templates/phpmyadmin/docker-compose.yml @@ -0,0 +1,28 @@ +version: "3.8" + +services: + db: + image: mysql:5.7 + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + MYSQL_DATABASE: tu_base_de_datos + MYSQL_USER: ${MYSQL_USER} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + volumes: + - db_data:/var/lib/mysql + networks: + - dokploy-network + + phpmyadmin: + image: phpmyadmin/phpmyadmin:5.2.1 + environment: + PMA_HOST: db + PMA_USER: ${MYSQL_USER} + PMA_PASSWORD: ${MYSQL_PASSWORD} + PMA_ARBITRARY: 1 + depends_on: + - db + +volumes: + db_data: + driver: local diff --git a/packages/builders/src/templates/phpmyadmin/index.ts b/packages/builders/src/templates/phpmyadmin/index.ts new file mode 100644 index 000000000..e1c976b9d --- /dev/null +++ b/packages/builders/src/templates/phpmyadmin/index.ts @@ -0,0 +1,32 @@ +import { + type DomainSchema, + type Schema, + type Template, + generatePassword, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + const rootPassword = generatePassword(32); + const password = generatePassword(32); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 80, + serviceName: "phpmyadmin", + }, + ]; + const envs = [ + `MYSQL_ROOT_PASSWORD=${rootPassword}`, + "MYSQL_DATABASE=mysql", + "MYSQL_USER=phpmyadmin", + `MYSQL_PASSWORD=${password}`, + ]; + + return { + envs, + domains, + }; +} diff --git a/packages/builders/src/templates/plausible/docker-compose.yml b/packages/builders/src/templates/plausible/docker-compose.yml new file mode 100644 index 000000000..62ce5ece4 --- /dev/null +++ b/packages/builders/src/templates/plausible/docker-compose.yml @@ -0,0 +1,44 @@ +version: "3.8" +services: + plausible_db: + image: postgres:16-alpine + restart: always + networks: + - dokploy-network + volumes: + - db-data:/var/lib/postgresql/data + environment: + - POSTGRES_PASSWORD=postgres + + plausible_events_db: + image: clickhouse/clickhouse-server:24.3.3.102-alpine + restart: always + networks: + - dokploy-network + volumes: + - event-data:/var/lib/clickhouse + - event-logs:/var/log/clickhouse-server + - ../files/clickhouse/clickhouse-config.xml:/etc/clickhouse-server/config.d/logging.xml:ro + - ../files/clickhouse/clickhouse-user-config.xml:/etc/clickhouse-server/users.d/logging.xml:ro + ulimits: + nofile: + soft: 262144 + hard: 262144 + + plausible: + image: ghcr.io/plausible/community-edition:v2.1.0 + restart: always + command: sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run" + depends_on: + - plausible_db + - plausible_events_db + env_file: + - .env + +volumes: + db-data: + driver: local + event-data: + driver: local + event-logs: + driver: local diff --git a/packages/builders/src/templates/plausible/index.ts b/packages/builders/src/templates/plausible/index.ts new file mode 100644 index 000000000..2bd1212b6 --- /dev/null +++ b/packages/builders/src/templates/plausible/index.ts @@ -0,0 +1,71 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateBase64, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + const secretBase = generateBase64(64); + const toptKeyBase = generateBase64(32); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 8000, + serviceName: "plausible", + }, + ]; + + const envs = [ + `BASE_URL=http://${mainDomain}`, + `SECRET_KEY_BASE=${secretBase}`, + `TOTP_VAULT_KEY=${toptKeyBase}`, + ]; + + const mounts: Template["mounts"] = [ + { + filePath: "/clickhouse/clickhouse-config.xml", + content: ` + + + warning + true + + + + + + + + + + + + + + `, + }, + { + filePath: "/clickhouse/clickhouse-user-config.xml", + content: ` + + + + 0 + 0 + + + + `, + }, + ]; + + return { + envs, + mounts, + domains, + }; +} diff --git a/packages/builders/src/templates/pocketbase/docker-compose.yml b/packages/builders/src/templates/pocketbase/docker-compose.yml new file mode 100644 index 000000000..fa6674afe --- /dev/null +++ b/packages/builders/src/templates/pocketbase/docker-compose.yml @@ -0,0 +1,9 @@ +version: "3.8" +services: + pocketbase: + image: spectado/pocketbase:0.22.12 + restart: unless-stopped + volumes: + - /etc/dokploy/templates/${HASH}/data:/pb_data + - /etc/dokploy/templates/${HASH}/public:/pb_public + - /etc/dokploy/templates/${HASH}/migrations:/pb_migrations diff --git a/packages/builders/src/templates/pocketbase/index.ts b/packages/builders/src/templates/pocketbase/index.ts new file mode 100644 index 000000000..f9fc7f8f0 --- /dev/null +++ b/packages/builders/src/templates/pocketbase/index.ts @@ -0,0 +1,22 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 80, + serviceName: "pocketbase", + }, + ]; + + return { + domains, + }; +} diff --git a/packages/builders/src/templates/rocketchat/docker-compose.yml b/packages/builders/src/templates/rocketchat/docker-compose.yml new file mode 100644 index 000000000..751bd845c --- /dev/null +++ b/packages/builders/src/templates/rocketchat/docker-compose.yml @@ -0,0 +1,35 @@ +version: "3.8" +services: + rocketchat: + image: registry.rocket.chat/rocketchat/rocket.chat:6.9.2 + restart: always + environment: + MONGO_URL: "mongodb://mongodb:27017/rocketchat?replicaSet=rs0" + MONGO_OPLOG_URL: "mongodb://mongodb:27017/local?replicaSet=rs0" + ROOT_URL: ${ROOT_URL:-http://${ROCKETCHAT_HOST}:${ROCKETCHAT_PORT}} + PORT: ${ROCKETCHAT_PORT} + DEPLOY_METHOD: docker + DEPLOY_PLATFORM: + REG_TOKEN: + depends_on: + - mongodb + + mongodb: + image: docker.io/bitnami/mongodb:5.0 + restart: always + volumes: + - mongodb_data:/bitnami/mongodb + environment: + MONGODB_REPLICA_SET_MODE: primary + MONGODB_REPLICA_SET_NAME: rs0 + MONGODB_PORT_NUMBER: 27017 + MONGODB_INITIAL_PRIMARY_HOST: mongodb + MONGODB_INITIAL_PRIMARY_PORT_NUMBER: 27017 + MONGODB_ADVERTISED_HOSTNAME: mongodb + MONGODB_ENABLE_JOURNAL: true + ALLOW_EMPTY_PASSWORD: yes + networks: + - dokploy-network + +volumes: + mongodb_data: { driver: local } diff --git a/packages/builders/src/templates/rocketchat/index.ts b/packages/builders/src/templates/rocketchat/index.ts new file mode 100644 index 000000000..0c10307a2 --- /dev/null +++ b/packages/builders/src/templates/rocketchat/index.ts @@ -0,0 +1,25 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 3000, + serviceName: "rocketchat", + }, + ]; + + const envs = [`ROCKETCHAT_HOST=${mainDomain}`, "ROCKETCHAT_PORT=3000"]; + + return { + envs, + domains, + }; +} diff --git a/packages/builders/src/templates/soketi/docker-compose.yml b/packages/builders/src/templates/soketi/docker-compose.yml new file mode 100644 index 000000000..1784cdc79 --- /dev/null +++ b/packages/builders/src/templates/soketi/docker-compose.yml @@ -0,0 +1,12 @@ +version: "3" + +services: + soketi: + image: quay.io/soketi/soketi:1.4-16-debian + container_name: soketi + environment: + SOKETI_DEBUG: "1" + SOKETI_HOST: "0.0.0.0" + SOKETI_PORT: "6001" + SOKETI_METRICS_SERVER_PORT: "9601" + restart: unless-stopped diff --git a/packages/builders/src/templates/soketi/index.ts b/packages/builders/src/templates/soketi/index.ts new file mode 100644 index 000000000..47aa461df --- /dev/null +++ b/packages/builders/src/templates/soketi/index.ts @@ -0,0 +1,28 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + const metricsDomain = generateRandomDomain(schema); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 6001, + serviceName: "soketi", + }, + { + host: metricsDomain, + port: 9601, + serviceName: "soketi", + }, + ]; + + return { + domains, + }; +} diff --git a/packages/builders/src/templates/supabase/docker-compose.yml b/packages/builders/src/templates/supabase/docker-compose.yml new file mode 100644 index 000000000..e1e187fde --- /dev/null +++ b/packages/builders/src/templates/supabase/docker-compose.yml @@ -0,0 +1,460 @@ +# Usage +# Start: docker compose up +# With helpers: docker compose -f docker-compose.yml -f ../files/dev/docker-compose.dev.yml up +# Stop: docker compose down +# Destroy: docker compose -f docker-compose.yml -f ../files/dev/docker-compose.dev.yml down -v --remove-orphans + +name: supabase +version: "3.8" + +services: + studio: + container_name: supabase-studio + image: supabase/studio:20240729-ce42139 + networks: + - dokploy-network + restart: unless-stopped + healthcheck: + test: + [ + "CMD", + "node", + "-e", + "require('http').get('http://localhost:3000/api/profile', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})", + ] + timeout: 5s + interval: 5s + retries: 3 + depends_on: + analytics: + condition: service_healthy + environment: + STUDIO_PG_META_URL: http://meta:8080 + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + + DEFAULT_ORGANIZATION_NAME: ${STUDIO_DEFAULT_ORGANIZATION} + DEFAULT_PROJECT_NAME: ${STUDIO_DEFAULT_PROJECT} + + SUPABASE_URL: http://kong:8000 + SUPABASE_PUBLIC_URL: http://${SUPABASE_HOST} + SUPABASE_ANON_KEY: ${ANON_KEY} + SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY} + AUTH_JWT_SECRET: ${JWT_SECRET} + + LOGFLARE_API_KEY: ${LOGFLARE_API_KEY} + LOGFLARE_URL: http://analytics:4000 + NEXT_PUBLIC_ENABLE_LOGS: true + # Comment to use Big Query backend for analytics + NEXT_ANALYTICS_BACKEND_PROVIDER: postgres + # Uncomment to use Big Query backend for analytics + # NEXT_ANALYTICS_BACKEND_PROVIDER: bigquery + + kong: + container_name: supabase-kong + image: kong:2.8.1 + restart: unless-stopped + networks: + - dokploy-network + # https://unix.stackexchange.com/a/294837 + entrypoint: bash -c 'eval "echo \"$$(cat ~/temp.yml)\"" > ~/kong.yml && /docker-entrypoint.sh kong docker-start' + #ports: + # - ${KONG_HTTP_PORT}:8000/tcp + # - ${KONG_HTTPS_PORT}:8443/tcp + expose: + - 8000 + - 8443 + depends_on: + analytics: + condition: service_healthy + environment: + KONG_DATABASE: "off" + KONG_DECLARATIVE_CONFIG: /home/kong/kong.yml + # https://github.com/supabase/cli/issues/14 + KONG_DNS_ORDER: LAST,A,CNAME + KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth + KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k + KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k + SUPABASE_ANON_KEY: ${ANON_KEY} + SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY} + DASHBOARD_USERNAME: ${DASHBOARD_USERNAME} + DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD} + volumes: + # https://github.com/supabase/supabase/issues/12661 + - ../files/volumes/api/kong.yml:/home/kong/temp.yml:ro + + auth: + container_name: supabase-auth + image: supabase/gotrue:v2.158.1 + networks: + - dokploy-network + depends_on: + db: + # Disable this if you are using an external Postgres database + condition: service_healthy + analytics: + condition: service_healthy + healthcheck: + test: + [ + "CMD", + "wget", + "--no-verbose", + "--tries=1", + "--spider", + "http://localhost:9999/health", + ] + timeout: 5s + interval: 5s + retries: 3 + restart: unless-stopped + environment: + GOTRUE_API_HOST: 0.0.0.0 + GOTRUE_API_PORT: 9999 + API_EXTERNAL_URL: http://${SUPABASE_HOST} + + GOTRUE_DB_DRIVER: postgres + GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOSTNAME}:${POSTGRES_PORT}/${POSTGRES_DB} + + GOTRUE_SITE_URL: http://${SUPABASE_HOST} + GOTRUE_URI_ALLOW_LIST: ${ADDITIONAL_REDIRECT_URLS} + GOTRUE_DISABLE_SIGNUP: ${DISABLE_SIGNUP} + + GOTRUE_JWT_ADMIN_ROLES: service_role + GOTRUE_JWT_AUD: authenticated + GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated + GOTRUE_JWT_EXP: ${JWT_EXPIRY} + GOTRUE_JWT_SECRET: ${JWT_SECRET} + + GOTRUE_EXTERNAL_EMAIL_ENABLED: ${ENABLE_EMAIL_SIGNUP} + GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: ${ENABLE_ANONYMOUS_USERS} + GOTRUE_MAILER_AUTOCONFIRM: ${ENABLE_EMAIL_AUTOCONFIRM} + # GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED: true + # GOTRUE_SMTP_MAX_FREQUENCY: 1s + GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_ADMIN_EMAIL} + GOTRUE_SMTP_HOST: ${SMTP_HOSTNAME} + GOTRUE_SMTP_PORT: ${SMTP_PORT} + GOTRUE_SMTP_USER: ${SMTP_USER} + GOTRUE_SMTP_PASS: ${SMTP_PASS} + GOTRUE_SMTP_SENDER_NAME: ${SMTP_SENDER_NAME} + GOTRUE_MAILER_URLPATHS_INVITE: ${MAILER_URLPATHS_INVITE} + GOTRUE_MAILER_URLPATHS_CONFIRMATION: ${MAILER_URLPATHS_CONFIRMATION} + GOTRUE_MAILER_URLPATHS_RECOVERY: ${MAILER_URLPATHS_RECOVERY} + GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: ${MAILER_URLPATHS_EMAIL_CHANGE} + + GOTRUE_EXTERNAL_PHONE_ENABLED: ${ENABLE_PHONE_SIGNUP} + GOTRUE_SMS_AUTOCONFIRM: ${ENABLE_PHONE_AUTOCONFIRM} + # Uncomment to enable custom access token hook. You'll need to create a public.custom_access_token_hook function and grant necessary permissions. + # See: https://supabase.com/docs/guides/auth/auth-hooks#hook-custom-access-token for details + # GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED="true" + # GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_URI="pg-functions://postgres/public/custom_access_token_hook" + + # GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_ENABLED="true" + # GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_URI="pg-functions://postgres/public/mfa_verification_attempt" + + # GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_ENABLED="true" + # GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_URI="pg-functions://postgres/public/password_verification_attempt" + + rest: + container_name: supabase-rest + image: postgrest/postgrest:v12.2.0 + networks: + - dokploy-network + depends_on: + db: + # Disable this if you are using an external Postgres database + condition: service_healthy + analytics: + condition: service_healthy + restart: unless-stopped + environment: + PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@${POSTGRES_HOSTNAME}:${POSTGRES_PORT}/${POSTGRES_DB} + PGRST_DB_SCHEMAS: ${PGRST_DB_SCHEMAS} + PGRST_DB_ANON_ROLE: anon + PGRST_JWT_SECRET: ${JWT_SECRET} + PGRST_DB_USE_LEGACY_GUCS: "false" + PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET} + PGRST_APP_SETTINGS_JWT_EXP: ${JWT_EXPIRY} + command: "postgrest" + + realtime: + # This container name looks inconsistent but is correct because realtime constructs tenant id by parsing the subdomain + container_name: realtime-dev.supabase-realtime + image: supabase/realtime:v2.30.23 + networks: + - dokploy-network + depends_on: + db: + # Disable this if you are using an external Postgres database + condition: service_healthy + analytics: + condition: service_healthy + healthcheck: + test: + [ + "CMD", + "curl", + "-sSfL", + "--head", + "-o", + "/dev/null", + "-H", + "Authorization: Bearer ${ANON_KEY}", + "http://localhost:4000/api/tenants/realtime-dev/health", + ] + timeout: 5s + interval: 5s + retries: 3 + restart: unless-stopped + environment: + PORT: 4000 + DB_HOST: ${POSTGRES_HOSTNAME} + DB_PORT: ${POSTGRES_PORT} + DB_USER: supabase_admin + DB_PASSWORD: ${POSTGRES_PASSWORD} + DB_NAME: ${POSTGRES_DB} + DB_AFTER_CONNECT_QUERY: "SET search_path TO _realtime" + DB_ENC_KEY: supabaserealtime + API_JWT_SECRET: ${JWT_SECRET} + SECRET_KEY_BASE: UpNVntn3cDxHJpq99YMc1T1AQgQpc8kfYTuRgBiYa15BLrx8etQoXz3gZv1/u2oq + ERL_AFLAGS: -proto_dist inet_tcp + DNS_NODES: "''" + RLIMIT_NOFILE: "10000" + APP_NAME: realtime + SEED_SELF_HOST: true + + # To use S3 backed storage: docker compose -f docker-compose.yml -f docker-compose.s3.yml up + storage: + container_name: supabase-storage + image: supabase/storage-api:v1.0.6 + networks: + - dokploy-network + depends_on: + db: + # Disable this if you are using an external Postgres database + condition: service_healthy + rest: + condition: service_started + imgproxy: + condition: service_started + healthcheck: + test: + [ + "CMD", + "wget", + "--no-verbose", + "--tries=1", + "--spider", + "http://localhost:5000/status", + ] + timeout: 5s + interval: 5s + retries: 3 + restart: unless-stopped + environment: + ANON_KEY: ${ANON_KEY} + SERVICE_KEY: ${SERVICE_ROLE_KEY} + POSTGREST_URL: http://rest:3000 + PGRST_JWT_SECRET: ${JWT_SECRET} + DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOSTNAME}:${POSTGRES_PORT}/${POSTGRES_DB} + FILE_SIZE_LIMIT: 52428800 + STORAGE_BACKEND: file + FILE_STORAGE_BACKEND_PATH: /var/lib/storage + TENANT_ID: stub + # TODO: https://github.com/supabase/storage-api/issues/55 + REGION: stub + GLOBAL_S3_BUCKET: stub + ENABLE_IMAGE_TRANSFORMATION: "true" + IMGPROXY_URL: http://imgproxy:5001 + volumes: + - ../files/volumes/storage:/var/lib/storage:z + + imgproxy: + container_name: supabase-imgproxy + image: darthsim/imgproxy:v3.8.0 + networks: + - dokploy-network + healthcheck: + test: ["CMD", "imgproxy", "health"] + timeout: 5s + interval: 5s + retries: 3 + environment: + IMGPROXY_BIND: ":5001" + IMGPROXY_LOCAL_FILESYSTEM_ROOT: / + IMGPROXY_USE_ETAG: "true" + IMGPROXY_ENABLE_WEBP_DETECTION: ${IMGPROXY_ENABLE_WEBP_DETECTION} + volumes: + - ../files/volumes/storage:/var/lib/storage:z + + meta: + container_name: supabase-meta + image: supabase/postgres-meta:v0.83.2 + networks: + - dokploy-network + depends_on: + db: + # Disable this if you are using an external Postgres database + condition: service_healthy + analytics: + condition: service_healthy + restart: unless-stopped + environment: + PG_META_PORT: 8080 + PG_META_DB_HOST: ${POSTGRES_HOSTNAME} + PG_META_DB_PORT: ${POSTGRES_PORT} + PG_META_DB_NAME: ${POSTGRES_DB} + PG_META_DB_USER: supabase_admin + PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD} + + functions: + container_name: supabase-edge-functions + image: supabase/edge-runtime:v1.56.0 + restart: unless-stopped + networks: + - dokploy-network + depends_on: + analytics: + condition: service_healthy + environment: + JWT_SECRET: ${JWT_SECRET} + SUPABASE_URL: http://kong:8000 + SUPABASE_ANON_KEY: ${ANON_KEY} + SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY} + SUPABASE_DB_URL: postgresql://postgres:${POSTGRES_PASSWORD}@${POSTGRES_HOSTNAME}:${POSTGRES_PORT}/${POSTGRES_DB} + # TODO: Allow configuring VERIFY_JWT per function. This PR might help: https://github.com/supabase/cli/pull/786 + VERIFY_JWT: "${FUNCTIONS_VERIFY_JWT}" + volumes: + - ../files/volumes/functions:/home/deno/functions:Z + command: + - start + - --main-service + - /home/deno/functions/main + + analytics: + container_name: supabase-analytics + image: supabase/logflare:1.4.0 + networks: + - dokploy-network + healthcheck: + test: ["CMD", "curl", "http://localhost:4000/health"] + timeout: 5s + interval: 5s + retries: 10 + restart: unless-stopped + depends_on: + db: + # Disable this if you are using an external Postgres database + condition: service_healthy + # Uncomment to use Big Query backend for analytics + # volumes: + # - type: bind + # source: ${PWD}/gcloud.json + # target: /opt/app/rel/logflare/bin/gcloud.json + # read_only: true + environment: + LOGFLARE_NODE_HOST: 127.0.0.1 + DB_USERNAME: supabase_admin + DB_DATABASE: ${POSTGRES_DB} + DB_HOSTNAME: ${POSTGRES_HOSTNAME} + DB_PORT: ${POSTGRES_PORT} + DB_PASSWORD: ${POSTGRES_PASSWORD} + DB_SCHEMA: _analytics + LOGFLARE_API_KEY: ${LOGFLARE_API_KEY} + LOGFLARE_SINGLE_TENANT: true + LOGFLARE_SUPABASE_MODE: true + LOGFLARE_MIN_CLUSTER_SIZE: 1 + + # Comment variables to use Big Query backend for analytics + POSTGRES_BACKEND_URL: postgresql://supabase_admin:${POSTGRES_PASSWORD}@${POSTGRES_HOSTNAME}:${POSTGRES_PORT}/${POSTGRES_DB} + POSTGRES_BACKEND_SCHEMA: _analytics + LOGFLARE_FEATURE_FLAG_OVERRIDE: multibackend=true + # Uncomment to use Big Query backend for analytics + # GOOGLE_PROJECT_ID: ${GOOGLE_PROJECT_ID} + # GOOGLE_PROJECT_NUMBER: ${GOOGLE_PROJECT_NUMBER} + #ports: + # - 4000:4000 + expose: + - 4000 + + # Comment out everything below this point if you are using an external Postgres database + db: + container_name: supabase-db + image: supabase/postgres:15.1.1.78 + networks: + - dokploy-network + healthcheck: + test: pg_isready -U postgres -h localhost + interval: 5s + timeout: 5s + retries: 10 + depends_on: + vector: + condition: service_healthy + command: + - postgres + - -c + - config_file=/etc/postgresql/postgresql.conf + - -c + - log_min_messages=fatal # prevents Realtime polling queries from appearing in logs + restart: unless-stopped + #ports: + # # Pass down internal port because it's set dynamically by other services + # - ${POSTGRES_PORT}:${POSTGRES_PORT} + expose: + - ${POSTGRES_PORT} + environment: + POSTGRES_HOST: /var/run/postgresql + PGPORT: ${POSTGRES_PORT} + POSTGRES_PORT: ${POSTGRES_PORT} + PGPASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + PGDATABASE: ${POSTGRES_DB} + POSTGRES_DB: ${POSTGRES_DB} + JWT_SECRET: ${JWT_SECRET} + JWT_EXP: ${JWT_EXPIRY} + volumes: + - ../files/volumes/db/realtime.sql:/docker-entrypoint-initdb.d/migrations/99-realtime.sql:Z + # Must be superuser to create event trigger + - ../files/volumes/db/webhooks.sql:/docker-entrypoint-initdb.d/init-scripts/98-webhooks.sql:Z + # Must be superuser to alter reserved role + - ../files/volumes/db/roles.sql:/docker-entrypoint-initdb.d/init-scripts/99-roles.sql:Z + # Initialize the database settings with JWT_SECRET and JWT_EXP + - ../files/volumes/db/jwt.sql:/docker-entrypoint-initdb.d/init-scripts/99-jwt.sql:Z + # PGDATA directory is persisted between restarts + - ../files/volumes/db/data:/var/lib/postgresql/data:Z + # Changes required for Analytics support + - ../files/volumes/db/logs.sql:/docker-entrypoint-initdb.d/migrations/99-logs.sql:Z + # Use named volume to persist pgsodium decryption key between restarts + - db-config:/etc/postgresql-custom + + vector: + container_name: supabase-vector + image: timberio/vector:0.28.1-alpine + networks: + - dokploy-network + healthcheck: + test: + [ + "CMD", + "wget", + "--no-verbose", + "--tries=1", + "--spider", + "http://vector:9001/health", + ] + timeout: 5s + interval: 5s + retries: 3 + volumes: + - ../files/volumes/logs/vector.yml:/etc/vector/vector.yml:ro + - ${DOCKER_SOCKET_LOCATION}:/var/run/docker.sock:ro + environment: + LOGFLARE_API_KEY: ${LOGFLARE_API_KEY} + command: ["--config", "etc/vector/vector.yml"] + +volumes: + db-config: + +networks: + dokploy-network: + external: true diff --git a/packages/builders/src/templates/supabase/index.ts b/packages/builders/src/templates/supabase/index.ts new file mode 100644 index 000000000..6922c77fb --- /dev/null +++ b/packages/builders/src/templates/supabase/index.ts @@ -0,0 +1,995 @@ +import { createHmac } from "node:crypto"; +import { + type DomainSchema, + type Schema, + type Template, + generateBase64, + generatePassword, + generateRandomDomain, +} from "../utils"; + +interface JWTPayload { + role: "anon" | "service_role"; + iss: string; + iat: number; + exp: number; +} + +function base64UrlEncode(str: string): string { + return Buffer.from(str) + .toString("base64") + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=/g, ""); +} + +function generateJWT(payload: JWTPayload, secret: string): string { + const header = { alg: "HS256", typ: "JWT" }; + + const encodedHeader = base64UrlEncode(JSON.stringify(header)); + const encodedPayload = base64UrlEncode(JSON.stringify(payload)); + + const signature = createHmac("sha256", secret) + .update(`${encodedHeader}.${encodedPayload}`) + .digest("base64url"); + + return `${encodedHeader}.${encodedPayload}.${signature}`; +} + +export function generateSupabaseAnonJWT(secret: string): string { + const now = Math.floor(Date.now() / 1000); + const payload: JWTPayload = { + role: "anon", + iss: "supabase", + iat: now, + exp: now + 100 * 365 * 24 * 60 * 60, // 100 years + }; + + return generateJWT(payload, secret); +} + +export function generateSupabaseServiceJWT(secret: string): string { + const now = Math.floor(Date.now() / 1000); + const payload: JWTPayload = { + role: "service_role", + iss: "supabase", + iat: now, + exp: now + 100 * 365 * 24 * 60 * 60, // 100 years + }; + + return generateJWT(payload, secret); +} + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + + const postgresPassword = generatePassword(32); + const jwtSecret = generateBase64(32); + const dashboardPassword = generatePassword(32); + const logflareApiKey = generatePassword(32); + + const annonKey = generateSupabaseAnonJWT(jwtSecret); + const serviceRoleKey = generateSupabaseServiceJWT(jwtSecret); + const domains: DomainSchema[] = [ + { + serviceName: "kong", + host: mainDomain, + port: 8000, + }, + ]; + + const envs = [ + `SUPABASE_HOST=${mainDomain}`, + `POSTGRES_PASSWORD=${postgresPassword}`, + `JWT_SECRET=${jwtSecret}`, + `ANON_KEY=${annonKey}`, + `SERVICE_ROLE_KEY=${serviceRoleKey}`, + "DASHBOARD_USERNAME=supabase", + `DASHBOARD_PASSWORD=${dashboardPassword}`, + "POSTGRES_HOSTNAME=db", + "POSTGRES_DB=postgres", + "POSTGRES_PORT=5432", + "KONG_HTTP_PORT=8000", + "KONG_HTTPS_PORT=8443", + "PGRST_DB_SCHEMAS=public,storage,graphql_public", + "ADDITIONAL_REDIRECT_URLS=", + "JWT_EXPIRY=3600", + "DISABLE_SIGNUP=false", + `MAILER_URLPATHS_CONFIRMATION=\"/auth/v1/verify\"`, + `MAILER_URLPATHS_INVITE=\"/auth/v1/verify\"`, + `MAILER_URLPATHS_RECOVERY=\"/auth/v1/verify\"`, + `MAILER_URLPATHS_EMAIL_CHANGE=\"/auth/v1/verify\"`, + "ENABLE_EMAIL_SIGNUP=true", + "ENABLE_EMAIL_AUTOCONFIRM=false", + "SMTP_ADMIN_EMAIL=admin@example.com", + "SMTP_HOSTNAME=supabase-mail", + "SMTP_PORT=2500", + "SMTP_USER=fake_mail_user", + "SMTP_PASS=fake_mail_password", + "SMTP_SENDER_NAME=fake_sender", + "ENABLE_ANONYMOUS_USERS=false", + "ENABLE_PHONE_SIGNUP=true", + "ENABLE_PHONE_AUTOCONFIRM=true", + "STUDIO_DEFAULT_ORGANIZATION=Default Organization", + "STUDIO_DEFAULT_PROJECT=Default Project", + "STUDIO_PORT=3000", + "IMGPROXY_ENABLE_WEBP_DETECTION=true", + "FUNCTIONS_VERIFY_JWT=false", + `LOGFLARE_LOGGER_BACKEND_API_KEY=${logflareApiKey}`, + `LOGFLARE_API_KEY=${logflareApiKey}`, + "DOCKER_SOCKET_LOCATION=/var/run/docker.sock", + "GOOGLE_PROJECT_ID=GOOGLE_PROJECT_ID", + "GOOGLE_PROJECT_NUMBER=GOOGLE_PROJECT_NUMBER", + ]; + + const mounts: Template["mounts"] = [ + { + filePath: "/volumes/api/kong.yml", + content: ` +_format_version: '2.1' +_transform: true + +### +### Consumers / Users +### +consumers: + - username: DASHBOARD + - username: anon + keyauth_credentials: + - key: $SUPABASE_ANON_KEY + - username: service_role + keyauth_credentials: + - key: $SUPABASE_SERVICE_KEY + +### +### Access Control List +### +acls: + - consumer: anon + group: anon + - consumer: service_role + group: admin + +### +### Dashboard credentials +### +basicauth_credentials: + - consumer: DASHBOARD + username: $DASHBOARD_USERNAME + password: $DASHBOARD_PASSWORD + +### +### API Routes +### +services: + ## Open Auth routes + - name: auth-v1-open + url: http://auth:9999/verify + routes: + - name: auth-v1-open + strip_path: true + paths: + - /auth/v1/verify + plugins: + - name: cors + - name: auth-v1-open-callback + url: http://auth:9999/callback + routes: + - name: auth-v1-open-callback + strip_path: true + paths: + - /auth/v1/callback + plugins: + - name: cors + - name: auth-v1-open-authorize + url: http://auth:9999/authorize + routes: + - name: auth-v1-open-authorize + strip_path: true + paths: + - /auth/v1/authorize + plugins: + - name: cors + + ## Secure Auth routes + - name: auth-v1 + _comment: 'GoTrue: /auth/v1/* -> http://auth:9999/*' + url: http://auth:9999/ + routes: + - name: auth-v1-all + strip_path: true + paths: + - /auth/v1/ + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + + ## Secure REST routes + - name: rest-v1 + _comment: 'PostgREST: /rest/v1/* -> http://rest:3000/*' + url: http://rest:3000/ + routes: + - name: rest-v1-all + strip_path: true + paths: + - /rest/v1/ + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: true + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + + ## Secure GraphQL routes + - name: graphql-v1 + _comment: 'PostgREST: /graphql/v1/* -> http://rest:3000/rpc/graphql' + url: http://rest:3000/rpc/graphql + routes: + - name: graphql-v1-all + strip_path: true + paths: + - /graphql/v1 + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: true + - name: request-transformer + config: + add: + headers: + - Content-Profile:graphql_public + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + + ## Secure Realtime routes + - name: realtime-v1-ws + _comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*' + url: http://realtime-dev.supabase-realtime:4000/socket + protocol: ws + routes: + - name: realtime-v1-ws + strip_path: true + paths: + - /realtime/v1/ + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + - name: realtime-v1-rest + _comment: 'Realtime: /realtime/v1/* -> ws://realtime:4000/socket/*' + url: http://realtime-dev.supabase-realtime:4000/api + protocol: http + routes: + - name: realtime-v1-rest + strip_path: true + paths: + - /realtime/v1/api + plugins: + - name: cors + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin + - anon + ## Storage routes: the storage server manages its own auth + - name: storage-v1 + _comment: 'Storage: /storage/v1/* -> http://storage:5000/*' + url: http://storage:5000/ + routes: + - name: storage-v1-all + strip_path: true + paths: + - /storage/v1/ + plugins: + - name: cors + + ## Edge Functions routes + - name: functions-v1 + _comment: 'Edge Functions: /functions/v1/* -> http://functions:9000/*' + url: http://functions:9000/ + routes: + - name: functions-v1-all + strip_path: true + paths: + - /functions/v1/ + plugins: + - name: cors + + ## Analytics routes + - name: analytics-v1 + _comment: 'Analytics: /analytics/v1/* -> http://logflare:4000/*' + url: http://analytics:4000/ + routes: + - name: analytics-v1-all + strip_path: true + paths: + - /analytics/v1/ + + ## Secure Database routes + - name: meta + _comment: 'pg-meta: /pg/* -> http://pg-meta:8080/*' + url: http://meta:8080/ + routes: + - name: meta-all + strip_path: true + paths: + - /pg/ + plugins: + - name: key-auth + config: + hide_credentials: false + - name: acl + config: + hide_groups_header: true + allow: + - admin + + ## Protected Dashboard - catch all remaining routes + - name: dashboard + _comment: 'Studio: /* -> http://studio:3000/*' + url: http://studio:3000/ + routes: + - name: dashboard-all + strip_path: true + paths: + - / + plugins: + - name: cors + - name: basic-auth + config: + hide_credentials: true + `, + }, + { + filePath: "/volumes/db/init/data.sql", + content: ` + `, + }, + { + filePath: "/volumes/db/jwt.sql", + content: ` +\\set jwt_secret \`echo "$JWT_SECRET"\` +\\set jwt_exp \`echo "$JWT_EXP"\` + +ALTER DATABASE postgres SET "app.settings.jwt_secret" TO :'jwt_secret'; +ALTER DATABASE postgres SET "app.settings.jwt_exp" TO :'jwt_exp'; + `, + }, + { + filePath: "/volumes/db/logs.sql", + content: ` +\\set pguser \`echo "$POSTGRES_USER"\` + +create schema if not exists _analytics; +alter schema _analytics owner to :pguser; + `, + }, + { + filePath: "/volumes/db/realtime.sql", + content: ` +\\set pguser \`echo "$POSTGRES_USER"\` + +create schema if not exists _realtime; +alter schema _realtime owner to :pguser; + `, + }, + { + filePath: "/volumes/db/roles.sql", + content: ` +-- NOTE: change to your own passwords for production environments +\\set pgpass \`echo "$POSTGRES_PASSWORD"\` + +ALTER USER authenticator WITH PASSWORD :'pgpass'; +ALTER USER pgbouncer WITH PASSWORD :'pgpass'; +ALTER USER supabase_auth_admin WITH PASSWORD :'pgpass'; +ALTER USER supabase_functions_admin WITH PASSWORD :'pgpass'; +ALTER USER supabase_storage_admin WITH PASSWORD :'pgpass'; + `, + }, + { + filePath: "/volumes/db/webhooks.sql", + content: ` +BEGIN; + -- Create pg_net extension + CREATE EXTENSION IF NOT EXISTS pg_net SCHEMA extensions; + -- Create supabase_functions schema + CREATE SCHEMA supabase_functions AUTHORIZATION supabase_admin; + GRANT USAGE ON SCHEMA supabase_functions TO postgres, anon, authenticated, service_role; + ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON TABLES TO postgres, anon, authenticated, service_role; + ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON FUNCTIONS TO postgres, anon, authenticated, service_role; + ALTER DEFAULT PRIVILEGES IN SCHEMA supabase_functions GRANT ALL ON SEQUENCES TO postgres, anon, authenticated, service_role; + -- supabase_functions.migrations definition + CREATE TABLE supabase_functions.migrations ( + version text PRIMARY KEY, + inserted_at timestamptz NOT NULL DEFAULT NOW() + ); + -- Initial supabase_functions migration + INSERT INTO supabase_functions.migrations (version) VALUES ('initial'); + -- supabase_functions.hooks definition + CREATE TABLE supabase_functions.hooks ( + id bigserial PRIMARY KEY, + hook_table_id integer NOT NULL, + hook_name text NOT NULL, + created_at timestamptz NOT NULL DEFAULT NOW(), + request_id bigint + ); + CREATE INDEX supabase_functions_hooks_request_id_idx ON supabase_functions.hooks USING btree (request_id); + CREATE INDEX supabase_functions_hooks_h_table_id_h_name_idx ON supabase_functions.hooks USING btree (hook_table_id, hook_name); + COMMENT ON TABLE supabase_functions.hooks IS 'Supabase Functions Hooks: Audit trail for triggered hooks.'; + CREATE FUNCTION supabase_functions.http_request() + RETURNS trigger + LANGUAGE plpgsql + AS $function$ + DECLARE + request_id bigint; + payload jsonb; + url text := TG_ARGV[0]::text; + method text := TG_ARGV[1]::text; + headers jsonb DEFAULT '{}'::jsonb; + params jsonb DEFAULT '{}'::jsonb; + timeout_ms integer DEFAULT 1000; + BEGIN + IF url IS NULL OR url = 'null' THEN + RAISE EXCEPTION 'url argument is missing'; + END IF; + + IF method IS NULL OR method = 'null' THEN + RAISE EXCEPTION 'method argument is missing'; + END IF; + + IF TG_ARGV[2] IS NULL OR TG_ARGV[2] = 'null' THEN + headers = '{"Content-Type": "application/json"}'::jsonb; + ELSE + headers = TG_ARGV[2]::jsonb; + END IF; + + IF TG_ARGV[3] IS NULL OR TG_ARGV[3] = 'null' THEN + params = '{}'::jsonb; + ELSE + params = TG_ARGV[3]::jsonb; + END IF; + + IF TG_ARGV[4] IS NULL OR TG_ARGV[4] = 'null' THEN + timeout_ms = 1000; + ELSE + timeout_ms = TG_ARGV[4]::integer; + END IF; + + CASE + WHEN method = 'GET' THEN + SELECT http_get INTO request_id FROM net.http_get( + url, + params, + headers, + timeout_ms + ); + WHEN method = 'POST' THEN + payload = jsonb_build_object( + 'old_record', OLD, + 'record', NEW, + 'type', TG_OP, + 'table', TG_TABLE_NAME, + 'schema', TG_TABLE_SCHEMA + ); + + SELECT http_post INTO request_id FROM net.http_post( + url, + payload, + params, + headers, + timeout_ms + ); + ELSE + RAISE EXCEPTION 'method argument % is invalid', method; + END CASE; + + INSERT INTO supabase_functions.hooks + (hook_table_id, hook_name, request_id) + VALUES + (TG_RELID, TG_NAME, request_id); + + RETURN NEW; + END + $function$; + -- Supabase super admin + DO + $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_roles + WHERE rolname = 'supabase_functions_admin' + ) + THEN + CREATE USER supabase_functions_admin NOINHERIT CREATEROLE LOGIN NOREPLICATION; + END IF; + END + $$; + GRANT ALL PRIVILEGES ON SCHEMA supabase_functions TO supabase_functions_admin; + GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA supabase_functions TO supabase_functions_admin; + GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA supabase_functions TO supabase_functions_admin; + ALTER USER supabase_functions_admin SET search_path = "supabase_functions"; + ALTER table "supabase_functions".migrations OWNER TO supabase_functions_admin; + ALTER table "supabase_functions".hooks OWNER TO supabase_functions_admin; + ALTER function "supabase_functions".http_request() OWNER TO supabase_functions_admin; + GRANT supabase_functions_admin TO postgres; + -- Remove unused supabase_pg_net_admin role + DO + $$ + BEGIN + IF EXISTS ( + SELECT 1 + FROM pg_roles + WHERE rolname = 'supabase_pg_net_admin' + ) + THEN + REASSIGN OWNED BY supabase_pg_net_admin TO supabase_admin; + DROP OWNED BY supabase_pg_net_admin; + DROP ROLE supabase_pg_net_admin; + END IF; + END + $$; + -- pg_net grants when extension is already enabled + DO + $$ + BEGIN + IF EXISTS ( + SELECT 1 + FROM pg_extension + WHERE extname = 'pg_net' + ) + THEN + GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role; + ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; + ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; + ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; + ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; + REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; + REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; + GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; + GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; + END IF; + END + $$; + -- Event trigger for pg_net + CREATE OR REPLACE FUNCTION extensions.grant_pg_net_access() + RETURNS event_trigger + LANGUAGE plpgsql + AS $$ + BEGIN + IF EXISTS ( + SELECT 1 + FROM pg_event_trigger_ddl_commands() AS ev + JOIN pg_extension AS ext + ON ev.objid = ext.oid + WHERE ext.extname = 'pg_net' + ) + THEN + GRANT USAGE ON SCHEMA net TO supabase_functions_admin, postgres, anon, authenticated, service_role; + ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; + ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SECURITY DEFINER; + ALTER function net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; + ALTER function net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) SET search_path = net; + REVOKE ALL ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; + REVOKE ALL ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) FROM PUBLIC; + GRANT EXECUTE ON FUNCTION net.http_get(url text, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; + GRANT EXECUTE ON FUNCTION net.http_post(url text, body jsonb, params jsonb, headers jsonb, timeout_milliseconds integer) TO supabase_functions_admin, postgres, anon, authenticated, service_role; + END IF; + END; + $$; + COMMENT ON FUNCTION extensions.grant_pg_net_access IS 'Grants access to pg_net'; + DO + $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_event_trigger + WHERE evtname = 'issue_pg_net_access' + ) THEN + CREATE EVENT TRIGGER issue_pg_net_access ON ddl_command_end WHEN TAG IN ('CREATE EXTENSION') + EXECUTE PROCEDURE extensions.grant_pg_net_access(); + END IF; + END + $$; + INSERT INTO supabase_functions.migrations (version) VALUES ('20210809183423_update_grants'); + ALTER function supabase_functions.http_request() SECURITY DEFINER; + ALTER function supabase_functions.http_request() SET search_path = supabase_functions; + REVOKE ALL ON FUNCTION supabase_functions.http_request() FROM PUBLIC; + GRANT EXECUTE ON FUNCTION supabase_functions.http_request() TO postgres, anon, authenticated, service_role; +COMMIT; + `, + }, + { + filePath: "/volumes/functions/hello/index.ts", + content: ` +// Follow this setup guide to integrate the Deno language server with your editor: +// https://deno.land/manual/getting_started/setup_your_environment +// This enables autocomplete, go to definition, etc. + +import { serve } from "https://deno.land/std@0.177.1/http/server.ts" + +serve(async () => { + return new Response( + \`"Hello from Edge Functions!"\`, + { headers: { "Content-Type": "application/json" } }, + ) +}) + +// To invoke: +// curl 'http://localhost:/functions/v1/hello' \\ +// --header 'Authorization: Bearer ' + `, + }, + { + filePath: "/volumes/functions/main/index.ts", + content: ` +import { serve } from 'https://deno.land/std@0.131.0/http/server.ts' +import * as jose from 'https://deno.land/x/jose@v4.14.4/index.ts' + +console.log('main function started') + +const JWT_SECRET = Deno.env.get('JWT_SECRET') +const VERIFY_JWT = Deno.env.get('VERIFY_JWT') === 'true' + +function getAuthToken(req: Request) { + const authHeader = req.headers.get('authorization') + if (!authHeader) { + throw new Error('Missing authorization header') + } + const [bearer, token] = authHeader.split(' ') + if (bearer !== 'Bearer') { + throw new Error(\`Auth header is not 'Bearer {token}'\`) + } + return token +} + +async function verifyJWT(jwt: string): Promise { + const encoder = new TextEncoder() + const secretKey = encoder.encode(JWT_SECRET) + try { + await jose.jwtVerify(jwt, secretKey) + } catch (err) { + console.error(err) + return false + } + return true +} + +serve(async (req: Request) => { + if (req.method !== 'OPTIONS' && VERIFY_JWT) { + try { + const token = getAuthToken(req) + const isValidJWT = await verifyJWT(token) + + if (!isValidJWT) { + return new Response(JSON.stringify({ msg: 'Invalid JWT' }), { + status: 401, + headers: { 'Content-Type': 'application/json' }, + }) + } + } catch (e) { + console.error(e) + return new Response(JSON.stringify({ msg: e.toString() }), { + status: 401, + headers: { 'Content-Type': 'application/json' }, + }) + } + } + + const url = new URL(req.url) + const { pathname } = url + const path_parts = pathname.split('/') + const service_name = path_parts[1] + + if (!service_name || service_name === '') { + const error = { msg: 'missing function name in request' } + return new Response(JSON.stringify(error), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }) + } + + const servicePath = \`/home/deno/functions/\${service_name}\` + console.error(\`serving the request with \${servicePath}\`) + + const memoryLimitMb = 150 + const workerTimeoutMs = 1 * 60 * 1000 + const noModuleCache = false + const importMapPath = null + const envVarsObj = Deno.env.toObject() + const envVars = Object.keys(envVarsObj).map((k) => [k, envVarsObj[k]]) + + try { + const worker = await EdgeRuntime.userWorkers.create({ + servicePath, + memoryLimitMb, + workerTimeoutMs, + noModuleCache, + importMapPath, + envVars, + }) + return await worker.fetch(req) + } catch (e) { + const error = { msg: e.toString() } + return new Response(JSON.stringify(error), { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }) + } +}) + `, + }, + { + filePath: "/volumes/logs/vector.yml", + content: ` +api: + enabled: true + address: 0.0.0.0:9001 + +sources: + docker_host: + type: docker_logs + exclude_containers: + - supabase-vector + +transforms: + project_logs: + type: remap + inputs: + - docker_host + source: |- + .project = "default" + .event_message = del(.message) + .appname = del(.container_name) + del(.container_created_at) + del(.container_id) + del(.source_type) + del(.stream) + del(.label) + del(.image) + del(.host) + del(.stream) + router: + type: route + inputs: + - project_logs + route: + kong: '.appname == "supabase-kong"' + auth: '.appname == "supabase-auth"' + rest: '.appname == "supabase-rest"' + realtime: '.appname == "supabase-realtime"' + storage: '.appname == "supabase-storage"' + functions: '.appname == "supabase-functions"' + db: '.appname == "supabase-db"' + # Ignores non nginx errors since they are related with kong booting up + kong_logs: + type: remap + inputs: + - router.kong + source: |- + req, err = parse_nginx_log(.event_message, "combined") + if err == null { + .timestamp = req.timestamp + .metadata.request.headers.referer = req.referer + .metadata.request.headers.user_agent = req.agent + .metadata.request.headers.cf_connecting_ip = req.client + .metadata.request.method = req.method + .metadata.request.path = req.path + .metadata.request.protocol = req.protocol + .metadata.response.status_code = req.status + } + if err != null { + abort + } + # Ignores non nginx errors since they are related with kong booting up + kong_err: + type: remap + inputs: + - router.kong + source: |- + .metadata.request.method = "GET" + .metadata.response.status_code = 200 + parsed, err = parse_nginx_log(.event_message, "error") + if err == null { + .timestamp = parsed.timestamp + .severity = parsed.severity + .metadata.request.host = parsed.host + .metadata.request.headers.cf_connecting_ip = parsed.client + url, err = split(parsed.request, " ") + if err == null { + .metadata.request.method = url[0] + .metadata.request.path = url[1] + .metadata.request.protocol = url[2] + } + } + if err != null { + abort + } + # Gotrue logs are structured json strings which frontend parses directly. But we keep metadata for consistency. + auth_logs: + type: remap + inputs: + - router.auth + source: |- + parsed, err = parse_json(.event_message) + if err == null { + .metadata.timestamp = parsed.time + .metadata = merge!(.metadata, parsed) + } + # PostgREST logs are structured so we separate timestamp from message using regex + rest_logs: + type: remap + inputs: + - router.rest + source: |- + parsed, err = parse_regex(.event_message, r'^(?P