Compare commits

...

128 Commits

Author SHA1 Message Date
Mauricio Siu
2a89be6efc Merge pull request #2069 from Dokploy/2065-rollback-feature-dns-issues
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(rollbacks): enhance fullContext type and refactor createRollback…
2025-06-22 18:01:21 +02:00
autofix-ci[bot]
412bb9e874 [autofix.ci] apply automated fixes 2025-06-22 16:00:36 +00:00
Mauricio Siu
6290c217f1 feat(rollbacks): add alert for storage usage in rollback settings
- Introduced an AlertBlock component to inform users about increased storage usage when rollbacks are enabled.
- Added cautionary note regarding the potential deletion of rollback images during manual cache cleaning.
2025-06-22 10:00:14 -06:00
Mauricio Siu
4babdd45ea chore: update version in package.json to v0.23.3 2025-06-22 09:58:35 -06:00
Mauricio Siu
24bff96898 feat(rollbacks): enhance fullContext type and refactor createRollback logic
- Updated fullContext type in rollbacks schema to include Application and Project types.
- Refactored createRollback function to separate fullContext from input and handle it more efficiently.
- Integrated environment variable preparation into the rollback process.
2025-06-22 09:56:36 -06:00
Mauricio Siu
892f272108 Merge pull request #2066 from nikolajjsj/feat/reset-2fa-script
Feat/reset 2fa script
2025-06-22 16:40:47 +02:00
Mauricio Siu
fca537ee40 feat(esbuild): add entry point for reset-2fa script 2025-06-22 08:40:13 -06:00
Mauricio Siu
ae24aa8be5 Merge pull request #2067 from Dokploy/fix/envs-not-reset
fix: simplify useEffect condition in ShowEnvironment component
2025-06-22 16:32:54 +02:00
Mauricio Siu
b74d3995ee chore: update version in package.json to v0.23.2 2025-06-22 08:32:20 -06:00
Mauricio Siu
f7fd77f7e9 fix: simplify useEffect condition in ShowEnvironment component 2025-06-22 08:31:46 -06:00
nikolajjsj
db8a4e6edf feat(scripts): add command to run reset-2fa script 2025-06-22 15:11:12 +02:00
nikolajjsj
fa16cfec2a feat(scripts): add script to reset 2fa for admin
Similar style to existing reset-password script
2025-06-22 15:10:57 +02:00
Mauricio Siu
f35d084dd4 chore: update version in package.json to v0.23.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
2025-06-22 00:58:00 -06:00
Mauricio Siu
274daf52c0 Merge pull request #2062 from Dokploy/fix/migration-git-permissions
refactor(git_provider): update userId assignment to use owner_id from…
2025-06-22 08:57:10 +02:00
Mauricio Siu
da52d767eb refactor(git_provider): update userId assignment to use owner_id from organization table
- Changed the SQL update statement to directly select the owner_id from the organization table instead of joining with the account table, simplifying the query.
2025-06-22 00:52:45 -06:00
Mauricio Siu
45a178e705 chore: update version in package.json to v0.23.0 2025-06-21 23:58:27 -06:00
Mauricio Siu
ebf9db7cc0 Merge pull request #2037 from Marukome0743/sort
chore: alphabetize the package.json dependencies
2025-06-22 07:50:55 +02:00
Mauricio Siu
ec6c685a28 Merge pull request #2019 from zuohuadong/canary
chore(server-setup.ts) support opencloudos
2025-06-22 07:37:22 +02:00
Mauricio Siu
7b14e4c5d2 Merge pull request #1986 from Dokploy/319-ability-to-roll-back-service-depoyments
Ability to roll back service deployments
2025-06-22 07:35:36 +02:00
Mauricio Siu
316f592e09 refactor(rollback): clean up unused code in rollback router
- Removed commented-out code and unused imports from the rollback router file to streamline the codebase and improve readability.
2025-06-21 23:35:11 -06:00
Mauricio Siu
bd82199ae0 feat(rollback): implement rollback creation in deployment process
- Added logic to create a rollback entry if the application has an active rollback during the deployment process.
- Enhanced the rollback handling by determining the appropriate tag image based on the application's source type (docker or app name).
2025-06-21 23:24:53 -06:00
Mauricio Siu
89d573a2f5 refactor: remove ShowEnv component from rollbacks
- Deleted the ShowEnv component responsible for displaying environment variables in the rollback context, streamlining the codebase.
2025-06-21 23:18:06 -06:00
Mauricio Siu
3d285ca437 feat(rollback): add rollback constraints and snapshots
- Introduced two new SQL files for rollback constraints, updating foreign key relationships with different delete actions (set null and cascade).
- Updated the journal and snapshot files to include the new rollback schema changes for versions 0096 and 0097.
- Enhanced the application service to handle rollback image tagging based on source type.
- Implemented rollback removal logic in the deployment service to ensure proper cleanup of rollback entries.
2025-06-21 23:17:21 -06:00
Mauricio Siu
8c5e34c528 refactor: remove limitRollback property from rollback settings schema
- Eliminated the limitRollback property from the form schema in show-rollback-settings.tsx to simplify the rollback configuration.
2025-06-21 21:53:14 -06:00
Mauricio Siu
98199e65bf refactor: remove limitRollback property and add rollback table schema
- Removed the limitRollback property from the baseApp configuration in drop.test.test.ts and traefik.test.ts files.
- Introduced a new SQL file to create a rollback table with relevant fields and constraints.
- Updated the journal and snapshot files to reflect the new rollback schema changes.
2025-06-21 21:21:29 -06:00
Mauricio Siu
bf1026af7a Merge branch 'canary' into 319-ability-to-roll-back-service-depoyments 2025-06-21 21:18:05 -06:00
Mauricio Siu
7c9767d90f chore: remove rollback-related SQL files and snapshots
- Deleted SQL files for the "rollback" table and related schema changes, including the "funny_leper_queen", "true_marvel_zombies", and "sweet_venom" migrations.
- Removed corresponding snapshot files to clean up the database schema history.
2025-06-21 21:17:54 -06:00
Mauricio Siu
688f6478f1 Merge pull request #1981 from ayham291/canary
feat: Git Provider Permissions
2025-06-22 05:16:11 +02:00
Mauricio Siu
cad17e0f7f fix(certificates): improve ASN.1 time parsing and handle edge cases
- Added TypeScript ignore directive to suppress type checking in the utility file.
- Refactored the time parsing logic to use Number.parseInt for better clarity.
- Adjusted the flow to throw an error for invalid ASN.1 time formats, ensuring robustness in certificate expiration date extraction.
2025-06-21 21:08:49 -06:00
Mauricio Siu
d97461d820 refactor(git-provider): update UnauthorizedGitProvider to use service prop and enhance access handling
- Changed the prop name from 'application' to 'service' in the UnauthorizedGitProvider component for clarity.
- Updated the logic to check for unauthorized access to the git provider in the compose router, returning a new field 'hasGitProviderAccess'.
- Implemented a disconnect functionality for git providers in the ShowProviderFormCompose component, providing user feedback on success or failure.
2025-06-21 21:03:31 -06:00
Mauricio Siu
9686848090 feat(git-provider): add userId column to git_provider table and update relationships
- Introduced a new userId column in the git_provider table to associate git providers with users.
- Updated the foreign key reference for userId to point to the users_temp table instead of the account table.
- Modified the UnauthorizedGitProvider component to include a dialog action for disconnecting repositories, enhancing user experience.
- Added a migration script to update existing git providers with the new userId values based on the organization owner.
2025-06-21 20:50:07 -06:00
Mauricio Siu
a7b644e403 Merge branch 'canary' into ayham291/canary 2025-06-21 20:21:41 -06:00
Mauricio Siu
96b4c334da remove: delete migration script and associated journal entries for 0093_elite_warlock
This commit removes the migration script for adding a userId column to the git_provider table, along with its corresponding journal entries. The migration was deemed unnecessary following recent changes to the handling of existing git providers.
2025-06-21 20:21:24 -06:00
Mauricio Siu
1b99c3ac23 Merge pull request #2059 from tarikyalcinkaya/fix/valid-name-regex
fix: allow dot character in project name validation (#2042)
2025-06-22 04:18:40 +02:00
Mauricio Siu
a12b514525 Merge pull request #2060 from Dokploy/2043-running-manual-backup-on-service-does-not-remove-outdated-backups-over-keep-latest
feat(backup): implement keepLatestNBackups function to manage backup …
2025-06-22 04:16:51 +02:00
Mauricio Siu
ea91b01461 feat(backup): implement keepLatestNBackups function to manage backup retention
- Added keepLatestNBackups function calls after each backup operation for Postgres, MySQL, MariaDB, Compose, and MongoDB to ensure only the latest N backups are retained.
2025-06-21 20:16:27 -06:00
Tarık Yalçınkaya
149b8f70d8 fix: allow dot character in project name validation (#2042) 2025-06-22 04:09:21 +03:00
Mauricio Siu
6be4984649 Merge pull request #2050 from dsincl12/canary
Fix typo: Clonning → Cloning
2025-06-22 01:55:10 +02:00
Mauricio Siu
7ec68e688b Merge pull request #2025 from onurguzel/fix-cert-expiration-date
fix: parse pem certificates correctly
2025-06-22 01:54:52 +02:00
autofix-ci[bot]
b30f8944c4 [autofix.ci] apply automated fixes 2025-06-21 23:53:09 +00:00
Mauricio Siu
f0d242b9b9 Merge pull request #2058 from Dokploy/2016-compose-and-environnement-variable-tab-keeps-resetting-themself
fix: update form reset conditions in environment and compose file edi…
2025-06-22 00:29:49 +02:00
Mauricio Siu
b6d86b4732 fix: update form reset conditions in environment and compose file editors
- Modified the reset logic in ShowEnvironment to only reset when there are no changes.
- Adjusted the reset condition in ComposeFileEditor to check if composeFile is empty before resetting.
- Cleaned up the query in the compose service page by removing unnecessary refetchInterval.
2025-06-21 16:27:58 -06:00
Mauricio Siu
304134cdda Merge pull request #2056 from Dokploy/1834-user-invite-email-not-sending
feat(invitation): add email provider selection and notification handl…
2025-06-21 21:10:38 +02:00
Mauricio Siu
c84b271511 feat(invitation): add email provider selection and notification handling for user invitations
- Introduced a new optional field for notificationId in the invitation form.
- Implemented fetching of email providers based on the active organization.
- Enhanced invitation sending logic to include email notifications when applicable.
- Updated UI to conditionally display email provider selection based on cloud status.
2025-06-21 13:08:49 -06:00
David Sinclair
96dd8d37a5 Fix typo: Clonning → Cloning 2025-06-20 11:30:38 +02:00
Mauricio Siu
be91b53c86 Merge pull request #2049 from Dokploy/1977-volumes-cant-be-edited
fix: update FormItem styles for better layout in UpdateVolume component
2025-06-20 08:41:15 +02:00
Mauricio Siu
98c77d539e fix: update FormItem styles for better layout in UpdateVolume component 2025-06-20 00:40:56 -06:00
Mauricio Siu
67f5befa48 Merge pull request #2007 from victorboudet/canary
fix api: return compose informations when created from template
2025-06-20 08:16:02 +02:00
Mauricio Siu
5b2056101f Merge pull request #1984 from TorstenDittmann/fix-ip-validation-behind-bunny-fastly
fix[domains]: Add CDN provider detection with dynamic display names
2025-06-20 08:15:39 +02:00
Mauricio Siu
000b4ba49e Merge pull request #2048 from Dokploy/1970-deploy-crashes-when-opening-requests
feat(database): set default value for logCleanupCron and update exist…
2025-06-20 07:44:22 +02:00
Mauricio Siu
4efa56aae5 Merge pull request #2034 from Marukome0743/syntax
build: add syntax directive to Dockerfiles
2025-06-20 07:37:27 +02:00
Mauricio Siu
a788a73fa3 feat(database): set default value for logCleanupCron and update existing records
- Added SQL script to set default value for "logCleanupCron" in "user_temp" table.
- Updated existing records with NULL "logCleanupCron" to the new default value.
- Updated user schema to reflect the default value for "logCleanupCron".
- Enhanced log cleanup functionality with error handling and logging.
2025-06-19 23:32:49 -06:00
Marukome0743
319ca6944d chore: sort the package.json dependencies 2025-06-16 13:06:44 +09:00
Marukome0743
238736db8d build: add syntax directive to Dockerfiles 2025-06-13 11:11:48 +09:00
Onur Güzel
556a437251 fix: parse pem certificates correctly 2025-06-10 16:10:19 +02:00
huadong zuo
ef5e1d6818 chore(server-setup.ts) support opencloudos 2025-06-09 16:10:48 +08:00
Mauricio Siu
1089a8247d refactor(auth): remove logger configuration for production environment
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-06-08 16:56:31 -06:00
Mauricio Siu
ef0cef99a1 refactor: remove limitRollback from application settings and related UI components
- Eliminated the "limitRollback" property from the application schema and the ShowRollbackSettings component, streamlining rollback configuration.
- Updated the database schema to drop the "limitRollback" column from the "application" table, ensuring consistency across the application.
2025-06-08 16:53:23 -06:00
Victor Boudet
8737dc86c9 fix api: return compose informations when created from template 2025-06-05 14:24:43 +00:00
Mauricio Siu
cf06e5369a fix: update docker system prune command to remove unnecessary 'all' flag
- Modified the command in the cleanUpSystemPrune function to remove the '--all' flag, streamlining the Docker system prune operation.
2025-06-03 00:13:00 -06:00
Mauricio Siu
973de2a610 feat: add rollback configuration to base application settings
- Introduced "limitRollback" and "rollbackActive" properties to the base application configuration in both drop and traefik test files.
- These additions enhance the rollback functionality by allowing configuration of rollback limits and activation status.
2025-06-02 21:04:45 -06:00
Mauricio Siu
f8baf6fe41 feat: add fullContext column to rollback table and update related functionality
- Introduced a new "fullContext" JSONB column in the "rollback" table to store additional context for rollbacks.
- Removed the "env" column from the "rollback" table to streamline data management.
- Updated the rollbacks service to handle the new "fullContext" field during rollback creation.
- Adjusted the application service to eliminate environment variable handling in rollback operations.
2025-06-02 21:02:17 -06:00
ayham291
3e05be4513 fix(migration): handle existing git providers by assigning to org owner
Previously the migration would fail in production when trying to add
a NOT NULL userId column to git_provider table with existing data.
Now existing providers are automatically assigned to their organization owner.
2025-06-02 15:42:56 +02:00
ayham291
b3b009761a fix: made an opsie.. check the organization as well as the user for getAll git providers 2025-06-02 14:45:10 +02:00
autofix-ci[bot]
a659594134 [autofix.ci] apply automated fixes 2025-06-02 10:07:40 +00:00
Torsten Dittmann
9a1f0b467d fix: domain validation message display logic
Check for both message and cdnProvider before showing CDN status to
prevent displaying "Behind undefined" when cdnProvider is missing.
2025-06-02 12:07:18 +02:00
Torsten Dittmann
e8b3abb7c9 fix: Add validation for CIDR format in isIPInCIDR function 2025-06-02 12:03:14 +02:00
ayham291
8215d2e79f feat: implement unauthorized Git provider handling and disconnect functionality
- Added UnauthorizedGitProvider component to display information for applications connected to unauthorized Git providers.
- Implemented disconnectGitProvider mutation to allow users to disconnect from their Git provider, with success and error notifications.
- Updated application query to include access checks for Git providers, ensuring users can only interact with their authorized repositories.
2025-06-02 11:32:43 +02:00
Mauricio Siu
9c19b1efa3 Create SECURITY.md
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-06-02 01:09:48 -06:00
Mauricio Siu
4966bbeb73 refactor: update icon in ShowDeployments component
- Replaced the ArrowDownToLine icon with RefreshCcw in the rollback button for improved clarity.
- Cleaned up unused imports from the component to streamline the code.
2025-06-01 22:56:18 -06:00
Mauricio Siu
df97dc0179 refactor: update ShowDeployments component and remove ShowRollbacks
- Enhanced the ShowDeployments component to conditionally display rollback options based on deployment status and type.
- Removed the ShowRollbacks component and its references from the application, streamlining the UI and functionality.
2025-06-01 22:56:00 -06:00
Mauricio Siu
b14b9300c0 feat: enhance rollback functionality with UI updates and database schema changes
- Updated Tailwind configuration for responsive design.
- Modified the ShowDeployments component to include rollback settings and actions.
- Introduced a new "rollback" table in the database schema with foreign key relationships.
- Updated deployment and application schemas to support rollback features.
- Added rollback mutation to the API for initiating rollbacks.
2025-06-01 22:52:33 -06:00
Mauricio Siu
a7d1fabd81 feat: add rollback functionality with new table and application schema updates
- Created a new "rollback" table to manage rollback operations.
- Added "rollbackActive" and "limitRollback" columns to the "application" table to support rollback features.
- Established foreign key constraints between the "rollback" and "application" tables for data integrity.
2025-06-01 19:45:33 -06:00
Mauricio Siu
d171e3da91 Merge branch 'canary' into 319-ability-to-roll-back-service-depoyments 2025-06-01 19:44:02 -06:00
Mauricio Siu
2c77029dad chore: remove rollback-related SQL files and snapshots
- Deleted SQL files for rollback table and related schema changes.
- Removed corresponding snapshot files to clean up the database schema history.
2025-06-01 19:43:48 -06:00
autofix-ci[bot]
030e482fce [autofix.ci] apply automated fixes 2025-06-02 00:15:31 +00:00
Mauricio Siu
e53c67f0d9 Merge pull request #1983 from TorstenDittmann/fix-railpack-env-vars
fix[railpack]: env parsing and update railpack to v0.0.66
2025-06-01 18:13:11 -06:00
Mauricio Siu
0c12d967e2 Update Dockerfile 2025-06-01 18:12:57 -06:00
Mauricio Siu
98aabd7bd8 Merge pull request #1930 from nktnet1/fix-traefik-failing-silently
fix: throw error if traefik container creation fails for a reason other than port taken
2025-06-01 15:42:00 -06:00
Torsten Dittmann
88e862544b fix[domains]: Add CDN provider detection with dynamic display names
Implements generic CDN detection service supporting Cloudflare, Fastly,
and Bunny CDN. Replaces hardcoded "Behind Cloudflare" text with
dynamic provider names and adds IP range validation for comprehensive
CDN detection.
2025-06-01 23:03:00 +02:00
Torsten Dittmann
7f9c19bc11 fix[railpack]: environment variable validation for empty strings
Allow empty string values to be processed as valid environment
variables by checking for existence rather than non-empty length.
2025-06-01 22:22:16 +02:00
Torsten Dittmann
9535276fe6 fix[railpack]: env parsing and update railpack to v0.0.66
Improve environment variable parsing to handle values containing equals
signs by extracting a dedicated parseEnvironmentKeyValuePair function
and updating Railpack secret formatting.
2025-06-01 22:03:16 +02:00
ayham291
56d21aff60 fix: add authorization checks in GitHub router to include userId validation
- Updated conditional checks to ensure that the GitHub provider's userId matches the session userId, in addition to the organizationId, for improved security and access control.
2025-06-01 20:53:54 +02:00
ayham291
8436d364be refactor: linter fixes 2025-06-01 20:46:32 +02:00
ayham291
5d5e56d144 feat: GitHub and GitLab provider integration with user association
- Added userId to the GitHub and GitLab provider setup to associate providers with the user who created them.
- Updated redirect URL in GitHub provider to include userId for better tracking.
- Modified API handlers and service functions to accommodate userId in provider creation and validation.
2025-06-01 20:45:29 +02:00
ayham291
0627b6fd3a refactor: clean up conditional checks in Bitbucket and Gitea routers for improved readability 2025-05-31 01:52:24 +02:00
ayham291
39af44daef feat: add user property to git-providers (bitbucket, gitea)
- relate a provider to the user who created it.
- for now the provider is only visible to its user.
2025-05-31 01:21:46 +02:00
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
Khiet Tam Nguyen
2fa2e76e2e fix: throw error if traefik container creation fails for a reason other than port 2025-05-20 15:28:45 +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
2ad8bf355b feat: implement rollback functionality with UI components and database schema updates
- Added ShowEnv and ShowRollbackSettings components for displaying and configuring rollback settings.
- Implemented ShowRollbacks component to list and manage rollbacks for applications.
- Created rollback database schema and updated application schema to include rollback settings.
- Added API routes for managing rollbacks, including fetching, creating, and deleting rollbacks.
- Integrated rollback functionality into the application deployment process.
2025-05-10 20:28:34 -06:00
148 changed files with 40127 additions and 4039 deletions

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

@@ -1,3 +1,4 @@
# syntax=docker/dockerfile:1
FROM node:20.9-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
@@ -29,7 +30,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

View File

@@ -1,3 +1,4 @@
# syntax=docker/dockerfile:1
FROM node:20.9-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"

View File

@@ -1,3 +1,4 @@
# syntax=docker/dockerfile:1
# Build stage
FROM golang:1.21-alpine3.19 AS builder

View File

@@ -1,3 +1,4 @@
# syntax=docker/dockerfile:1
FROM node:20.9-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"

View File

@@ -1,3 +1,4 @@
# syntax=docker/dockerfile:1
FROM node:20.9-slim AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"

View File

@@ -148,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.

28
SECURITY.md Normal file
View File

@@ -0,0 +1,28 @@
# Dokploy Security Policy
At Dokploy, security is a top priority. We appreciate the help of security researchers and the community in identifying and reporting vulnerabilities.
## How to Report a Vulnerability
If you have discovered a security vulnerability in Dokploy, we ask that you report it responsibly by following these guidelines:
1. **Contact us:** Send an email to [contact@dokploy.com](mailto:contact@dokploy.com).
2. **Provide clear details:** Include as much information as possible to help us understand and reproduce the vulnerability. This should include:
* A clear description of the vulnerability.
* Steps to reproduce the vulnerability.
* Any sample code, screenshots, or videos that might be helpful.
* The potential impact of the vulnerability.
3. **Do not make the vulnerability public:** Please refrain from publicly disclosing the vulnerability until we have had the opportunity to investigate and address it. This is crucial for protecting our users.
4. **Allow us time:** We will endeavor to acknowledge receipt of your report as soon as possible and keep you informed of our progress. The time to resolve the vulnerability may vary depending on its complexity and severity.
## What We Expect From You
* Do not access user data or systems beyond what is necessary to demonstrate the vulnerability.
* Do not perform denial-of-service (DoS) attacks, spamming, or social engineering.
* Do not modify or destroy data that does not belong to you.
## Our Commitment
We are committed to working with you quickly and responsibly to address any legitimate security vulnerability.
Thank you for helping us keep Dokploy secure for everyone.

View File

@@ -9,25 +9,25 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"pino": "9.4.0",
"pino-pretty": "11.2.2",
"@hono/zod-validator": "0.3.0",
"zod": "^3.23.4",
"react": "18.2.0",
"react-dom": "18.2.0",
"@dokploy/server": "workspace:*",
"@hono/node-server": "^1.12.1",
"hono": "^4.5.8",
"@hono/zod-validator": "0.3.0",
"@nerimity/mimiqueue": "1.2.3",
"dotenv": "^16.3.1",
"hono": "^4.5.8",
"pino": "9.4.0",
"pino-pretty": "11.2.2",
"react": "18.2.0",
"react-dom": "18.2.0",
"redis": "4.7.0",
"@nerimity/mimiqueue": "1.2.3"
"zod": "^3.23.4"
},
"devDependencies": {
"typescript": "^5.4.2",
"@types/node": "^20.11.17",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@types/node": "^20.11.17",
"tsx": "^4.7.1"
"tsx": "^4.7.1",
"typescript": "^5.4.2"
},
"packageManager": "pnpm@9.5.0"
}

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 git-lfs && git lfs install && 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,
@@ -120,6 +121,7 @@ const baseApp: ApplicationNested = {
updateConfigSwarm: null,
username: null,
dockerContextPath: null,
rollbackActive: false,
};
describe("unzipDrop using real zip files", () => {
@@ -149,67 +151,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

@@ -5,6 +5,7 @@ import { createRouterConfig } from "@dokploy/server";
import { expect, test } from "vitest";
const baseApp: ApplicationNested = {
rollbackActive: false,
applicationId: "",
herokuVersion: "",
giteaRepository: "",
@@ -85,6 +86,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

@@ -247,7 +247,7 @@ export const UpdateVolume = ({
control={form.control}
name="content"
render={({ field }) => (
<FormItem>
<FormItem className="max-w-full max-w-[45rem]">
<FormLabel>Content</FormLabel>
<FormControl>
<FormControl>
@@ -256,7 +256,7 @@ export const UpdateVolume = ({
placeholder={`NODE_ENV=production
PORT=3000
`}
className="h-96 font-mono"
className="h-96 font-mono w-full"
{...field}
/>
</FormControl>

View File

@@ -2,6 +2,7 @@ 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,
@@ -63,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),
}),
]);
@@ -83,6 +85,7 @@ interface ApplicationData {
dockerBuildStage?: string | null;
herokuVersion?: string | null;
publishDirectory?: string | null;
isStaticSpa?: boolean | null;
}
function isValidBuildType(value: string): value is BuildType {
@@ -115,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;
}
}
};
@@ -174,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");
@@ -364,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,14 @@ import {
CardTitle,
} from "@/components/ui/card";
import { type RouterOutputs, api } from "@/utils/api";
import { RocketIcon, Clock, Loader2 } from "lucide-react";
import { Clock, Loader2, RocketIcon, Settings, RefreshCcw } 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";
import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings";
import { DialogAction } from "@/components/shared/dialog-action";
import { toast } from "sonner";
interface Props {
id: string;
@@ -57,6 +60,9 @@ export const ShowDeployments = ({
},
);
const { mutateAsync: rollback, isLoading: isRollingBack } =
api.rollback.rollback.useMutation();
const [url, setUrl] = React.useState("");
useEffect(() => {
setUrl(document.location.origin);
@@ -71,9 +77,18 @@ export const ShowDeployments = ({
See all the 10 last deployments for this {type}
</CardDescription>
</div>
{(type === "application" || type === "compose") && (
<CancelQueues id={id} type={type} />
)}
<div className="flex flex-row items-center gap-2">
{(type === "application" || type === "compose") && (
<CancelQueues id={id} type={type} />
)}
{type === "application" && (
<ShowRollbackSettings applicationId={id}>
<Button variant="outline">
Configure Rollbacks <Settings className="size-4" />
</Button>
</ShowRollbackSettings>
)}
</div>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{refreshToken && (
@@ -154,13 +169,47 @@ export const ShowDeployments = ({
)}
</div>
<Button
onClick={() => {
setActiveLog(deployment);
}}
>
View
</Button>
<div className="flex flex-row items-center gap-2">
<Button
onClick={() => {
setActiveLog(deployment);
}}
>
View
</Button>
{deployment?.rollback &&
deployment.status === "done" &&
type === "application" && (
<DialogAction
title="Rollback to this deployment"
description="Are you sure you want to rollback to this deployment?"
type="default"
onClick={async () => {
await rollback({
rollbackId: deployment.rollback.rollbackId,
})
.then(() => {
toast.success(
"Rollback initiated successfully",
);
})
.catch(() => {
toast.error("Error initiating rollback");
});
}}
>
<Button
variant="secondary"
size="sm"
isLoading={isRollingBack}
>
<RefreshCcw className="size-4 text-primary group-hover:text-red-500" />
Rollback
</Button>
</DialogAction>
)}
</div>
</div>
</div>
))}

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;
@@ -39,6 +39,7 @@ export type ValidationState = {
error?: string;
resolvedIp?: string;
message?: string;
cdnProvider?: string;
};
export type ValidationStates = Record<string, ValidationState>;
@@ -119,6 +120,7 @@ export const ShowDomains = ({ id, type }: Props) => {
isValid: result.isValid,
error: result.error,
resolvedIp: result.resolvedIp,
cdnProvider: result.cdnProvider,
message: result.error && result.isValid ? result.error : undefined,
},
}));
@@ -354,8 +356,9 @@ export const ShowDomains = ({ id, type }: Props) => {
) : validationState?.isValid ? (
<>
<CheckCircle2 className="size-3 mr-1" />
{validationState.message
? "Behind Cloudflare"
{validationState.message &&
validationState.cdnProvider
? `Behind ${validationState.cdnProvider}`
: "DNS Valid"}
</>
) : validationState?.error ? (

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";

View File

@@ -16,9 +16,11 @@ import { api } from "@/utils/api";
import { GitBranch, Loader2, UploadCloud } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { toast } from "sonner";
import { SaveBitbucketProvider } from "./save-bitbucket-provider";
import { SaveDragNDrop } from "./save-drag-n-drop";
import { SaveGitlabProvider } from "./save-gitlab-provider";
import { UnauthorizedGitProvider } from "./unauthorized-git-provider";
type TabState =
| "github"
@@ -43,12 +45,31 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
const { data: giteaProviders, isLoading: isLoadingGitea } =
api.gitea.giteaProviders.useQuery();
const { data: application } = api.application.one.useQuery({ applicationId });
const { data: application, refetch } = api.application.one.useQuery({
applicationId,
});
const { mutateAsync: disconnectGitProvider } =
api.application.disconnectGitProvider.useMutation();
const [tab, setSab] = useState<TabState>(application?.sourceType || "github");
const isLoading =
isLoadingGithub || isLoadingGitlab || isLoadingBitbucket || isLoadingGitea;
const handleDisconnect = async () => {
try {
await disconnectGitProvider({ applicationId });
toast.success("Repository disconnected successfully");
await refetch();
} catch (error) {
toast.error(
`Failed to disconnect repository: ${
error instanceof Error ? error.message : "Unknown error"
}`,
);
}
};
if (isLoading) {
return (
<Card className="group relative w-full bg-transparent">
@@ -77,6 +98,38 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
);
}
// Check if user doesn't have access to the current git provider
if (
application &&
!application.hasGitProviderAccess &&
application.sourceType !== "docker" &&
application.sourceType !== "drop"
) {
return (
<Card className="group relative w-full bg-transparent">
<CardHeader>
<CardTitle className="flex items-start justify-between">
<div className="flex flex-col gap-2">
<span className="flex flex-col space-y-0.5">Provider</span>
<p className="flex items-center text-sm font-normal text-muted-foreground">
Repository connection through unauthorized provider
</p>
</div>
<div className="hidden space-y-1 text-sm font-normal md:block">
<GitBranch className="size-6 text-muted-foreground" />
</div>
</CardTitle>
</CardHeader>
<CardContent>
<UnauthorizedGitProvider
service={application}
onDisconnect={handleDisconnect}
/>
</CardContent>
</Card>
);
}
return (
<Card className="group relative w-full bg-transparent">
<CardHeader>

View File

@@ -0,0 +1,149 @@
import {
BitbucketIcon,
GitIcon,
GiteaIcon,
GithubIcon,
GitlabIcon,
} from "@/components/icons/data-tools-icons";
import { DialogAction } from "@/components/shared/dialog-action";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import type { RouterOutputs } from "@/utils/api";
import { AlertCircle, GitBranch, Unlink } from "lucide-react";
interface Props {
service:
| RouterOutputs["application"]["one"]
| RouterOutputs["compose"]["one"];
onDisconnect: () => void;
}
export const UnauthorizedGitProvider = ({ service, onDisconnect }: Props) => {
const getProviderIcon = (sourceType: string) => {
switch (sourceType) {
case "github":
return <GithubIcon className="size-5 text-muted-foreground" />;
case "gitlab":
return <GitlabIcon className="size-5 text-muted-foreground" />;
case "bitbucket":
return <BitbucketIcon className="size-5 text-muted-foreground" />;
case "gitea":
return <GiteaIcon className="size-5 text-muted-foreground" />;
case "git":
return <GitIcon className="size-5 text-muted-foreground" />;
default:
return <GitBranch className="size-5 text-muted-foreground" />;
}
};
const getRepositoryInfo = () => {
switch (service.sourceType) {
case "github":
return {
repo: service.repository,
branch: service.branch,
owner: service.owner,
};
case "gitlab":
return {
repo: service.gitlabRepository,
branch: service.gitlabBranch,
owner: service.gitlabOwner,
};
case "bitbucket":
return {
repo: service.bitbucketRepository,
branch: service.bitbucketBranch,
owner: service.bitbucketOwner,
};
case "gitea":
return {
repo: service.giteaRepository,
branch: service.giteaBranch,
owner: service.giteaOwner,
};
case "git":
return {
repo: service.customGitUrl,
branch: service.customGitBranch,
owner: null,
};
default:
return { repo: null, branch: null, owner: null };
}
};
const { repo, branch, owner } = getRepositoryInfo();
return (
<div className="space-y-4">
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
This application is connected to a {service.sourceType} repository
through a git provider that you don't have access to. You can see
basic repository information below, but cannot modify the
configuration.
</AlertDescription>
</Alert>
<Card className="border-dashed border-2 border-muted-foreground/20 bg-transparent">
<CardHeader>
<CardTitle className="flex items-center gap-2">
{getProviderIcon(service.sourceType)}
<span className="capitalize text-sm font-medium">
{service.sourceType} Repository
</span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{owner && (
<div>
<span className="text-sm font-medium text-muted-foreground">
Owner:
</span>
<p className="text-sm">{owner}</p>
</div>
)}
{repo && (
<div>
<span className="text-sm font-medium text-muted-foreground">
Repository:
</span>
<p className="text-sm">{repo}</p>
</div>
)}
{branch && (
<div>
<span className="text-sm font-medium text-muted-foreground">
Branch:
</span>
<p className="text-sm">{branch}</p>
</div>
)}
<div className="pt-4 border-t">
<DialogAction
title="Disconnect Repository"
description="Are you sure you want to disconnect this repository?"
type="default"
onClick={async () => {
onDisconnect();
}}
>
<Button variant="secondary" className="w-full">
<Unlink className="size-4 mr-2" />
Disconnect Repository
</Button>
</DialogAction>
<p className="text-xs text-muted-foreground mt-2">
Disconnecting will allow you to configure a new repository with
your own git providers.
</p>
</div>
</CardContent>
</Card>
</div>
);
};

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

@@ -0,0 +1,123 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
} from "@/components/ui/form";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const formSchema = z.object({
rollbackActive: z.boolean(),
});
type FormValues = z.infer<typeof formSchema>;
interface Props {
applicationId: string;
children?: React.ReactNode;
}
export const ShowRollbackSettings = ({ applicationId, children }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { data: application, refetch } = api.application.one.useQuery(
{
applicationId,
},
{
enabled: !!applicationId,
},
);
const { mutateAsync: updateApplication, isLoading } =
api.application.update.useMutation();
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
rollbackActive: application?.rollbackActive ?? false,
},
});
const onSubmit = async (data: FormValues) => {
await updateApplication({
applicationId,
rollbackActive: data.rollbackActive,
})
.then(() => {
toast.success("Rollback settings updated");
setIsOpen(false);
refetch();
})
.catch(() => {
toast.error("Failed to update rollback settings");
});
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Rollback Settings</DialogTitle>
<DialogDescription>
Configure how rollbacks work for this application
</DialogDescription>
<AlertBlock>
Having rollbacks enabled increases storage usage. Be careful with
this option. Note that manually cleaning the cache may delete
rollback images, making them unavailable for future rollbacks.
</AlertBlock>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="rollbackActive"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base">
Enable Rollbacks
</FormLabel>
<FormDescription>
Allow rolling back to previous deployments
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<Button type="submit" className="w-full" isLoading={isLoading}>
Save Settings
</Button>
</form>
</Form>
</DialogContent>
</Dialog>
);
};

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

@@ -44,8 +44,10 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
resolver: zodResolver(AddComposeFile),
});
const composeFile = form.watch("composeFile");
useEffect(() => {
if (data) {
if (data && !composeFile) {
form.reset({
composeFile: data.composeFile || "",
});

View File

@@ -18,6 +18,8 @@ import { SaveGitProviderCompose } from "./save-git-provider-compose";
import { SaveGiteaProviderCompose } from "./save-gitea-provider-compose";
import { SaveGithubProviderCompose } from "./save-github-provider-compose";
import { SaveGitlabProviderCompose } from "./save-gitlab-provider-compose";
import { UnauthorizedGitProvider } from "@/components/dashboard/application/general/generic/unauthorized-git-provider";
import { toast } from "sonner";
type TabState = "github" | "git" | "raw" | "gitlab" | "bitbucket" | "gitea";
interface Props {
@@ -34,12 +36,29 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
const { data: giteaProviders, isLoading: isLoadingGitea } =
api.gitea.giteaProviders.useQuery();
const { data: compose } = api.compose.one.useQuery({ composeId });
const { mutateAsync: disconnectGitProvider } =
api.compose.disconnectGitProvider.useMutation();
const { data: compose, refetch } = api.compose.one.useQuery({ composeId });
const [tab, setSab] = useState<TabState>(compose?.sourceType || "github");
const isLoading =
isLoadingGithub || isLoadingGitlab || isLoadingBitbucket || isLoadingGitea;
const handleDisconnect = async () => {
try {
await disconnectGitProvider({ composeId });
toast.success("Repository disconnected successfully");
await refetch();
} catch (error) {
toast.error(
`Failed to disconnect repository: ${
error instanceof Error ? error.message : "Unknown error"
}`,
);
}
};
if (isLoading) {
return (
<Card className="group relative w-full bg-transparent">
@@ -68,6 +87,37 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
);
}
// Check if user doesn't have access to the current git provider
if (
compose &&
!compose.hasGitProviderAccess &&
compose.sourceType !== "raw"
) {
return (
<Card className="group relative w-full bg-transparent">
<CardHeader>
<CardTitle className="flex items-start justify-between">
<div className="flex flex-col gap-2">
<span className="flex flex-col space-y-0.5">Provider</span>
<p className="flex items-center text-sm font-normal text-muted-foreground">
Repository connection through unauthorized provider
</p>
</div>
<div className="hidden space-y-1 text-sm font-normal md:block">
<GitBranch className="size-6 text-muted-foreground" />
</div>
</CardTitle>
</CardHeader>
<CardContent>
<UnauthorizedGitProvider
service={compose}
onDisconnect={handleDisconnect}
/>
</CardContent>
</Card>
);
}
return (
<Card className="group relative w-full bg-transparent">
<CardHeader>

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

@@ -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

@@ -1,24 +1,9 @@
"use client";
import { authClient } from "@/lib/auth-client";
import { useEffect, useState } from "react";
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 {
CheckIcon,
ChevronsUpDown,
Settings2,
UserIcon,
XIcon,
Shield,
Calendar,
Key,
Copy,
Fingerprint,
Building2,
CreditCard,
Server,
} from "lucide-react";
import { toast } from "sonner";
import {
Command,
CommandEmpty,
@@ -32,19 +17,34 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { Logo } from "@/components/shared/logo";
import { Badge } from "@/components/ui/badge";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { format } from "date-fns";
import copy from "copy-to-clipboard";
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;

View File

@@ -10,12 +10,12 @@ 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";
import { useState } from "react";
import { toast } from "sonner";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
export type Services = {
appName: string;

View File

@@ -38,7 +38,7 @@ const AddProjectSchema = z.object({
(name) => {
const trimmedName = name.trim();
const validNameRegex =
/^[\p{L}\p{N}_-][\p{L}\p{N}\s_-]*[\p{L}\p{N}_-]$/u;
/^[\p{L}\p{N}_-][\p{L}\p{N}\s_.-]*[\p{L}\p{N}_-]$/u;
return validNameRegex.test(trimmedName);
},
{

View File

@@ -1,80 +1,93 @@
// @ts-nocheck
export const extractExpirationDate = (certData: string): Date | null => {
try {
const match = certData.match(
/-----BEGIN CERTIFICATE-----\s*([^-]+)\s*-----END CERTIFICATE-----/,
);
if (!match?.[1]) return null;
const base64Cert = match[1].replace(/\s/g, "");
const binaryStr = window.atob(base64Cert);
const bytes = new Uint8Array(binaryStr.length);
for (let i = 0; i < binaryStr.length; i++) {
bytes[i] = binaryStr.charCodeAt(i);
// Decode PEM base64 to DER binary
const b64 = certData.replace(/-----[^-]+-----/g, "").replace(/\s+/g, "");
const binStr = atob(b64);
const der = new Uint8Array(binStr.length);
for (let i = 0; i < binStr.length; i++) {
der[i] = binStr.charCodeAt(i);
}
// ASN.1 tag for UTCTime is 0x17, GeneralizedTime is 0x18
// We need to find the second occurrence of either tag as it's the "not after" (expiration) date
let dateFound = false;
for (let i = 0; i < bytes.length - 2; i++) {
// Look for sequence containing validity period (0x30)
if (bytes[i] === 0x30) {
// Check next bytes for UTCTime or GeneralizedTime
let j = i + 1;
while (j < bytes.length - 2) {
if (bytes[j] === 0x17 || bytes[j] === 0x18) {
const dateType = bytes[j];
const dateLength = bytes[j + 1];
if (typeof dateLength === "undefined") break;
let offset = 0;
if (!dateFound) {
// Skip "not before" date
dateFound = true;
j += dateLength + 2;
continue;
}
// Found "not after" date
let dateStr = "";
for (let k = 0; k < dateLength; k++) {
const charCode = bytes[j + 2 + k];
if (typeof charCode === "undefined") continue;
dateStr += String.fromCharCode(charCode);
}
if (dateType === 0x17) {
// UTCTime (YYMMDDhhmmssZ)
const year = Number.parseInt(dateStr.slice(0, 2));
const fullYear = year >= 50 ? 1900 + year : 2000 + year;
return new Date(
Date.UTC(
fullYear,
Number.parseInt(dateStr.slice(2, 4)) - 1,
Number.parseInt(dateStr.slice(4, 6)),
Number.parseInt(dateStr.slice(6, 8)),
Number.parseInt(dateStr.slice(8, 10)),
Number.parseInt(dateStr.slice(10, 12)),
),
);
}
// GeneralizedTime (YYYYMMDDhhmmssZ)
return new Date(
Date.UTC(
Number.parseInt(dateStr.slice(0, 4)),
Number.parseInt(dateStr.slice(4, 6)) - 1,
Number.parseInt(dateStr.slice(6, 8)),
Number.parseInt(dateStr.slice(8, 10)),
Number.parseInt(dateStr.slice(10, 12)),
Number.parseInt(dateStr.slice(12, 14)),
),
);
}
j++;
// Helper: read ASN.1 length field
function readLength(pos: number): { length: number; offset: number } {
// biome-ignore lint/style/noParameterAssign: <explanation>
let len = der[pos++];
if (len & 0x80) {
const bytes = len & 0x7f;
len = 0;
for (let i = 0; i < bytes; i++) {
// biome-ignore lint/style/noParameterAssign: <explanation>
len = (len << 8) + der[pos++];
}
}
return { length: len, offset: pos };
}
return null;
// Skip the outer certificate sequence
if (der[offset++] !== 0x30) throw new Error("Expected sequence");
({ offset } = readLength(offset));
// Skip tbsCertificate sequence
if (der[offset++] !== 0x30) throw new Error("Expected tbsCertificate");
({ offset } = readLength(offset));
// Check for optional version field (context-specific tag [0])
if (der[offset] === 0xa0) {
offset++;
const versionLen = readLength(offset);
offset = versionLen.offset + versionLen.length;
}
// Skip serialNumber, signature, issuer
for (let i = 0; i < 3; i++) {
if (der[offset] !== 0x30 && der[offset] !== 0x02)
throw new Error("Unexpected structure");
offset++;
const fieldLen = readLength(offset);
offset = fieldLen.offset + fieldLen.length;
}
// Validity sequence (notBefore and notAfter)
if (der[offset++] !== 0x30) throw new Error("Expected validity sequence");
const validityLen = readLength(offset);
offset = validityLen.offset;
// notBefore
offset++;
const notBeforeLen = readLength(offset);
offset = notBeforeLen.offset + notBeforeLen.length;
// notAfter
offset++;
const notAfterLen = readLength(offset);
const notAfterStr = new TextDecoder().decode(
der.slice(notAfterLen.offset, notAfterLen.offset + notAfterLen.length),
);
// Parse GeneralizedTime (15 chars) or UTCTime (13 chars)
function parseTime(str: string): Date {
if (str.length === 13) {
// UTCTime YYMMDDhhmmssZ
const year = Number.parseInt(str.slice(0, 2), 10);
const fullYear = year < 50 ? 2000 + year : 1900 + year;
return new Date(
`${fullYear}-${str.slice(2, 4)}-${str.slice(4, 6)}T${str.slice(6, 8)}:${str.slice(8, 10)}:${str.slice(10, 12)}Z`,
);
}
if (str.length === 15) {
// GeneralizedTime YYYYMMDDhhmmssZ
return new Date(
`${str.slice(0, 4)}-${str.slice(4, 6)}-${str.slice(6, 8)}T${str.slice(8, 10)}:${str.slice(10, 12)}:${str.slice(12, 14)}Z`,
);
}
throw new Error("Invalid ASN.1 time format");
}
return parseTime(notAfterStr);
} catch (error) {
console.error("Error parsing certificate:", error);
return null;

View File

@@ -18,6 +18,7 @@ import { useEffect, useState } from "react";
export const AddGithubProvider = () => {
const [isOpen, setIsOpen] = useState(false);
const { data: activeOrganization } = authClient.useActiveOrganization();
const { data: session } = authClient.useSession();
const { data } = api.user.get.useQuery();
const [manifest, setManifest] = useState("");
const [isOrganization, setIsOrganization] = useState(false);
@@ -27,7 +28,7 @@ export const AddGithubProvider = () => {
const url = document.location.origin;
const manifest = JSON.stringify(
{
redirect_url: `${origin}/api/providers/github/setup?organizationId=${activeOrganization?.id}`,
redirect_url: `${origin}/api/providers/github/setup?organizationId=${activeOrganization?.id}&userId=${session?.user?.id}`,
name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}`,
url: origin,
hook_attributes: {

View File

@@ -18,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";
@@ -29,7 +30,6 @@ import { toast } from "sonner";
import { z } from "zod";
import { Disable2FA } from "./disable-2fa";
import { Enable2FA } from "./enable-2fa";
import { Switch } from "@/components/ui/switch";
const profileSchema = z.object({
email: z.string(),

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

@@ -41,6 +41,7 @@ const addInvitation = z.object({
.min(1, "Email is required")
.email({ message: "Invalid email" }),
role: z.enum(["member", "admin"]),
notificationId: z.string().optional(),
});
type AddInvitation = z.infer<typeof addInvitation>;
@@ -49,6 +50,10 @@ export const AddInvitation = () => {
const [open, setOpen] = useState(false);
const utils = api.useUtils();
const [isLoading, setIsLoading] = useState(false);
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: emailProviders } =
api.notification.getEmailProviders.useQuery();
const { mutateAsync: sendInvitation } = api.user.sendInvitation.useMutation();
const [error, setError] = useState<string | null>(null);
const { data: activeOrganization } = authClient.useActiveOrganization();
@@ -56,6 +61,7 @@ export const AddInvitation = () => {
defaultValues: {
email: "",
role: "member",
notificationId: "",
},
resolver: zodResolver(addInvitation),
});
@@ -74,7 +80,20 @@ export const AddInvitation = () => {
if (result.error) {
setError(result.error.message || "");
} else {
toast.success("Invitation created");
if (!isCloud && data.notificationId) {
await sendInvitation({
invitationId: result.data.id,
notificationId: data.notificationId || "",
})
.then(() => {
toast.success("Invitation created and email sent");
})
.catch((error: any) => {
toast.error(error.message);
});
} else {
toast.success("Invitation created");
}
setError(null);
setOpen(false);
}
@@ -149,6 +168,47 @@ export const AddInvitation = () => {
);
}}
/>
{!isCloud && (
<FormField
control={form.control}
name="notificationId"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Email Provider</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select an email provider" />
</SelectTrigger>
</FormControl>
<SelectContent>
{emailProviders?.map((provider) => (
<SelectItem
key={provider.notificationId}
value={provider.notificationId}
>
{provider.name}
</SelectItem>
))}
<SelectItem value="none" disabled>
None
</SelectItem>
</SelectContent>
</Select>
<FormDescription>
Select the email provider to send the invitation
</FormDescription>
<FormMessage />
</FormItem>
);
}}
/>
)}
<DialogFooter className="flex w-full flex-row">
<Button
isLoading={isLoading}

View File

@@ -1,6 +1,7 @@
import Page from "./side";
import { ImpersonationBar } from "../dashboard/impersonation/impersonation-bar";
import { api } from "@/utils/api";
import { ImpersonationBar } from "../dashboard/impersonation/impersonation-bar";
import { ChatwootWidget } from "../shared/ChatwootWidget";
import Page from "./side";
interface Props {
children: React.ReactNode;
@@ -9,10 +10,15 @@ interface Props {
export const DashboardLayout = ({ children }: Props) => {
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 "application" ADD COLUMN "isStaticSpa" boolean;

View File

@@ -0,0 +1,3 @@
ALTER TABLE "user_temp" ALTER COLUMN "logCleanupCron" SET DEFAULT '0 0 * * *';
UPDATE "user_temp" SET "logCleanupCron" = '0 0 * * *' WHERE "logCleanupCron" IS NULL;

View File

@@ -0,0 +1,14 @@
ALTER TABLE "git_provider" ADD COLUMN "userId" text;--> statement-breakpoint
-- Update existing git providers to be owned by the organization owner
-- We can get the owner_id directly from the organization table
UPDATE "git_provider"
SET "userId" = (
SELECT o."owner_id"
FROM "organization" o
WHERE o.id = "git_provider"."organizationId"
);--> statement-breakpoint
-- Now make the column NOT NULL since all rows should have values
ALTER TABLE "git_provider" ALTER COLUMN "userId" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "git_provider" ADD CONSTRAINT "git_provider_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;

View File

@@ -0,0 +1,13 @@
CREATE TABLE "rollback" (
"rollbackId" text PRIMARY KEY NOT NULL,
"deploymentId" text NOT NULL,
"version" serial NOT NULL,
"image" text,
"createdAt" text NOT NULL,
"fullContext" jsonb
);
--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "rollbackActive" boolean DEFAULT false;--> statement-breakpoint
ALTER TABLE "deployment" ADD COLUMN "rollbackId" text;--> statement-breakpoint
ALTER TABLE "rollback" ADD CONSTRAINT "rollback_deploymentId_deployment_deploymentId_fk" FOREIGN KEY ("deploymentId") REFERENCES "public"."deployment"("deploymentId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "deployment" ADD CONSTRAINT "deployment_rollbackId_rollback_rollbackId_fk" FOREIGN KEY ("rollbackId") REFERENCES "public"."rollback"("rollbackId") ON DELETE cascade ON UPDATE no action;

View File

@@ -0,0 +1,3 @@
ALTER TABLE "rollback" DROP CONSTRAINT "rollback_deploymentId_deployment_deploymentId_fk";
--> statement-breakpoint
ALTER TABLE "rollback" ADD CONSTRAINT "rollback_deploymentId_deployment_deploymentId_fk" FOREIGN KEY ("deploymentId") REFERENCES "public"."deployment"("deploymentId") ON DELETE set null ON UPDATE no action;

View File

@@ -0,0 +1,3 @@
ALTER TABLE "rollback" DROP CONSTRAINT "rollback_deploymentId_deployment_deploymentId_fk";
--> statement-breakpoint
ALTER TABLE "rollback" ADD CONSTRAINT "rollback_deploymentId_deployment_deploymentId_fk" FOREIGN KEY ("deploymentId") REFERENCES "public"."deployment"("deploymentId") ON DELETE cascade ON UPDATE no action;

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

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

@@ -645,6 +645,48 @@
"when": 1746518402168,
"tag": "0091_spotty_kulan_gath",
"breakpoints": true
},
{
"idx": 92,
"version": "7",
"when": 1747713229160,
"tag": "0092_stiff_the_watchers",
"breakpoints": true
},
{
"idx": 93,
"version": "7",
"when": 1750397258622,
"tag": "0093_nice_gorilla_man",
"breakpoints": true
},
{
"idx": 94,
"version": "7",
"when": 1750559214977,
"tag": "0094_numerous_carmella_unuscione",
"breakpoints": true
},
{
"idx": 95,
"version": "7",
"when": 1750562292392,
"tag": "0095_curly_justice",
"breakpoints": true
},
{
"idx": 96,
"version": "7",
"when": 1750566830268,
"tag": "0096_small_shaman",
"breakpoints": true
},
{
"idx": 97,
"version": "7",
"when": 1750567641441,
"tag": "0097_hard_lizard",
"breakpoints": true
}
]
}

View File

@@ -21,6 +21,7 @@ try {
entryPoints: {
server: "server/server.ts",
"reset-password": "reset-password.ts",
"reset-2fa": "reset-2fa.ts",
},
bundle: true,
platform: "node",

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.22.5",
"version": "v0.23.3",
"private": true,
"license": "Apache-2.0",
"type": "module",
@@ -11,6 +11,7 @@
"build-next": "next build",
"setup": "tsx -r dotenv/config setup.ts && sleep 5 && pnpm run migration:run",
"reset-password": "node -r dotenv/config dist/reset-password.mjs",
"reset-2fa": "node -r dotenv/config dist/reset-2fa.mjs",
"dev": "tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ",
"dev-turbopack": "TURBOPACK=1 tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json",
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
@@ -125,6 +126,8 @@
"octokit": "3.1.2",
"ollama-ai-provider": "^1.1.0",
"otpauth": "^9.2.3",
"pino": "9.4.0",
"pino-pretty": "11.2.2",
"postgres": "3.4.4",
"public-ip": "6.0.2",
"qrcode": "^1.5.3",
@@ -145,13 +148,13 @@
"swagger-ui-react": "^5.17.14",
"tailwind-merge": "^2.2.0",
"tailwindcss-animate": "^1.0.7",
"toml": "3.0.0",
"undici": "^6.19.2",
"use-resize-observer": "9.1.0",
"ws": "8.16.0",
"xterm-addon-fit": "^0.8.0",
"zod": "^3.23.4",
"zod-form-data": "^2.0.2",
"toml": "3.0.0"
"zod-form-data": "^2.0.2"
},
"devDependencies": {
"@types/adm-zip": "^0.5.5",
@@ -186,7 +189,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

@@ -10,13 +10,14 @@ type Query = {
state: string;
installation_id: string;
setup_action: string;
userId: string;
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const { code, state, installation_id }: Query = req.query as Query;
const { code, state, installation_id, userId }: Query = req.query as Query;
if (!code) {
return res.status(400).json({ error: "Missing code parameter" });
@@ -44,6 +45,7 @@ export default async function handler(
githubPrivateKey: data.pem,
},
value as string,
userId,
);
} else if (action === "gh_setup") {
await db

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";
@@ -74,12 +74,7 @@ const Service = (
}
}, [router.query.tab]);
const { data } = api.compose.one.useQuery(
{ composeId },
{
refetchInterval: 5000,
},
);
const { data } = api.compose.one.useQuery({ composeId });
const { data: auth } = api.user.get.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
@@ -366,7 +361,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 (

27
apps/dokploy/reset-2fa.ts Normal file
View File

@@ -0,0 +1,27 @@
import { findAdmin } from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { users_temp } from "@dokploy/server/db/schema";
import { eq } from "drizzle-orm";
(async () => {
try {
const result = await findAdmin();
const update = await db
.update(users_temp)
.set({
twoFactorEnabled: false,
})
.where(eq(users_temp.id, result.userId));
if (update) {
console.log("2FA reset successful");
} else {
console.log("Password reset failed");
}
process.exit(0);
} catch (error) {
console.log("Error resetting 2FA", error);
}
})();

View File

@@ -36,6 +36,7 @@ import { stripeRouter } from "./routers/stripe";
import { swarmRouter } from "./routers/swarm";
import { userRouter } from "./routers/user";
import { scheduleRouter } from "./routers/schedule";
import { rollbackRouter } from "./routers/rollbacks";
/**
* This is the primary router for your server.
*
@@ -80,6 +81,7 @@ export const appRouter = createTRPCRouter({
ai: aiRouter,
organization: organizationRouter,
schedule: scheduleRouter,
rollback: rollbackRouter,
});
// export type definition of API

View File

@@ -31,6 +31,7 @@ import {
createApplication,
deleteAllMiddlewares,
findApplicationById,
findGitProviderById,
findProjectById,
getApplicationStats,
mechanizeDockerContainer,
@@ -126,7 +127,45 @@ export const applicationRouter = createTRPCRouter({
message: "You are not authorized to access this application",
});
}
return application;
let hasGitProviderAccess = true;
let unauthorizedProvider: string | null = null;
const getGitProviderId = () => {
switch (application.sourceType) {
case "github":
return application.github?.gitProviderId;
case "gitlab":
return application.gitlab?.gitProviderId;
case "bitbucket":
return application.bitbucket?.gitProviderId;
case "gitea":
return application.gitea?.gitProviderId;
default:
return null;
}
};
const gitProviderId = getGitProviderId();
if (gitProviderId) {
try {
const gitProvider = await findGitProviderById(gitProviderId);
if (gitProvider.userId !== ctx.session.userId) {
hasGitProviderAccess = false;
unauthorizedProvider = application.sourceType;
}
} catch {
hasGitProviderAccess = false;
unauthorizedProvider = application.sourceType;
}
}
return {
...application,
hasGitProviderAccess,
unauthorizedProvider,
};
}),
reload: protectedProcedure
@@ -330,6 +369,7 @@ export const applicationRouter = createTRPCRouter({
dockerContextPath: input.dockerContextPath,
dockerBuildStage: input.dockerBuildStage,
herokuVersion: input.herokuVersion,
isStaticSpa: input.isStaticSpa,
});
return true;
@@ -487,6 +527,67 @@ export const applicationRouter = createTRPCRouter({
enableSubmodules: input.enableSubmodules,
});
return true;
}),
disconnectGitProvider: protectedProcedure
.input(apiFindOneApplication)
.mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId);
if (
application.project.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to disconnect this git provider",
});
}
// Reset all git provider related fields
await updateApplication(input.applicationId, {
// GitHub fields
repository: null,
branch: null,
owner: null,
buildPath: "/",
githubId: null,
triggerType: "push",
// GitLab fields
gitlabRepository: null,
gitlabOwner: null,
gitlabBranch: null,
gitlabBuildPath: null,
gitlabId: null,
gitlabProjectId: null,
gitlabPathNamespace: null,
// Bitbucket fields
bitbucketRepository: null,
bitbucketOwner: null,
bitbucketBranch: null,
bitbucketBuildPath: null,
bitbucketId: null,
// Gitea fields
giteaRepository: null,
giteaOwner: null,
giteaBranch: null,
giteaBuildPath: null,
giteaId: null,
// Custom Git fields
customGitBranch: null,
customGitBuildPath: null,
customGitUrl: null,
customGitSSHKeyId: null,
// Common fields
sourceType: "github", // Reset to default
applicationStatus: "idle",
watchPaths: null,
enableSubmodules: false,
});
return true;
}),
markRunning: protectedProcedure

View File

@@ -22,6 +22,7 @@ import {
findPostgresByBackupId,
findPostgresById,
findServerById,
keepLatestNBackups,
removeBackupById,
removeScheduleBackup,
runMariadbBackup,
@@ -197,6 +198,8 @@ export const backupRouter = createTRPCRouter({
const backup = await findBackupById(input.backupId);
const postgres = await findPostgresByBackupId(backup.backupId);
await runPostgresBackup(postgres, backup);
await keepLatestNBackups(backup, postgres?.serverId);
return true;
} catch (error) {
const message =
@@ -217,6 +220,7 @@ export const backupRouter = createTRPCRouter({
const backup = await findBackupById(input.backupId);
const mysql = await findMySqlByBackupId(backup.backupId);
await runMySqlBackup(mysql, backup);
await keepLatestNBackups(backup, mysql?.serverId);
return true;
} catch (error) {
throw new TRPCError({
@@ -233,6 +237,7 @@ export const backupRouter = createTRPCRouter({
const backup = await findBackupById(input.backupId);
const mariadb = await findMariadbByBackupId(backup.backupId);
await runMariadbBackup(mariadb, backup);
await keepLatestNBackups(backup, mariadb?.serverId);
return true;
} catch (error) {
throw new TRPCError({
@@ -249,6 +254,7 @@ export const backupRouter = createTRPCRouter({
const backup = await findBackupById(input.backupId);
const compose = await findComposeByBackupId(backup.backupId);
await runComposeBackup(compose, backup);
await keepLatestNBackups(backup, compose?.serverId);
return true;
} catch (error) {
throw new TRPCError({
@@ -265,6 +271,7 @@ export const backupRouter = createTRPCRouter({
const backup = await findBackupById(input.backupId);
const mongo = await findMongoByBackupId(backup.backupId);
await runMongoBackup(mongo, backup);
await keepLatestNBackups(backup, mongo?.serverId);
return true;
} catch (error) {
throw new TRPCError({

View File

@@ -22,7 +22,11 @@ export const bitbucketRouter = createTRPCRouter({
.input(apiCreateBitbucket)
.mutation(async ({ input, ctx }) => {
try {
return await createBitbucket(input, ctx.session.activeOrganizationId);
return await createBitbucket(
input,
ctx.session.activeOrganizationId,
ctx.session.userId,
);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -37,7 +41,8 @@ export const bitbucketRouter = createTRPCRouter({
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
if (
bitbucketProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
bitbucketProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -56,11 +61,13 @@ export const bitbucketRouter = createTRPCRouter({
},
});
result = result.filter(
(provider) =>
result = result.filter((provider) => {
return (
provider.gitProvider.organizationId ===
ctx.session.activeOrganizationId,
);
ctx.session.activeOrganizationId &&
provider.gitProvider.userId === ctx.session.userId
);
});
return result;
}),
@@ -70,7 +77,8 @@ export const bitbucketRouter = createTRPCRouter({
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
if (
bitbucketProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
bitbucketProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -87,7 +95,8 @@ export const bitbucketRouter = createTRPCRouter({
);
if (
bitbucketProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
bitbucketProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -103,7 +112,8 @@ export const bitbucketRouter = createTRPCRouter({
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
if (
bitbucketProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
bitbucketProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -126,7 +136,8 @@ export const bitbucketRouter = createTRPCRouter({
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
if (
bitbucketProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
bitbucketProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",

View File

@@ -28,6 +28,7 @@ import {
deleteMount,
findComposeById,
findDomainsByComposeId,
findGitProviderById,
findProjectById,
findServerById,
findUserById,
@@ -51,9 +52,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";
@@ -119,7 +120,45 @@ export const composeRouter = createTRPCRouter({
message: "You are not authorized to access this compose",
});
}
return compose;
let hasGitProviderAccess = true;
let unauthorizedProvider: string | null = null;
const getGitProviderId = () => {
switch (compose.sourceType) {
case "github":
return compose.github?.gitProviderId;
case "gitlab":
return compose.gitlab?.gitProviderId;
case "bitbucket":
return compose.bitbucket?.gitProviderId;
case "gitea":
return compose.gitea?.gitProviderId;
default:
return null;
}
};
const gitProviderId = getGitProviderId();
if (gitProviderId) {
try {
const gitProvider = await findGitProviderById(gitProviderId);
if (gitProvider.userId !== ctx.session.userId) {
hasGitProviderAccess = false;
unauthorizedProvider = compose.sourceType;
}
} catch {
hasGitProviderAccess = false;
unauthorizedProvider = compose.sourceType;
}
}
return {
...compose,
hasGitProviderAccess,
unauthorizedProvider,
};
}),
update: protectedProcedure
@@ -496,7 +535,7 @@ export const composeRouter = createTRPCRouter({
}
}
return null;
return compose;
}),
templates: publicProcedure
@@ -526,6 +565,61 @@ export const composeRouter = createTRPCRouter({
const uniqueTags = _.uniq(allTags);
return uniqueTags;
}),
disconnectGitProvider: protectedProcedure
.input(apiFindCompose)
.mutation(async ({ input, ctx }) => {
const compose = await findComposeById(input.composeId);
if (compose.project.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to disconnect this git provider",
});
}
// Reset all git provider related fields
await updateCompose(input.composeId, {
// GitHub fields
repository: null,
branch: null,
owner: null,
composePath: undefined,
githubId: null,
triggerType: "push",
// GitLab fields
gitlabRepository: null,
gitlabOwner: null,
gitlabBranch: null,
gitlabId: null,
gitlabProjectId: null,
gitlabPathNamespace: null,
// Bitbucket fields
bitbucketRepository: null,
bitbucketOwner: null,
bitbucketBranch: null,
bitbucketId: null,
// Gitea fields
giteaRepository: null,
giteaOwner: null,
giteaBranch: null,
giteaId: null,
// Custom Git fields
customGitBranch: null,
customGitUrl: null,
customGitSSHKeyId: null,
// Common fields
sourceType: "github", // Reset to default
composeStatus: "idle",
watchPaths: null,
enableSubmodules: false,
});
return true;
}),
move: protectedProcedure
.input(

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
@@ -65,7 +65,11 @@ export const deploymentRouter = createTRPCRouter({
const deploymentsList = await db.query.deployments.findMany({
where: eq(deployments[`${input.type}Id`], input.id),
orderBy: desc(deployments.createdAt),
with: {
rollback: true,
},
});
return deploymentsList;
}),
});

View File

@@ -3,7 +3,7 @@ import { db } from "@/server/db";
import { apiRemoveGitProvider, gitProvider } from "@/server/db/schema";
import { findGitProviderById, removeGitProvider } from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { desc, eq } from "drizzle-orm";
import { and, desc, eq } from "drizzle-orm";
export const gitProviderRouter = createTRPCRouter({
getAll: protectedProcedure.query(async ({ ctx }) => {
@@ -15,7 +15,10 @@ export const gitProviderRouter = createTRPCRouter({
gitea: true,
},
orderBy: desc(gitProvider.createdAt),
where: eq(gitProvider.organizationId, ctx.session.activeOrganizationId),
where: and(
eq(gitProvider.userId, ctx.session.userId),
eq(gitProvider.organizationId, ctx.session.activeOrganizationId),
),
});
}),
remove: protectedProcedure

View File

@@ -26,7 +26,11 @@ export const giteaRouter = createTRPCRouter({
.input(apiCreateGitea)
.mutation(async ({ input, ctx }) => {
try {
return await createGitea(input, ctx.session.activeOrganizationId);
return await createGitea(
input,
ctx.session.activeOrganizationId,
ctx.session.userId,
);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -42,7 +46,8 @@ export const giteaRouter = createTRPCRouter({
const giteaProvider = await findGiteaById(input.giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
giteaProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -62,7 +67,8 @@ export const giteaRouter = createTRPCRouter({
result = result.filter(
(provider) =>
provider.gitProvider.organizationId ===
ctx.session.activeOrganizationId,
ctx.session.activeOrganizationId &&
provider.gitProvider.userId === ctx.session.userId,
);
const filtered = result
@@ -94,7 +100,8 @@ export const giteaRouter = createTRPCRouter({
const giteaProvider = await findGiteaById(giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
giteaProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -130,7 +137,8 @@ export const giteaRouter = createTRPCRouter({
const giteaProvider = await findGiteaById(giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
giteaProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -162,7 +170,8 @@ export const giteaRouter = createTRPCRouter({
const giteaProvider = await findGiteaById(giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
giteaProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -190,7 +199,8 @@ export const giteaRouter = createTRPCRouter({
const giteaProvider = await findGiteaById(input.giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
giteaProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -231,7 +241,8 @@ export const giteaRouter = createTRPCRouter({
const giteaProvider = await findGiteaById(giteaId);
if (
giteaProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
giteaProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",

View File

@@ -21,7 +21,8 @@ export const githubRouter = createTRPCRouter({
const githubProvider = await findGithubById(input.githubId);
if (
githubProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
githubProvider.gitProvider.userId === ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -36,7 +37,8 @@ export const githubRouter = createTRPCRouter({
const githubProvider = await findGithubById(input.githubId);
if (
githubProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
githubProvider.gitProvider.userId === ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -51,7 +53,8 @@ export const githubRouter = createTRPCRouter({
const githubProvider = await findGithubById(input.githubId || "");
if (
githubProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
githubProvider.gitProvider.userId === ctx.session.userId
) {
//TODO: Remove this line when the cloud version is ready
throw new TRPCError({
@@ -71,7 +74,8 @@ export const githubRouter = createTRPCRouter({
result = result.filter(
(provider) =>
provider.gitProvider.organizationId ===
ctx.session.activeOrganizationId,
ctx.session.activeOrganizationId &&
provider.gitProvider.userId === ctx.session.userId,
);
const filtered = result
@@ -95,7 +99,8 @@ export const githubRouter = createTRPCRouter({
const githubProvider = await findGithubById(input.githubId);
if (
githubProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
githubProvider.gitProvider.userId === ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -117,7 +122,8 @@ export const githubRouter = createTRPCRouter({
const githubProvider = await findGithubById(input.githubId);
if (
githubProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
githubProvider.gitProvider.userId === ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",

View File

@@ -25,7 +25,11 @@ export const gitlabRouter = createTRPCRouter({
.input(apiCreateGitlab)
.mutation(async ({ input, ctx }) => {
try {
return await createGitlab(input, ctx.session.activeOrganizationId);
return await createGitlab(
input,
ctx.session.activeOrganizationId,
ctx.session.userId,
);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
@@ -40,7 +44,8 @@ export const gitlabRouter = createTRPCRouter({
const gitlabProvider = await findGitlabById(input.gitlabId);
if (
gitlabProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
gitlabProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -56,11 +61,13 @@ export const gitlabRouter = createTRPCRouter({
},
});
result = result.filter(
(provider) =>
result = result.filter((provider) => {
return (
provider.gitProvider.organizationId ===
ctx.session.activeOrganizationId,
);
ctx.session.activeOrganizationId &&
provider.gitProvider.userId === ctx.session.userId
);
});
const filtered = result
.filter((provider) => haveGitlabRequirements(provider))
.map((provider) => {
@@ -80,7 +87,8 @@ export const gitlabRouter = createTRPCRouter({
const gitlabProvider = await findGitlabById(input.gitlabId);
if (
gitlabProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
gitlabProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -96,7 +104,8 @@ export const gitlabRouter = createTRPCRouter({
const gitlabProvider = await findGitlabById(input.gitlabId || "");
if (
gitlabProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
gitlabProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -112,7 +121,8 @@ export const gitlabRouter = createTRPCRouter({
const gitlabProvider = await findGitlabById(input.gitlabId || "");
if (
gitlabProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
gitlabProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
@@ -135,7 +145,8 @@ export const gitlabRouter = createTRPCRouter({
const gitlabProvider = await findGitlabById(input.gitlabId);
if (
gitlabProvider.gitProvider.organizationId !==
ctx.session.activeOrganizationId
ctx.session.activeOrganizationId &&
gitlabProvider.gitProvider.userId !== ctx.session.userId
) {
throw new TRPCError({
code: "UNAUTHORIZED",

View File

@@ -446,4 +446,12 @@ export const notificationRouter = createTRPCRouter({
});
}
}),
getEmailProviders: adminProcedure.query(async ({ ctx }) => {
return await db.query.notifications.findMany({
where: eq(notifications.organizationId, ctx.session.activeOrganizationId),
with: {
email: true,
},
});
}),
});

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";

View File

@@ -0,0 +1,37 @@
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { apiFindOneRollback } from "@/server/db/schema";
import { removeRollbackById, rollback } from "@dokploy/server";
import { TRPCError } from "@trpc/server";
export const rollbackRouter = createTRPCRouter({
delete: protectedProcedure
.input(apiFindOneRollback)
.mutation(async ({ input }) => {
try {
return removeRollbackById(input.rollbackId);
} catch (error) {
const message =
error instanceof Error
? error.message
: "Error input: Deleting rollback";
throw new TRPCError({
code: "BAD_REQUEST",
message,
});
}
}),
rollback: protectedProcedure
.input(apiFindOneRollback)
.mutation(async ({ input }) => {
try {
return await rollback(input.rollbackId);
} catch (error) {
console.error(error);
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Rolling back",
cause: error,
});
}
}),
});

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

@@ -825,6 +825,9 @@ export const settingsRouter = createTRPCRouter({
}),
)
.mutation(async ({ input }) => {
if (IS_CLOUD) {
return true;
}
if (input.cronExpression) {
return startLogCleanup(input.cronExpression);
}

View File

@@ -1,10 +1,13 @@
import {
IS_CLOUD,
createApiKey,
findAdmin,
findNotificationById,
findOrganizationById,
findUserById,
getUserByToken,
removeUserById,
sendEmailNotification,
updateUser,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
@@ -362,4 +365,59 @@ export const userRouter = createTRPCRouter({
return organizations.length;
}),
sendInvitation: adminProcedure
.input(
z.object({
invitationId: z.string().min(1),
notificationId: z.string().min(1),
}),
)
.mutation(async ({ input, ctx }) => {
if (IS_CLOUD) {
return;
}
const notification = await findNotificationById(input.notificationId);
const email = notification.email;
const currentInvitation = await db.query.invitation.findFirst({
where: eq(invitation.id, input.invitationId),
});
if (!email) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Email notification not found",
});
}
const admin = await findAdmin();
const host =
process.env.NODE_ENV === "development"
? "http://localhost:3000"
: admin.user.host;
const inviteLink = `${host}/invitation?token=${input.invitationId}`;
const organization = await findOrganizationById(
ctx.session.activeOrganizationId,
);
try {
await sendEmailNotification(
{
...email,
toAddresses: [currentInvitation?.email || ""],
},
"Invitation to join organization",
`
<p>You are invited to join ${organization?.name || "organization"} on Dokploy. Click the link to accept the invitation: <a href="${inviteLink}">Accept Invitation</a></p>
`,
);
} catch (error) {
console.log(error);
throw error;
}
return inviteLink;
}),
});

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>,

View File

@@ -15,7 +15,7 @@ const config = {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
"2xl": "87.5rem",
},
},
extend: {

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

@@ -8,26 +8,26 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"drizzle-orm": "^0.39.1",
"ioredis": "5.4.1",
"bullmq": "5.4.2",
"@hono/zod-validator": "0.3.0",
"zod": "^3.23.4",
"react": "18.2.0",
"react-dom": "18.2.0",
"@dokploy/server": "workspace:*",
"@hono/node-server": "^1.12.1",
"hono": "^4.5.8",
"@hono/zod-validator": "0.3.0",
"bullmq": "5.4.2",
"dotenv": "^16.3.1",
"drizzle-orm": "^0.39.1",
"hono": "^4.5.8",
"ioredis": "5.4.1",
"pino": "9.4.0",
"pino-pretty": "11.2.2"
"pino-pretty": "11.2.2",
"react": "18.2.0",
"react-dom": "18.2.0",
"zod": "^3.23.4"
},
"devDependencies": {
"typescript": "^5.4.2",
"@types/node": "^20.11.17",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@types/node": "^20.11.17",
"tsx": "^4.7.1"
"tsx": "^4.7.1",
"typescript": "^5.4.2"
},
"packageManager": "pnpm@9.5.0"
}

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

@@ -20,19 +20,19 @@
"format-and-lint:fix": "biome check . --write"
},
"devDependencies": {
"dotenv": "16.4.5",
"esbuild": "0.20.2",
"tsx": "4.16.2",
"lint-staged": "^15.2.7",
"@biomejs/biome": "1.9.4",
"lefthook": "1.8.4",
"@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^19.2.2",
"@types/node": "^18.17.0"
"@types/node": "^18.17.0",
"dotenv": "16.4.5",
"esbuild": "0.20.2",
"lefthook": "1.8.4",
"lint-staged": "^15.2.7",
"tsx": "4.16.2"
},
"packageManager": "pnpm@9.5.0",
"engines": {
"node": "^20.9.0",
"node": "^20.16.0",
"pnpm": ">=9.5.0"
},
"lint-staged": {

View File

@@ -28,7 +28,6 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"micromatch": "4.0.8",
"@ai-sdk/anthropic": "^1.0.6",
"@ai-sdk/azure": "^1.0.15",
"@ai-sdk/cohere": "^1.0.6",
@@ -37,27 +36,29 @@
"@ai-sdk/openai": "^1.0.12",
"@ai-sdk/openai-compatible": "^0.0.13",
"@better-auth/utils": "0.2.4",
"@oslojs/encoding": "1.1.0",
"@oslojs/crypto": "1.0.1",
"drizzle-dbml-generator": "0.10.0",
"better-auth": "v1.2.8-beta.7",
"@faker-js/faker": "^8.4.1",
"@octokit/auth-app": "^6.0.4",
"@octokit/rest": "^20.0.2",
"@oslojs/crypto": "1.0.1",
"@oslojs/encoding": "1.1.0",
"@react-email/components": "^0.0.21",
"@trpc/server": "^10.43.6",
"adm-zip": "^0.5.14",
"ai": "^4.0.23",
"bcrypt": "5.1.1",
"better-auth": "v1.2.8-beta.7",
"bl": "6.0.11",
"boxen": "^7.1.1",
"date-fns": "3.6.0",
"dockerode": "4.0.2",
"dotenv": "16.4.5",
"drizzle-dbml-generator": "0.10.0",
"drizzle-orm": "^0.39.1",
"drizzle-zod": "0.5.1",
"hi-base32": "^0.5.1",
"js-yaml": "4.1.0",
"lodash": "4.17.21",
"micromatch": "4.0.8",
"nanoid": "3",
"node-os-utils": "1.3.7",
"node-pty": "1.0.0",
@@ -66,6 +67,8 @@
"octokit": "3.1.2",
"ollama-ai-provider": "^1.1.0",
"otpauth": "^9.2.3",
"pino": "9.4.0",
"pino-pretty": "11.2.2",
"postgres": "3.4.4",
"public-ip": "6.0.2",
"qrcode": "^1.5.3",
@@ -73,19 +76,18 @@
"react-dom": "18.2.0",
"rotating-file-stream": "3.2.3",
"slugify": "^1.6.6",
"ws": "8.16.0",
"zod": "^3.23.4",
"ssh2": "1.15.0",
"@octokit/rest": "^20.0.2",
"toml": "3.0.0"
"toml": "3.0.0",
"ws": "8.16.0",
"zod": "^3.23.4"
},
"devDependencies": {
"@types/micromatch": "4.0.9",
"@types/adm-zip": "^0.5.5",
"@types/bcrypt": "5.0.2",
"@types/dockerode": "3.3.23",
"@types/js-yaml": "4.0.9",
"@types/lodash": "4.17.4",
"@types/micromatch": "4.0.9",
"@types/node": "^18.17.0",
"@types/node-os-utils": "1.3.4",
"@types/node-schedule": "2.1.6",

View File

@@ -27,7 +27,6 @@ import { server } from "./server";
import { applicationStatus, certificateType, triggerType } from "./shared";
import { sshKeys } from "./ssh-key";
import { generateAppName } from "./utils";
export const sourceType = pgEnum("sourceType", [
"docker",
"git",
@@ -132,6 +131,7 @@ export const applications = pgTable("application", {
isPreviewDeploymentsActive: boolean("isPreviewDeploymentsActive").default(
false,
),
rollbackActive: boolean("rollbackActive").default(false),
buildArgs: text("buildArgs"),
memoryReservation: text("memoryReservation"),
memoryLimit: text("memoryLimit"),
@@ -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", [

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