Compare commits

...

74 Commits

Author SHA1 Message Date
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
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
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
82 changed files with 31084 additions and 245 deletions

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

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

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

@@ -121,6 +121,7 @@ const baseApp: ApplicationNested = {
updateConfigSwarm: null,
username: null,
dockerContextPath: null,
rollbackActive: false,
};
describe("unzipDrop using real zip files", () => {

View File

@@ -5,6 +5,7 @@ import { createRouterConfig } from "@dokploy/server";
import { expect, test } from "vitest";
const baseApp: ApplicationNested = {
rollbackActive: false,
applicationId: "",
herokuVersion: "",
giteaRepository: "",

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

@@ -10,11 +10,14 @@ import {
CardTitle,
} from "@/components/ui/card";
import { type RouterOutputs, api } from "@/utils/api";
import { Clock, Loader2, RocketIcon } 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 { 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

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

@@ -49,7 +49,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
currentBuildArgs !== (data?.buildArgs || "");
useEffect(() => {
if (data) {
if (data && !hasChanges) {
form.reset({
env: data.env || "",
buildArgs: data.buildArgs || "",

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

@@ -0,0 +1,117 @@
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>
</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

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

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

@@ -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,7 +1,7 @@
import { api } from "@/utils/api";
import { ImpersonationBar } from "../dashboard/impersonation/impersonation-bar";
import Page from "./side";
import { ChatwootWidget } from "../shared/ChatwootWidget";
import Page from "./side";
interface Props {
children: React.ReactNode;

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,15 @@
ALTER TABLE "git_provider" ADD COLUMN "userId" text;--> statement-breakpoint
-- Update existing git providers to be owned by the organization owner
-- We need to get the account.user_id for the organization owner
UPDATE "git_provider"
SET "userId" = (
SELECT a.user_id
FROM "organization" o
JOIN "account" a ON o."owner_id" = a.user_id
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

View File

@@ -652,6 +652,41 @@
"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

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.22.7",
"version": "v0.23.0",
"private": true,
"license": "Apache-2.0",
"type": "module",
@@ -36,8 +36,6 @@
"test": "vitest --config __test__/vitest.config.ts"
},
"dependencies": {
"pino": "9.4.0",
"pino-pretty": "11.2.2",
"@ai-sdk/anthropic": "^1.0.6",
"@ai-sdk/azure": "^1.0.15",
"@ai-sdk/cohere": "^1.0.6",
@@ -127,6 +125,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",
@@ -147,13 +147,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",

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

@@ -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();

View File

@@ -28,7 +28,6 @@ import { projectRouter } from "./routers/project";
import { redirectsRouter } from "./routers/redirects";
import { redisRouter } from "./routers/redis";
import { registryRouter } from "./routers/registry";
import { scheduleRouter } from "./routers/schedule";
import { securityRouter } from "./routers/security";
import { serverRouter } from "./routers/server";
import { settingsRouter } from "./routers/settings";
@@ -36,6 +35,8 @@ import { sshRouter } from "./routers/ssh-key";
import { stripeRouter } from "./routers/stripe";
import { swarmRouter } from "./routers/swarm";
import { userRouter } from "./routers/user";
import { scheduleRouter } from "./routers/schedule";
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
@@ -488,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,
@@ -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

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

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

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

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

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

@@ -20,15 +20,15 @@
"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": {

View File

@@ -28,9 +28,6 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"pino": "9.4.0",
"pino-pretty": "11.2.2",
"micromatch": "4.0.8",
"@ai-sdk/anthropic": "^1.0.6",
"@ai-sdk/azure": "^1.0.15",
"@ai-sdk/cohere": "^1.0.6",
@@ -39,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",
@@ -68,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",
@@ -75,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"),

View File

@@ -15,6 +15,7 @@ import { compose } from "./compose";
import { previewDeployments } from "./preview-deployments";
import { schedules } from "./schedule";
import { server } from "./server";
import { rollbacks } from "./rollbacks";
export const deploymentStatus = pgEnum("deploymentStatus", [
"running",
"done",
@@ -58,6 +59,10 @@ export const deployments = pgTable("deployment", {
backupId: text("backupId").references((): AnyPgColumn => backups.backupId, {
onDelete: "cascade",
}),
rollbackId: text("rollbackId").references(
(): AnyPgColumn => rollbacks.rollbackId,
{ onDelete: "cascade" },
),
});
export const deploymentsRelations = relations(deployments, ({ one }) => ({
@@ -85,6 +90,10 @@ export const deploymentsRelations = relations(deployments, ({ one }) => ({
fields: [deployments.backupId],
references: [backups.backupId],
}),
rollback: one(rollbacks, {
fields: [deployments.deploymentId],
references: [rollbacks.deploymentId],
}),
}));
const schema = createInsertSchema(deployments, {

View File

@@ -8,6 +8,7 @@ import { bitbucket } from "./bitbucket";
import { gitea } from "./gitea";
import { github } from "./github";
import { gitlab } from "./gitlab";
import { users_temp } from "./user";
export const gitProviderType = pgEnum("gitProviderType", [
"github",
@@ -29,6 +30,9 @@ export const gitProvider = pgTable("git_provider", {
organizationId: text("organizationId")
.notNull()
.references(() => organization.id, { onDelete: "cascade" }),
userId: text("userId")
.notNull()
.references(() => users_temp.id, { onDelete: "cascade" }),
});
export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
@@ -52,6 +56,10 @@ export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
fields: [gitProvider.organizationId],
references: [organization.id],
}),
user: one(users_temp, {
fields: [gitProvider.userId],
references: [users_temp.id],
}),
}));
const createSchema = createInsertSchema(gitProvider);

View File

@@ -32,3 +32,4 @@ export * from "./preview-deployments";
export * from "./ai";
export * from "./account";
export * from "./schedule";
export * from "./rollbacks";

View File

@@ -0,0 +1,45 @@
import { relations } from "drizzle-orm";
import { jsonb, pgTable, serial, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { deployments } from "./deployment";
export const rollbacks = pgTable("rollback", {
rollbackId: text("rollbackId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
deploymentId: text("deploymentId")
.notNull()
.references(() => deployments.deploymentId, {
onDelete: "cascade",
}),
version: serial(),
image: text("image"),
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
fullContext: jsonb("fullContext"),
});
export type Rollback = typeof rollbacks.$inferSelect;
export const rollbacksRelations = relations(rollbacks, ({ one }) => ({
deployment: one(deployments, {
fields: [rollbacks.deploymentId],
references: [deployments.deploymentId],
}),
}));
export const createRollbackSchema = createInsertSchema(rollbacks).extend({
appName: z.string().min(1),
});
export const updateRollbackSchema = createRollbackSchema.extend({
rollbackId: z.string().min(1),
});
export const apiFindOneRollback = z.object({
rollbackId: z.string().min(1),
});

View File

@@ -56,7 +56,7 @@ export const users_temp = pgTable("user_temp", {
letsEncryptEmail: text("letsEncryptEmail"),
sshPrivateKey: text("sshPrivateKey"),
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
logCleanupCron: text("logCleanupCron"),
logCleanupCron: text("logCleanupCron").default("0 0 * * *"),
role: text("role").notNull().default("user"),
// Metrics
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),

View File

@@ -32,6 +32,7 @@ export * from "./services/gitea";
export * from "./services/server";
export * from "./services/schedule";
export * from "./services/application";
export * from "./services/rollbacks";
export * from "./utils/databases/rebuild";
export * from "./setup/config-paths";
export * from "./setup/postgres-setup";

View File

@@ -18,9 +18,6 @@ const { handler, api } = betterAuth({
provider: "pg",
schema: schema,
}),
logger: {
disabled: process.env.NODE_ENV === "production",
},
appName: "Dokploy",
socialProviders: {
github: {

View File

@@ -60,6 +60,7 @@ import {
updatePreviewDeployment,
} from "./preview-deployment";
import { validUniqueServerAppName } from "./project";
import { createRollback } from "./rollbacks";
export type Application = typeof applications.$inferSelect;
export const createApplication = async (
@@ -214,6 +215,17 @@ export const deployApplication = async ({
await updateDeploymentStatus(deployment.deploymentId, "done");
await updateApplicationStatus(applicationId, "done");
if (application.rollbackActive) {
const tagImage =
application.sourceType === "docker"
? application.dockerImage
: application.appName;
await createRollback({
appName: tagImage || "",
deploymentId: deployment.deploymentId,
});
}
await sendBuildSuccessNotifications({
projectName: application.project.name,
applicationName: application.name,
@@ -338,6 +350,17 @@ export const deployRemoteApplication = async ({
await updateDeploymentStatus(deployment.deploymentId, "done");
await updateApplicationStatus(applicationId, "done");
if (application.rollbackActive) {
const tagImage =
application.sourceType === "docker"
? application.dockerImage
: application.appName;
await createRollback({
appName: tagImage || "",
deploymentId: deployment.deploymentId,
});
}
await sendBuildSuccessNotifications({
projectName: application.project.name,
applicationName: application.name,

View File

@@ -13,6 +13,7 @@ export type Bitbucket = typeof bitbucket.$inferSelect;
export const createBitbucket = async (
input: typeof apiCreateBitbucket._type,
organizationId: string,
userId: string,
) => {
return await db.transaction(async (tx) => {
const newGitProvider = await tx
@@ -21,6 +22,7 @@ export const createBitbucket = async (
providerType: "bitbucket",
organizationId: organizationId,
name: input.name,
userId: userId,
})
.returning()
.then((response) => response[0]);

View File

@@ -0,0 +1,634 @@
// CDN Provider Interface
export interface CDNProvider {
name: string;
displayName: string;
checkIp: (ip: string) => boolean;
warningMessage: string;
}
const isIPInCIDR = (ip: string, cidr: string): boolean => {
const [network, prefixLength] = cidr.split("/");
if (!network || !prefixLength) return false;
const prefix = Number.parseInt(prefixLength, 10);
// Convert IP addresses to 32-bit integers
const ipToInt = (ipStr: string): number => {
return (
ipStr
.split(".")
.reduce((acc, octet) => (acc << 8) + Number.parseInt(octet, 10), 0) >>>
0
);
};
const ipInt = ipToInt(ip);
const networkInt = ipToInt(network);
const mask = (0xffffffff << (32 - prefix)) >>> 0;
return (ipInt & mask) === (networkInt & mask);
};
// Cloudflare IP ranges
// https://www.cloudflare.com/ips-v4
const CLOUDFLARE_IP_RANGES = [
"173.245.48.0/20",
"103.21.244.0/22",
"103.22.200.0/22",
"103.31.4.0/22",
"141.101.64.0/18",
"108.162.192.0/18",
"190.93.240.0/20",
"188.114.96.0/20",
"197.234.240.0/22",
"198.41.128.0/17",
"162.158.0.0/15",
"104.16.0.0/13",
"104.24.0.0/14",
"172.64.0.0/13",
"131.0.72.0/22",
];
// Fastly IP ranges
// https://api.fastly.com/public-ip-list
const FASTLY_IP_RANGES = [
"23.235.32.0/20",
"43.249.72.0/22",
"103.244.50.0/24",
"103.245.222.0/23",
"103.245.224.0/24",
"104.156.80.0/20",
"140.248.64.0/18",
"140.248.128.0/17",
"146.75.0.0/17",
"151.101.0.0/16",
"157.52.64.0/18",
"167.82.0.0/17",
"167.82.128.0/20",
"167.82.160.0/20",
"167.82.224.0/20",
"172.111.64.0/18",
"185.31.16.0/22",
"199.27.72.0/21",
"199.232.0.0/16",
];
// Bunny CDN IP addresses
// https://bunnycdn.com/api/system/edgeserverlist
const BUNNY_CDN_IPS = new Set([
"89.187.188.227",
"89.187.188.228",
"139.180.134.196",
"89.38.96.158",
"89.187.162.249",
"89.187.162.242",
"185.102.217.65",
"185.93.1.243",
"156.146.40.49",
"185.59.220.199",
"185.59.220.198",
"195.181.166.158",
"185.180.12.68",
"138.199.24.209",
"138.199.24.211",
"79.127.216.111",
"79.127.216.112",
"89.187.169.47",
"138.199.24.218",
"185.40.106.117",
"200.25.45.4",
"200.25.57.5",
"193.162.131.1",
"200.25.11.8",
"200.25.53.5",
"200.25.13.98",
"41.242.2.18",
"200.25.62.5",
"200.25.38.69",
"200.25.42.70",
"200.25.36.166",
"195.206.229.106",
"194.242.11.186",
"185.164.35.8",
"94.20.154.22",
"185.93.1.244",
"156.59.145.154",
"143.244.49.177",
"138.199.46.66",
"138.199.37.227",
"138.199.37.231",
"138.199.37.230",
"138.199.37.229",
"138.199.46.69",
"138.199.46.68",
"138.199.46.67",
"185.93.1.246",
"138.199.37.232",
"195.181.163.196",
"107.182.163.162",
"195.181.163.195",
"84.17.46.53",
"212.102.40.114",
"84.17.46.54",
"138.199.40.58",
"143.244.38.134",
"143.244.38.136",
"185.152.64.17",
"84.17.59.115",
"89.187.165.194",
"138.199.15.193",
"89.35.237.170",
"37.19.216.130",
"185.93.1.247",
"185.93.3.244",
"143.244.49.179",
"143.244.49.180",
"138.199.9.104",
"185.152.66.243",
"143.244.49.178",
"169.150.221.147",
"200.25.18.73",
"84.17.63.178",
"200.25.32.131",
"37.19.207.34",
"192.189.65.146",
"143.244.45.177",
"185.93.1.249",
"185.93.1.250",
"169.150.215.115",
"209.177.87.197",
"156.146.56.162",
"156.146.56.161",
"185.93.2.246",
"185.93.2.245",
"212.102.40.113",
"185.93.2.244",
"143.244.50.82",
"143.244.50.83",
"156.146.56.163",
"129.227.217.178",
"129.227.217.179",
"200.25.69.94",
"128.1.52.179",
"200.25.16.103",
"15.235.54.226",
"102.67.138.155",
"156.146.43.65",
"195.181.163.203",
"195.181.163.202",
"156.146.56.169",
"156.146.56.170",
"156.146.56.166",
"156.146.56.171",
"169.150.207.210",
"156.146.56.167",
"143.244.50.84",
"143.244.50.85",
"143.244.50.86",
"143.244.50.87",
"156.146.56.168",
"169.150.207.211",
"212.102.50.59",
"146.185.248.15",
"143.244.50.90",
"143.244.50.91",
"143.244.50.88",
"143.244.50.209",
"143.244.50.213",
"143.244.50.214",
"143.244.49.183",
"143.244.50.89",
"143.244.50.210",
"143.244.50.211",
"143.244.50.212",
"5.42.206.66",
"94.46.27.186",
"169.150.207.213",
"169.150.207.214",
"169.150.207.215",
"169.150.207.212",
"169.150.219.114",
"169.150.202.210",
"169.150.242.193",
"185.93.1.251",
"169.150.207.216",
"169.150.207.217",
"169.150.238.19",
"102.219.126.20",
"156.59.66.182",
"122.10.251.130",
"185.24.11.18",
"138.199.36.7",
"138.199.36.8",
"138.199.36.9",
"138.199.36.10",
"138.199.36.11",
"138.199.37.225",
"84.17.46.49",
"84.17.37.217",
"169.150.225.35",
"169.150.225.36",
"169.150.225.37",
"169.150.225.38",
"169.150.225.39",
"169.150.225.34",
"169.150.236.97",
"169.150.236.98",
"169.150.236.99",
"169.150.236.100",
"93.189.63.149",
"143.244.56.49",
"143.244.56.50",
"143.244.56.51",
"169.150.247.40",
"169.150.247.33",
"169.150.247.34",
"169.150.247.35",
"169.150.247.36",
"169.150.247.37",
"169.150.247.38",
"169.150.247.39",
"38.142.94.218",
"87.249.137.52",
"38.104.169.186",
"66.181.163.74",
"84.17.38.227",
"84.17.38.228",
"84.17.38.229",
"84.17.38.230",
"84.17.38.231",
"84.17.38.232",
"169.150.225.41",
"169.150.225.42",
"169.150.249.162",
"169.150.249.163",
"169.150.249.164",
"169.150.249.165",
"169.150.249.166",
"169.150.249.167",
"169.150.249.168",
"169.150.249.169",
"185.131.64.124",
"103.112.0.22",
"37.236.234.2",
"169.150.252.209",
"212.102.46.118",
"192.169.120.162",
"93.180.217.214",
"37.19.203.178",
"107.155.47.146",
"193.201.190.174",
"156.59.95.218",
"213.170.143.139",
"129.227.186.154",
"195.238.127.98",
"200.25.22.6",
"204.16.244.92",
"200.25.70.101",
"200.25.66.100",
"139.180.209.182",
"103.108.231.41",
"103.108.229.5",
"103.216.220.9",
"169.150.225.40",
"212.102.50.49",
"212.102.50.52",
"109.61.83.242",
"109.61.83.243",
"212.102.50.50",
"169.150.225.43",
"45.125.247.57",
"103.235.199.170",
"128.1.35.170",
"38.32.110.58",
"169.150.220.228",
"169.150.220.229",
"169.150.220.230",
"169.150.220.231",
"138.199.4.179",
"207.211.214.145",
"109.61.86.193",
"103.214.20.95",
"178.175.134.51",
"138.199.4.178",
"172.255.253.140",
"185.24.11.19",
"109.61.83.244",
"109.61.83.245",
"84.17.38.250",
"84.17.38.251",
"146.59.69.202",
"146.70.80.218",
"200.25.80.74",
"79.127.213.214",
"79.127.213.215",
"79.127.213.216",
"79.127.213.217",
"195.69.140.112",
"109.61.83.247",
"109.61.83.246",
"185.93.2.248",
"109.61.83.249",
"109.61.83.250",
"109.61.83.251",
"46.199.75.115",
"141.164.35.160",
"109.61.83.97",
"109.61.83.98",
"109.61.83.99",
"129.227.179.18",
"185.180.14.250",
"152.89.160.26",
"5.189.202.62",
"98.98.242.142",
"156.59.92.126",
"84.17.59.117",
"79.127.216.66",
"79.127.204.113",
"79.127.237.132",
"169.150.236.104",
"169.150.236.105",
"37.27.135.61",
"158.51.123.205",
"156.146.43.70",
"156.146.43.71",
"156.146.43.72",
"180.149.231.175",
"185.93.2.243",
"143.244.56.52",
"143.244.56.53",
"143.244.56.54",
"143.244.56.55",
"143.244.56.56",
"143.244.56.57",
"143.244.56.58",
"144.76.236.44",
"88.198.57.50",
"78.46.69.199",
"136.243.16.49",
"138.201.86.122",
"136.243.42.90",
"88.99.95.221",
"178.63.2.112",
"5.9.98.45",
"136.243.42.10",
"169.150.236.106",
"169.150.236.107",
"185.93.1.242",
"185.93.1.245",
"143.244.60.193",
"195.181.163.194",
"79.127.188.193",
"79.127.188.196",
"79.127.188.194",
"79.127.188.195",
"104.166.144.106",
"156.59.126.78",
"185.135.85.154",
"38.54.5.37",
"38.54.3.92",
"185.165.170.74",
"207.121.80.118",
"207.121.46.228",
"207.121.46.236",
"207.121.46.244",
"207.121.46.252",
"216.202.235.164",
"207.121.46.220",
"207.121.75.132",
"207.121.80.12",
"207.121.80.172",
"207.121.90.60",
"207.121.90.68",
"207.121.97.204",
"207.121.90.252",
"207.121.97.236",
"207.121.99.12",
"138.199.24.219",
"185.93.2.251",
"138.199.46.65",
"207.121.41.196",
"207.121.99.20",
"207.121.99.36",
"207.121.99.44",
"207.121.99.52",
"207.121.99.60",
"207.121.23.68",
"207.121.23.124",
"207.121.23.244",
"207.121.23.180",
"207.121.23.188",
"207.121.23.196",
"207.121.23.204",
"207.121.24.52",
"207.121.24.60",
"207.121.24.68",
"207.121.24.76",
"207.121.24.92",
"207.121.24.100",
"207.121.24.108",
"207.121.24.116",
"154.95.86.76",
"5.9.99.73",
"78.46.92.118",
"144.76.65.213",
"78.46.156.89",
"88.198.9.155",
"144.76.79.22",
"103.1.215.93",
"103.137.12.33",
"103.107.196.31",
"116.90.72.155",
"103.137.14.5",
"116.90.75.65",
"37.19.207.37",
"208.83.234.224",
"116.202.155.146",
"116.202.193.178",
"116.202.224.168",
"188.40.126.227",
"88.99.26.189",
"168.119.39.238",
"88.99.26.97",
"168.119.12.188",
"176.9.139.55",
"142.132.223.79",
"142.132.223.80",
"142.132.223.81",
"46.4.116.17",
"46.4.119.81",
"167.235.114.167",
"159.69.68.171",
"178.63.21.52",
"46.4.120.152",
"116.202.80.247",
"5.9.71.119",
"195.201.11.156",
"78.46.123.17",
"46.4.113.143",
"136.243.2.236",
"195.201.81.217",
"148.251.42.123",
"94.130.68.122",
"88.198.22.103",
"46.4.102.90",
"157.90.180.205",
"162.55.135.11",
"195.201.109.59",
"148.251.41.244",
"116.202.235.16",
"128.140.70.141",
"78.46.74.86",
"78.46.74.85",
"178.63.41.242",
"178.63.41.247",
"178.63.41.234",
"104.237.53.74",
"104.237.54.154",
"104.237.51.58",
"64.185.235.90",
"64.185.234.114",
"64.185.232.194",
"64.185.232.178",
"64.185.232.82",
"103.60.15.169",
"103.60.15.170",
"103.60.15.171",
"103.60.15.172",
"103.60.15.173",
"103.60.15.162",
"103.60.15.163",
"103.60.15.164",
"103.60.15.165",
"103.60.15.166",
"103.60.15.167",
"103.60.15.168",
"109.248.43.116",
"109.248.43.117",
"109.248.43.162",
"109.248.43.163",
"109.248.43.164",
"109.248.43.165",
"49.12.71.27",
"49.12.0.158",
"78.47.94.156",
"109.248.43.159",
"109.248.43.160",
"109.248.43.208",
"109.248.43.179",
"109.248.43.232",
"109.248.43.231",
"109.248.43.241",
"109.248.43.236",
"109.248.43.240",
"109.248.43.103",
"116.202.118.194",
"116.202.80.29",
"159.69.57.80",
"139.180.129.216",
"139.99.174.7",
"89.187.169.18",
"89.187.179.7",
"143.244.62.213",
"185.93.3.246",
"195.181.163.198",
"185.152.64.19",
"84.17.37.211",
"212.102.50.54",
"212.102.46.115",
"143.244.38.135",
"169.150.238.21",
"169.150.207.51",
"169.150.207.49",
"84.17.38.226",
"84.17.38.225",
"37.19.222.248",
"37.19.222.249",
"169.150.247.139",
"169.150.247.177",
"169.150.247.178",
"169.150.213.49",
"212.102.46.119",
"84.17.38.234",
"84.17.38.233",
"169.150.247.179",
"169.150.247.180",
"169.150.247.181",
"169.150.247.182",
"169.150.247.183",
"169.150.247.138",
"169.150.247.184",
"169.150.247.185",
"156.146.58.83",
"212.102.43.88",
"89.187.169.26",
"109.61.89.57",
"109.61.89.58",
"109.61.83.241",
"84.17.38.243",
"84.17.38.244",
"84.17.38.246",
"84.17.38.247",
"84.17.38.245",
"143.244.38.129",
"84.17.38.248",
"89.187.176.34",
"185.152.64.23",
"79.127.213.209",
"79.127.213.210",
"84.17.37.209",
"156.146.43.68",
"185.93.3.243",
"79.127.219.198",
"138.199.33.57",
"79.127.242.89",
"138.199.4.136",
"169.150.220.235",
"138.199.4.129",
"138.199.4.177",
"37.19.222.34",
"46.151.193.85",
"212.104.158.17",
"212.104.158.18",
"212.104.158.19",
"212.104.158.20",
"212.104.158.21",
"212.104.158.22",
"212.104.158.24",
"212.104.158.26",
"79.127.237.134",
"89.187.184.177",
"89.187.184.179",
"89.187.184.173",
"89.187.184.178",
"89.187.184.176",
]);
const CDN_PROVIDERS: CDNProvider[] = [
{
name: "cloudflare",
displayName: "Cloudflare",
checkIp: (ip: string) =>
CLOUDFLARE_IP_RANGES.some((range) => isIPInCIDR(ip, range)),
warningMessage:
"Domain is behind Cloudflare - actual IP is masked by Cloudflare proxy",
},
{
name: "bunnycdn",
displayName: "Bunny CDN",
checkIp: (ip: string) => BUNNY_CDN_IPS.has(ip),
warningMessage:
"Domain is behind Bunny CDN - actual IP is masked by CDN proxy",
},
{
name: "fastly",
displayName: "Fastly",
checkIp: (ip: string) =>
FASTLY_IP_RANGES.some((range) => isIPInCIDR(ip, range)),
warningMessage:
"Domain is behind Fastly - actual IP is masked by CDN proxy",
},
];
export const detectCDNProvider = (ip: string): CDNProvider | null => {
return CDN_PROVIDERS.find((provider) => provider.checkIp(ip)) || null;
};

View File

@@ -31,23 +31,38 @@ import {
updatePreviewDeployment,
} from "./preview-deployment";
import { findScheduleById } from "./schedule";
import { removeRollbackById } from "./rollbacks";
export type Deployment = typeof deployments.$inferSelect;
export const findDeploymentById = async (applicationId: string) => {
const application = await db.query.deployments.findFirst({
where: eq(deployments.applicationId, applicationId),
export const findDeploymentById = async (deploymentId: string) => {
const deployment = await db.query.deployments.findFirst({
where: eq(deployments.deploymentId, deploymentId),
with: {
application: true,
},
});
if (!application) {
if (!deployment) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Deployment not found",
});
}
return application;
return deployment;
};
export const findDeploymentByApplicationId = async (applicationId: string) => {
const deployment = await db.query.deployments.findFirst({
where: eq(deployments.applicationId, applicationId),
});
if (!deployment) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Deployment not found",
});
}
return deployment;
};
export const createDeployment = async (
@@ -481,6 +496,9 @@ const getDeploymentsByType = async (
const deploymentList = await db.query.deployments.findMany({
where: eq(deployments[`${type}Id`], id),
orderBy: desc(deployments.createdAt),
with: {
rollback: true,
},
});
return deploymentList;
};
@@ -515,6 +533,9 @@ const removeLastTenDeployments = async (
let command = "";
for (const oldDeployment of deploymentsToDelete) {
const logPath = path.join(oldDeployment.logPath);
if (oldDeployment.rollbackId) {
await removeRollbackById(oldDeployment.rollbackId);
}
command += `
rm -rf ${logPath};
@@ -525,8 +546,11 @@ const removeLastTenDeployments = async (
await execAsyncRemote(serverId, command);
} else {
for (const oldDeployment of deploymentsToDelete) {
if (oldDeployment.rollbackId) {
await removeRollbackById(oldDeployment.rollbackId);
}
const logPath = path.join(oldDeployment.logPath);
if (existsSync(logPath)) {
if (existsSync(logPath) && !oldDeployment.errorMessage) {
await fsPromises.unlink(logPath);
}
await removeDeployment(oldDeployment.deploymentId);

View File

@@ -8,6 +8,7 @@ import { eq } from "drizzle-orm";
import { type apiCreateDomain, domains } from "../db/schema";
import { findUserById } from "./admin";
import { findApplicationById } from "./application";
import { detectCDNProvider } from "./cdn";
import { findServerById } from "./server";
export type Domain = typeof domains.$inferSelect;
@@ -142,28 +143,6 @@ export const getDomainHost = (domain: Domain) => {
const resolveDns = promisify(dns.resolve4);
// Cloudflare IP ranges (simplified - these are some common ones)
const CLOUDFLARE_IPS = [
"172.67.",
"104.21.",
"104.16.",
"104.17.",
"104.18.",
"104.19.",
"104.20.",
"104.22.",
"104.23.",
"104.24.",
"104.25.",
"104.26.",
"104.27.",
"104.28.",
];
const isCloudflareIp = (ip: string) => {
return CLOUDFLARE_IPS.some((range) => ip.startsWith(range));
};
export const validateDomain = async (
domain: string,
expectedIp?: string,
@@ -172,6 +151,7 @@ export const validateDomain = async (
resolvedIp?: string;
error?: string;
isCloudflare?: boolean;
cdnProvider?: string;
}> => {
try {
// Remove protocol and path if present
@@ -182,17 +162,18 @@ export const validateDomain = async (
const resolvedIps = ips.map((ip) => ip.toString());
// Check if it's a Cloudflare IP
const behindCloudflare = ips.some((ip) => isCloudflareIp(ip));
// Check if any IP belongs to a CDN provider
const cdnProvider = ips
.map((ip) => detectCDNProvider(ip))
.find((provider) => provider !== null);
// If behind Cloudflare, we consider it valid but inform the user
if (behindCloudflare) {
// If behind a CDN, we consider it valid but inform the user
if (cdnProvider) {
return {
isValid: true,
resolvedIp: resolvedIps.join(", "),
isCloudflare: true,
error:
"Domain is behind Cloudflare - actual IP is masked by Cloudflare proxy",
cdnProvider: cdnProvider.displayName,
error: cdnProvider.warningMessage,
};
}

View File

@@ -12,6 +12,7 @@ export type Gitea = typeof gitea.$inferSelect;
export const createGitea = async (
input: typeof apiCreateGitea._type,
organizationId: string,
userId: string,
) => {
return await db.transaction(async (tx) => {
const newGitProvider = await tx
@@ -20,6 +21,7 @@ export const createGitea = async (
providerType: "gitea",
organizationId: organizationId,
name: input.name,
userId: userId,
})
.returning()
.then((response) => response[0]);

View File

@@ -13,6 +13,7 @@ export type Github = typeof github.$inferSelect;
export const createGithub = async (
input: typeof apiCreateGithub._type,
organizationId: string,
userId: string,
) => {
return await db.transaction(async (tx) => {
const newGitProvider = await tx
@@ -21,6 +22,7 @@ export const createGithub = async (
providerType: "github",
organizationId: organizationId,
name: input.name,
userId: userId,
})
.returning()
.then((response) => response[0]);

View File

@@ -12,6 +12,7 @@ export type Gitlab = typeof gitlab.$inferSelect;
export const createGitlab = async (
input: typeof apiCreateGitlab._type,
organizationId: string,
userId: string,
) => {
return await db.transaction(async (tx) => {
const newGitProvider = await tx
@@ -20,6 +21,7 @@ export const createGitlab = async (
providerType: "gitlab",
organizationId: organizationId,
name: input.name,
userId: userId,
})
.returning()
.then((response) => response[0]);

View File

@@ -0,0 +1,191 @@
import { eq } from "drizzle-orm";
import { db } from "../db";
import {
type createRollbackSchema,
rollbacks,
deployments as deploymentsSchema,
} from "../db/schema";
import type { z } from "zod";
import { findApplicationById } from "./application";
import { getRemoteDocker } from "../utils/servers/remote-docker";
import type { ApplicationNested } from "../utils/builders";
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
import type { CreateServiceOptions } from "dockerode";
import { findDeploymentById } from "./deployment";
export const createRollback = async (
input: z.infer<typeof createRollbackSchema>,
) => {
await db.transaction(async (tx) => {
const rollback = await tx
.insert(rollbacks)
.values(input)
.returning()
.then((res) => res[0]);
if (!rollback) {
throw new Error("Failed to create rollback");
}
const tagImage = `${input.appName}:v${rollback.version}`;
const deployment = await findDeploymentById(rollback.deploymentId);
if (!deployment?.applicationId) {
throw new Error("Deployment not found");
}
const {
deployments: _,
bitbucket,
github,
gitlab,
gitea,
...rest
} = await findApplicationById(deployment.applicationId);
await tx
.update(rollbacks)
.set({
image: tagImage,
fullContext: JSON.stringify(rest),
})
.where(eq(rollbacks.rollbackId, rollback.rollbackId));
// Update the deployment to reference this rollback
await tx
.update(deploymentsSchema)
.set({
rollbackId: rollback.rollbackId,
})
.where(eq(deploymentsSchema.deploymentId, rollback.deploymentId));
await createRollbackImage(rest, tagImage);
return rollback;
});
};
const findRollbackById = async (rollbackId: string) => {
const result = await db.query.rollbacks.findFirst({
where: eq(rollbacks.rollbackId, rollbackId),
});
if (!result) {
throw new Error("Rollback not found");
}
return result;
};
const createRollbackImage = async (
application: ApplicationNested,
tagImage: string,
) => {
const docker = await getRemoteDocker(application.serverId);
const appTagName =
application.sourceType === "docker"
? application.dockerImage
: `${application.appName}:latest`;
const result = docker.getImage(appTagName || "");
const [repo, version] = tagImage.split(":");
await result.tag({
repo,
tag: version,
});
};
const deleteRollbackImage = async (image: string, serverId?: string | null) => {
const command = `docker image rm ${image} --force`;
if (serverId) {
await execAsyncRemote(command, serverId);
} else {
await execAsync(command);
}
};
export const removeRollbackById = async (rollbackId: string) => {
const rollback = await findRollbackById(rollbackId);
if (!rollback) {
throw new Error("Rollback not found");
}
if (rollback?.image) {
try {
const deployment = await findDeploymentById(rollback.deploymentId);
if (!deployment?.applicationId) {
throw new Error("Deployment not found");
}
const application = await findApplicationById(deployment.applicationId);
await deleteRollbackImage(rollback.image, application.serverId);
await db
.delete(rollbacks)
.where(eq(rollbacks.rollbackId, rollbackId))
.returning()
.then((res) => res[0]);
} catch (error) {
console.error(error);
}
}
return rollback;
};
export const rollback = async (rollbackId: string) => {
const result = await findRollbackById(rollbackId);
const deployment = await findDeploymentById(result.deploymentId);
if (!deployment?.applicationId) {
throw new Error("Deployment not found");
}
const application = await findApplicationById(deployment.applicationId);
await rollbackApplication(
application.appName,
result.image || "",
application.serverId,
);
};
const rollbackApplication = async (
appName: string,
image: string,
serverId?: string | null,
) => {
const docker = await getRemoteDocker(serverId);
const settings: CreateServiceOptions = {
Name: appName,
TaskTemplate: {
ContainerSpec: {
Image: image,
},
},
};
try {
const service = docker.getService(appName);
const inspect = await service.inspect();
await service.update({
version: Number.parseInt(inspect.Version.Index),
...settings,
TaskTemplate: {
...settings.TaskTemplate,
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
},
});
} catch (_error: unknown) {
await docker.createService(settings);
}
};

View File

@@ -116,7 +116,7 @@ if [ "$OS_TYPE" = 'amzn' ]; then
fi
case "$OS_TYPE" in
arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed | almalinux | amzn | alpine) ;;
arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed | almalinux | opencloudos | amzn | alpine) ;;
*)
echo "This script only supports Debian, Redhat, Arch Linux, Alpine Linux, or SLES based operating systems for now."
exit
@@ -367,7 +367,7 @@ const installUtilities = () => `
DEBIAN_FRONTEND=noninteractive apt-get update -y >/dev/null
DEBIAN_FRONTEND=noninteractive apt-get install -y unzip curl wget git git-lfs jq openssl >/dev/null
;;
centos | fedora | rhel | ol | rocky | almalinux | amzn)
centos | fedora | rhel | ol | rocky | almalinux | opencloudos | amzn)
if [ "$OS_TYPE" = "amzn" ]; then
dnf install -y wget git git-lfs jq openssl >/dev/null
else
@@ -418,6 +418,16 @@ if ! [ -x "$(command -v docker)" ]; then
systemctl start docker >/dev/null 2>&1
systemctl enable docker >/dev/null 2>&1
;;
"opencloudos")
dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo >/dev/null 2>&1
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1
if ! [ -x "$(command -v docker)" ]; then
echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue."
exit 1
fi
systemctl start docker >/dev/null 2>&1
systemctl enable docker >/dev/null 2>&1
;;
"alpine")
apk add docker docker-cli-compose >/dev/null 2>&1
rc-update add docker default >/dev/null 2>&1

View File

@@ -139,6 +139,8 @@ export const initializeTraefik = async ({
const newContainer = docker.getContainer(containerName);
await newContainer.start();
console.log("Traefik container started successfully after retry");
} else {
throw error;
}
}
} catch (error) {

View File

@@ -37,7 +37,8 @@ export const startLogCleanup = async (
}
return true;
} catch (_) {
} catch (error) {
console.error("Error starting log cleanup:", error);
return false;
}
};

View File

@@ -88,6 +88,7 @@ export const initCronJobs = async () => {
}
if (admin?.user.logCleanupCron) {
console.log("Starting log requests cleanup", admin.user.logCleanupCron);
await startLogCleanup(admin.user.logCleanupCron);
}
};

View File

@@ -2,7 +2,10 @@ import { createHash } from "node:crypto";
import type { WriteStream } from "node:fs";
import { nanoid } from "nanoid";
import type { ApplicationNested } from ".";
import { prepareEnvironmentVariables } from "../docker/utils";
import {
parseEnvironmentKeyValuePair,
prepareEnvironmentVariables,
} from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory";
import { execAsync } from "../process/execAsync";
import { spawnAsync } from "../process/spawnAsync";
@@ -81,10 +84,10 @@ export const buildRailpack = async (
// Add secrets properly formatted
const env: { [key: string]: string } = {};
for (const envVar of envVariables) {
const [key, value] = envVar.split("=");
for (const pair of envVariables) {
const [key, value] = parseEnvironmentKeyValuePair(pair);
if (key && value) {
buildArgs.push("--secret", `id=${key},env='${key}'`);
buildArgs.push("--secret", `id=${key},env=${key}`);
env[key] = value;
}
}
@@ -161,11 +164,11 @@ export const getRailpackCommand = (
// Add secrets properly formatted
const exportEnvs = [];
for (const envVar of envVariables) {
const [key, value] = envVar.split("=");
for (const pair of envVariables) {
const [key, value] = parseEnvironmentKeyValuePair(pair);
if (key && value) {
buildArgs.push("--secret", `id=${key},env='${key}'`);
exportEnvs.push(`export ${key}=${value}`);
buildArgs.push("--secret", `id=${key},env=${key}`);
exportEnvs.push(`export ${key}='${value}'`);
}
}

View File

@@ -212,7 +212,7 @@ export const cleanUpDockerBuilder = async (serverId?: string) => {
};
export const cleanUpSystemPrune = async (serverId?: string) => {
const command = "docker system prune --all --force --volumes";
const command = "docker system prune --force --volumes";
if (serverId) {
await execAsyncRemote(serverId, command);
} else {
@@ -279,6 +279,17 @@ export const prepareEnvironmentVariables = (
return resolvedVars;
};
export const parseEnvironmentKeyValuePair = (
pair: string,
): [string, string] => {
const [key, ...valueParts] = pair.split("=");
if (!key || !valueParts.length) {
throw new Error(`Invalid environment variable pair: ${pair}`);
}
return [key, valueParts.join("")];
};
export const getEnviromentVariablesObject = (
input: string | null,
projectEnv?: string | null,
@@ -288,7 +299,7 @@ export const getEnviromentVariablesObject = (
const jsonObject: Record<string, string> = {};
for (const pair of envs) {
const [key, value] = pair.split("=");
const [key, value] = parseEnvironmentKeyValuePair(pair);
if (key && value) {
jsonObject[key] = value;
}

View File

@@ -73,7 +73,7 @@ export const cloneBitbucketRepository = async (
});
writeStream.write(`\nCloned ${repoclone} to ${outputPath}: ✅\n`);
} catch (error) {
writeStream.write(`ERROR Clonning: ${error}: ❌`);
writeStream.write(`ERROR Cloning: ${error}: ❌`);
throw error;
} finally {
writeStream.end();

View File

@@ -155,7 +155,7 @@ export const getGiteaCloneCommand = async (
const cloneCommand = `
rm -rf ${outputPath};
mkdir -p ${outputPath};
if ! git clone --branch ${giteaBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} >> ${logPath} 2>&1; then
echo "❌ [ERROR] Failed to clone the repository ${repoClone}" >> ${logPath};
exit 1;
@@ -232,7 +232,7 @@ export const cloneGiteaRepository = async (
);
writeStream.write(`\nCloned ${repoClone}: ✅\n`);
} catch (error) {
writeStream.write(`ERROR Clonning: ${error}: ❌`);
writeStream.write(`ERROR Cloning: ${error}: ❌`);
throw error;
} finally {
writeStream.end();

View File

@@ -149,7 +149,7 @@ export const cloneGithubRepository = async ({
});
writeStream.write(`\nCloned ${repoclone}: ✅\n`);
} catch (error) {
writeStream.write(`ERROR Clonning: ${error}: ❌`);
writeStream.write(`ERROR Cloning: ${error}: ❌`);
throw error;
} finally {
writeStream.end();

View File

@@ -132,7 +132,7 @@ export const cloneGitlabRepository = async (
const cloneUrl = `https://oauth2:${gitlab?.accessToken}@${repoclone}`;
try {
writeStream.write(`\nClonning Repo ${repoclone} to ${outputPath}: ✅\n`);
writeStream.write(`\nCloning Repo ${repoclone} to ${outputPath}: ✅\n`);
const cloneArgs = [
"clone",
"--branch",
@@ -152,7 +152,7 @@ export const cloneGitlabRepository = async (
});
writeStream.write(`\nCloned ${repoclone}: ✅\n`);
} catch (error) {
writeStream.write(`ERROR Clonning: ${error}: ❌`);
writeStream.write(`ERROR Cloning: ${error}: ❌`);
throw error;
} finally {
writeStream.end();