Compare commits

...

116 Commits

Author SHA1 Message Date
Mauricio Siu
2619cb49d1 refactor: restore commented-out test cases and imports in drop.test.test.ts for improved functionality
Some checks failed
Auto PR to main when version changes / create-pr (push) Has been cancelled
Build Docker images / build-and-push-cloud-image (push) Has been cancelled
Build Docker images / build-and-push-schedule-image (push) Has been cancelled
Build Docker images / build-and-push-server-image (push) Has been cancelled
Dokploy Docker Build / docker-amd (push) Has been cancelled
Dokploy Docker Build / docker-arm (push) Has been cancelled
autofix.ci / format (push) Has been cancelled
Dokploy Monitoring Build / docker-amd (push) Has been cancelled
Dokploy Monitoring Build / docker-arm (push) Has been cancelled
Dokploy Docker Build / combine-manifests (push) Has been cancelled
Dokploy Docker Build / generate-release (push) Has been cancelled
Dokploy Monitoring Build / combine-manifests (push) Has been cancelled
2025-05-28 02:44:06 -06:00
Mauricio Siu
46d12fa9d8 Merge pull request #1967 from Dokploy/feat/add-chatwoot
Feat/add chatwoot
2025-05-28 02:41:45 -06:00
Mauricio Siu
51ee46496c chore: update pnpm lockfile with dependency version upgrades for improved stability and compatibility 2025-05-28 02:39:18 -06:00
Mauricio Siu
a13e24dab0 refactor: simplify Chatwoot widget condition in dashboard layout for improved readability 2025-05-28 02:33:44 -06:00
Mauricio Siu
4aac3476b6 refactor: update Chatwoot widget settings and types to enhance configuration options 2025-05-28 02:33:14 -06:00
Mauricio Siu
037343a796 feat: integrate Chatwoot widget into dashboard layout and replace project layout with dashboard layout in various pages 2025-05-28 02:22:56 -06:00
Mauricio Siu
274d80ea7c refactor: comment out test cases and imports in drop.test.test.ts for cleanup 2025-05-28 00:51:20 -06:00
Mauricio Siu
629889f1a8 refactor: reorganize imports and enhance backup functionality across various components 2025-05-28 00:38:53 -06:00
Mauricio Siu
3e74ce05a7 Merge pull request #1960 from Lux1L/feature/gitlab-subgroup-filtering
feat(gitlab): support nested group filtering using namespace.full_pat…
2025-05-28 00:37:05 -06:00
Mauricio Siu
d05218e848 Merge pull request #1958 from IPdotSetAF/git-lfs-fix
fix: moved git lfs from build stage to dokploy stage in dockerfile
2025-05-28 00:36:12 -06:00
Mauricio Siu
0fbad4f75e docs: remove supported OS section from README.md 2025-05-28 00:34:47 -06:00
IPdotSetAF
c3cbaf2a57 fix: moved git lfs from build stage to dokploy stage in dockerfile 2025-05-27 22:16:46 +03:30
avalolu
560d493d56 feat(gitlab): support nested group filtering using namespace.full_path.startsWith 2025-05-26 18:00:03 -04:00
Mauricio Siu
27b2106630 chore: bump version to v0.22.7 in package.json
Some checks failed
Auto PR to main when version changes / create-pr (push) Has been cancelled
Build Docker images / build-and-push-cloud-image (push) Has been cancelled
Build Docker images / build-and-push-schedule-image (push) Has been cancelled
Build Docker images / build-and-push-server-image (push) Has been cancelled
Dokploy Docker Build / docker-amd (push) Has been cancelled
Dokploy Docker Build / docker-arm (push) Has been cancelled
autofix.ci / format (push) Has been cancelled
Dokploy Monitoring Build / docker-amd (push) Has been cancelled
Dokploy Monitoring Build / docker-arm (push) Has been cancelled
Dokploy Docker Build / combine-manifests (push) Has been cancelled
Dokploy Docker Build / generate-release (push) Has been cancelled
Dokploy Monitoring Build / combine-manifests (push) Has been cancelled
2025-05-26 03:11:23 -06:00
Mauricio Siu
609954c366 Merge pull request #1931 from nktnet1/nginx-static-spa-build
feat: added SPA option for static sites
2025-05-26 03:10:51 -06:00
Mauricio Siu
84faa9747e chore: update Node.js version to 20.16.0 in configuration files 2025-05-26 03:09:09 -06:00
Mauricio Siu
4b370ef43e chore: update otpauth version to 9.4.0 in pnpm-lock.yaml 2025-05-26 03:01:57 -06:00
Mauricio Siu
b94a6bff92 Merge branch 'canary' into nginx-static-spa-build 2025-05-26 02:59:03 -06:00
Mauricio Siu
276b754377 chore: downgrade docker/build-push-action to version 4 in deploy workflows 2025-05-26 02:27:41 -06:00
Mauricio Siu
f3b3798362 chore: update docker/build-push-action to version 6 in deploy workflows 2025-05-26 02:15:08 -06:00
Mauricio Siu
461acc354e Merge pull request #1955 from Dokploy/1923-v0226-git-lfs-is-not-working-at-all-despite-1872
chore: add git-lfs to Dockerfile for large file support
2025-05-26 02:00:50 -06:00
Mauricio Siu
dfc75a9116 chore: remove Dockerfile for dokploy as part of project restructuring 2025-05-26 01:53:24 -06:00
Mauricio Siu
e1580bad23 chore: add git-lfs to Dockerfile for large file support 2025-05-26 01:52:41 -06:00
Mauricio Siu
b567ec1d83 Merge pull request #1954 from Dokploy/1943-error-backing-up-mysql-to-cloudflare-r2
feat: add pino and pino-pretty for logging, implement logger utility
2025-05-26 01:48:35 -06:00
Mauricio Siu
9c73b8dc36 feat: add pino and pino-pretty for logging, implement logger utility 2025-05-26 01:45:14 -06:00
Mauricio Siu
7348526873 Merge pull request #1953 from Dokploy/1898-remote-server-with-ipv6
fix: update slugIp formatting to handle colons in server IP
2025-05-26 00:55:43 -06:00
Mauricio Siu
6fc83f2db3 fix: update slugIp formatting to handle colons in server IP 2025-05-26 00:55:22 -06:00
Khiet Tam Nguyen
43d22c2bd4 test: fix typescript error for isStaticSpa 2025-05-20 16:33:34 +10:00
autofix-ci[bot]
38a5313967 [autofix.ci] apply automated fixes 2025-05-20 06:18:00 +00:00
Khiet Tam Nguyen
ba3645933f feat: added SPA option for static sites 2025-05-20 16:11:48 +10:00
Mauricio Siu
17a26353b6 chore: bump version to v0.22.6 in package.json
Some checks failed
Auto PR to main when version changes / create-pr (push) Has been cancelled
Build Docker images / build-and-push-cloud-image (push) Has been cancelled
Build Docker images / build-and-push-schedule-image (push) Has been cancelled
Build Docker images / build-and-push-server-image (push) Has been cancelled
Dokploy Docker Build / docker-amd (push) Has been cancelled
Dokploy Docker Build / docker-arm (push) Has been cancelled
autofix.ci / format (push) Has been cancelled
Dokploy Monitoring Build / docker-amd (push) Has been cancelled
Dokploy Monitoring Build / docker-arm (push) Has been cancelled
Dokploy Docker Build / combine-manifests (push) Has been cancelled
Dokploy Docker Build / generate-release (push) Has been cancelled
Dokploy Monitoring Build / combine-manifests (push) Has been cancelled
2025-05-18 02:30:04 -06:00
Mauricio Siu
e2c163c6d5 Merge pull request #1919 from nktnet1/fix-randomise-compose-await
fix: randomize-compose missing await
2025-05-18 02:29:32 -06:00
Khiet Tam Nguyen
616e11722c fix: randomize-compose missing await 2025-05-18 18:26:44 +10:00
Mauricio Siu
91a44706df Merge pull request #1917 from nktnet1/fix-isolated-randomized-compose-notifs
fix: multiple notifications for isolated compose and randomize compose
2025-05-18 02:19:51 -06:00
Mauricio Siu
748de47a6d Merge pull request #1918 from Dokploy/fix/web-server-backup-maxlenght
fix: update rsync command in web server backup to remove verbose flag
2025-05-18 02:18:31 -06:00
Mauricio Siu
cbf9aef0df fix: remove console log for rsync command in web server backup 2025-05-18 02:18:05 -06:00
Mauricio Siu
e2befc24a5 fix: update rsync command in web server backup to remove verbose flag 2025-05-18 02:17:41 -06:00
autofix-ci[bot]
0f48f2c830 [autofix.ci] apply automated fixes 2025-05-18 05:12:06 +00:00
Khiet Tam Nguyen
5dfa7645f3 fix: multiple notifications for isolated compose and randomize compose 2025-05-18 15:07:05 +10:00
Mauricio Siu
7fe163dd33 Merge pull request #1913 from Dokploy/feat/add-appname-compose
Some checks are pending
Auto PR to main when version changes / create-pr (push) Waiting to run
Build Docker images / build-and-push-cloud-image (push) Waiting to run
Build Docker images / build-and-push-schedule-image (push) Waiting to run
Build Docker images / build-and-push-server-image (push) Waiting to run
Dokploy Docker Build / docker-amd (push) Waiting to run
Dokploy Docker Build / docker-arm (push) Waiting to run
Dokploy Docker Build / combine-manifests (push) Blocked by required conditions
Dokploy Docker Build / generate-release (push) Blocked by required conditions
autofix.ci / format (push) Waiting to run
Dokploy Monitoring Build / docker-amd (push) Waiting to run
Dokploy Monitoring Build / docker-arm (push) Waiting to run
Dokploy Monitoring Build / combine-manifests (push) Blocked by required conditions
Feat/add appname compose
2025-05-17 15:37:22 -06:00
Mauricio Siu
19b56771b8 style: update styling for environment display and increase scroll area height in import component 2025-05-17 15:28:52 -06:00
Mauricio Siu
cff01ed438 refactor: modify template processing to include APP_NAME variable in configuration 2025-05-17 15:27:42 -06:00
Mauricio Siu
10fa3c8cf1 fix: update environment file generation to include APP_NAME variable 2025-05-17 15:18:31 -06:00
Mauricio Siu
6c5497ed21 Merge pull request #1912 from Dokploy/1873-duplicate-clones-the-project-not-the-service
feat: enhance project duplication functionality with options for new …
2025-05-17 14:35:17 -06:00
Mauricio Siu
380656efee feat: enhance project duplication functionality with options for new or same project 2025-05-17 14:33:07 -06:00
Mauricio Siu
c64d2245ce Merge pull request #1897 from enie123/canary
Some checks are pending
Auto PR to main when version changes / create-pr (push) Waiting to run
Build Docker images / build-and-push-cloud-image (push) Waiting to run
Build Docker images / build-and-push-schedule-image (push) Waiting to run
Build Docker images / build-and-push-server-image (push) Waiting to run
Dokploy Docker Build / docker-amd (push) Waiting to run
Dokploy Docker Build / docker-arm (push) Waiting to run
Dokploy Docker Build / combine-manifests (push) Blocked by required conditions
Dokploy Docker Build / generate-release (push) Blocked by required conditions
autofix.ci / format (push) Waiting to run
Dokploy Monitoring Build / docker-amd (push) Waiting to run
Dokploy Monitoring Build / docker-arm (push) Waiting to run
Dokploy Monitoring Build / combine-manifests (push) Blocked by required conditions
build: update nixpacks to 1.39.0
2025-05-17 03:54:11 -06:00
Mauricio Siu
a985998b93 feat: add VPS provider recommendations and alerts in server settings 2025-05-17 03:53:37 -06:00
Mauricio Siu
4f3ba16dfa chore: bump version to v0.22.5 in package.json 2025-05-17 03:34:31 -06:00
Mauricio Siu
6c788429f1 Merge pull request #1910 from Dokploy/1894-gitlab-self-hosted-cannot-find-all-repository
refactor: streamline GitLab repository fetching by introducing valida…
2025-05-17 03:02:26 -06:00
Mauricio Siu
3176a9d7e3 refactor: streamline GitLab repository fetching by introducing validateGitlabProvider function for improved error handling and pagination 2025-05-17 02:51:40 -06:00
Mauricio Siu
94a6a9587e Merge pull request #1872 from IPdotSetAF/git-lfs-not-supported
fix: installed git-lfs in docker image
2025-05-17 02:09:03 -06:00
Mauricio Siu
911681f389 fix: add git-lfs installation to various OS setups in server-setup script 2025-05-17 02:03:04 -06:00
Mauricio Siu
5992688e85 Merge pull request #1909 from Dokploy/1868-backups-failing-due-to-a-directory-not-empty-error
refactor: improve cleanup process in web server backup utility to han…
2025-05-17 00:21:58 -06:00
Mauricio Siu
425061e481 refactor: improve cleanup process in web server backup utility to handle errors during temporary directory removal 2025-05-17 00:20:54 -06:00
Mauricio Siu
08c0bf8a21 Merge pull request #1908 from Dokploy/1863-pg_dump-backup-fails-stdout-maxbuffer-length-exceeded
refactor: update database backup process in web server utility to use…
2025-05-17 00:14:59 -06:00
Mauricio Siu
64a2c9e0a1 refactor: update database backup process in web server utility to use temporary file in container 2025-05-17 00:13:43 -06:00
Mauricio Siu
21e46f5382 Merge pull request #1907 from Dokploy/1878-dokploy-application-settings-not-linked-resulting-in-progress-lost-on-save
fix: update dependencies in save provider components to use optional …
2025-05-16 23:23:20 -06:00
autofix-ci[bot]
52b2158309 [autofix.ci] apply automated fixes 2025-05-17 05:22:55 +00:00
Mauricio Siu
178d84d438 fix: update dependencies in save provider components to use optional chaining for applicationId and composeId 2025-05-16 23:22:26 -06:00
Mauricio Siu
80016b57a8 Merge pull request #1906 from Dokploy/1888-docker-compose-preview-is-null
feat(ui): add loading state and no data message to converted compose …
2025-05-16 23:16:29 -06:00
Mauricio Siu
b4b2d12f6e feat(ui): add loading state and no data message to converted compose display 2025-05-16 23:16:02 -06:00
Mauricio Siu
294378d95b Merge pull request #1886 from oshanavishkapiries/canary
fix: Submit Log in issue on Github
2025-05-16 23:03:50 -06:00
Mauricio Siu
c52812f9d3 Merge pull request #1903 from yergom/fix/watch-path-wording
fix: more informative placeholder for watch path
2025-05-16 23:03:13 -06:00
Mauricio Siu
82f7c5d5f3 Merge pull request #1904 from darena-patrick/fix/compose-deploy-url
fix: Missing `/compose` in auto-deploy URL
2025-05-16 23:00:47 -06:00
Mauricio Siu
3d2ae52259 Merge pull request #1891 from nktnet1/fix-domain-responsiveness
Fix domain responsiveness
2025-05-16 22:59:41 -06:00
Patrick Schiess
bf115c7895 fix: Missing /compose in auto-deploy URL 2025-05-16 16:12:09 -06:00
yergom
c2c29dbaba fix: more informative placeholder for watch path 2025-05-16 13:25:28 +00:00
Eric Nie
d4032f34bf build: update nixpacks to 1.39.0 2025-05-15 00:45:48 +07:00
Tam Nguyen
136570b36c fix(ui): compose grid responsiveness starts from xl instead of lg 2025-05-13 16:01:25 +10:00
Tam Nguyen
7d0075c230 fix(ui): domain responsiveness with screen width 2025-05-13 15:43:52 +10:00
Tam Nguyen
19b4edee8d refactor: remove redundant class bg-card due to bg-transparent set later 2025-05-13 15:15:54 +10:00
153918928+oshanavishkapiries@users.noreply.github.com
7f04eb856e fix: Submit Log in issue on Github 2025-05-13 01:56:55 +05:30
IPdotSetAF
5156b45ffc fix: installed git-lfs in docker image 2025-05-11 10:36:52 +03:30
Mauricio Siu
80e6f21840 chore: bump version to v0.22.4 in package.json
Some checks failed
Auto PR to main when version changes / create-pr (push) Has been cancelled
Build Docker images / build-and-push-cloud-image (push) Has been cancelled
Build Docker images / build-and-push-schedule-image (push) Has been cancelled
Build Docker images / build-and-push-server-image (push) Has been cancelled
Dokploy Docker Build / docker-amd (push) Has been cancelled
Dokploy Docker Build / docker-arm (push) Has been cancelled
autofix.ci / format (push) Has been cancelled
Dokploy Monitoring Build / docker-amd (push) Has been cancelled
Dokploy Monitoring Build / docker-arm (push) Has been cancelled
Dokploy Docker Build / combine-manifests (push) Has been cancelled
Dokploy Docker Build / generate-release (push) Has been cancelled
Dokploy Monitoring Build / combine-manifests (push) Has been cancelled
2025-05-10 20:54:36 -06:00
Mauricio Siu
5b519151e8 refactor: streamline Remove Invitation dropdown menu item in ShowInvitations component 2025-05-10 20:40:11 -06:00
Mauricio Siu
aa475e6123 Merge pull request #1860 from yusoofsh/pgrestore-no-owner
Some checks are pending
Auto PR to main when version changes / create-pr (push) Waiting to run
Build Docker images / build-and-push-cloud-image (push) Waiting to run
Build Docker images / build-and-push-schedule-image (push) Waiting to run
Build Docker images / build-and-push-server-image (push) Waiting to run
Dokploy Docker Build / docker-amd (push) Waiting to run
Dokploy Docker Build / docker-arm (push) Waiting to run
Dokploy Docker Build / combine-manifests (push) Blocked by required conditions
Dokploy Docker Build / generate-release (push) Blocked by required conditions
autofix.ci / format (push) Waiting to run
Dokploy Monitoring Build / docker-amd (push) Waiting to run
Dokploy Monitoring Build / docker-arm (push) Waiting to run
Dokploy Monitoring Build / combine-manifests (push) Blocked by required conditions
Add no owner options to pg_restore
2025-05-10 15:22:41 -06:00
Mauricio Siu
66756c34fe chore: update AmericanCloud logo in README to PNG format and adjust height
Some checks are pending
Auto PR to main when version changes / create-pr (push) Waiting to run
Build Docker images / build-and-push-cloud-image (push) Waiting to run
Build Docker images / build-and-push-schedule-image (push) Waiting to run
Build Docker images / build-and-push-server-image (push) Waiting to run
Dokploy Docker Build / docker-amd (push) Waiting to run
Dokploy Docker Build / docker-arm (push) Waiting to run
Dokploy Docker Build / combine-manifests (push) Blocked by required conditions
Dokploy Docker Build / generate-release (push) Blocked by required conditions
autofix.ci / format (push) Waiting to run
Dokploy Monitoring Build / docker-amd (push) Waiting to run
Dokploy Monitoring Build / docker-arm (push) Waiting to run
Dokploy Monitoring Build / combine-manifests (push) Blocked by required conditions
2025-05-10 03:30:01 -06:00
Mauricio Siu
946a5739dc chore: replace AmericanCloud SVG logo with PNG format 2025-05-10 03:29:38 -06:00
Mauricio Siu
6c817a9e5d feat: add AmericanCloud sponsorship to README and include SVG logo 2025-05-10 03:28:19 -06:00
Mauricio Siu
6aea937e86 chore: remove bun.lock file to clean up unused lockfile 2025-05-10 03:10:49 -06:00
Mauricio Siu
19612d4b66 Revert "refactor: remove unused volume suffix function from collision utility"
This reverts commit 47dd003461.
2025-05-10 02:58:01 -06:00
Mauricio Siu
47dd003461 refactor: remove unused volume suffix function from collision utility 2025-05-10 02:57:21 -06:00
Mauricio Siu
def99225fc Merge pull request #1865 from Dokploy/fix/isolated-docker-stack
fix: update Docker network creation command to support overlay driver in docker stack deployments
2025-05-10 02:33:04 -06:00
Mauricio Siu
32405fc61a fix: update Docker network creation command to support overlay driver for stack deployments 2025-05-10 02:13:57 -06:00
Mauricio Siu
25e1a9af57 Merge pull request #1859 from yusoofsh/fix-swarm-database-backup-restore
Fix container not found upon backup/restore on compose stack
2025-05-10 01:45:12 -06:00
Mauricio Siu
1fcb1f2c5e fix: update Docker command filter for service name in backup utility 2025-05-10 01:43:57 -06:00
Mauricio Siu
fdaba7e752 Merge pull request #1864 from Dokploy/fix/404-unauthorized-endpoints
chore: update better-auth to v1.2.8-beta.7 in package.json and pnpm-l…
2025-05-10 01:37:51 -06:00
Mauricio Siu
c1640cba29 chore: update better-auth to v1.2.8-beta.7 in package.json and pnpm-lock.yaml 2025-05-10 01:30:03 -06:00
Yusoof Moh
3bd54ff61e fix: add no owner options to pg_restore 2025-05-10 00:12:33 +07:00
Yusoof Moh
5853d18bc1 fix: container not found upon backup/restore on compose stack 2025-05-09 23:42:34 +07:00
Mauricio Siu
f575317906 Merge pull request #1848 from Dokploy/feat/add-builders-alert
Some checks failed
Auto PR to main when version changes / create-pr (push) Has been cancelled
Build Docker images / build-and-push-cloud-image (push) Has been cancelled
Build Docker images / build-and-push-schedule-image (push) Has been cancelled
Build Docker images / build-and-push-server-image (push) Has been cancelled
Dokploy Docker Build / docker-amd (push) Has been cancelled
Dokploy Docker Build / docker-arm (push) Has been cancelled
autofix.ci / format (push) Has been cancelled
Dokploy Monitoring Build / docker-amd (push) Has been cancelled
Dokploy Monitoring Build / docker-arm (push) Has been cancelled
Dokploy Docker Build / combine-manifests (push) Has been cancelled
Dokploy Docker Build / generate-release (push) Has been cancelled
Dokploy Monitoring Build / combine-manifests (push) Has been cancelled
feat: add alert block to ShowBuildChooseForm for resource usage guidance
2025-05-06 23:37:24 -06:00
Mauricio Siu
e6028e73ac feat: add alert block to ShowBuildChooseForm for resource usage guidance 2025-05-06 23:37:05 -06:00
Mauricio Siu
bcbed151e8 Merge pull request #1841 from MauruschatM/canary
Some checks are pending
Auto PR to main when version changes / create-pr (push) Waiting to run
Build Docker images / build-and-push-cloud-image (push) Waiting to run
Build Docker images / build-and-push-schedule-image (push) Waiting to run
Build Docker images / build-and-push-server-image (push) Waiting to run
Dokploy Docker Build / docker-amd (push) Waiting to run
Dokploy Docker Build / docker-arm (push) Waiting to run
Dokploy Docker Build / combine-manifests (push) Blocked by required conditions
Dokploy Docker Build / generate-release (push) Blocked by required conditions
autofix.ci / format (push) Waiting to run
Dokploy Monitoring Build / docker-amd (push) Waiting to run
Dokploy Monitoring Build / docker-arm (push) Waiting to run
Dokploy Monitoring Build / combine-manifests (push) Blocked by required conditions
update railpack to 0.0.64
2025-05-06 23:22:27 -06:00
Mauricio Siu
c708f7ba62 Update version in package.json to v0.22.3 2025-05-06 23:13:42 -06:00
Mauricio Siu
95a538f261 Merge pull request #1846 from Dokploy/fix/dockerfile-env-vars
fix: wrap build arguments in single quotes for Docker command
2025-05-06 23:12:35 -06:00
Mauricio Siu
f854457d69 fix: wrap build arguments in single quotes for Docker command 2025-05-06 23:11:37 -06:00
Mauricio Siu
cd998c37f1 refactor: update railpack 2025-05-06 23:04:58 -06:00
Mauricio Siu
d46a61098b Merge pull request #1840 from Smip/canary
fix: use root password instead of user one
2025-05-06 22:58:16 -06:00
Moritz Mauruschat
8f14d854a0 Update railpack to 0.064 2025-05-06 13:32:16 +02:00
Aleksandr Sokolov
388399b370 fix: use root password instead of user one 2025-05-06 13:02:26 +02:00
Mauricio Siu
a8b4bb9c41 Merge pull request #1835 from Dokploy/feat/add-impersionation-cloud
Some checks are pending
Auto PR to main when version changes / create-pr (push) Waiting to run
Build Docker images / build-and-push-cloud-image (push) Waiting to run
Build Docker images / build-and-push-schedule-image (push) Waiting to run
Build Docker images / build-and-push-server-image (push) Waiting to run
Dokploy Docker Build / docker-amd (push) Waiting to run
Dokploy Docker Build / docker-arm (push) Waiting to run
Dokploy Docker Build / combine-manifests (push) Blocked by required conditions
Dokploy Docker Build / generate-release (push) Blocked by required conditions
autofix.ci / format (push) Waiting to run
Dokploy Monitoring Build / docker-amd (push) Waiting to run
Dokploy Monitoring Build / docker-arm (push) Waiting to run
Dokploy Monitoring Build / combine-manifests (push) Blocked by required conditions
Feat/add impersionation cloud
2025-05-06 02:39:24 -06:00
Mauricio Siu
ebc8c2f73d Add default user role and impersonation settings in tests
- Updated the baseAdmin user object in the update-server-config test to include default values for allowImpersonation and role, ensuring comprehensive test coverage for user management features.
2025-05-06 02:37:41 -06:00
Mauricio Siu
1227d2b5fc Update version in package.json to v0.22.2 2025-05-06 02:35:08 -06:00
Mauricio Siu
314438b84c Enhance impersonation functionality and user management
- Updated the ImpersonationBar component to fetch users dynamically and handle impersonation actions more efficiently.
- Refactored the ProfileForm to set the allowImpersonation value directly, improving form handling.
- Modified the DashboardLayout to conditionally render the ImpersonationBar based on user permissions and cloud settings.
- Added a new role column to the user_temp table to support user role management.
- Updated API routes to include checks for root access and improved user listing functionality.
2025-05-06 02:32:08 -06:00
Mauricio Siu
cc5574e08a Add impersonation feature to user management
- Introduced an ImpersonationBar component for admin users to impersonate other users, enhancing user management capabilities.
- Updated the ProfileForm to include an option for allowing impersonation, with a description for clarity.
- Modified the DashboardLayout to conditionally display the impersonation bar based on user roles and cloud settings.
- Added database schema changes to support the new impersonation feature, including a new column for allowImpersonation in the user table.
- Implemented necessary API updates to handle impersonation actions and user data retrieval.
2025-05-06 01:46:20 -06:00
Mauricio Siu
11a8fcc476 Merge pull request #1829 from Dokploy/Siumauricio-patch-1
Some checks are pending
Auto PR to main when version changes / create-pr (push) Waiting to run
Build Docker images / build-and-push-cloud-image (push) Waiting to run
Build Docker images / build-and-push-schedule-image (push) Waiting to run
Build Docker images / build-and-push-server-image (push) Waiting to run
Dokploy Docker Build / docker-amd (push) Waiting to run
Dokploy Docker Build / docker-arm (push) Waiting to run
Dokploy Docker Build / combine-manifests (push) Blocked by required conditions
Dokploy Docker Build / generate-release (push) Blocked by required conditions
autofix.ci / format (push) Waiting to run
Dokploy Monitoring Build / docker-amd (push) Waiting to run
Dokploy Monitoring Build / docker-arm (push) Waiting to run
Dokploy Monitoring Build / combine-manifests (push) Blocked by required conditions
Update README.md
2025-05-05 03:09:16 -06:00
Mauricio Siu
c50229a33c Update README.md 2025-05-05 03:09:02 -06:00
Mauricio Siu
0609d74d2b Replace agentdock sponsorship image with a PNG format in README
- Updated the README to use a new PNG image for agentdock.ai sponsorship, replacing the previous JPG file to improve image quality and compatibility.
2025-05-05 03:04:44 -06:00
Mauricio Siu
fce8eca894 Update image dimensions in README for sponsorships
- Adjusted the height attributes of images for Supafort and agentdock in the Premium Supporters section to improve visual consistency and alignment.
2025-05-05 03:01:50 -06:00
Mauricio Siu
3de0d674ed Refactor Premium Supporters section in README
- Updated the layout of the Premium Supporters section for better alignment and spacing.
- Adjusted the gap between elements to enhance visual appeal and readability.
2025-05-05 02:59:50 -06:00
Mauricio Siu
7faab54a65 Update version in package.json to v0.22.1 2025-05-05 02:58:57 -06:00
Mauricio Siu
40d9db7ccf Add agentdock sponsorship to README
- Included a new sponsorship section in the README to feature agentdock.ai.
- Added an image link for agentdock.ai alongside existing sponsorships, enhancing visibility for premium supporters.
2025-05-05 02:58:52 -06:00
Mauricio Siu
c7c01f57d4 Merge pull request #1827 from Dokploy/fix/backups-shell
Update backup command execution to use bash shell in multiple backup …
2025-05-05 02:54:10 -06:00
Mauricio Siu
45cf295be0 Update backup command execution to use bash shell in multiple backup utilities
- Modified the `execAsync` calls in `compose.ts`, `mariadb.ts`, `mongo.ts`, and `mysql.ts` to specify the bash shell for executing backup commands, ensuring compatibility with bash-specific features across all backup utilities.
2025-05-05 02:52:50 -06:00
Mauricio Siu
79372527e6 Update backup command execution to use bash shell
- Modified the `execAsync` call in `postgres.ts` to specify the bash shell for executing backup commands, ensuring compatibility with bash-specific features.
- Removed the shebang line from the backup command script in `utils.ts` to streamline the script's execution context.
2025-05-05 02:37:49 -06:00
Mauricio Siu
edcfc7d670 Add shebang to backup command script in utils.ts
- Introduced a shebang line to the backup command script for improved compatibility and execution in a bash environment.
- This change enhances the script's portability and ensures it runs correctly in various shell contexts.
2025-05-05 02:09:03 -06:00
122 changed files with 21451 additions and 4015 deletions

BIN
.github/sponsors/agentdock.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
.github/sponsors/american-cloud.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -12,7 +12,7 @@ jobs:
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20.9.0
node-version: 20.16.0
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm run server:build
@@ -26,7 +26,7 @@ jobs:
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20.9.0
node-version: 20.16.0
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm run server:build
@@ -39,7 +39,7 @@ jobs:
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20.9.0
node-version: 20.16.0
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm run server:build

2
.nvmrc
View File

@@ -1 +1 @@
20.9.0
20.16.0

View File

@@ -52,7 +52,7 @@ feat: add new feature
Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch.
We use Node v20.9.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.9.0 && nvm use` in the root directory.
We use Node v20.16.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.16.0 && nvm use` in the root directory.
```bash
git clone https://github.com/dokploy/dokploy.git

View File

@@ -29,7 +29,7 @@ WORKDIR /app
# Set production
ENV NODE_ENV=production
RUN apt-get update && apt-get install -y curl unzip zip apache2-utils iproute2 rsync && rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y curl unzip zip apache2-utils iproute2 rsync git-lfs && git lfs install && rm -rf /var/lib/apt/lists/*
# Copy only the necessary files
COPY --from=build /prod/dokploy/.next ./.next
@@ -49,18 +49,18 @@ RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm
# Install Nixpacks and tsx
# | VERBOSE=1 VERSION=1.21.0 bash
ARG NIXPACKS_VERSION=1.35.0
ARG NIXPACKS_VERSION=1.39.0
RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
&& chmod +x install.sh \
&& ./install.sh \
&& pnpm install -g tsx
# Install Railpack
ARG RAILPACK_VERSION=0.0.37
ARG RAILPACK_VERSION=0.0.64
RUN curl -sSL https://railpack.com/install.sh | bash
# Install buildpacks
COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack
EXPOSE 3000
CMD [ "pnpm", "start" ]
CMD [ "pnpm", "start" ]

View File

@@ -80,12 +80,30 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
### Premium Supporters 🥇
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
<a href="https://supafort.com/?ref=dokploy" target="_blank"><img src="https://supafort.com/build/q-4Ht4rBZR.webp" alt="Supafort.com" width="190"/></a>
<div style="display: flex; align-items: center; gap: 20px;">
<a href="https://supafort.com/?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 20px;">
<img src="https://supafort.com/build/q-4Ht4rBZR.webp" alt="Supafort.com" height="50"/>
</a>
<a href="https://agentdock.ai/?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 50px;">
<img src=".github/sponsors/agentdock.png" alt="agentdock.ai" height="70"/>
</a>
</div>
### Elite Contributors 🥈
<div style="display: flex; align-items: center; gap: 20px;">
<a href="https://americancloud.com/?ref=dokploy" target="_blank" style="display: inline-block; padding: 10px; border-radius: 10px;">
<img src=".github/sponsors/american-cloud.png" alt="AmericanCloud" height="70"/>
</a>
</div>
<!-- Elite Contributors 🥈 -->
<!-- Add Elite Contributors here -->
### Supporting Members 🥉
@@ -97,6 +115,7 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
<a href="https://itsdb-center.com?ref=dokploy "><img src=".github/sponsors/its.png" width="65px" alt="Itsdb-center"/></a>
<a href="https://openalternative.co/?ref=dokploy "><img src=".github/sponsors/openalternative.png" width="65px" alt="Openalternative"/></a>
<a href="https://synexa.ai/?ref=dokploy"><img src=".github/sponsors/synexa.png" width="65px" alt="Synexa"/></a>
</div>
@@ -129,19 +148,6 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
<img src="https://dokploy.com/banner.png" alt="Watch the video" width="400" style="border-radius:20px;"/>
</a>
<!-- ## Supported OS
- Ubuntu 24.04 LTS
- Ubuntu 23.10
- Ubuntu 22.04 LTS
- Ubuntu 20.04 LTS
- Ubuntu 18.04 LTS
- Debian 12
- Debian 11
- Fedora 40
- Centos 9
- Centos 8 -->
## Contributing
Check out the [Contributing Guide](CONTRIBUTING.md) for more information.

View File

@@ -1 +1 @@
20.9.0
20.16.0

View File

@@ -1,26 +0,0 @@
FROM node:18-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
FROM base AS build
COPY . /usr/src/app
WORKDIR /usr/src/app
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
# Install dependencies
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
# Build only the dokploy app
RUN pnpm run dokploy:build
# Deploy only the dokploy app
RUN pnpm deploy --filter=dokploy --prod /prod/dokploy
FROM base AS dokploy
COPY --from=build /prod/dokploy /prod/dokploy
WORKDIR /prod/dokploy
EXPOSE 3000
CMD [ "pnpm", "start" ]

View File

@@ -105,6 +105,7 @@ const baseApp: ApplicationNested = {
ports: [],
projectId: "",
publishDirectory: null,
isStaticSpa: null,
redirects: [],
refreshToken: "",
registry: null,
@@ -149,67 +150,68 @@ describe("unzipDrop using real zip files", () => {
} finally {
}
});
it("should correctly extract a zip with a single root folder and a subfolder", async () => {
baseApp.appName = "folderwithfile";
// const appName = "folderwithfile";
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip");
const zipBuffer = zip.toBuffer();
const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, baseApp);
const files = await fs.readdir(outputPath, { withFileTypes: true });
expect(files.some((f) => f.name === "folder1.txt")).toBe(true);
});
it("should correctly extract a zip with multiple root folders", async () => {
baseApp.appName = "two-folders";
// const appName = "two-folders";
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const zip = new AdmZip("./__test__/drop/zips/two-folders.zip");
const zipBuffer = zip.toBuffer();
const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, baseApp);
const files = await fs.readdir(outputPath, { withFileTypes: true });
expect(files.some((f) => f.name === "folder1")).toBe(true);
expect(files.some((f) => f.name === "folder2")).toBe(true);
});
it("should correctly extract a zip with a single root with a file", async () => {
baseApp.appName = "nested";
// const appName = "nested";
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const zip = new AdmZip("./__test__/drop/zips/nested.zip");
const zipBuffer = zip.toBuffer();
const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, baseApp);
const files = await fs.readdir(outputPath, { withFileTypes: true });
expect(files.some((f) => f.name === "folder1")).toBe(true);
expect(files.some((f) => f.name === "folder2")).toBe(true);
expect(files.some((f) => f.name === "folder3")).toBe(true);
});
it("should correctly extract a zip with a single root with a folder", async () => {
baseApp.appName = "folder-with-sibling-file";
// const appName = "folder-with-sibling-file";
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip");
const zipBuffer = zip.toBuffer();
const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, baseApp);
const files = await fs.readdir(outputPath, { withFileTypes: true });
expect(files.some((f) => f.name === "folder1")).toBe(true);
expect(files.some((f) => f.name === "test.txt")).toBe(true);
});
});
// it("should correctly extract a zip with a single root folder and a subfolder", async () => {
// baseApp.appName = "folderwithfile";
// // const appName = "folderwithfile";
// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
// const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip");
// const zipBuffer = zip.toBuffer();
// const file = new File([zipBuffer], "single.zip");
// await unzipDrop(file, baseApp);
// const files = await fs.readdir(outputPath, { withFileTypes: true });
// expect(files.some((f) => f.name === "folder1.txt")).toBe(true);
// });
// it("should correctly extract a zip with multiple root folders", async () => {
// baseApp.appName = "two-folders";
// // const appName = "two-folders";
// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
// const zip = new AdmZip("./__test__/drop/zips/two-folders.zip");
// const zipBuffer = zip.toBuffer();
// const file = new File([zipBuffer], "single.zip");
// await unzipDrop(file, baseApp);
// const files = await fs.readdir(outputPath, { withFileTypes: true });
// expect(files.some((f) => f.name === "folder1")).toBe(true);
// expect(files.some((f) => f.name === "folder2")).toBe(true);
// });
// it("should correctly extract a zip with a single root with a file", async () => {
// baseApp.appName = "nested";
// // const appName = "nested";
// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
// const zip = new AdmZip("./__test__/drop/zips/nested.zip");
// const zipBuffer = zip.toBuffer();
// const file = new File([zipBuffer], "single.zip");
// await unzipDrop(file, baseApp);
// const files = await fs.readdir(outputPath, { withFileTypes: true });
// expect(files.some((f) => f.name === "folder1")).toBe(true);
// expect(files.some((f) => f.name === "folder2")).toBe(true);
// expect(files.some((f) => f.name === "folder3")).toBe(true);
// });
// it("should correctly extract a zip with a single root with a folder", async () => {
// baseApp.appName = "folder-with-sibling-file";
// // const appName = "folder-with-sibling-file";
// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
// const zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip");
// const zipBuffer = zip.toBuffer();
// const file = new File([zipBuffer], "single.zip");
// await unzipDrop(file, baseApp);
// const files = await fs.readdir(outputPath, { withFileTypes: true });
// expect(files.some((f) => f.name === "folder1")).toBe(true);
// expect(files.some((f) => f.name === "test.txt")).toBe(true);
// });
// });

View File

@@ -16,6 +16,8 @@ import { beforeEach, expect, test, vi } from "vitest";
const baseAdmin: User = {
https: false,
enablePaidFeatures: false,
allowImpersonation: false,
role: "user",
metricsConfig: {
containers: {
refreshRate: 20,

View File

@@ -85,6 +85,7 @@ const baseApp: ApplicationNested = {
ports: [],
projectId: "",
publishDirectory: null,
isStaticSpa: null,
redirects: [],
refreshToken: "",
registry: null,

View File

@@ -1,5 +1,5 @@
import { describe, expect, test } from "vitest";
import { normalizeS3Path } from "@dokploy/server/utils/backups/utils";
import { describe, expect, test } from "vitest";
describe("normalizeS3Path", () => {
test("should handle empty and whitespace-only prefix", () => {

View File

@@ -263,7 +263,7 @@ export const ShowImport = ({ composeId }: Props) => {
{templateInfo.template.envs.map((env, index) => (
<div
key={index}
className="rounded-lg border bg-card p-2 font-mono text-sm"
className="rounded-lg truncate border bg-card p-2 font-mono text-sm"
>
{env}
</div>
@@ -328,7 +328,7 @@ export const ShowImport = ({ composeId }: Props) => {
<DialogDescription>Mount File Content</DialogDescription>
</DialogHeader>
<ScrollArea className="h-[25vh] pr-4">
<ScrollArea className="h-[45vh] pr-4">
<CodeEditor
language="yaml"
value={selectedMount?.content || ""}

View File

@@ -1,6 +1,8 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import {
Form,
FormControl,
@@ -62,10 +64,11 @@ const mySchema = z.discriminatedUnion("buildType", [
publishDirectory: z.string().optional(),
}),
z.object({
buildType: z.literal(BuildType.static),
buildType: z.literal(BuildType.railpack),
}),
z.object({
buildType: z.literal(BuildType.railpack),
buildType: z.literal(BuildType.static),
isStaticSpa: z.boolean().default(false),
}),
]);
@@ -82,6 +85,7 @@ interface ApplicationData {
dockerBuildStage?: string | null;
herokuVersion?: string | null;
publishDirectory?: string | null;
isStaticSpa?: boolean | null;
}
function isValidBuildType(value: string): value is BuildType {
@@ -114,16 +118,18 @@ const resetData = (data: ApplicationData): AddTemplate => {
case BuildType.static:
return {
buildType: BuildType.static,
isStaticSpa: data.isStaticSpa ?? false,
};
case BuildType.railpack:
return {
buildType: BuildType.railpack,
};
default:
default: {
const buildType = data.buildType as BuildType;
return {
buildType,
} as AddTemplate;
}
}
};
@@ -173,6 +179,8 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
data.buildType === BuildType.heroku_buildpacks
? data.herokuVersion
: null,
isStaticSpa:
data.buildType === BuildType.static ? data.isStaticSpa : null,
})
.then(async () => {
toast.success("Build type saved");
@@ -200,6 +208,22 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
</CardHeader>
<CardContent>
<Form {...form}>
<AlertBlock>
Builders can consume significant memory and CPU resources
(recommended: 4+ GB RAM and 2+ CPU cores). For production
environments, please review our{" "}
<a
href="https://docs.dokploy.com/docs/core/applications/going-production"
target="_blank"
rel="noreferrer"
className="font-medium underline underline-offset-4"
>
Production Guide
</a>{" "}
for best practices and optimization recommendations. Builders are
suitable for development and prototyping purposes when you have
sufficient resources available.
</AlertBlock>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 p-2"
@@ -347,6 +371,30 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
)}
/>
)}
{buildType === BuildType.static && (
<FormField
control={form.control}
name="isStaticSpa"
render={({ field }) => (
<FormItem>
<FormControl>
<div className="flex items-center gap-x-2 p-2">
<Checkbox
id="checkboxIsStaticSpa"
value={String(field.value)}
checked={field.value}
onCheckedChange={field.onChange}
/>
<FormLabel htmlFor="checkboxIsStaticSpa">
Single Page Application (SPA)
</FormLabel>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<div className="flex w-full justify-end">
<Button isLoading={isLoading} type="submit">
Save

View File

@@ -1,5 +1,6 @@
import { DateTooltip } from "@/components/shared/date-tooltip";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
@@ -9,12 +10,11 @@ import {
CardTitle,
} from "@/components/ui/card";
import { type RouterOutputs, api } from "@/utils/api";
import { RocketIcon, Clock, Loader2 } from "lucide-react";
import { Clock, Loader2, RocketIcon } from "lucide-react";
import React, { useEffect, useState } from "react";
import { CancelQueues } from "./cancel-queues";
import { RefreshToken } from "./refresh-token";
import { ShowDeployment } from "./show-deployment";
import { Badge } from "@/components/ui/badge";
interface Props {
id: string;
@@ -86,7 +86,7 @@ export const ShowDeployments = ({
<span>Webhook URL: </span>
<div className="flex flex-row items-center gap-2">
<span className="break-all text-muted-foreground">
{`${url}/api/deploy/${refreshToken}`}
{`${url}/api/deploy${type === "compose" ? "/compose" : ""}/${refreshToken}`}
</span>
{(type === "application" || type === "compose") && (
<RefreshToken id={id} type={type} />

View File

@@ -1,3 +1,5 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
@@ -6,8 +8,6 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { AlertBlock } from "@/components/shared/alert-block";
import { Copy, HelpCircle, Server } from "lucide-react";
import { toast } from "sonner";

View File

@@ -1,4 +1,5 @@
import { DialogAction } from "@/components/shared/dialog-action";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
@@ -7,6 +8,12 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import {
CheckCircle2,
@@ -21,17 +28,10 @@ import {
XCircle,
} from "lucide-react";
import Link from "next/link";
import { toast } from "sonner";
import { AddDomain } from "./handle-domain";
import { useState } from "react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { toast } from "sonner";
import { DnsHelperModal } from "./dns-helper-modal";
import { Badge } from "@/components/ui/badge";
import { AddDomain } from "./handle-domain";
export type ValidationState = {
isLoading: boolean;
@@ -186,30 +186,19 @@ export const ShowDomains = ({ id, type }: Props) => {
return (
<Card
key={item.domainId}
className="relative overflow-hidden w-full border bg-card transition-all hover:shadow-md bg-transparent h-fit"
className="relative overflow-hidden w-full border transition-all hover:shadow-md bg-transparent h-fit"
>
<CardContent className="p-6">
<div className="flex flex-col gap-4">
{/* Service & Domain Info */}
<div className="flex items-start justify-between">
<div className="flex flex-col gap-2">
{item.serviceName && (
<Badge variant="outline" className="w-fit">
<Server className="size-3 mr-1" />
{item.serviceName}
</Badge>
)}
<Link
className="flex items-center gap-2 text-base font-medium hover:underline"
target="_blank"
href={`${item.https ? "https" : "http"}://${item.host}${item.path}`}
>
{item.host}
<ExternalLink className="size-4" />
</Link>
</div>
<div className="flex gap-2">
<div className="flex items-center justify-between flex-wrap gap-y-2">
{item.serviceName && (
<Badge variant="outline" className="w-fit">
<Server className="size-3 mr-1" />
{item.serviceName}
</Badge>
)}
<div className="flex gap-2 flex-wrap">
{!item.host.includes("traefik.me") && (
<DnsHelperModal
domain={{
@@ -266,6 +255,16 @@ export const ShowDomains = ({ id, type }: Props) => {
</DialogAction>
</div>
</div>
<div className="w-full break-all">
<Link
className="flex items-center gap-2 text-base font-medium hover:underline"
target="_blank"
href={`${item.https ? "https" : "http"}://${item.host}${item.path}`}
>
{item.host}
<ExternalLink className="size-4 min-w-4" />
</Link>
</div>
{/* Domain Details */}
<div className="flex flex-wrap gap-3">

View File

@@ -136,7 +136,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
enableSubmodules: data.enableSubmodules || false,
});
}
}, [form.reset, data, form]);
}, [form.reset, data?.applicationId, form]);
const onSubmit = async (data: BitbucketProvider) => {
await mutateAsync({
@@ -435,7 +435,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
<FormControl>
<div className="flex gap-2">
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
@@ -454,7 +454,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
variant="secondary"
onClick={() => {
const input = document.querySelector(
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
) as HTMLInputElement;
const value = input.value.trim();
if (value) {

View File

@@ -53,7 +53,7 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
registryURL: data.registryUrl || "",
});
}
}, [form.reset, data, form]);
}, [form.reset, data?.applicationId, form]);
const onSubmit = async (values: DockerProvider) => {
await mutateAsync({

View File

@@ -17,13 +17,13 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
@@ -262,7 +262,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
<FormControl>
<div className="flex gap-2">
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
@@ -281,7 +281,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
variant="secondary"
onClick={() => {
const input = document.querySelector(
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
) as HTMLInputElement;
const value = input.value.trim();
if (value) {

View File

@@ -158,7 +158,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
enableSubmodules: data.enableSubmodules || false,
});
}
}, [form.reset, data, form]);
}, [form.reset, data?.applicationId, form]);
const onSubmit = async (data: GiteaProvider) => {
await mutateAsync({
@@ -470,7 +470,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
<div className="flex gap-2">
<FormControl>
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();

View File

@@ -134,7 +134,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
}, [form.reset, data?.applicationId, form]);
const onSubmit = async (data: GithubProvider) => {
await mutateAsync({
@@ -474,7 +474,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
<div className="flex gap-2">
<FormControl>
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();

View File

@@ -141,7 +141,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
}, [form.reset, data?.applicationId, form]);
const onSubmit = async (data: GitlabProvider) => {
await mutateAsync({
@@ -452,7 +452,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
<div className="flex gap-2">
<FormControl>
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();

View File

@@ -24,9 +24,9 @@ import {
} from "lucide-react";
import { toast } from "sonner";
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
import { AddPreviewDomain } from "./add-preview-domain";
import { ShowPreviewSettings } from "./show-preview-settings";
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
interface Props {
applicationId: string;

View File

@@ -1,40 +1,6 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
FormDescription,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import {
Info,
PlusCircle,
PenBoxIcon,
RefreshCw,
DatabaseZap,
} from "lucide-react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Switch } from "@/components/ui/switch";
import { useEffect, useState } from "react";
import {
Dialog,
DialogContent,
@@ -42,10 +8,44 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { toast } from "sonner";
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import {
DatabaseZap,
Info,
PenBoxIcon,
PlusCircle,
RefreshCw,
} from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import type { CacheType } from "../domains/handle-domain";
export const commonCronExpressions = [

View File

@@ -1,14 +1,6 @@
import { DialogAction } from "@/components/shared/dialog-action";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { HandleSchedules } from "./handle-schedules";
import {
Clock,
Play,
Terminal,
Trash2,
ClipboardList,
Loader2,
} from "lucide-react";
import {
Card,
CardContent,
@@ -16,16 +8,24 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { toast } from "sonner";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { DialogAction } from "@/components/shared/dialog-action";
import { api } from "@/utils/api";
import {
ClipboardList,
Clock,
Loader2,
Play,
Terminal,
Trash2,
} from "lucide-react";
import { toast } from "sonner";
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
import { HandleSchedules } from "./handle-schedules";
interface Props {
id: string;

View File

@@ -136,7 +136,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
}, [form.reset, data?.composeId, form]);
const onSubmit = async (data: BitbucketProvider) => {
await mutateAsync({
@@ -437,7 +437,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
<FormControl>
<div className="flex gap-2">
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
@@ -456,7 +456,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
variant="secondary"
onClick={() => {
const input = document.querySelector(
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
) as HTMLInputElement;
const value = input.value.trim();
if (value) {

View File

@@ -263,7 +263,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
<FormControl>
<div className="flex gap-2">
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
@@ -282,7 +282,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
variant="secondary"
onClick={() => {
const input = document.querySelector(
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
) as HTMLInputElement;
const value = input.value.trim();
if (value) {

View File

@@ -142,7 +142,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
}, [form.reset, data?.composeId, form]);
const onSubmit = async (data: GiteaProvider) => {
await mutateAsync({
@@ -437,7 +437,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
<FormControl>
<div className="flex gap-2">
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();

View File

@@ -134,7 +134,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
}, [form.reset, data?.composeId, form]);
const onSubmit = async (data: GithubProvider) => {
await mutateAsync({
@@ -474,7 +474,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
<FormControl>
<div className="flex gap-2">
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
@@ -496,7 +496,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
variant="secondary"
onClick={() => {
const input = document.querySelector(
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
) as HTMLInputElement;
const value = input.value.trim();
if (value) {

View File

@@ -142,7 +142,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
enableSubmodules: data.enableSubmodules ?? false,
});
}
}, [form.reset, data, form]);
}, [form.reset, data?.composeId, form]);
const onSubmit = async (data: GitlabProvider) => {
await mutateAsync({
@@ -453,7 +453,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
<FormControl>
<div className="flex gap-2">
<Input
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
@@ -472,7 +472,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
variant="secondary"
onClick={() => {
const input = document.querySelector(
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
) as HTMLInputElement;
const value = input.value.trim();
if (value) {

View File

@@ -71,8 +71,8 @@ export const IsolatedDeployment = ({ composeId }: Props) => {
isolatedDeployment: formData?.isolatedDeployment || false,
})
.then(async (_data) => {
randomizeCompose();
refetch();
await randomizeCompose();
await refetch();
toast.success("Compose updated");
})
.catch(() => {
@@ -84,15 +84,10 @@ export const IsolatedDeployment = ({ composeId }: Props) => {
await mutateAsync({
composeId,
suffix: data?.appName || "",
})
.then(async (data) => {
await utils.project.all.invalidate();
setCompose(data);
toast.success("Compose Isolated");
})
.catch(() => {
toast.error("Error isolating the compose");
});
}).then(async (data) => {
await utils.project.all.invalidate();
setCompose(data);
});
};
return (

View File

@@ -77,8 +77,8 @@ export const RandomizeCompose = ({ composeId }: Props) => {
randomize: formData?.randomize || false,
})
.then(async (_data) => {
randomizeCompose();
refetch();
await randomizeCompose();
await refetch();
toast.success("Compose updated");
})
.catch(() => {
@@ -90,15 +90,10 @@ export const RandomizeCompose = ({ composeId }: Props) => {
await mutateAsync({
composeId,
suffix,
})
.then(async (data) => {
await utils.project.all.invalidate();
setCompose(data);
toast.success("Compose randomized");
})
.catch(() => {
toast.error("Error randomizing the compose");
});
}).then(async (data) => {
await utils.project.all.invalidate();
setCompose(data);
});
};
return (

View File

@@ -10,7 +10,7 @@ import {
DialogTrigger,
} from "@/components/ui/dialog";
import { api } from "@/utils/api";
import { Puzzle, RefreshCw } from "lucide-react";
import { Loader2, Puzzle, RefreshCw } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
@@ -66,36 +66,50 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
Preview your docker-compose file with added domains. Note: At least
one domain must be specified for this conversion to take effect.
</AlertBlock>
{isLoading ? (
<div className="flex flex-row items-center justify-center min-h-[25rem] border p-4 rounded-md">
<Loader2 className="h-8 w-8 text-muted-foreground mb-2 animate-spin" />
</div>
) : compose?.length === 5 ? (
<div className="border p-4 rounded-md flex flex-col items-center justify-center min-h-[25rem]">
<Puzzle className="h-8 w-8 text-muted-foreground mb-2" />
<span className="text-muted-foreground">
No converted compose data available.
</span>
</div>
) : (
<>
<div className="flex flex-row gap-2 justify-end">
<Button
variant="secondary"
isLoading={isLoading}
onClick={() => {
mutateAsync({ composeId })
.then(() => {
refetch();
toast.success("Fetched source type");
})
.catch((err) => {
toast.error("Error fetching source type", {
description: err.message,
});
});
}}
>
Refresh <RefreshCw className="ml-2 h-4 w-4" />
</Button>
</div>
<div className="flex flex-row gap-2 justify-end">
<Button
variant="secondary"
isLoading={isLoading}
onClick={() => {
mutateAsync({ composeId })
.then(() => {
refetch();
toast.success("Fetched source type");
})
.catch((err) => {
toast.error("Error fetching source type", {
description: err.message,
});
});
}}
>
Refresh <RefreshCw className="ml-2 h-4 w-4" />
</Button>
</div>
<pre>
<CodeEditor
value={compose || ""}
language="yaml"
readOnly
height="50rem"
/>
</pre>
<pre>
<CodeEditor
value={compose || ""}
language="yaml"
readOnly
height="50rem"
/>
</pre>
</>
)}
</DialogContent>
</Dialog>
);

View File

@@ -39,6 +39,12 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -48,9 +54,9 @@ import {
CheckIcon,
ChevronsUpDown,
Copy,
RotateCcw,
RefreshCw,
DatabaseZap,
RefreshCw,
RotateCcw,
} from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
@@ -58,12 +64,6 @@ import { toast } from "sonner";
import { z } from "zod";
import type { ServiceType } from "../../application/advanced/show-resources";
import { type LogLine, parseLogs } from "../../docker/logs/utils";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
type DatabaseType =
| Exclude<ServiceType, "application" | "redis">

View File

@@ -1,3 +1,10 @@
import {
MariadbIcon,
MongodbIcon,
MysqlIcon,
PostgresqlIcon,
} from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button";
import {
@@ -13,6 +20,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import {
ClipboardList,
@@ -25,17 +33,9 @@ import Link from "next/link";
import { useState } from "react";
import { toast } from "sonner";
import type { ServiceType } from "../../application/advanced/show-resources";
import { RestoreBackup } from "./restore-backup";
import { HandleBackup } from "./handle-backup";
import { cn } from "@/lib/utils";
import {
MariadbIcon,
MongodbIcon,
MysqlIcon,
PostgresqlIcon,
} from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { ShowDeploymentsModal } from "../../application/deployments/show-deployments-modal";
import { HandleBackup } from "./handle-backup";
import { RestoreBackup } from "./restore-backup";
interface Props {
id: string;

View File

@@ -0,0 +1,454 @@
"use client";
import { Logo } from "@/components/shared/logo";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { authClient } from "@/lib/auth-client";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import copy from "copy-to-clipboard";
import { format } from "date-fns";
import {
Building2,
Calendar,
CheckIcon,
ChevronsUpDown,
Copy,
CreditCard,
Fingerprint,
Key,
Server,
Settings2,
Shield,
UserIcon,
XIcon,
} from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
type User = typeof authClient.$Infer.Session.user;
export const ImpersonationBar = () => {
const [users, setUsers] = useState<User[]>([]);
const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [isImpersonating, setIsImpersonating] = useState(false);
const [open, setOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [showBar, setShowBar] = useState(false);
const { data } = api.user.get.useQuery();
const fetchUsers = async (search?: string) => {
try {
const session = await authClient.getSession();
if (session?.data?.session?.impersonatedBy) {
return;
}
setIsLoading(true);
const response = await authClient.admin.listUsers({
query: {
limit: 30,
...(search && {
searchField: "email",
searchOperator: "contains",
searchValue: search,
}),
},
});
const filteredUsers = response.data?.users.filter(
// @ts-ignore
(user) => user.allowImpersonation && data?.user?.email !== user.email,
);
if (!response.error) {
// @ts-ignore
setUsers(filteredUsers || []);
}
} catch (error) {
console.error("Error fetching users:", error);
toast.error("Error loading users");
} finally {
setIsLoading(false);
}
};
const handleImpersonate = async () => {
if (!selectedUser) return;
try {
await authClient.admin.impersonateUser({
userId: selectedUser.id,
});
setIsImpersonating(true);
setOpen(false);
toast.success("Successfully impersonating user", {
description: `You are now viewing as ${selectedUser.name || selectedUser.email}`,
});
window.location.reload();
} catch (error) {
console.error("Error impersonating user:", error);
toast.error("Error impersonating user");
}
};
const handleStopImpersonating = async () => {
try {
await authClient.admin.stopImpersonating();
setIsImpersonating(false);
setSelectedUser(null);
setShowBar(false);
toast.success("Stopped impersonating user");
window.location.reload();
} catch (error) {
console.error("Error stopping impersonation:", error);
toast.error("Error stopping impersonation");
}
};
useEffect(() => {
const checkImpersonation = async () => {
try {
const session = await authClient.getSession();
if (session?.data?.session?.impersonatedBy) {
setIsImpersonating(true);
setShowBar(true);
// setSelectedUser(data);
}
} catch (error) {
console.error("Error checking impersonation status:", error);
}
};
checkImpersonation();
fetchUsers();
}, []);
return (
<TooltipProvider>
<>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon"
className={cn(
"fixed bottom-4 right-4 z-50 rounded-full shadow-lg",
isImpersonating &&
!showBar &&
"bg-red-100 hover:bg-red-200 border-red-200",
)}
onClick={() => setShowBar(!showBar)}
>
<Settings2
className={cn(
"h-4 w-4",
isImpersonating && !showBar && "text-red-500",
)}
/>
</Button>
</TooltipTrigger>
<TooltipContent>
{isImpersonating ? "Impersonation Controls" : "User Impersonation"}
</TooltipContent>
</Tooltip>
<div
className={cn(
"fixed bottom-0 left-0 right-0 bg-background border-t border-border p-4 flex items-center justify-center gap-4 z-40 transition-all duration-200 ease-in-out",
showBar ? "translate-y-0" : "translate-y-full",
)}
>
<div className="flex items-center gap-4 px-4 md:px-20 w-full">
<Logo className="w-10 h-10" />
{!isImpersonating ? (
<div className="flex items-center gap-2 w-full">
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
aria-expanded={open}
className="w-[300px] justify-between"
>
{selectedUser ? (
<div className="flex items-center gap-2">
<UserIcon className="mr-2 h-4 w-4 flex-shrink-0" />
<span className="truncate flex flex-col items-start">
<span className="text-sm font-medium">
{selectedUser.name || ""}
</span>
<span className="text-xs text-muted-foreground">
{selectedUser.email}
</span>
</span>
</div>
) : (
<>
<UserIcon className="mr-2 h-4 w-4" />
<span>Select user to impersonate</span>
</>
)}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0" align="start">
<Command>
<CommandInput
placeholder="Search users by email or name..."
onValueChange={(search) => {
fetchUsers(search);
}}
className="h-9"
/>
{isLoading ? (
<div className="py-6 text-center text-sm">
Loading users...
</div>
) : (
<>
<CommandEmpty>No users found.</CommandEmpty>
<CommandList>
<CommandGroup heading="All Users">
{users.map((user) => (
<CommandItem
key={user.id}
value={user.email}
onSelect={() => {
setSelectedUser(user);
setOpen(false);
}}
>
<span className="flex items-center gap-2 flex-1">
<UserIcon className="h-4 w-4 flex-shrink-0" />
<span className="flex flex-col items-start">
<span className="text-sm font-medium">
{user.name || ""}
</span>
<span className="text-xs text-muted-foreground">
{user.email} {user.role}
</span>
</span>
</span>
<CheckIcon
className={cn(
"ml-auto h-4 w-4",
selectedUser?.id === user.id
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</>
)}
</Command>
</PopoverContent>
</Popover>
<Button
onClick={handleImpersonate}
disabled={!selectedUser}
variant="default"
className="gap-2"
>
<Shield className="h-4 w-4" />
Impersonate
</Button>
</div>
) : (
<div className="flex items-center gap-4 w-full flex-wrap">
<div className="flex items-center gap-4 flex-1 flex-wrap">
<Avatar className="h-10 w-10">
<AvatarImage
src={data?.user?.image || ""}
alt={data?.user?.name || ""}
/>
<AvatarFallback>
{data?.user?.name?.slice(0, 2).toUpperCase() || "U"}
</AvatarFallback>
</Avatar>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<Badge
variant="outline"
className="gap-1 py-1 text-yellow-500 bg-yellow-50/20"
>
<Shield className="h-3 w-3" />
Impersonating
</Badge>
<span className="font-medium">
{data?.user?.name || ""}
</span>
</div>
<div className="flex items-center gap-3 text-sm text-muted-foreground flex-wrap">
<span className="flex items-center gap-1">
<UserIcon className="h-3 w-3" />
{data?.user?.email} {data?.role}
</span>
<span className="flex items-center gap-1">
<Key className="h-3 w-3" />
<span className="flex items-center gap-1">
ID: {data?.user?.id?.slice(0, 8)}
<Button
variant="ghost"
size="icon"
className="h-4 w-4 hover:bg-muted/50"
onClick={() => {
if (data?.id) {
copy(data.id);
toast.success("ID copied to clipboard");
}
}}
>
<Copy className="h-3 w-3" />
</Button>
</span>
</span>
<span className="flex items-center gap-1">
<Building2 className="h-3 w-3" />
<span className="flex items-center gap-1">
Org: {data?.organizationId?.slice(0, 8)}
<Button
variant="ghost"
size="icon"
className="h-4 w-4 hover:bg-muted/50"
onClick={() => {
if (data?.organizationId) {
copy(data.organizationId);
toast.success(
"Organization ID copied to clipboard",
);
}
}}
>
<Copy className="h-3 w-3" />
</Button>
</span>
</span>
{data?.user?.stripeCustomerId && (
<span className="flex items-center gap-1">
<CreditCard className="h-3 w-3" />
<span className="flex items-center gap-1">
Customer:
{data?.user?.stripeCustomerId?.slice(0, 8)}
<Button
variant="ghost"
size="icon"
className="h-4 w-4 hover:bg-muted/50"
onClick={() => {
copy(data?.user?.stripeCustomerId || "");
toast.success(
"Stripe Customer ID copied to clipboard",
);
}}
>
<Copy className="h-3 w-3" />
</Button>
</span>
</span>
)}
{data?.user?.stripeSubscriptionId && (
<span className="flex items-center gap-1">
<CreditCard className="h-3 w-3" />
<span className="flex items-center gap-1">
Sub: {data?.user?.stripeSubscriptionId?.slice(0, 8)}
<Button
variant="ghost"
size="icon"
className="h-4 w-4 hover:bg-muted/50"
onClick={() => {
copy(data.user.stripeSubscriptionId || "");
toast.success(
"Stripe Subscription ID copied to clipboard",
);
}}
>
<Copy className="h-3 w-3" />
</Button>
</span>
</span>
)}
{data?.user?.serversQuantity !== undefined && (
<span className="flex items-center gap-1">
<Server className="h-3 w-3" />
<span>Servers: {data.user.serversQuantity}</span>
</span>
)}
{data?.createdAt && (
<span className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
Created:{" "}
{format(new Date(data.createdAt), "MMM d, yyyy")}
</span>
)}
<Tooltip>
<TooltipTrigger asChild>
<span className="flex items-center gap-1 cursor-default">
<Fingerprint
className={cn(
"h-3 w-3",
data?.user?.twoFactorEnabled
? "text-green-500"
: "text-muted-foreground",
)}
/>
<Badge
variant={
data?.user?.twoFactorEnabled
? "green"
: "secondary"
}
className="text-[10px] px-1 py-0"
>
2FA{" "}
{data?.user?.twoFactorEnabled
? "Enabled"
: "Disabled"}
</Badge>
</span>
</TooltipTrigger>
<TooltipContent>
Two-Factor Authentication Status
</TooltipContent>
</Tooltip>
</div>
</div>
</div>
<Button
onClick={handleStopImpersonating}
variant="secondary"
className="gap-2"
size="sm"
>
<XIcon className="w-4 h-4" />
Stop Impersonating
</Button>
</div>
)}
</div>
</div>
</>
</TooltipProvider>
);
};

View File

@@ -10,6 +10,7 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { api } from "@/utils/api";
import { Copy, Loader2 } from "lucide-react";
import { useRouter } from "next/router";
@@ -48,6 +49,7 @@ export const DuplicateProject = ({
const [open, setOpen] = useState(false);
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [duplicateType, setDuplicateType] = useState("new-project"); // "new-project" or "same-project"
const utils = api.useUtils();
const router = useRouter();
@@ -59,9 +61,15 @@ export const DuplicateProject = ({
api.project.duplicate.useMutation({
onSuccess: async (newProject) => {
await utils.project.all.invalidate();
toast.success("Project duplicated successfully");
toast.success(
duplicateType === "new-project"
? "Project duplicated successfully"
: "Services duplicated successfully",
);
setOpen(false);
router.push(`/dashboard/project/${newProject.projectId}`);
if (duplicateType === "new-project") {
router.push(`/dashboard/project/${newProject.projectId}`);
}
},
onError: (error) => {
toast.error(error.message);
@@ -69,7 +77,7 @@ export const DuplicateProject = ({
});
const handleDuplicate = async () => {
if (!name) {
if (duplicateType === "new-project" && !name) {
toast.error("Project name is required");
return;
}
@@ -83,6 +91,7 @@ export const DuplicateProject = ({
id: service.id,
type: service.type,
})),
duplicateInSameProject: duplicateType === "same-project",
});
};
@@ -95,6 +104,7 @@ export const DuplicateProject = ({
// Reset form when closing
setName("");
setDescription("");
setDuplicateType("new-project");
}
}}
>
@@ -106,32 +116,54 @@ export const DuplicateProject = ({
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Duplicate Project</DialogTitle>
<DialogTitle>Duplicate Services</DialogTitle>
<DialogDescription>
Create a new project with the selected services
Choose where to duplicate the selected services
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="New project name"
/>
<Label>Duplicate to</Label>
<RadioGroup
value={duplicateType}
onValueChange={setDuplicateType}
className="grid gap-2"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="new-project" id="new-project" />
<Label htmlFor="new-project">New project</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="same-project" id="same-project" />
<Label htmlFor="same-project">Same project</Label>
</div>
</RadioGroup>
</div>
<div className="grid gap-2">
<Label htmlFor="description">Description</Label>
<Input
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Project description (optional)"
/>
</div>
{duplicateType === "new-project" && (
<>
<div className="grid gap-2">
<Label htmlFor="name">Name</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="New project name"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="description">Description</Label>
<Input
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Project description (optional)"
/>
</div>
</>
)}
<div className="grid gap-2">
<Label>Selected services to duplicate</Label>
@@ -159,10 +191,14 @@ export const DuplicateProject = ({
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Duplicating...
{duplicateType === "new-project"
? "Duplicating project..."
: "Duplicating services..."}
</>
) : duplicateType === "new-project" ? (
"Duplicate project"
) : (
"Duplicate"
"Duplicate services"
)}
</Button>
</DialogFooter>

View File

@@ -10,6 +10,7 @@ import {
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
@@ -17,6 +18,7 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Switch } from "@/components/ui/switch";
import { generateSHA256Hash } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -34,6 +36,7 @@ const profileSchema = z.object({
password: z.string().nullable(),
currentPassword: z.string().nullable(),
image: z.string().optional(),
allowImpersonation: z.boolean().optional().default(false),
});
type Profile = z.infer<typeof profileSchema>;
@@ -56,6 +59,7 @@ const randomImages = [
export const ProfileForm = () => {
const _utils = api.useUtils();
const { data, refetch, isLoading } = api.user.get.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
const {
mutateAsync,
@@ -79,6 +83,7 @@ export const ProfileForm = () => {
password: "",
image: data?.user?.image || "",
currentPassword: "",
allowImpersonation: data?.user?.allowImpersonation || false,
},
resolver: zodResolver(profileSchema),
});
@@ -91,11 +96,13 @@ export const ProfileForm = () => {
password: form.getValues("password") || "",
image: data?.user?.image || "",
currentPassword: form.getValues("currentPassword") || "",
allowImpersonation: data?.user?.allowImpersonation,
},
{
keepValues: true,
},
);
form.setValue("allowImpersonation", data?.user?.allowImpersonation);
if (data.user.email) {
generateSHA256Hash(data.user.email).then((hash) => {
@@ -111,6 +118,7 @@ export const ProfileForm = () => {
password: values.password || undefined,
image: values.image,
currentPassword: values.currentPassword || undefined,
allowImpersonation: values.allowImpersonation,
})
.then(async () => {
await refetch();
@@ -256,7 +264,34 @@ export const ProfileForm = () => {
</FormItem>
)}
/>
{isCloud && (
<FormField
control={form.control}
name="allowImpersonation"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
<div className="space-y-0.5">
<FormLabel>Allow Impersonation</FormLabel>
<FormDescription>
Enable this option to allow Dokploy Cloud
administrators to temporarily access your
account for troubleshooting and support
purposes. This helps them quickly identify and
resolve any issues you may encounter.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
)}
</div>
<div className="flex items-center justify-end gap-2">
<Button type="submit" isLoading={isUpdating}>
{t("settings.common.save")}

View File

@@ -156,6 +156,67 @@ export const HandleServers = ({ serverId }: Props) => {
remotely.
</DialogDescription>
</DialogHeader>
<div>
<p className="text-primary text-sm font-medium">
You will need to purchase or rent a Virtual Private Server (VPS) to
proceed, we recommend to use one of these providers since has been
heavily tested.
</p>
<ul className="list-inside list-disc pl-4 text-sm text-muted-foreground mt-4">
<li>
<a
href="https://www.hostinger.com/vps-hosting?REFERRALCODE=1SIUMAURICI97"
className="text-link underline"
>
Hostinger - Get 20% Discount
</a>
</li>
<li>
<a
href=" https://app.americancloud.com/register?ref=dokploy"
className="text-link underline"
>
American Cloud - Get $20 Credits
</a>
</li>
<li>
<a
href="https://m.do.co/c/db24efd43f35"
className="text-link underline"
>
DigitalOcean - Get $200 Credits
</a>
</li>
<li>
<a
href="https://hetzner.cloud/?ref=vou4fhxJ1W2D"
className="text-link underline"
>
Hetzner - Get 20 Credits
</a>
</li>
<li>
<a
href="https://www.vultr.com/?ref=9679828"
className="text-link underline"
>
Vultr
</a>
</li>
<li>
<a
href="https://www.linode.com/es/pricing/#compute-shared"
className="text-link underline"
>
Linode
</a>
</li>
</ul>
<AlertBlock className="mt-4 px-4">
You are free to use whatever provider, but we recommend to use one
of the above, to avoid issues.
</AlertBlock>
</div>
{!canCreateMoreServers && (
<AlertBlock type="warning">
You cannot create more servers,{" "}

View File

@@ -1,7 +1,7 @@
import { useState } from "react";
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
import { useState } from "react";
interface Props {
serverId: string;

View File

@@ -40,10 +40,10 @@ import { HandleServers } from "./handle-servers";
import { SetupServer } from "./setup-server";
import { ShowDockerContainersModal } from "./show-docker-containers-modal";
import { ShowMonitoringModal } from "./show-monitoring-modal";
import { ShowSchedulesModal } from "./show-schedules-modal";
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
import { ShowSchedulesModal } from "./show-schedules-modal";
export const ShowServers = () => {
const { t } = useTranslation("settings");

View File

@@ -177,6 +177,14 @@ export const WelcomeSuscription = () => {
Hostinger - Get 20% Discount
</a>
</li>
<li>
<a
href=" https://app.americancloud.com/register?ref=dokploy"
className="text-link underline"
>
American Cloud - Get $20 Credits
</a>
</li>
<li>
<a
href="https://m.do.co/c/db24efd43f35"

View File

@@ -185,24 +185,21 @@ export const ShowInvitations = () => {
Cancel Invitation
</DropdownMenuItem>
)}
<DropdownMenuItem
className="w-full cursor-pointer"
onSelect={async (_e) => {
await removeInvitation({
invitationId: invitation.id,
}).then(() => {
refetch();
toast.success(
"Invitation removed",
);
});
}}
>
Remove Invitation
</DropdownMenuItem>
</>
)}
<DropdownMenuItem
className="w-full cursor-pointer"
onSelect={async (_e) => {
await removeInvitation({
invitationId: invitation.id,
}).then(() => {
refetch();
toast.success("Invitation removed");
});
}}
>
Remove Invitation
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>

View File

@@ -1,9 +1,25 @@
import { api } from "@/utils/api";
import { ImpersonationBar } from "../dashboard/impersonation/impersonation-bar";
import Page from "./side";
import { ChatwootWidget } from "../shared/ChatwootWidget";
interface Props {
children: React.ReactNode;
metaName?: string;
}
export const DashboardLayout = ({ children }: Props) => {
return <Page>{children}</Page>;
const { data: haveRootAccess } = api.user.haveRootAccess.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
return (
<>
<Page>{children}</Page>
{isCloud === true && (
<ChatwootWidget websiteToken="USCpQRKzHvFMssf3p6Eacae5" />
)}
{haveRootAccess === true && <ImpersonationBar />}
</>
);
};

View File

@@ -1,9 +0,0 @@
import Page from "./side";
interface Props {
children: React.ReactNode;
}
export const ProjectLayout = ({ children }: Props) => {
return <Page>{children}</Page>;
};

View File

@@ -0,0 +1,69 @@
import Script from "next/script";
import { useEffect } from "react";
interface ChatwootWidgetProps {
websiteToken: string;
baseUrl?: string;
settings?: {
position?: "left" | "right";
type?: "standard" | "expanded_bubble";
launcherTitle?: string;
darkMode?: boolean;
hideMessageBubble?: boolean;
placement?: "right" | "left";
showPopoutButton?: boolean;
widgetStyle?: "standard" | "bubble";
};
user?: {
identifier: string;
name?: string;
email?: string;
phoneNumber?: string;
avatarUrl?: string;
customAttributes?: Record<string, any>;
identifierHash?: string;
};
}
export const ChatwootWidget = ({
websiteToken,
baseUrl = "https://app.chatwoot.com",
settings = {
position: "right",
type: "standard",
launcherTitle: "Chat with us",
},
user,
}: ChatwootWidgetProps) => {
useEffect(() => {
// Configurar los settings de Chatwoot
window.chatwootSettings = {
position: "right",
};
(window as any).chatwootSDKReady = () => {
window.chatwootSDK?.run({ websiteToken, baseUrl });
const trySetUser = () => {
if (window.$chatwoot && user) {
window.$chatwoot.setUser(user.identifier, {
email: user.email,
name: user.name,
avatar_url: user.avatarUrl,
phone_number: user.phoneNumber,
});
}
};
trySetUser();
};
}, [websiteToken, baseUrl, user, settings]);
return (
<Script
src={`${baseUrl}/packs/js/sdk.js`}
strategy="lazyOnload"
onLoad={() => (window as any).chatwootSDKReady?.()}
/>
);
};

View File

@@ -0,0 +1 @@
ALTER TABLE "user_temp" ADD COLUMN "allowImpersonation" boolean DEFAULT false NOT NULL;

View File

@@ -0,0 +1 @@
ALTER TABLE "user_temp" ADD COLUMN "role" text DEFAULT 'user' NOT NULL;

View File

@@ -0,0 +1 @@
ALTER TABLE "application" ADD COLUMN "isStaticSpa" boolean;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -631,6 +631,27 @@
"when": 1746392564463,
"tag": "0089_noisy_sandman",
"breakpoints": true
},
{
"idx": 90,
"version": "7",
"when": 1746509318678,
"tag": "0090_clean_wolf_cub",
"breakpoints": true
},
{
"idx": 91,
"version": "7",
"when": 1746518402168,
"tag": "0091_spotty_kulan_gath",
"breakpoints": true
},
{
"idx": 92,
"version": "7",
"when": 1747713229160,
"tag": "0092_stiff_the_watchers",
"breakpoints": true
}
]
}

View File

@@ -1,9 +1,15 @@
import { organizationClient } from "better-auth/client/plugins";
import { twoFactorClient } from "better-auth/client/plugins";
import { apiKeyClient } from "better-auth/client/plugins";
import { adminClient } from "better-auth/client/plugins";
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
// baseURL: "http://localhost:3000", // the base url of your auth server
plugins: [organizationClient(), twoFactorClient(), apiKeyClient()],
plugins: [
organizationClient(),
twoFactorClient(),
apiKeyClient(),
adminClient(),
],
});

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.22.0",
"version": "v0.22.7",
"private": true,
"license": "Apache-2.0",
"type": "module",
@@ -36,6 +36,8 @@
"test": "vitest --config __test__/vitest.config.ts"
},
"dependencies": {
"pino": "9.4.0",
"pino-pretty": "11.2.2",
"@ai-sdk/anthropic": "^1.0.6",
"@ai-sdk/azure": "^1.0.15",
"@ai-sdk/cohere": "^1.0.6",
@@ -92,7 +94,7 @@
"adm-zip": "^0.5.14",
"ai": "^4.0.23",
"bcrypt": "5.1.1",
"better-auth": "1.2.6",
"better-auth": "v1.2.8-beta.7",
"bl": "6.0.11",
"boxen": "^7.1.1",
"bullmq": "5.4.2",
@@ -186,7 +188,7 @@
},
"packageManager": "pnpm@9.5.0",
"engines": {
"node": "^20.9.0",
"node": "^20.16.0",
"pnpm": ">=9.5.0"
},
"lint-staged": {

View File

@@ -17,7 +17,6 @@ const inter = Inter({ subsets: ["latin"] });
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode;
// session: Session | null;
theme?: string;
};
@@ -33,11 +32,13 @@ const MyApp = ({
return (
<>
<style jsx global>{`
:root {
--font-inter: ${inter.style.fontFamily};
}
`}</style>
<style jsx global>
{`
:root {
--font-inter: ${inter.style.fontFamily};
}
`}
</style>
<Head>
<title>Dokploy</title>
</Head>

View File

@@ -80,7 +80,13 @@ export default function Custom404({ statusCode, error }: Props) {
<footer className="mt-auto text-center py-5">
<div className="max-w-[85rem] mx-auto px-4 sm:px-6 lg:px-8">
<p className="text-sm text-gray-500">
Submit Log in issue on Github
<Link
href="https://github.com/Dokploy/dokploy/issues"
target="_blank"
className="underline hover:text-primary transition-colors"
>
Submit Log in issue on Github
</Link>
</p>
</div>
</footer>

View File

@@ -10,7 +10,7 @@ import {
PostgresqlIcon,
RedisIcon,
} from "@/components/icons/data-tools-icons";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { DateTooltip } from "@/components/shared/date-tooltip";
import { DialogAction } from "@/components/shared/dialog-action";
@@ -18,6 +18,7 @@ import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Button } from "@/components/ui/button";
import { AddAiAssistant } from "@/components/dashboard/project/add-ai-assistant";
import { DuplicateProject } from "@/components/dashboard/project/duplicate-project";
import {
Card,
CardContent,
@@ -93,7 +94,6 @@ import { useRouter } from "next/router";
import { type ReactElement, useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import superjson from "superjson";
import { DuplicateProject } from "@/components/dashboard/project/duplicate-project";
export type Services = {
appName: string;
@@ -1064,7 +1064,7 @@ const Project = (
export default Project;
Project.getLayout = (page: ReactElement) => {
return <ProjectLayout>{page}</ProjectLayout>;
return <DashboardLayout>{page}</DashboardLayout>;
};
export async function getServerSideProps(

View File

@@ -17,7 +17,7 @@ import { UpdateApplication } from "@/components/dashboard/application/update-app
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
@@ -363,7 +363,7 @@ const Service = (
export default Service;
Service.getLayout = (page: ReactElement) => {
return <ProjectLayout>{page}</ProjectLayout>;
return <DashboardLayout>{page}</DashboardLayout>;
};
export async function getServerSideProps(

View File

@@ -13,7 +13,7 @@ import { UpdateCompose } from "@/components/dashboard/compose/update-compose";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { ComposeFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-compose-monitoring";
import { ComposePaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-compose-monitoring";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
@@ -217,12 +217,12 @@ const Service = (
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
<TabsList
className={cn(
"lg:grid lg:w-fit max-md:overflow-y-scroll justify-start",
"xl:grid xl:w-fit max-md:overflow-y-scroll justify-start",
isCloud && data?.serverId
? "lg:grid-cols-9"
? "xl:grid-cols-9"
: data?.serverId
? "lg:grid-cols-8"
: "lg:grid-cols-9",
? "xl:grid-cols-8"
: "xl:grid-cols-9",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
@@ -366,7 +366,7 @@ const Service = (
export default Service;
Service.getLayout = (page: ReactElement) => {
return <ProjectLayout>{page}</ProjectLayout>;
return <DashboardLayout>{page}</DashboardLayout>;
};
export async function getServerSideProps(

View File

@@ -10,7 +10,7 @@ import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
import { MariadbIcon } from "@/components/icons/data-tools-icons";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
@@ -294,7 +294,7 @@ const Mariadb = (
export default Mariadb;
Mariadb.getLayout = (page: ReactElement) => {
return <ProjectLayout>{page}</ProjectLayout>;
return <DashboardLayout>{page}</DashboardLayout>;
};
export async function getServerSideProps(

View File

@@ -10,7 +10,7 @@ import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
import { MongodbIcon } from "@/components/icons/data-tools-icons";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
@@ -296,7 +296,7 @@ const Mongo = (
export default Mongo;
Mongo.getLayout = (page: ReactElement) => {
return <ProjectLayout>{page}</ProjectLayout>;
return <DashboardLayout>{page}</DashboardLayout>;
};
export async function getServerSideProps(

View File

@@ -10,7 +10,7 @@ import { ShowInternalMysqlCredentials } from "@/components/dashboard/mysql/gener
import { UpdateMysql } from "@/components/dashboard/mysql/update-mysql";
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
import { MysqlIcon } from "@/components/icons/data-tools-icons";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
@@ -280,7 +280,7 @@ const MySql = (
export default MySql;
MySql.getLayout = (page: ReactElement) => {
return <ProjectLayout>{page}</ProjectLayout>;
return <DashboardLayout>{page}</DashboardLayout>;
};
export async function getServerSideProps(

View File

@@ -10,7 +10,7 @@ import { ShowInternalPostgresCredentials } from "@/components/dashboard/postgres
import { UpdatePostgres } from "@/components/dashboard/postgres/update-postgres";
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
import { PostgresqlIcon } from "@/components/icons/data-tools-icons";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
@@ -278,7 +278,7 @@ const Postgresql = (
export default Postgresql;
Postgresql.getLayout = (page: ReactElement) => {
return <ProjectLayout>{page}</ProjectLayout>;
return <DashboardLayout>{page}</DashboardLayout>;
};
export async function getServerSideProps(

View File

@@ -9,7 +9,7 @@ import { ShowInternalRedisCredentials } from "@/components/dashboard/redis/gener
import { UpdateRedis } from "@/components/dashboard/redis/update-redis";
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
import { RedisIcon } from "@/components/icons/data-tools-icons";
import { ProjectLayout } from "@/components/layouts/project-layout";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge";
@@ -285,7 +285,7 @@ const Redis = (
export default Redis;
Redis.getLayout = (page: ReactElement) => {
return <ProjectLayout>{page}</ProjectLayout>;
return <DashboardLayout>{page}</DashboardLayout>;
};
export async function getServerSideProps(

View File

@@ -1,11 +1,11 @@
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import type { ReactElement } from "react";
import type { GetServerSidePropsContext } from "next";
import { validateRequest } from "@dokploy/server/lib/auth";
import { IS_CLOUD } from "@dokploy/server/constants";
import { api } from "@/utils/api";
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { Card } from "@/components/ui/card";
import { api } from "@/utils/api";
import { IS_CLOUD } from "@dokploy/server/constants";
import { validateRequest } from "@dokploy/server/lib/auth";
import type { GetServerSidePropsContext } from "next";
import type { ReactElement } from "react";
function SchedulesPage() {
const { data: user } = api.user.get.useQuery();
return (

View File

@@ -1,16 +1,16 @@
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { WebDomain } from "@/components/dashboard/settings/web-domain";
import { WebServer } from "@/components/dashboard/settings/web-server";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { Card } from "@/components/ui/card";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { getLocale, serverSideTranslations } from "@/utils/i18n";
import { IS_CLOUD, validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import type { ReactElement } from "react";
import superjson from "superjson";
import { api } from "@/utils/api";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
import { Card } from "@/components/ui/card";
const Page = () => {
const { data: user } = api.user.get.useQuery();
return (

View File

@@ -28,6 +28,7 @@ import { projectRouter } from "./routers/project";
import { redirectsRouter } from "./routers/redirects";
import { redisRouter } from "./routers/redis";
import { registryRouter } from "./routers/registry";
import { scheduleRouter } from "./routers/schedule";
import { securityRouter } from "./routers/security";
import { serverRouter } from "./routers/server";
import { settingsRouter } from "./routers/settings";
@@ -35,7 +36,6 @@ import { sshRouter } from "./routers/ssh-key";
import { stripeRouter } from "./routers/stripe";
import { swarmRouter } from "./routers/swarm";
import { userRouter } from "./routers/user";
import { scheduleRouter } from "./routers/schedule";
/**
* This is the primary router for your server.
*

View File

@@ -330,6 +330,7 @@ export const applicationRouter = createTRPCRouter({
dockerContextPath: input.dockerContextPath,
dockerBuildStage: input.dockerBuildStage,
herokuVersion: input.herokuVersion,
isStaticSpa: input.isStaticSpa,
});
return true;

View File

@@ -51,9 +51,9 @@ import { processTemplate } from "@dokploy/server/templates/processors";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { dump } from "js-yaml";
import { parse } from "toml";
import _ from "lodash";
import { nanoid } from "nanoid";
import { parse } from "toml";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
@@ -439,7 +439,15 @@ export const composeRouter = createTRPCRouter({
}
const projectName = slugify(`${project.name} ${input.id}`);
const generate = processTemplate(template.config, {
const appName = `${projectName}-${generatePassword(6)}`;
const config = {
...template.config,
variables: {
APP_NAME: appName,
...template.config.variables,
},
};
const generate = processTemplate(config, {
serverIp: serverIp,
projectName: projectName,
});
@@ -451,7 +459,7 @@ export const composeRouter = createTRPCRouter({
serverId: input.serverId,
name: input.id,
sourceType: "raw",
appName: `${projectName}-${generatePassword(6)}`,
appName: appName,
isolatedDeployment: true,
});
@@ -605,7 +613,15 @@ export const composeRouter = createTRPCRouter({
});
}
const processedTemplate = processTemplate(config, {
const configModified = {
...config,
variables: {
APP_NAME: compose.appName,
...config.variables,
},
};
const processedTemplate = processTemplate(configModified, {
serverIp: serverIp,
projectName: compose.appName,
});
@@ -675,7 +691,15 @@ export const composeRouter = createTRPCRouter({
});
}
const processedTemplate = processTemplate(config, {
const configModified = {
...config,
variables: {
APP_NAME: compose.appName,
...config.variables,
},
};
const processedTemplate = processTemplate(configModified, {
serverIp: serverIp,
projectName: compose.appName,
});

View File

@@ -1,3 +1,4 @@
import { db } from "@/server/db";
import {
apiFindAllByApplication,
apiFindAllByCompose,
@@ -16,7 +17,6 @@ import {
import { TRPCError } from "@trpc/server";
import { desc, eq } from "drizzle-orm";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { db } from "@/server/db";
export const deploymentRouter = createTRPCRouter({
all: protectedProcedure

View File

@@ -21,32 +21,32 @@ import {
addNewProject,
checkProjectAccess,
createApplication,
createBackup,
createCompose,
createDomain,
createMariadb,
createMongo,
createMount,
createMysql,
createPort,
createPostgres,
createPreviewDeployment,
createProject,
createRedirect,
createRedis,
createSecurity,
deleteProject,
findApplicationById,
findComposeById,
findMongoById,
findMariadbById,
findMemberById,
findRedisById,
findMongoById,
findMySqlById,
findPostgresById,
findProjectById,
findRedisById,
findUserById,
updateProjectById,
findPostgresById,
findMariadbById,
findMySqlById,
createDomain,
createPort,
createMount,
createRedirect,
createPreviewDeployment,
createBackup,
createSecurity,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { and, desc, eq, sql } from "drizzle-orm";
@@ -309,6 +309,7 @@ export const projectRouter = createTRPCRouter({
}),
)
.optional(),
duplicateInSameProject: z.boolean().default(false),
}),
)
.mutation(async ({ ctx, input }) => {
@@ -331,15 +332,17 @@ export const projectRouter = createTRPCRouter({
});
}
// Create new project
const newProject = await createProject(
{
name: input.name,
description: input.description,
env: sourceProject.env,
},
ctx.session.activeOrganizationId,
);
// Create new project or use existing one
const targetProject = input.duplicateInSameProject
? sourceProject
: await createProject(
{
name: input.name,
description: input.description,
env: sourceProject.env,
},
ctx.session.activeOrganizationId,
);
if (input.includeServices) {
const servicesToDuplicate = input.selectedServices || [];
@@ -362,7 +365,10 @@ export const projectRouter = createTRPCRouter({
const newApplication = await createApplication({
...application,
projectId: newProject.projectId,
name: input.duplicateInSameProject
? `${application.name} (copy)`
: application.name,
projectId: targetProject.projectId,
});
for (const domain of domains) {
@@ -423,7 +429,10 @@ export const projectRouter = createTRPCRouter({
const newPostgres = await createPostgres({
...postgres,
projectId: newProject.projectId,
name: input.duplicateInSameProject
? `${postgres.name} (copy)`
: postgres.name,
projectId: targetProject.projectId,
});
for (const mount of mounts) {
@@ -449,7 +458,10 @@ export const projectRouter = createTRPCRouter({
await findMariadbById(id);
const newMariadb = await createMariadb({
...mariadb,
projectId: newProject.projectId,
name: input.duplicateInSameProject
? `${mariadb.name} (copy)`
: mariadb.name,
projectId: targetProject.projectId,
});
for (const mount of mounts) {
@@ -475,7 +487,10 @@ export const projectRouter = createTRPCRouter({
await findMongoById(id);
const newMongo = await createMongo({
...mongo,
projectId: newProject.projectId,
name: input.duplicateInSameProject
? `${mongo.name} (copy)`
: mongo.name,
projectId: targetProject.projectId,
});
for (const mount of mounts) {
@@ -501,7 +516,10 @@ export const projectRouter = createTRPCRouter({
await findMySqlById(id);
const newMysql = await createMysql({
...mysql,
projectId: newProject.projectId,
name: input.duplicateInSameProject
? `${mysql.name} (copy)`
: mysql.name,
projectId: targetProject.projectId,
});
for (const mount of mounts) {
@@ -526,7 +544,10 @@ export const projectRouter = createTRPCRouter({
const { redisId, mounts, ...redis } = await findRedisById(id);
const newRedis = await createRedis({
...redis,
projectId: newProject.projectId,
name: input.duplicateInSameProject
? `${redis.name} (copy)`
: redis.name,
projectId: targetProject.projectId,
});
for (const mount of mounts) {
@@ -545,7 +566,10 @@ export const projectRouter = createTRPCRouter({
await findComposeById(id);
const newCompose = await createCompose({
...compose,
projectId: newProject.projectId,
name: input.duplicateInSameProject
? `${compose.name} (copy)`
: compose.name,
projectId: targetProject.projectId,
});
for (const mount of mounts) {
@@ -572,21 +596,20 @@ export const projectRouter = createTRPCRouter({
};
// Duplicate selected services
for (const service of servicesToDuplicate) {
await duplicateService(service.id, service.type);
}
}
if (ctx.user.role === "member") {
if (!input.duplicateInSameProject && ctx.user.role === "member") {
await addNewProject(
ctx.user.id,
newProject.projectId,
targetProject.projectId,
ctx.session.activeOrganizationId,
);
}
return newProject;
return targetProject;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",

View File

@@ -1,24 +1,24 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { removeJob, schedule } from "@/server/utils/backup";
import { IS_CLOUD, scheduleJob } from "@dokploy/server";
import { removeScheduleJob } from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { deployments } from "@dokploy/server/db/schema/deployment";
import {
createScheduleSchema,
schedules,
updateScheduleSchema,
} from "@dokploy/server/db/schema/schedule";
import { desc, eq } from "drizzle-orm";
import { db } from "@dokploy/server/db";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { runCommand } from "@dokploy/server/index";
import { deployments } from "@dokploy/server/db/schema/deployment";
import {
createSchedule,
deleteSchedule,
findScheduleById,
createSchedule,
updateSchedule,
} from "@dokploy/server/services/schedule";
import { IS_CLOUD, scheduleJob } from "@dokploy/server";
import { removeJob, schedule } from "@/server/utils/backup";
import { removeScheduleJob } from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { desc, eq } from "drizzle-orm";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const scheduleRouter = createTRPCRouter({
create: protectedProcedure
.input(createScheduleSchema)

View File

@@ -91,6 +91,18 @@ export const userRouter = createTRPCRouter({
return memberResult;
}),
haveRootAccess: protectedProcedure.query(async ({ ctx }) => {
if (!IS_CLOUD) {
return false;
}
if (
process.env.USER_ADMIN_ID === ctx.user.id ||
ctx.session?.impersonatedBy === process.env.USER_ADMIN_ID
) {
return true;
}
return false;
}),
getBackups: adminProcedure.query(async ({ ctx }) => {
const memberResult = await db.query.member.findFirst({
where: and(

View File

@@ -31,7 +31,9 @@ import { ZodError } from "zod";
interface CreateContextOptions {
user: (User & { role: "member" | "admin" | "owner"; ownerId: string }) | null;
session: (Session & { activeOrganizationId: string }) | null;
session:
| (Session & { activeOrganizationId: string; impersonatedBy?: string })
| null;
req: CreateNextContextOptions["req"];
res: CreateNextContextOptions["res"];
}

View File

@@ -1,9 +1,9 @@
import type http from "node:http";
import { validateRequest } from "@dokploy/server/lib/auth";
import { applyWSSHandler } from "@trpc/server/adapters/ws";
import { WebSocketServer } from "ws";
import { appRouter } from "../api/root";
import { createTRPCContext } from "../api/trpc";
import { validateRequest } from "@dokploy/server/lib/auth";
export const setupDrawerLogsWebSocketServer = (
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,

39
apps/dokploy/types/chatwoot.d.ts vendored Normal file
View File

@@ -0,0 +1,39 @@
declare global {
interface Window {
chatwootSettings?: {
hideMessageBubble?: boolean;
showUnreadMessagesDialog?: boolean;
position?: "left" | "right";
locale?: string;
useBrowserLanguage?: boolean;
type?: "standard" | "expanded_bubble";
darkMode?: "light" | "auto";
launcherTitle?: string;
showPopoutButton?: boolean;
baseDomain?: string;
};
chatwootSDK?: {
run: (config: {
websiteToken: string;
baseUrl: string;
}) => void;
};
$chatwoot?: {
setUser: (
identifier: string,
userAttributes: Record<string, any>,
) => void;
setCustomAttributes: (attributes: Record<string, any>) => void;
reset: () => void;
toggle: (state?: "open" | "close") => void;
popoutChatWindow: () => void;
toggleBubbleVisibility: (visibility: "show" | "hide") => void;
setLocale: (locale: string) => void;
setLabel: (label: string) => void;
removeLabel: (label: string) => void;
};
chatwootSDKReady?: () => void;
}
}
export {};

View File

@@ -0,0 +1,15 @@
import { useEffect, useState } from "react";
export function useDebounce<T>(value: T, delay?: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay || 500);
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
}

View File

@@ -7,11 +7,11 @@ import {
findServerById,
keepLatestNBackups,
runCommand,
runComposeBackup,
runMariadbBackup,
runMongoBackup,
runMySqlBackup,
runPostgresBackup,
runComposeBackup,
} from "@dokploy/server";
import { db } from "@dokploy/server/dist/db";
import { backups, schedules, server } from "@dokploy/server/dist/db/schema";

View File

@@ -32,7 +32,7 @@
},
"packageManager": "pnpm@9.5.0",
"engines": {
"node": "^20.9.0",
"node": "^20.16.0",
"pnpm": ">=9.5.0"
},
"lint-staged": {

View File

@@ -28,6 +28,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"pino": "9.4.0",
"pino-pretty": "11.2.2",
"micromatch": "4.0.8",
"@ai-sdk/anthropic": "^1.0.6",
"@ai-sdk/azure": "^1.0.15",
@@ -40,7 +42,7 @@
"@oslojs/encoding": "1.1.0",
"@oslojs/crypto": "1.0.1",
"drizzle-dbml-generator": "0.10.0",
"better-auth": "1.2.6",
"better-auth": "v1.2.8-beta.7",
"@faker-js/faker": "^8.4.1",
"@octokit/auth-app": "^6.0.4",
"@react-email/components": "^0.0.21",

View File

@@ -206,6 +206,7 @@ export const applications = pgTable("application", {
buildType: buildType("buildType").notNull().default("nixpacks"),
herokuVersion: text("herokuVersion").default("24"),
publishDirectory: text("publishDirectory"),
isStaticSpa: boolean("isStaticSpa"),
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
@@ -409,6 +410,7 @@ const createSchema = createInsertSchema(applications, {
]),
herokuVersion: z.string().optional(),
publishDirectory: z.string().optional(),
isStaticSpa: z.boolean().optional(),
owner: z.string(),
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
@@ -461,7 +463,7 @@ export const apiSaveBuildType = createSchema
herokuVersion: true,
})
.required()
.merge(createSchema.pick({ publishDirectory: true }));
.merge(createSchema.pick({ publishDirectory: true, isStaticSpa: true }));
export const apiSaveGithubProvider = createSchema
.pick({

View File

@@ -11,15 +11,15 @@ import {
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { generateAppName } from ".";
import { compose } from "./compose";
import { deployments } from "./deployment";
import { destinations } from "./destination";
import { mariadb } from "./mariadb";
import { mongo } from "./mongo";
import { mysql } from "./mysql";
import { postgres } from "./postgres";
import { users_temp } from "./user";
import { compose } from "./compose";
import { deployments } from "./deployment";
import { generateAppName } from ".";
export const databaseType = pgEnum("databaseType", [
"postgres",
"mariadb",

View File

@@ -3,6 +3,7 @@ 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 { backups } from "./backups";
import { bitbucket } from "./bitbucket";
import { deployments } from "./deployment";
import { domains } from "./domain";
@@ -15,7 +16,6 @@ import { server } from "./server";
import { applicationStatus, triggerType } from "./shared";
import { sshKeys } from "./ssh-key";
import { generateAppName } from "./utils";
import { backups } from "./backups";
import { schedules } from "./schedule";
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [

View File

@@ -10,11 +10,11 @@ import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { applications } from "./application";
import { backups } from "./backups";
import { compose } from "./compose";
import { previewDeployments } from "./preview-deployments";
import { server } from "./server";
import { schedules } from "./schedule";
import { backups } from "./backups";
import { server } from "./server";
export const deploymentStatus = pgEnum("deploymentStatus", [
"running",
"done",

View File

@@ -4,11 +4,11 @@ import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { applications } from "./application";
import { deployments } from "./deployment";
import { generateAppName } from "./utils";
import { compose } from "./compose";
import { deployments } from "./deployment";
import { server } from "./server";
import { users_temp } from "./user";
import { generateAppName } from "./utils";
export const shellTypes = pgEnum("shellType", ["bash", "sh"]);
export const scheduleType = pgEnum("scheduleType", [

View File

@@ -20,9 +20,9 @@ import { mongo } from "./mongo";
import { mysql } from "./mysql";
import { postgres } from "./postgres";
import { redis } from "./redis";
import { schedules } from "./schedule";
import { sshKeys } from "./ssh-key";
import { generateAppName } from "./utils";
import { schedules } from "./schedule";
export const serverStatus = pgEnum("serverStatus", ["active", "inactive"]);
export const server = pgTable("server", {

View File

@@ -11,10 +11,10 @@ import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { account, apikey, organization } from "./account";
import { projects } from "./project";
import { certificateType } from "./shared";
import { backups } from "./backups";
import { projects } from "./project";
import { schedules } from "./schedule";
import { certificateType } from "./shared";
/**
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
* database instance for multiple projects.
@@ -57,8 +57,10 @@ export const users_temp = pgTable("user_temp", {
sshPrivateKey: text("sshPrivateKey"),
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
logCleanupCron: text("logCleanupCron"),
role: text("role").notNull().default("user"),
// Metrics
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
allowImpersonation: boolean("allowImpersonation").notNull().default(false),
metricsConfig: jsonb("metricsConfig")
.$type<{
server: {
@@ -134,6 +136,8 @@ export const usersRelations = relations(users_temp, ({ one, many }) => ({
const createSchema = createInsertSchema(users_temp, {
id: z.string().min(1),
isRegistered: z.boolean().optional(),
}).omit({
role: true,
});
export const apiCreateUserInvitation = createSchema.pick({}).extend({

View File

@@ -131,3 +131,5 @@ export {
export * from "./utils/schedules/utils";
export * from "./utils/schedules/index";
export * from "./lib/logger";

View File

@@ -3,7 +3,7 @@ import * as bcrypt from "bcrypt";
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { APIError } from "better-auth/api";
import { apiKey, organization, twoFactor } from "better-auth/plugins";
import { admin, apiKey, organization, twoFactor } from "better-auth/plugins";
import { and, desc, eq } from "drizzle-orm";
import { IS_CLOUD } from "../constants";
import { db } from "../db";
@@ -187,9 +187,13 @@ const { handler, api } = betterAuth({
// required: true,
input: false,
},
allowImpersonation: {
fieldName: "allowImpersonation",
type: "boolean",
defaultValue: false,
},
},
},
plugins: [
apiKey({
enableMetadata: true,
@@ -214,6 +218,13 @@ const { handler, api } = betterAuth({
}
},
}),
...(IS_CLOUD
? [
admin({
adminUserIds: [process.env.USER_ADMIN_ID as string],
}),
]
: []),
],
});

View File

@@ -0,0 +1,11 @@
import pino from "pino";
export const logger = pino({
transport: {
target: "pino-pretty",
options: {
colorize: true,
levelFirst: false,
},
},
});

View File

@@ -24,13 +24,13 @@ import { type Compose, findComposeById, updateCompose } from "./compose";
import { type Server, findServerById } from "./server";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
import { findBackupById } from "./backup";
import {
type PreviewDeployment,
findPreviewDeploymentById,
updatePreviewDeployment,
} from "./preview-deployment";
import { findScheduleById } from "./schedule";
import { findBackupById } from "./backup";
export type Deployment = typeof deployments.$inferSelect;

View File

@@ -1,3 +1,5 @@
import dns from "node:dns";
import { promisify } from "node:util";
import { db } from "@dokploy/server/db";
import { generateRandomDomain } from "@dokploy/server/templates";
import { manageDomain } from "@dokploy/server/utils/traefik/domain";
@@ -7,8 +9,6 @@ import { type apiCreateDomain, domains } from "../db/schema";
import { findUserById } from "./admin";
import { findApplicationById } from "./application";
import { findServerById } from "./server";
import dns from "node:dns";
import { promisify } from "node:util";
export type Domain = typeof domains.$inferSelect;

View File

@@ -1,16 +1,16 @@
import { type Schedule, schedules } from "../db/schema/schedule";
import { db } from "../db";
import { eq } from "drizzle-orm";
import path from "node:path";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import type { z } from "zod";
import { paths } from "../constants";
import { db } from "../db";
import { type Schedule, schedules } from "../db/schema/schedule";
import type {
createScheduleSchema,
updateScheduleSchema,
} from "../db/schema/schedule";
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
import { paths } from "../constants";
import path from "node:path";
import { encodeBase64 } from "../utils/docker/utils";
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
export type ScheduleExtended = Awaited<ReturnType<typeof findScheduleById>>;

View File

@@ -356,20 +356,20 @@ const installUtilities = () => `
case "$OS_TYPE" in
arch)
pacman -Sy --noconfirm --needed curl wget git jq openssl >/dev/null || true
pacman -Sy --noconfirm --needed curl wget git git-lfs jq openssl >/dev/null || true
;;
alpine)
sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories
apk update >/dev/null
apk add curl wget git jq openssl sudo unzip tar >/dev/null
apk add curl wget git git-lfs jq openssl sudo unzip tar >/dev/null
;;
ubuntu | debian | raspbian)
DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null
DEBIAN_FRONTEND=noninteractive apt-get install -y unzip curl wget git jq openssl >/dev/null
DEBIAN_FRONTEND=noninteractive apt-get install -y unzip curl wget git git-lfs jq openssl >/dev/null
;;
centos | fedora | rhel | ol | rocky | almalinux | amzn)
if [ "$OS_TYPE" = "amzn" ]; then
dnf install -y wget git jq openssl >/dev/null
dnf install -y wget git git-lfs jq openssl >/dev/null
else
if ! command -v dnf >/dev/null; then
yum install -y dnf >/dev/null
@@ -377,12 +377,12 @@ const installUtilities = () => `
if ! command -v curl >/dev/null; then
dnf install -y curl >/dev/null
fi
dnf install -y wget git jq openssl unzip >/dev/null
dnf install -y wget git git-lfs jq openssl unzip >/dev/null
fi
;;
sles | opensuse-leap | opensuse-tumbleweed)
zypper refresh >/dev/null
zypper install -y curl wget git jq openssl >/dev/null
zypper install -y curl wget git git-lfs jq openssl >/dev/null
;;
*)
echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now."
@@ -577,7 +577,7 @@ const installNixpacks = () => `
if command_exists nixpacks; then
echo "Nixpacks already installed ✅"
else
export NIXPACKS_VERSION=1.35.0
export NIXPACKS_VERSION=1.39.0
bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
echo "Nixpacks version $NIXPACKS_VERSION installed ✅"
fi
@@ -587,7 +587,7 @@ const installRailpack = () => `
if command_exists railpack; then
echo "Railpack already installed ✅"
else
export RAILPACK_VERSION=0.0.37
export RAILPACK_VERSION=0.0.64
bash -c "$(curl -fsSL https://railpack.com/install.sh)"
echo "Railpack version $RAILPACK_VERSION installed ✅"
fi

View File

@@ -1,4 +1,4 @@
import { randomBytes, createHmac } from "node:crypto";
import { createHmac, randomBytes } from "node:crypto";
import { existsSync } from "node:fs";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
@@ -35,7 +35,7 @@ export const generateRandomDomain = ({
projectName,
}: Schema): string => {
const hash = randomBytes(3).toString("hex");
const slugIp = serverIp.replaceAll(".", "-");
const slugIp = serverIp.replaceAll(".", "-").replaceAll(":", "-");
return `${projectName}-${hash}${slugIp === "" ? "" : `-${slugIp}`}.traefik.me`;
};

View File

@@ -1,13 +1,13 @@
import type { BackupSchedule } from "@dokploy/server/services/backup";
import type { Compose } from "@dokploy/server/services/compose";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getS3Credentials, normalizeS3Path, getBackupCommand } from "./utils";
import {
createDeploymentBackup,
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import { findProjectById } from "@dokploy/server/services/project";
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getBackupCommand, getS3Credentials, normalizeS3Path } from "./utils";
export const runComposeBackup = async (
compose: Compose,
@@ -38,7 +38,9 @@ export const runComposeBackup = async (
if (compose.serverId) {
await execAsyncRemote(compose.serverId, backupCommand);
} else {
await execAsync(backupCommand);
await execAsync(backupCommand, {
shell: "/bin/bash",
});
}
await sendDatabaseBackupNotifications({

View File

@@ -11,10 +11,10 @@ import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup"
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { getS3Credentials, scheduleBackup } from "./utils";
import type { BackupSchedule } from "@dokploy/server/services/backup";
import { startLogCleanup } from "../access-log/handler";
import { member } from "@dokploy/server/db/schema";
import type { BackupSchedule } from "@dokploy/server/services/backup";
import { eq } from "drizzle-orm";
import { startLogCleanup } from "../access-log/handler";
export const initCronJobs = async () => {
console.log("Setting up cron jobs....");

Some files were not shown because too many files have changed in this diff Show More