Compare commits

...

58 Commits

Author SHA1 Message Date
Mauricio Siu
17e9154887 Merge pull request #2257 from Dokploy/fix/send-build-error-on-remote-servers
Fix/send build error on remote servers
2025-07-28 01:52:57 -06:00
Mauricio Siu
2442494096 fix(application): simplify error message handling in deployment notifications 2025-07-28 01:51:21 -06:00
Mauricio Siu
bac2afb423 refactor(application): exclude appName from updateApplication data to streamline database updates 2025-07-28 01:50:58 -06:00
Mauricio Siu
4e9630e976 Merge pull request #2256 from Dokploy/feat/enhancements-cloud-version-ui
feat(dashboard): enhance application and database forms with tooltips…
2025-07-28 01:50:26 -06:00
Mauricio Siu
558f6aecae fix(application): improve error handling and notification messages during deployment 2025-07-28 01:48:33 -06:00
Mauricio Siu
c3e2b0d0f1 feat(dashboard): enhance application and database forms with tooltips for better user guidance 2025-07-28 01:12:43 -06:00
Mauricio Siu
11d584316a chore(package): bump version to v0.24.5 2025-07-28 00:57:44 -06:00
Mauricio Siu
f78dc555b2 Merge pull request #2244 from jhon2c/feat/improve-server-ux
feat(ux): Improve UX Based on Community Feedback
2025-07-27 23:21:24 -06:00
Mauricio Siu
5812b12a59 Merge pull request #2236 from masesisaac/canary
fix(dashboard): Update app security view to hide password
2025-07-27 23:16:07 -06:00
Mauricio Siu
7301d15e8f Merge pull request #2230 from amustapha/patch-1
fix: wrap user prompt in ai modal to prevent text stretch
2025-07-27 23:15:01 -06:00
Mauricio Siu
f79796a6c8 Merge pull request #2188 from Marukome0743/vscode
chore: add biome settings for vscode editor
2025-07-27 23:14:33 -06:00
Mauricio Siu
4122b37abd Merge pull request #2250 from Dokploy/feat/add-name-field-to-profile
feat(profile): add optional name field to user profile form and schema
2025-07-27 23:13:26 -06:00
Mauricio Siu
79e9593663 feat(profile): add optional name field to user profile form and schema 2025-07-27 23:13:06 -06:00
masesisaac
def3fa0030 fix(security): change password input type to 'password' 2025-07-28 04:58:43 +03:00
autofix-ci[bot]
d561068bcd [autofix.ci] apply automated fixes 2025-07-26 17:26:20 +00:00
Jhon
212c1b2d5f feat(dashboard): show "Action Required" badge for incomplete Git provider setup 2025-07-26 14:18:26 -03:00
Jhon
d3a54172b5 feat(ux): add conditional server selection functionality to application forms 2025-07-26 13:53:28 -03:00
masesisaac
cda33eb291 refactor(dashboard): reorder imports in show-security.tsx for consistency 2025-07-24 17:45:26 +03:00
masesisaac
c178234e53 fix(dashboard): hide basic auth password by default 2025-07-24 17:41:51 +03:00
Abdulhakeem Adetunji Mustapha
329db1fd1a fix: wrap user prompt in ai modal to prevent text stretch 2025-07-23 19:30:47 +01:00
Marukome0743
6efbf030a7 chore: add biome settings for vscode editor 2025-07-23 08:49:59 +09:00
Mauricio Siu
b95dfed8fc chore(package): bump version to v0.24.4 2025-07-20 20:06:47 -06:00
Mauricio Siu
7fe3418d55 Merge pull request #2218 from Dokploy/2179-reloading-traefik-on-the-remote-server-will-cause-traefik-on-the-instance-to-change-accordingly
fix(traefik): remove duplicate file write operation in writeTraefikCo…
2025-07-20 20:05:48 -06:00
Mauricio Siu
288d86c73b fix(traefik): remove duplicate file write operation in writeTraefikConfigInPath function 2025-07-20 20:05:30 -06:00
Mauricio Siu
ffd5ccd386 Merge pull request #2202 from gentslava/feat/traefik-config
feat(config): Traefik
2025-07-20 19:45:53 -06:00
Mauricio Siu
98ddd096e5 Update packages/server/src/setup/traefik-setup.ts 2025-07-20 19:45:41 -06:00
Mauricio Siu
da6cc9fe72 Merge pull request #2190 from Marukome0743/format
chore: version up format.yml actions
2025-07-20 19:44:20 -06:00
Mauricio Siu
22d0af269e Merge pull request #2200 from Marukome0743/server
refactor: lint and sort imports on dokploy/server
2025-07-20 19:42:15 -06:00
Mauricio Siu
f0fdc46de5 Merge pull request #2187 from Marukome0743/v2
chore: upgrade to Biome v2
2025-07-20 19:41:49 -06:00
Mauricio Siu
9aea24115d Merge pull request #2199 from Marukome0743/lint
refactor: lint and sort import on dokploy application
2025-07-20 19:41:02 -06:00
Mauricio Siu
a9ee6c2393 Merge pull request #2194 from Marukome0743/pnpm
chore(package): version up pnpm to v9.12.0
2025-07-20 19:40:09 -06:00
Mauricio Siu
349717044c Merge pull request #2196 from Marukome0743/dispatch
ci: remove custom branch and add workflow_dispatch event
2025-07-20 19:37:27 -06:00
Mauricio Siu
f94f32695f Merge pull request #2195 from Marukome0743/monitoring
chore: remove `apps/monitoring` from `pnpm-workspace.yaml`
2025-07-20 19:37:07 -06:00
Mauricio Siu
37b78ea09c Merge pull request #2217 from Dokploy/2201-daily-docker-cleanup-not-working-on-remote-server
fix(dashboard): update Docker cleanup toggle logic to prioritize serv…
2025-07-20 19:01:46 -06:00
Mauricio Siu
9b89b4631f fix(dashboard): update Docker cleanup toggle logic to prioritize server settings 2025-07-20 19:01:20 -06:00
Mauricio Siu
7100095f2b Merge pull request #2216 from Dokploy/2209-update-s3-destination-form-loses-its-state-when-current-tab-loses-its-focus
fix(dashboard): disable refetch on window focus for destination handling
2025-07-20 18:57:33 -06:00
Mauricio Siu
a36ab65aa6 fix(dashboard): disable refetch on window focus for destination handling 2025-07-20 18:56:35 -06:00
Mauricio Siu
bf81ba20ff Merge pull request #2215 from Dokploy/2197-git-provider-api-undefined_value-error
refactor(auth): simplify user session structure in validateRequest fu…
2025-07-20 18:55:16 -06:00
Mauricio Siu
658a4a9b99 refactor(auth): simplify user session structure in validateRequest function
- Changed user object in mockSession to only include userId, removing email and name for a more streamlined session representation.
2025-07-20 18:54:57 -06:00
Mauricio Siu
47cb096cf3 Merge pull request #2214 from Dokploy/2203-identical-webhook-redeploy-url-after-duplicating-project
feat(project): add refreshToken to application and compose data retri…
2025-07-20 18:45:39 -06:00
Mauricio Siu
f3856722da feat(project): add refreshToken to application and compose data retrieval
- Included refreshToken in the data returned from findApplicationById and findComposeById functions to enhance application state management.
2025-07-20 18:45:18 -06:00
Vyacheslav Scherbinin
a67c3eb979 feat(conf): accessLog filePath 2025-07-16 16:46:47 +07:00
Vyacheslav Scherbinin
aaa205f104 feat(conf): disable sendAnonymousUsage 2025-07-16 12:29:31 +07:00
Marukome0743
cadea7ff28 refactor: lint and sort imports on dokploy/server 2025-07-15 14:22:37 +09:00
Marukome0743
9ab937f726 refactor: lint dokploy application 2025-07-15 14:13:32 +09:00
Marukome0743
d0af517eb7 ci: remove custom branch and add workflow_dispatch event 2025-07-14 19:03:41 +09:00
Marukome0743
66bdf9bf0a chore: remove apps/monitoring from pnpm-workspace.yaml 2025-07-14 18:24:26 +09:00
Marukome0743
d4a3af475a chore(package): version up pnpm to v9.12.0 2025-07-14 15:58:20 +09:00
autofix-ci[bot]
e92a8d7c98 [autofix.ci] apply automated fixes 2025-07-14 15:30:24 +09:00
Marukome0743
c4fec8cee5 chore: upgrade to Biome v2 2025-07-14 15:30:23 +09:00
Marukome0743
55f75bce53 chore: version up format.yml actions 2025-07-14 15:30:06 +09:00
Mauricio Siu
fdc524d79d fix(ui): adjust layout in UpdateServer component
- Removed unnecessary padding from DialogContent for a cleaner appearance.
- Added margin-top to the button container for improved spacing.
2025-07-13 23:37:05 -06:00
Mauricio Siu
93d6662466 docs(preview): update collaborator permission description in preview settings 2025-07-13 23:26:41 -06:00
Mauricio Siu
1977235d31 Merge pull request #2192 from Dokploy/fix/preview-deployments-public-repos
feat(preview): add collaborator permission requirement for preview de…
2025-07-13 23:20:51 -06:00
Mauricio Siu
1dd713a1d1 fix(deploy): change preview deployment limit check to be exclusive 2025-07-13 23:20:23 -06:00
Mauricio Siu
18b65f28f2 chore(package): bump version to v0.24.3 and comment out unused trustedOrigins function in auth.ts 2025-07-13 23:19:31 -06:00
Mauricio Siu
666db23b8e test: add previewRequireCollaboratorPermissions field to drop and traefik test cases 2025-07-13 23:17:32 -06:00
Mauricio Siu
2ca5321fdc feat(preview): add collaborator permission requirement for preview deployments
- Introduced a new boolean field `previewRequireCollaboratorPermissions` in the application schema to enforce permission checks for preview deployments.
- Updated the UI to include a toggle for this setting in the preview deployment settings.
- Enhanced GitHub deployment handler to validate PR authors against the required permissions, blocking unauthorized deployments and providing security notifications.
- Added SQL migration to update the database schema accordingly.
2025-07-13 23:12:09 -06:00
74 changed files with 7167 additions and 558 deletions

View File

@@ -2,7 +2,8 @@ name: Build Docker images
on: on:
push: push:
branches: ["canary", "main", "feat/monitoring"] branches: [main, canary]
workflow_dispatch:
jobs: jobs:
build-and-push-cloud-image: build-and-push-cloud-image:

View File

@@ -2,7 +2,8 @@ name: Dokploy Docker Build
on: on:
push: push:
branches: [main, canary, "1061-custom-docker-service-hostname"] branches: [main, canary]
workflow_dispatch:
env: env:
IMAGE_NAME: dokploy/dokploy IMAGE_NAME: dokploy/dokploy

View File

@@ -11,12 +11,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Setup biomeJs - name: Setup biomeJs
uses: biomejs/setup-biome@v2 uses: biomejs/setup-biome@v2
- name: Run Biome formatter - name: Run Biome formatter
run: biome format . --write run: biome format --write
- uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef - uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27 # v1.3.2

3
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["biomejs.biome"]
}

8
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,8 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "biomejs.biome",
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit",
"source.organizeImports.biome": "explicit"
}
}

View File

@@ -29,5 +29,9 @@
"tsx": "^4.16.2", "tsx": "^4.16.2",
"typescript": "^5.8.3" "typescript": "^5.8.3"
}, },
"packageManager": "pnpm@9.5.0" "packageManager": "pnpm@9.12.0",
"engines": {
"node": "^20.16.0",
"pnpm": ">=9.12.0"
}
} }

View File

@@ -29,6 +29,7 @@ const baseApp: ApplicationNested = {
herokuVersion: "", herokuVersion: "",
giteaBranch: "", giteaBranch: "",
giteaBuildPath: "", giteaBuildPath: "",
previewRequireCollaboratorPermissions: false,
giteaId: "", giteaId: "",
giteaOwner: "", giteaOwner: "",
giteaRepository: "", giteaRepository: "",

View File

@@ -18,6 +18,7 @@ const baseApp: ApplicationNested = {
appName: "", appName: "",
autoDeploy: true, autoDeploy: true,
enableSubmodules: false, enableSubmodules: false,
previewRequireCollaboratorPermissions: false,
serverId: "", serverId: "",
branch: null, branch: null,
dockerBuildStage: "", dockerBuildStage: "",

View File

@@ -151,7 +151,7 @@ export const HandleSecurity = ({
<FormItem> <FormItem>
<FormLabel>Password</FormLabel> <FormLabel>Password</FormLabel>
<FormControl> <FormControl>
<Input placeholder="test" {...field} /> <Input placeholder="test" type="password" {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />

View File

@@ -7,6 +7,9 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { LockKeyhole, Trash2 } from "lucide-react"; import { LockKeyhole, Trash2 } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -58,19 +61,18 @@ export const ShowSecurity = ({ applicationId }: Props) => {
<div className="flex flex-col gap-6 "> <div className="flex flex-col gap-6 ">
{data?.security.map((security) => ( {data?.security.map((security) => (
<div key={security.securityId}> <div key={security.securityId}>
<div className="flex w-full flex-col sm:flex-row justify-between sm:items-center gap-4 sm:gap-10 border rounded-lg p-4"> <div className="flex w-full flex-col md:flex-row justify-between md:items-center gap-4 md:gap-10 border rounded-lg p-4">
<div className="grid grid-cols-1 sm:grid-cols-2 flex-col gap-4 sm:gap-8"> <div className="grid grid-cols-1 md:grid-cols-2 flex-col gap-4 md:gap-8">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-2">
<span className="font-medium">Username</span> <Label>Username</Label>
<span className="text-sm text-muted-foreground"> <Input disabled value={security.username} />
{security.username}
</span>
</div> </div>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-2">
<span className="font-medium">Password</span> <Label>Password</Label>
<span className="text-sm text-muted-foreground"> <ToggleVisibilityInput
{security.password} value={security.password}
</span> disabled
/>
</div> </div>
</div> </div>
<div className="flex flex-row gap-2"> <div className="flex flex-row gap-2">

View File

@@ -1,4 +1,5 @@
import { DateTooltip } from "@/components/shared/date-tooltip"; import { DateTooltip } from "@/components/shared/date-tooltip";
import { DialogAction } from "@/components/shared/dialog-action";
import { StatusTooltip } from "@/components/shared/status-tooltip"; import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -10,14 +11,13 @@ import {
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { type RouterOutputs, api } from "@/utils/api"; import { type RouterOutputs, api } from "@/utils/api";
import { Clock, Loader2, RocketIcon, Settings, RefreshCcw } from "lucide-react"; import { Clock, Loader2, RefreshCcw, RocketIcon, Settings } from "lucide-react";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { toast } from "sonner";
import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings";
import { CancelQueues } from "./cancel-queues"; import { CancelQueues } from "./cancel-queues";
import { RefreshToken } from "./refresh-token"; import { RefreshToken } from "./refresh-token";
import { ShowDeployment } from "./show-deployment"; 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 { interface Props {
id: string; id: string;

View File

@@ -46,6 +46,7 @@ const schema = z
previewPath: z.string(), previewPath: z.string(),
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]), previewCertificateType: z.enum(["letsencrypt", "none", "custom"]),
previewCustomCertResolver: z.string().optional(), previewCustomCertResolver: z.string().optional(),
previewRequireCollaboratorPermissions: z.boolean(),
}) })
.superRefine((input, ctx) => { .superRefine((input, ctx) => {
if ( if (
@@ -83,6 +84,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
previewHttps: false, previewHttps: false,
previewPath: "/", previewPath: "/",
previewCertificateType: "none", previewCertificateType: "none",
previewRequireCollaboratorPermissions: true,
}, },
resolver: zodResolver(schema), resolver: zodResolver(schema),
}); });
@@ -105,6 +107,8 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
previewPath: data.previewPath || "/", previewPath: data.previewPath || "/",
previewCertificateType: data.previewCertificateType || "none", previewCertificateType: data.previewCertificateType || "none",
previewCustomCertResolver: data.previewCustomCertResolver || "", previewCustomCertResolver: data.previewCustomCertResolver || "",
previewRequireCollaboratorPermissions:
data.previewRequireCollaboratorPermissions || true,
}); });
} }
}, [data]); }, [data]);
@@ -121,6 +125,8 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
previewPath: formData.previewPath, previewPath: formData.previewPath,
previewCertificateType: formData.previewCertificateType, previewCertificateType: formData.previewCertificateType,
previewCustomCertResolver: formData.previewCustomCertResolver, previewCustomCertResolver: formData.previewCustomCertResolver,
previewRequireCollaboratorPermissions:
formData.previewRequireCollaboratorPermissions,
}) })
.then(() => { .then(() => {
toast.success("Preview Deployments settings updated"); toast.success("Preview Deployments settings updated");
@@ -312,6 +318,37 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
</div> </div>
</div> </div>
<div className="grid gap-4 lg:grid-cols-2">
<FormField
control={form.control}
name="previewRequireCollaboratorPermissions"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm col-span-2">
<div className="space-y-0.5">
<FormLabel>
Require Collaborator Permissions
</FormLabel>
<FormDescription>
Require collaborator permissions to preview
deployments, valid roles are:
<ul>
<li>Admin</li>
<li>Maintain</li>
<li>Write</li>
</ul>
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</div>
<FormField <FormField
control={form.control} control={form.control}
name="env" name="env"

View File

@@ -1,3 +1,4 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { DrawerLogs } from "@/components/shared/drawer-logs"; import { DrawerLogs } from "@/components/shared/drawer-logs";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -42,9 +43,8 @@ import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { type LogLine, parseLogs } from "../../docker/logs/utils";
import { formatBytes } from "../../database/backups/restore-backup"; import { formatBytes } from "../../database/backups/restore-backup";
import { AlertBlock } from "@/components/shared/alert-block"; import { type LogLine, parseLogs } from "../../docker/logs/utils";
interface Props { interface Props {
id: string; id: string;

View File

@@ -23,8 +23,8 @@ import {
Trash2, Trash2,
} from "lucide-react"; } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { HandleVolumeBackups } from "./handle-volume-backups";
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal"; import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
import { HandleVolumeBackups } from "./handle-volume-backups";
import { RestoreVolumeBackups } from "./restore-volume-backups"; import { RestoreVolumeBackups } from "./restore-volume-backups";
interface Props { interface Props {

View File

@@ -1,3 +1,4 @@
import { UnauthorizedGitProvider } from "@/components/dashboard/application/general/generic/unauthorized-git-provider";
import { import {
BitbucketIcon, BitbucketIcon,
GitIcon, GitIcon,
@@ -11,6 +12,7 @@ import { api } from "@/utils/api";
import { CodeIcon, GitBranch, Loader2 } from "lucide-react"; import { CodeIcon, GitBranch, Loader2 } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner";
import { ComposeFileEditor } from "../compose-file-editor"; import { ComposeFileEditor } from "../compose-file-editor";
import { ShowConvertedCompose } from "../show-converted-compose"; import { ShowConvertedCompose } from "../show-converted-compose";
import { SaveBitbucketProviderCompose } from "./save-bitbucket-provider-compose"; import { SaveBitbucketProviderCompose } from "./save-bitbucket-provider-compose";
@@ -18,8 +20,6 @@ import { SaveGitProviderCompose } from "./save-git-provider-compose";
import { SaveGiteaProviderCompose } from "./save-gitea-provider-compose"; import { SaveGiteaProviderCompose } from "./save-gitea-provider-compose";
import { SaveGithubProviderCompose } from "./save-github-provider-compose"; import { SaveGithubProviderCompose } from "./save-github-provider-compose";
import { SaveGitlabProviderCompose } from "./save-gitlab-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"; type TabState = "github" | "git" | "raw" | "gitlab" | "bitbucket" | "gitea";
interface Props { interface Props {

View File

@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Folder, HelpCircle } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -37,12 +43,6 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { slugify } from "@/lib/slug"; import { slugify } from "@/lib/slug";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Folder, HelpCircle } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const AddTemplateSchema = z.object({ const AddTemplateSchema = z.object({
name: z.string().min(1, { name: z.string().min(1, {
@@ -75,6 +75,8 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
const slug = slugify(projectName); const slug = slugify(projectName);
const { data: servers } = api.server.withSSHKey.useQuery(); const { data: servers } = api.server.withSSHKey.useQuery();
const hasServers = servers && servers.length > 0;
const { mutateAsync, isLoading, error, isError } = const { mutateAsync, isLoading, error, isError } =
api.application.create.useMutation(); api.application.create.useMutation();
@@ -155,6 +157,7 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
{hasServers && (
<FormField <FormField
control={form.control} control={form.control}
name="serverId" name="serverId"
@@ -211,12 +214,27 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
)}
<FormField <FormField
control={form.control} control={form.control}
name="appName" name="appName"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>App Name</FormLabel> <FormLabel className="flex items-center gap-2">
App Name
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent side="right">
<p>
This will be the name of the Docker Swarm service
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<FormControl> <FormControl>
<Input placeholder="my-app" {...field} /> <Input placeholder="my-app" {...field} />
</FormControl> </FormControl>

View File

@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { CircuitBoard, HelpCircle } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -37,12 +43,6 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { slugify } from "@/lib/slug"; import { slugify } from "@/lib/slug";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CircuitBoard, HelpCircle } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const AddComposeSchema = z.object({ const AddComposeSchema = z.object({
composeType: z.enum(["docker-compose", "stack"]).optional(), composeType: z.enum(["docker-compose", "stack"]).optional(),
@@ -78,6 +78,8 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
const { mutateAsync, isLoading, error, isError } = const { mutateAsync, isLoading, error, isError } =
api.compose.create.useMutation(); api.compose.create.useMutation();
const hasServers = servers && servers.length > 0;
const form = useForm<AddCompose>({ const form = useForm<AddCompose>({
defaultValues: { defaultValues: {
name: "", name: "",
@@ -163,6 +165,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
)} )}
/> />
</div> </div>
{hasServers && (
<FormField <FormField
control={form.control} control={form.control}
name="serverId" name="serverId"
@@ -219,6 +222,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
)}
<FormField <FormField
control={form.control} control={form.control}
name="appName" name="appName"

View File

@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, Database, HelpCircle } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { import {
MariadbIcon, MariadbIcon,
MongodbIcon, MongodbIcon,
@@ -37,14 +43,14 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { slugify } from "@/lib/slug"; import { slugify } from "@/lib/slug";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, Database } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
type DbType = typeof mySchema._type.type; type DbType = typeof mySchema._type.type;
@@ -163,6 +169,8 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
const mariadbMutation = api.mariadb.create.useMutation(); const mariadbMutation = api.mariadb.create.useMutation();
const mysqlMutation = api.mysql.create.useMutation(); const mysqlMutation = api.mysql.create.useMutation();
const hasServers = servers && servers.length > 0;
const form = useForm<AddDatabase>({ const form = useForm<AddDatabase>({
defaultValues: { defaultValues: {
type: "postgres", type: "postgres",
@@ -374,6 +382,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
{hasServers && (
<FormField <FormField
control={form.control} control={form.control}
name="serverId" name="serverId"
@@ -407,12 +416,28 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
)}
<FormField <FormField
control={form.control} control={form.control}
name="appName" name="appName"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>App Name</FormLabel> <FormLabel className="flex items-center gap-2">
App Name
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent side="right">
<p>
This will be the name of the Docker Swarm
service
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<FormControl> <FormControl>
<Input placeholder="my-app" {...field} /> <Input placeholder="my-app" {...field} />
</FormControl> </FormControl>

View File

@@ -1,3 +1,18 @@
import {
BookText,
CheckIcon,
ChevronsUpDown,
Globe,
HelpCircle,
LayoutGrid,
List,
Loader2,
PuzzleIcon,
SearchIcon,
} from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { GithubIcon } from "@/components/icons/data-tools-icons"; import { GithubIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { import {
@@ -54,21 +69,6 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import {
BookText,
CheckIcon,
ChevronsUpDown,
Globe,
HelpCircle,
LayoutGrid,
List,
Loader2,
PuzzleIcon,
SearchIcon,
} from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { toast } from "sonner";
const TEMPLATE_BASE_URL_KEY = "dokploy_template_base_url"; const TEMPLATE_BASE_URL_KEY = "dokploy_template_base_url";
@@ -137,6 +137,8 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
return matchesTags && matchesQuery; return matchesTags && matchesQuery;
}) || []; }) || [];
const hasServers = servers && servers.length > 0;
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger className="w-full"> <DialogTrigger className="w-full">
@@ -425,6 +427,7 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
project. project.
</AlertDialogDescription> </AlertDialogDescription>
{hasServers && (
<div> <div>
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<Tooltip> <Tooltip>
@@ -441,9 +444,9 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
side="top" side="top"
> >
<span> <span>
If no server is selected, the application If no server is selected, the
will be deployed on the server where the application will be deployed on the
user is logged in. server where the user is logged in.
</span> </span>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
@@ -479,6 +482,7 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
)}
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>Cancel</AlertDialogCancel>

View File

@@ -25,6 +25,7 @@ const examples = [
export const StepOne = ({ setTemplateInfo, templateInfo }: any) => { export const StepOne = ({ setTemplateInfo, templateInfo }: any) => {
// Get servers from the API // Get servers from the API
const { data: servers } = api.server.withSSHKey.useQuery(); const { data: servers } = api.server.withSSHKey.useQuery();
const hasServers = servers && servers.length > 0;
const handleExampleClick = (example: string) => { const handleExampleClick = (example: string) => {
setTemplateInfo({ ...templateInfo, userInput: example }); setTemplateInfo({ ...templateInfo, userInput: example });
@@ -47,6 +48,7 @@ export const StepOne = ({ setTemplateInfo, templateInfo }: any) => {
/> />
</div> </div>
{hasServers && (
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="server-deploy"> <Label htmlFor="server-deploy">
Select the server where you want to deploy (optional) Select the server where you want to deploy (optional)
@@ -78,6 +80,7 @@ export const StepOne = ({ setTemplateInfo, templateInfo }: any) => {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
)}
<div className="space-y-2"> <div className="space-y-2">
<Label>Examples:</Label> <Label>Examples:</Label>

View File

@@ -199,7 +199,7 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Generating template suggestions based on your input... Generating template suggestions based on your input...
</p> </p>
<pre>{templateInfo.userInput}</pre> <pre className="whitespace-normal">{templateInfo.userInput}</pre>
</div> </div>
); );
} }

View File

@@ -70,6 +70,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
}, },
{ {
enabled: !!destinationId, enabled: !!destinationId,
refetchOnWindowFocus: false,
}, },
); );
const { const {

View File

@@ -33,6 +33,7 @@ import { AddGithubProvider } from "./github/add-github-provider";
import { EditGithubProvider } from "./github/edit-github-provider"; import { EditGithubProvider } from "./github/edit-github-provider";
import { AddGitlabProvider } from "./gitlab/add-gitlab-provider"; import { AddGitlabProvider } from "./gitlab/add-gitlab-provider";
import { EditGitlabProvider } from "./gitlab/edit-gitlab-provider"; import { EditGitlabProvider } from "./gitlab/edit-gitlab-provider";
import { Badge } from "@/components/ui/badge";
export const ShowGitProviders = () => { export const ShowGitProviders = () => {
const { data, isLoading, refetch } = api.gitProvider.getAll.useQuery(); const { data, isLoading, refetch } = api.gitProvider.getAll.useQuery();
@@ -158,7 +159,13 @@ export const ShowGitProviders = () => {
<div className="flex flex-row gap-1"> <div className="flex flex-row gap-1">
{!haveGithubRequirements && isGithub && ( {!haveGithubRequirements && isGithub && (
<div className="flex flex-col gap-1"> <div className="flex flex-row gap-1 items-center">
<Badge
variant="outline"
className="text-xs"
>
Action Required
</Badge>
<Link <Link
href={`${gitProvider?.github?.githubAppName}/installations/new?state=gh_setup:${gitProvider?.github.githubId}`} href={`${gitProvider?.github?.githubAppName}/installations/new?state=gh_setup:${gitProvider?.github.githubId}`}
className={buttonVariants({ className={buttonVariants({
@@ -185,7 +192,13 @@ export const ShowGitProviders = () => {
</div> </div>
)} )}
{!haveGitlabRequirements && isGitlab && ( {!haveGitlabRequirements && isGitlab && (
<div className="flex flex-col gap-1"> <div className="flex flex-row gap-1 items-center">
<Badge
variant="outline"
className="text-xs"
>
Action Required
</Badge>
<Link <Link
href={getGitlabUrl( href={getGitlabUrl(
gitProvider.gitlab?.applicationId || "", gitProvider.gitlab?.applicationId || "",

View File

@@ -36,6 +36,7 @@ const profileSchema = z.object({
password: z.string().nullable(), password: z.string().nullable(),
currentPassword: z.string().nullable(), currentPassword: z.string().nullable(),
image: z.string().optional(), image: z.string().optional(),
name: z.string().optional(),
allowImpersonation: z.boolean().optional().default(false), allowImpersonation: z.boolean().optional().default(false),
}); });
@@ -84,6 +85,7 @@ export const ProfileForm = () => {
image: data?.user?.image || "", image: data?.user?.image || "",
currentPassword: "", currentPassword: "",
allowImpersonation: data?.user?.allowImpersonation || false, allowImpersonation: data?.user?.allowImpersonation || false,
name: data?.user?.name || "",
}, },
resolver: zodResolver(profileSchema), resolver: zodResolver(profileSchema),
}); });
@@ -97,6 +99,7 @@ export const ProfileForm = () => {
image: data?.user?.image || "", image: data?.user?.image || "",
currentPassword: form.getValues("currentPassword") || "", currentPassword: form.getValues("currentPassword") || "",
allowImpersonation: data?.user?.allowImpersonation, allowImpersonation: data?.user?.allowImpersonation,
name: data?.user?.name || "",
}, },
{ {
keepValues: true, keepValues: true,
@@ -119,6 +122,7 @@ export const ProfileForm = () => {
image: values.image, image: values.image,
currentPassword: values.currentPassword || undefined, currentPassword: values.currentPassword || undefined,
allowImpersonation: values.allowImpersonation, allowImpersonation: values.allowImpersonation,
name: values.name || undefined,
}) })
.then(async () => { .then(async () => {
await refetch(); await refetch();
@@ -128,6 +132,7 @@ export const ProfileForm = () => {
password: "", password: "",
image: values.image, image: values.image,
currentPassword: "", currentPassword: "",
name: values.name || "",
}); });
}) })
.catch(() => { .catch(() => {
@@ -167,6 +172,19 @@ export const ProfileForm = () => {
className="grid gap-4" className="grid gap-4"
> >
<div className="space-y-4"> <div className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="email" name="email"

View File

@@ -20,7 +20,9 @@ export const ToggleDockerCleanup = ({ serverId }: Props) => {
}, },
); );
const enabled = data?.user.enableDockerCleanup || server?.enableDockerCleanup; const enabled = serverId
? server?.enableDockerCleanup
: data?.user.enableDockerCleanup;
const { mutateAsync } = api.settings.updateDockerCleanup.useMutation(); const { mutateAsync } = api.settings.updateDockerCleanup.useMutation();

View File

@@ -1,3 +1,11 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon } from "lucide-react";
import Link from "next/link";
import { useTranslation } from "next-i18next";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -30,14 +38,6 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon } from "lucide-react";
import { useTranslation } from "next-i18next";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const Schema = z.object({ const Schema = z.object({
name: z.string().min(1, { name: z.string().min(1, {
@@ -218,7 +218,7 @@ export const HandleServers = ({ serverId }: Props) => {
</AlertBlock> </AlertBlock>
</div> </div>
{!canCreateMoreServers && ( {!canCreateMoreServers && (
<AlertBlock type="warning"> <AlertBlock type="warning" className="mt-4">
You cannot create more servers,{" "} You cannot create more servers,{" "}
<Link href="/dashboard/settings/billing" className="text-primary"> <Link href="/dashboard/settings/billing" className="text-primary">
Please upgrade your plan Please upgrade your plan

View File

@@ -1,3 +1,9 @@
import { format } from "date-fns";
import { KeyIcon, Loader2, MoreHorizontal, ServerIcon } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import { toast } from "sonner";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { DialogAction } from "@/components/shared/dialog-action"; import { DialogAction } from "@/components/shared/dialog-action";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -27,12 +33,6 @@ import {
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { format } from "date-fns";
import { KeyIcon, Loader2, MoreHorizontal, ServerIcon } from "lucide-react";
import { useTranslation } from "next-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
import { toast } from "sonner";
import { ShowNodesModal } from "../cluster/nodes/show-nodes-modal"; import { ShowNodesModal } from "../cluster/nodes/show-nodes-modal";
import { TerminalModal } from "../web-server/terminal-modal"; import { TerminalModal } from "../web-server/terminal-modal";
import { ShowServerActions } from "./actions/show-server-actions"; import { ShowServerActions } from "./actions/show-server-actions";
@@ -115,24 +115,6 @@ export const ShowServers = () => {
</div> </div>
) : ( ) : (
<div className="flex flex-col gap-4 min-h-[25vh]"> <div className="flex flex-col gap-4 min-h-[25vh]">
{!canCreateMoreServers && (
<AlertBlock type="warning">
<div className="flex flex-row items-center gap-3 justify-center">
<span>
<div>
You cannot create more servers,{" "}
<Link
href="/dashboard/settings/billing"
className="text-primary"
>
Please upgrade your plan
</Link>
</div>
</span>
</div>
</AlertBlock>
)}
<Table> <Table>
<TableCaption> <TableCaption>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">

View File

@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
@@ -22,12 +28,6 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const Schema = z.object({ const Schema = z.object({
name: z.string().min(1, { name: z.string().min(1, {
@@ -108,7 +108,7 @@ export const CreateServer = ({ stepper }: Props) => {
<Card className="bg-background flex flex-col gap-4"> <Card className="bg-background flex flex-col gap-4">
<div className="flex flex-col gap-2 pt-5 px-4"> <div className="flex flex-col gap-2 pt-5 px-4">
{!canCreateMoreServers && ( {!canCreateMoreServers && (
<AlertBlock type="warning"> <AlertBlock type="warning" className="mt-2">
You cannot create more servers,{" "} You cannot create more servers,{" "}
<Link href="/dashboard/settings/billing" className="text-primary"> <Link href="/dashboard/settings/billing" className="text-primary">
Please upgrade your plan Please upgrade your plan

View File

@@ -1,18 +1,22 @@
import copy from "copy-to-clipboard";
import { CopyIcon, ExternalLinkIcon, Loader2 } from "lucide-react";
import Link from "next/link";
import { useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { CodeEditor } from "@/components/shared/code-editor"; import { CodeEditor } from "@/components/shared/code-editor";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import copy from "copy-to-clipboard";
import { ExternalLinkIcon, Loader2 } from "lucide-react";
import { CopyIcon } from "lucide-react";
import Link from "next/link";
import { useEffect, useRef } from "react";
import { toast } from "sonner";
export const CreateSSHKey = () => { export const CreateSSHKey = () => {
const { data, refetch } = api.sshKey.all.useQuery(); const { data, refetch } = api.sshKey.all.useQuery();
const generateMutation = api.sshKey.generate.useMutation(); const generateMutation = api.sshKey.generate.useMutation();
const { mutateAsync, isLoading } = api.sshKey.create.useMutation(); const { mutateAsync, isLoading } = api.sshKey.create.useMutation();
const hasCreatedKey = useRef(false); const hasCreatedKey = useRef(false);
const [selectedOption, setSelectedOption] = useState<"manual" | "provider">(
"manual",
);
const cloudSSHKey = data?.find( const cloudSSHKey = data?.find(
(sshKey) => sshKey.name === "dokploy-cloud-ssh-key", (sshKey) => sshKey.name === "dokploy-cloud-ssh-key",
@@ -60,27 +64,51 @@ export const CreateSSHKey = () => {
</div> </div>
) : ( ) : (
<> <>
<div className="flex flex-col gap-2 text-sm text-muted-foreground"> <div className="flex flex-col gap-4 text-sm text-muted-foreground">
<p className="text-primary text-base font-semibold"> <p className="text-primary text-base font-semibold">
You have two options to add SSH Keys to your server: Choose how to add SSH Keys to your server:
</p> </p>
<ul> {/* Radio button options */}
<li>1. Add The SSH Key to Server Manually</li> <div className="grid gap-2">
<RadioGroup
value={selectedOption}
onValueChange={(value) => {
setSelectedOption(value as "manual" | "provider");
}}
className="grid gap-3"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="manual" id="manual" />
<Label
htmlFor="manual"
className="text-primary font-medium cursor-pointer"
>
Add SSH Key to Server Manually
</Label>
</div>
<li> <div className="flex items-center space-x-2">
2. Add the public SSH Key when you create a server in your <RadioGroupItem value="provider" id="provider" />
preffered provider (Hostinger, Digital Ocean, Hetzner, etc){" "} <Label
</li> htmlFor="provider"
</ul> className="text-primary font-medium cursor-pointer"
>
Add SSH Key when creating server in your provider
</Label>
</div>
</RadioGroup>
</div>
{/* Content based on selected option */}
{selectedOption === "manual" && (
<div className="flex flex-col gap-2 w-full border rounded-lg p-4"> <div className="flex flex-col gap-2 w-full border rounded-lg p-4">
<span className="text-base font-semibold text-primary"> <span className="text-base font-semibold text-primary">
Option 1 Manual Setup Instructions
</span> </span>
<ul> <ul className="space-y-2">
<li className="items-center flex gap-1"> <li className="items-center flex gap-1">
1. Login to your server{" "} 1. Login to your server
</li> </li>
<li> <li>
2. When you are logged in run the following command 2. When you are logged in run the following command
@@ -107,14 +135,17 @@ export const CreateSSHKey = () => {
</div> </div>
</li> </li>
<li className="mt-1"> <li className="mt-1">
3. You're done, follow the next step to insert the details 3. You're done, follow the next step to insert the
of your server. details of your server.
</li> </li>
</ul> </ul>
</div> </div>
<div className="flex flex-col gap-2 w-full mt-2 border rounded-lg p-4"> )}
{selectedOption === "provider" && (
<div className="flex flex-col gap-2 w-full border rounded-lg p-4">
<span className="text-base font-semibold text-primary"> <span className="text-base font-semibold text-primary">
Option 2 Provider Setup Instructions
</span> </span>
<div className="flex flex-col gap-4 w-full overflow-auto"> <div className="flex flex-col gap-4 w-full overflow-auto">
<div className="flex relative flex-col gap-2 overflow-y-auto"> <div className="flex relative flex-col gap-2 overflow-y-auto">
@@ -135,14 +166,20 @@ export const CreateSSHKey = () => {
</div> </div>
</div> </div>
</div> </div>
<p className="text-sm mt-2">
Use this public key when creating a server in your
preferred provider (Hostinger, Digital Ocean, Hetzner,
etc.)
</p>
<Link <Link
href="https://docs.dokploy.com/docs/core/multi-server/instructions#requirements" href="https://docs.dokploy.com/docs/core/multi-server/instructions#requirements"
target="_blank" target="_blank"
className="text-primary flex flex-row gap-2" className="text-primary flex flex-row gap-2 mt-2"
> >
View Tutorial <ExternalLinkIcon className="size-4" /> View Tutorial <ExternalLinkIcon className="size-4" />
</Link> </Link>
</div> </div>
)}
</div> </div>
</> </>
)} )}

View File

@@ -126,7 +126,7 @@ export const UpdateServer = ({
</TooltipProvider> </TooltipProvider>
)} )}
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-w-lg p-6"> <DialogContent className="max-w-lg">
<div className="flex items-center justify-between mb-8"> <div className="flex items-center justify-between mb-8">
<DialogTitle className="text-2xl font-semibold"> <DialogTitle className="text-2xl font-semibold">
Web Server Update Web Server Update
@@ -253,7 +253,7 @@ export const UpdateServer = ({
<ToggleAutoCheckUpdates disabled={isLoading} /> <ToggleAutoCheckUpdates disabled={isLoading} />
</div> </div>
<div className="space-y-4 flex items-center justify-end"> <div className="space-y-4 flex items-center justify-end mt-4 ">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button variant="outline" onClick={() => onOpenChange?.(false)}> <Button variant="outline" onClick={() => onOpenChange?.(false)}>
Cancel Cancel

View File

@@ -96,10 +96,7 @@ type SingleNavItem = {
title: string; title: string;
url: string; url: string;
icon?: LucideIcon; icon?: LucideIcon;
isEnabled?: (opts: { isEnabled?: (opts: { auth?: AuthQueryOutput; isCloud: boolean }) => boolean;
auth?: AuthQueryOutput;
isCloud: boolean;
}) => boolean;
}; };
// NavItem type // NavItem type
@@ -125,10 +122,7 @@ type ExternalLink = {
name: string; name: string;
url: string; url: string;
icon: React.ComponentType<{ className?: string }>; icon: React.ComponentType<{ className?: string }>;
isEnabled?: (opts: { isEnabled?: (opts: { auth?: AuthQueryOutput; isCloud: boolean }) => boolean;
auth?: AuthQueryOutput;
isCloud: boolean;
}) => boolean;
}; };
// Menu type // Menu type

View File

@@ -0,0 +1 @@
ALTER TABLE "application" ADD COLUMN "previewRequireCollaboratorPermissions" boolean DEFAULT true;

File diff suppressed because it is too large Load Diff

View File

@@ -722,6 +722,13 @@
"when": 1751848685503, "when": 1751848685503,
"tag": "0102_opposite_grandmaster", "tag": "0102_opposite_grandmaster",
"breakpoints": true "breakpoints": true
},
{
"idx": 103,
"version": "7",
"when": 1752465764072,
"tag": "0103_cultured_pestilence",
"breakpoints": true
} }
] ]
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.24.2", "version": "v0.24.5",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
@@ -187,10 +187,10 @@
"ct3aMetadata": { "ct3aMetadata": {
"initVersion": "7.25.2" "initVersion": "7.25.2"
}, },
"packageManager": "pnpm@9.5.0", "packageManager": "pnpm@9.12.0",
"engines": { "engines": {
"node": "^20.16.0", "node": "^20.16.0",
"pnpm": ">=9.5.0" "pnpm": ">=9.12.0"
}, },
"lint-staged": { "lint-staged": {
"*": [ "*": [
@@ -198,6 +198,8 @@
] ]
}, },
"commitlint": { "commitlint": {
"extends": ["@commitlint/config-conventional"] "extends": [
"@commitlint/config-conventional"
]
} }
} }

View File

@@ -5,7 +5,10 @@ import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy"; import { deploy } from "@/server/utils/deploy";
import { import {
IS_CLOUD, IS_CLOUD,
checkUserRepositoryPermissions,
createPreviewDeployment, createPreviewDeployment,
createSecurityBlockedComment,
findGithubById,
findPreviewDeploymentByApplicationId, findPreviewDeploymentByApplicationId,
findPreviewDeploymentsByPullRequestId, findPreviewDeploymentsByPullRequestId,
removePreviewDeployment, removePreviewDeployment,
@@ -346,6 +349,18 @@ export default async function handler(
const deploymentHash = githubBody?.pull_request?.head?.sha; const deploymentHash = githubBody?.pull_request?.head?.sha;
const branch = githubBody?.pull_request?.base?.ref; const branch = githubBody?.pull_request?.base?.ref;
const owner = githubBody?.repository?.owner?.login; const owner = githubBody?.repository?.owner?.login;
const prAuthor = githubBody?.pull_request?.user?.login;
// Validate PR author information is present
if (!prAuthor) {
console.warn(
"⚠️ SECURITY: PR author information missing in webhook payload",
);
res.status(400).json({
message: "PR author information missing",
});
return;
}
const apps = await db.query.applications.findMany({ const apps = await db.query.applications.findMany({
where: and( where: and(
@@ -361,13 +376,72 @@ export default async function handler(
}, },
}); });
// SECURITY: Check collaborator permissions per application setting
const secureApps: typeof apps = [];
const blockedApps: string[] = [];
let userPermission: string | null = null;
for (const app of apps) {
// If the app requires collaborator permissions, verify them
if (app.previewRequireCollaboratorPermissions !== false) {
try {
const githubProvider = await findGithubById(githubResult.githubId);
const { hasWriteAccess, permission } =
await checkUserRepositoryPermissions(
githubProvider,
owner,
repository,
prAuthor,
);
userPermission = permission; // Store permission for comment
if (!hasWriteAccess) {
console.warn(
`🚨 SECURITY: Blocked preview deployment for ${app.name} from unauthorized user ${prAuthor} on ${owner}/${repository}. Permission: ${permission || "none"}`,
);
blockedApps.push(app.name);
continue;
}
console.log(
`✅ SECURITY: Preview deployment authorized for ${app.name} from user ${prAuthor} on ${owner}/${repository}. Permission: ${permission}`,
);
} catch (error) {
console.error(
`Error validating PR author permissions for ${app.name}:`,
error,
);
blockedApps.push(app.name);
continue; // Skip this app on error
}
} else {
console.warn(
`⚠️ SECURITY: Preview deployment for ${app.name} allows deployment from any PR author (security check disabled)`,
);
}
secureApps.push(app);
}
const prBranch = githubBody?.pull_request?.head?.ref; const prBranch = githubBody?.pull_request?.head?.ref;
const prNumber = githubBody?.pull_request?.number; const prNumber = githubBody?.pull_request?.number;
const prTitle = githubBody?.pull_request?.title; const prTitle = githubBody?.pull_request?.title;
const prURL = githubBody?.pull_request?.html_url; const prURL = githubBody?.pull_request?.html_url;
for (const app of apps) { // Create security notification comment if any apps were blocked
if (blockedApps.length > 0) {
await createSecurityBlockedComment({
owner,
repository,
prNumber: Number.parseInt(prNumber),
prAuthor,
permission: userPermission,
githubId: githubResult.githubId,
});
}
for (const app of secureApps) {
const previewLimit = app?.previewLimit || 0; const previewLimit = app?.previewLimit || 0;
if (app?.previewDeployments?.length > previewLimit) { if (app?.previewDeployments?.length > previewLimit) {
continue; continue;

View File

@@ -37,7 +37,6 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server/lib/auth"; import { validateRequest } from "@dokploy/server/lib/auth";

View File

@@ -33,7 +33,6 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server/lib/auth"; import { validateRequest } from "@dokploy/server/lib/auth";

View File

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

View File

@@ -20,8 +20,8 @@ import {
} from "@dokploy/server"; } from "@dokploy/server";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { desc, eq } from "drizzle-orm"; import { desc, eq } from "drizzle-orm";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { z } from "zod"; import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const deploymentRouter = createTRPCRouter({ export const deploymentRouter = createTRPCRouter({
all: protectedProcedure all: protectedProcedure

View File

@@ -8,9 +8,9 @@ import {
getServiceContainersByAppName, getServiceContainersByAppName,
getStackContainersByAppName, getStackContainersByAppName,
} from "@dokploy/server"; } from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure } from "../trpc";
import { TRPCError } from "@trpc/server";
export const containerIdRegex = /^[a-zA-Z0-9.\-_]+$/; export const containerIdRegex = /^[a-zA-Z0-9.\-_]+$/;

View File

@@ -12,8 +12,8 @@ import {
getServiceContainer, getServiceContainer,
updateMount, updateMount,
} from "@dokploy/server"; } from "@dokploy/server";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { z } from "zod"; import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const mountRouter = createTRPCRouter({ export const mountRouter = createTRPCRouter({
create: protectedProcedure create: protectedProcedure

View File

@@ -361,6 +361,7 @@ export const projectRouter = createTRPCRouter({
previewDeployments, previewDeployments,
mounts, mounts,
appName, appName,
refreshToken,
...application ...application
} = await findApplicationById(id); } = await findApplicationById(id);
const newAppName = appName.substring( const newAppName = appName.substring(
@@ -603,8 +604,14 @@ export const projectRouter = createTRPCRouter({
break; break;
} }
case "compose": { case "compose": {
const { composeId, mounts, domains, appName, ...compose } = const {
await findComposeById(id); composeId,
mounts,
domains,
appName,
refreshToken,
...compose
} = await findComposeById(id);
const newAppName = appName.substring( const newAppName = appName.substring(
0, 0,

View File

@@ -4,10 +4,10 @@ import {
getNodeInfo, getNodeInfo,
getSwarmNodes, getSwarmNodes,
} from "@dokploy/server"; } from "@dokploy/server";
import { findServerById } from "@dokploy/server";
import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure } from "../trpc";
import { TRPCError } from "@trpc/server";
import { findServerById } from "@dokploy/server";
import { containerIdRegex } from "./docker"; import { containerIdRegex } from "./docker";
export const swarmRouter = createTRPCRouter({ export const swarmRouter = createTRPCRouter({

View File

@@ -1,30 +1,30 @@
import { removeJob, schedule, updateJob } from "@/server/utils/backup";
import { import {
IS_CLOUD, IS_CLOUD,
updateVolumeBackup,
removeVolumeBackup,
createVolumeBackup, createVolumeBackup,
runVolumeBackup,
findVolumeBackupById, findVolumeBackupById,
restoreVolume, removeVolumeBackup,
scheduleVolumeBackup,
removeVolumeBackupJob, removeVolumeBackupJob,
restoreVolume,
runVolumeBackup,
scheduleVolumeBackup,
updateVolumeBackup,
} from "@dokploy/server"; } from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { import {
createVolumeBackupSchema, createVolumeBackupSchema,
updateVolumeBackupSchema, updateVolumeBackupSchema,
volumeBackups, volumeBackups,
} from "@dokploy/server/db/schema"; } from "@dokploy/server/db/schema";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { db } from "@dokploy/server/db";
import { eq } from "drizzle-orm";
import { observable } from "@trpc/server/observable";
import { import {
execAsyncRemote, execAsyncRemote,
execAsyncStream, execAsyncStream,
} from "@dokploy/server/utils/process/execAsync"; } from "@dokploy/server/utils/process/execAsync";
import { removeJob, schedule, updateJob } from "@/server/utils/backup";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable";
import { eq } from "drizzle-orm";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const volumeBackupsRouter = createTRPCRouter({ export const volumeBackupsRouter = createTRPCRouter({
list: protectedProcedure list: protectedProcedure

View File

@@ -5,6 +5,7 @@ import {
initializeTraefik, initializeTraefik,
} from "@dokploy/server/setup/traefik-setup"; } from "@dokploy/server/setup/traefik-setup";
import { execAsync } from "@dokploy/server";
import { setupDirectories } from "@dokploy/server/setup/config-paths"; import { setupDirectories } from "@dokploy/server/setup/config-paths";
import { initializePostgres } from "@dokploy/server/setup/postgres-setup"; import { initializePostgres } from "@dokploy/server/setup/postgres-setup";
import { initializeRedis } from "@dokploy/server/setup/redis-setup"; import { initializeRedis } from "@dokploy/server/setup/redis-setup";
@@ -12,7 +13,6 @@ import {
initializeNetwork, initializeNetwork,
initializeSwarm, initializeSwarm,
} from "@dokploy/server/setup/setup"; } from "@dokploy/server/setup/setup";
import { execAsync } from "@dokploy/server";
(async () => { (async () => {
try { try {
setupDirectories(); setupDirectories();

View File

@@ -13,10 +13,7 @@ declare global {
baseDomain?: string; baseDomain?: string;
}; };
chatwootSDK?: { chatwootSDK?: {
run: (config: { run: (config: { websiteToken: string; baseUrl: string }) => void;
websiteToken: string;
baseUrl: string;
}) => void;
}; };
$chatwoot?: { $chatwoot?: {
setUser: ( setUser: (

View File

@@ -29,5 +29,9 @@
"tsx": "^4.16.2", "tsx": "^4.16.2",
"typescript": "^5.8.3" "typescript": "^5.8.3"
}, },
"packageManager": "pnpm@9.5.0" "packageManager": "pnpm@9.12.0",
"engines": {
"node": "^20.16.0",
"pnpm": ">=9.12.0"
}
} }

View File

@@ -1,18 +1,17 @@
{ {
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json", "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"files": { "files": {
"ignore": [ "includes": [
"node_modules/**", "**",
".next/**", "!**/.docker",
"drizzle/**", "!**/.next/**",
".docker", "!**/dist",
"dist", "!**/drizzle/**",
"packages/server/package.json" "!node_modules/**",
"!packages/server/package.json"
] ]
}, },
"organizeImports": { "assist": { "actions": { "source": { "organizeImports": "on" } } },
"enabled": true
},
"linter": { "linter": {
"rules": { "rules": {
"security": { "security": {
@@ -20,7 +19,8 @@
}, },
"complexity": { "complexity": {
"noUselessCatch": "off", "noUselessCatch": "off",
"noBannedTypes": "off" "noBannedTypes": "off",
"noUselessFragments": "off"
}, },
"correctness": { "correctness": {
"useExhaustiveDependencies": "off", "useExhaustiveDependencies": "off",
@@ -30,7 +30,17 @@
"noUnusedVariables": "error" "noUnusedVariables": "error"
}, },
"style": { "style": {
"noNonNullAssertion": "off" "noNonNullAssertion": "off",
"noParameterAssign": "error",
"useAsConstAssertion": "error",
"useDefaultParameterLast": "error",
"useEnumInitializers": "error",
"useSelfClosingElements": "error",
"useSingleVarDeclarator": "error",
"noUnusedTemplateLiteral": "error",
"useNumberNamespace": "error",
"noInferrableTypes": "error",
"noUselessElse": "error"
}, },
"suspicious": { "suspicious": {
"noArrayIndexKey": "off", "noArrayIndexKey": "off",

View File

@@ -1,7 +1,10 @@
{ {
"name": "dokploy", "name": "dokploy",
"private": true, "private": true,
"workspaces": ["apps/*", "packages/*"], "workspaces": [
"apps/*",
"packages/*"
],
"scripts": { "scripts": {
"dokploy:setup": "pnpm --filter=dokploy run setup", "dokploy:setup": "pnpm --filter=dokploy run setup",
"dokploy:dev": "pnpm --filter=dokploy run dev", "dokploy:dev": "pnpm --filter=dokploy run dev",
@@ -20,7 +23,7 @@
"format-and-lint:fix": "biome check . --write" "format-and-lint:fix": "biome check . --write"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.9.4", "@biomejs/biome": "2.1.1",
"@commitlint/cli": "^19.8.1", "@commitlint/cli": "^19.8.1",
"@commitlint/config-conventional": "^19.8.1", "@commitlint/config-conventional": "^19.8.1",
"@types/node": "^18.19.104", "@types/node": "^18.19.104",
@@ -30,10 +33,10 @@
"lint-staged": "^15.5.2", "lint-staged": "^15.5.2",
"tsx": "4.16.2" "tsx": "4.16.2"
}, },
"packageManager": "pnpm@9.5.0", "packageManager": "pnpm@9.12.0",
"engines": { "engines": {
"node": "^20.16.0", "node": "^20.16.0",
"pnpm": ">=9.5.0" "pnpm": ">=9.12.0"
}, },
"lint-staged": { "lint-staged": {
"*": [ "*": [
@@ -41,7 +44,9 @@
] ]
}, },
"commitlint": { "commitlint": {
"extends": ["@commitlint/config-conventional"] "extends": [
"@commitlint/config-conventional"
]
}, },
"resolutions": { "resolutions": {
"@types/react": "18.3.5", "@types/react": "18.3.5",

View File

@@ -105,5 +105,10 @@
"tsc-alias": "1.8.10", "tsc-alias": "1.8.10",
"tsx": "^4.16.2", "tsx": "^4.16.2",
"typescript": "^5.8.3" "typescript": "^5.8.3"
},
"packageManager": "pnpm@9.12.0",
"engines": {
"node": "^20.16.0",
"pnpm": ">=9.12.0"
} }
} }

View File

@@ -131,6 +131,10 @@ export const applications = pgTable("application", {
isPreviewDeploymentsActive: boolean("isPreviewDeploymentsActive").default( isPreviewDeploymentsActive: boolean("isPreviewDeploymentsActive").default(
false, false,
), ),
// Security: Require collaborator permissions for preview deployments
previewRequireCollaboratorPermissions: boolean(
"previewRequireCollaboratorPermissions",
).default(true),
rollbackActive: boolean("rollbackActive").default(false), rollbackActive: boolean("rollbackActive").default(false),
buildArgs: text("buildArgs"), buildArgs: text("buildArgs"),
memoryReservation: text("memoryReservation"), memoryReservation: text("memoryReservation"),
@@ -428,6 +432,7 @@ const createSchema = createInsertSchema(applications, {
previewHttps: z.boolean().optional(), previewHttps: z.boolean().optional(),
previewPath: z.string().optional(), previewPath: z.string().optional(),
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]).optional(), previewCertificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
previewRequireCollaboratorPermissions: z.boolean().optional(),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
cleanCache: z.boolean().optional(), cleanCache: z.boolean().optional(),
}); });

View File

@@ -13,9 +13,9 @@ import { applications } from "./application";
import { backups } from "./backups"; import { backups } from "./backups";
import { compose } from "./compose"; import { compose } from "./compose";
import { previewDeployments } from "./preview-deployments"; import { previewDeployments } from "./preview-deployments";
import { rollbacks } from "./rollbacks";
import { schedules } from "./schedule"; import { schedules } from "./schedule";
import { server } from "./server"; import { server } from "./server";
import { rollbacks } from "./rollbacks";
import { volumeBackups } from "./volume-backups"; import { volumeBackups } from "./volume-backups";
export const deploymentStatus = pgEnum("deploymentStatus", [ export const deploymentStatus = pgEnum("deploymentStatus", [
"running", "running",

View File

@@ -1,14 +1,14 @@
import type { Application } from "@dokploy/server/services/application";
import type { Mount } from "@dokploy/server/services/mount";
import type { Port } from "@dokploy/server/services/port";
import type { Project } from "@dokploy/server/services/project";
import type { Registry } from "@dokploy/server/services/registry";
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { jsonb, pgTable, serial, text } from "drizzle-orm/pg-core"; import { jsonb, pgTable, serial, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import { deployments } from "./deployment"; import { deployments } from "./deployment";
import type { Application } from "@dokploy/server/services/application";
import type { Project } from "@dokploy/server/services/project";
import type { Mount } from "@dokploy/server/services/mount";
import type { Port } from "@dokploy/server/services/port";
import type { Registry } from "@dokploy/server/services/registry";
export const rollbacks = pgTable("rollback", { export const rollbacks = pgTable("rollback", {
rollbackId: text("rollbackId") rollbackId: text("rollbackId")

View File

@@ -1,3 +1,4 @@
import { paths } from "@dokploy/server/constants";
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { import {
boolean, boolean,
@@ -15,7 +16,6 @@ import { backups } from "./backups";
import { projects } from "./project"; import { projects } from "./project";
import { schedules } from "./schedule"; import { schedules } from "./schedule";
import { certificateType } from "./shared"; import { certificateType } from "./shared";
import { paths } from "@dokploy/server/constants";
/** /**
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
* database instance for multiple projects. * database instance for multiple projects.
@@ -323,6 +323,7 @@ export const apiUpdateWebServerMonitoring = z.object({
export const apiUpdateUser = createSchema.partial().extend({ export const apiUpdateUser = createSchema.partial().extend({
password: z.string().optional(), password: z.string().optional(),
currentPassword: z.string().optional(), currentPassword: z.string().optional(),
name: z.string().optional(),
metricsConfig: z metricsConfig: z
.object({ .object({
server: z.object({ server: z.object({

View File

@@ -3,16 +3,16 @@ import { boolean, integer, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
import { serviceType } from "./mount";
import { applications } from "./application"; import { applications } from "./application";
import { mongo } from "./mongo";
import { mysql } from "./mysql";
import { redis } from "./redis";
import { compose } from "./compose"; import { compose } from "./compose";
import { postgres } from "./postgres";
import { mariadb } from "./mariadb";
import { destinations } from "./destination";
import { deployments } from "./deployment"; import { deployments } from "./deployment";
import { destinations } from "./destination";
import { mariadb } from "./mariadb";
import { mongo } from "./mongo";
import { serviceType } from "./mount";
import { mysql } from "./mysql";
import { postgres } from "./postgres";
import { redis } from "./redis";
import { generateAppName } from "./utils"; import { generateAppName } from "./utils";
export const volumeBackups = pgTable("volume_backup", { export const volumeBackups = pgTable("volume_backup", {

View File

@@ -45,6 +45,7 @@ export * from "./setup/traefik-setup";
export * from "./setup/server-validate"; export * from "./setup/server-validate";
export * from "./setup/server-audit"; export * from "./setup/server-audit";
export * from "./utils/watch-paths/should-deploy"; export * from "./utils/watch-paths/should-deploy";
export * from "./utils/providers/github";
export * from "./utils/backups/index"; export * from "./utils/backups/index";
export * from "./utils/backups/mariadb"; export * from "./utils/backups/mariadb";
export * from "./utils/backups/mongo"; export * from "./utils/backups/mongo";

View File

@@ -298,11 +298,7 @@ export const validateRequest = async (request: IncomingMessage) => {
const mockSession = { const mockSession = {
session: { session: {
user: { userId: apiKeyRecord.user.id,
id: apiKeyRecord.user.id,
email: apiKeyRecord.user.email,
name: apiKeyRecord.user.name,
},
activeOrganizationId: organizationId || "", activeOrganizationId: organizationId || "",
}, },
user: { user: {

View File

@@ -237,6 +237,7 @@ export const deployApplication = async ({
} catch (error) { } catch (error) {
await updateDeploymentStatus(deployment.deploymentId, "error"); await updateDeploymentStatus(deployment.deploymentId, "error");
await updateApplicationStatus(applicationId, "error"); await updateApplicationStatus(applicationId, "error");
await sendBuildErrorNotifications({ await sendBuildErrorNotifications({
projectName: application.project.name, projectName: application.project.name,
applicationName: application.name, applicationName: application.name,
@@ -370,8 +371,9 @@ export const deployRemoteApplication = async ({
domains: application.domains, domains: application.domains,
}); });
} catch (error) { } catch (error) {
// @ts-ignore const errorMessage = error instanceof Error ? error.message : String(error);
const encodedContent = encodeBase64(error?.message);
const encodedContent = encodeBase64(errorMessage);
await execAsyncRemote( await execAsyncRemote(
application.serverId, application.serverId,
@@ -383,12 +385,12 @@ export const deployRemoteApplication = async ({
await updateDeploymentStatus(deployment.deploymentId, "error"); await updateDeploymentStatus(deployment.deploymentId, "error");
await updateApplicationStatus(applicationId, "error"); await updateApplicationStatus(applicationId, "error");
await sendBuildErrorNotifications({ await sendBuildErrorNotifications({
projectName: application.project.name, projectName: application.project.name,
applicationName: application.name, applicationName: application.name,
applicationType: "application", applicationType: "application",
// @ts-ignore errorMessage: `Please check the logs for details: ${errorMessage}`,
errorMessage: error?.message || "Error building",
buildLink, buildLink,
organizationId: application.project.organizationId, organizationId: application.project.organizationId,
}); });

View File

@@ -31,8 +31,8 @@ import {
findPreviewDeploymentById, findPreviewDeploymentById,
updatePreviewDeployment, updatePreviewDeployment,
} from "./preview-deployment"; } from "./preview-deployment";
import { findScheduleById } from "./schedule";
import { removeRollbackById } from "./rollbacks"; import { removeRollbackById } from "./rollbacks";
import { findScheduleById } from "./schedule";
import { findVolumeBackupById } from "./volume-backups"; import { findVolumeBackupById } from "./volume-backups";
export type Deployment = typeof deployments.$inferSelect; export type Deployment = typeof deployments.$inferSelect;

View File

@@ -192,3 +192,156 @@ export const createPreviewDeploymentComment = async ({
pullRequestCommentId: `${issue.data.id}`, pullRequestCommentId: `${issue.data.id}`,
}).then((response) => response[0]); }).then((response) => response[0]);
}; };
/**
* Generate security notification message for blocked PR deployments
*/
export const getSecurityBlockedMessage = (
prAuthor: string,
repositoryName: string,
permission: string | null,
) => {
return `### 🚨 Preview Deployment Blocked - Security Protection
**Your pull request was blocked from triggering preview deployments**
#### Why was this blocked?
- **User**: \`${prAuthor}\`
- **Repository**: \`${repositoryName}\`
- **Permission Level**: \`${permission || "none"}\`
- **Required Level**: \`write\`, \`maintain\`, or \`admin\`
#### How to resolve this:
**Option 1: Get Collaborator Access (Recommended)**
Ask a repository maintainer to invite you as a collaborator with **write permissions** or higher.
**Option 2: Request Permission Override**
Ask a repository administrator to disable security validation for this specific application if appropriate.
#### For Repository Administrators:
To disable this security check (⚠️ **not recommended for public repositories**):
Enter to preview settings and disable the security check.
---
*This security measure protects against malicious code execution in preview deployments. Only trusted collaborators should have the ability to trigger deployments.*
<details>
<summary>🛡️ Learn more about this security feature</summary>
This protection prevents unauthorized users from:
- Executing malicious code on the deployment server
- Accessing environment variables and secrets
- Potentially compromising the infrastructure
Preview deployments are powerful but require trust. Only users with repository write access can trigger them.
</details>`;
};
/**
* Check if a security notification comment already exists on a GitHub PR
* This prevents creating duplicate security comments on subsequent pushes
*/
export const hasExistingSecurityComment = async ({
owner,
repository,
prNumber,
githubId,
}: {
owner: string;
repository: string;
prNumber: number;
githubId: string;
}): Promise<boolean> => {
try {
const github = await findGithubById(githubId);
const octokit = authGithub(github);
// Get all comments for this PR
const { data: comments } = await octokit.rest.issues.listComments({
owner,
repo: repository,
issue_number: prNumber,
});
// Check if any comment contains our security notification marker
const securityCommentExists = comments.some((comment) =>
comment.body?.includes(
"🚨 Preview Deployment Blocked - Security Protection",
),
);
return securityCommentExists;
} catch (error) {
console.error(
`❌ Failed to check existing comments on PR #${prNumber}:`,
error,
);
// If we can't check, assume no comment exists to avoid blocking functionality
return false;
}
};
/**
* Create a security notification comment on a GitHub PR
*/
export const createSecurityBlockedComment = async ({
owner,
repository,
prNumber,
prAuthor,
permission,
githubId,
}: {
owner: string;
repository: string;
prNumber: number;
prAuthor: string;
permission: string | null;
githubId: string;
}) => {
try {
// Check if a security comment already exists to prevent duplicates
const commentExists = await hasExistingSecurityComment({
owner,
repository,
prNumber,
githubId,
});
if (commentExists) {
console.log(
` Security notification comment already exists on PR #${prNumber}, skipping duplicate`,
);
return null;
}
const github = await findGithubById(githubId);
const octokit = authGithub(github);
const securityMessage = getSecurityBlockedMessage(
prAuthor,
repository,
permission,
);
const issue = await octokit.rest.issues.createComment({
owner,
repo: repository,
issue_number: prNumber,
body: securityMessage,
});
console.log(
`✅ Security notification comment created on PR #${prNumber}: ${issue.data.html_url}`,
);
return issue.data;
} catch (error) {
console.error(
`❌ Failed to create security comment on PR #${prNumber}:`,
error,
);
// Don't throw error - security comment is nice-to-have, not critical
return null;
}
};

View File

@@ -1,27 +1,27 @@
import type { CreateServiceOptions } from "dockerode";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { z } from "zod";
import { db } from "../db"; import { db } from "../db";
import { import {
type createRollbackSchema, type createRollbackSchema,
rollbacks,
deployments as deploymentsSchema, deployments as deploymentsSchema,
rollbacks,
} from "../db/schema"; } from "../db/schema";
import type { z } from "zod"; import { type ApplicationNested, getAuthConfig } from "../utils/builders";
import { type Application, findApplicationById } from "./application";
import { getRemoteDocker } from "../utils/servers/remote-docker";
import { getAuthConfig, type ApplicationNested } from "../utils/builders";
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
import type { CreateServiceOptions } from "dockerode";
import { findDeploymentById } from "./deployment";
import { import {
prepareEnvironmentVariables,
calculateResources, calculateResources,
generateBindMounts, generateBindMounts,
generateConfigContainer, generateConfigContainer,
generateVolumeMounts, generateVolumeMounts,
prepareEnvironmentVariables,
} from "../utils/docker/utils"; } from "../utils/docker/utils";
import type { Project } from "./project"; import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
import { getRemoteDocker } from "../utils/servers/remote-docker";
import { type Application, findApplicationById } from "./application";
import { findDeploymentById } from "./deployment";
import type { Mount } from "./mount"; import type { Mount } from "./mount";
import type { Port } from "./port"; import type { Port } from "./port";
import type { Project } from "./project";
export const createRollback = async ( export const createRollback = async (
input: z.infer<typeof createRollbackSchema>, input: z.infer<typeof createRollbackSchema>,

View File

@@ -1,12 +1,12 @@
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import type { z } from "zod";
import { db } from "../db";
import { import {
type createVolumeBackupSchema, type createVolumeBackupSchema,
type updateVolumeBackupSchema, type updateVolumeBackupSchema,
volumeBackups, volumeBackups,
} from "../db/schema"; } from "../db/schema";
import { db } from "../db";
import { TRPCError } from "@trpc/server";
import type { z } from "zod";
export const findVolumeBackupById = async (volumeBackupId: string) => { export const findVolumeBackupById = async (volumeBackupId: string) => {
const volumeBackup = await db.query.volumeBackups.findFirst({ const volumeBackup = await db.query.volumeBackups.findFirst({

View File

@@ -191,6 +191,9 @@ export const createDefaultServerTraefikConfig = () => {
export const getDefaultTraefikConfig = () => { export const getDefaultTraefikConfig = () => {
const configObject: MainTraefikConfig = { const configObject: MainTraefikConfig = {
global: {
sendAnonymousUsage: false,
},
providers: { providers: {
...(process.env.NODE_ENV === "development" ...(process.env.NODE_ENV === "development"
? { ? {

View File

@@ -1,8 +1,8 @@
import { findComposeById } from "@dokploy/server/services/compose"; import { findComposeById } from "@dokploy/server/services/compose";
import { dump, load } from "js-yaml"; import { dump, load } from "js-yaml";
import { addAppNameToAllServiceNames } from "./collision/root-network"; import { addAppNameToAllServiceNames } from "./collision/root-network";
import { addSuffixToAllVolumes } from "./compose/volume";
import { generateRandomHash } from "./compose"; import { generateRandomHash } from "./compose";
import { addSuffixToAllVolumes } from "./compose/volume";
import type { ComposeSpecification } from "./types"; import type { ComposeSpecification } from "./types";
export const addAppNameToPreventCollision = ( export const addAppNameToPreventCollision = (

View File

@@ -65,6 +65,8 @@ export const sendBuildErrorNotifications = async ({
const decorate = (decoration: string, text: string) => const decorate = (decoration: string, text: string) =>
`${discord.decoration ? decoration : ""} ${text}`.trim(); `${discord.decoration ? decoration : ""} ${text}`.trim();
const limitCharacter = 800;
const truncatedErrorMessage = errorMessage.substring(0, limitCharacter);
await sendDiscordNotification(discord, { await sendDiscordNotification(discord, {
title: decorate(">", "`⚠️` Build Failed"), title: decorate(">", "`⚠️` Build Failed"),
color: 0xed4245, color: 0xed4245,
@@ -101,7 +103,7 @@ export const sendBuildErrorNotifications = async ({
}, },
{ {
name: decorate("`⚠️`", "Error Message"), name: decorate("`⚠️`", "Error Message"),
value: `\`\`\`${errorMessage}\`\`\``, value: `\`\`\`${truncatedErrorMessage}\`\`\``,
}, },
{ {
name: decorate("`🧷`", "Build Link"), name: decorate("`🧷`", "Build Link"),

View File

@@ -45,6 +45,49 @@ export const getGithubToken = async (
return installation.token; return installation.token;
}; };
/**
* Check if a GitHub user has write/admin permissions on a repository
* This is used to validate PR authors before allowing preview deployments
*/
export const checkUserRepositoryPermissions = async (
githubProvider: Github,
owner: string,
repo: string,
username: string,
): Promise<{ hasWriteAccess: boolean; permission: string | null }> => {
try {
const octokit = authGithub(githubProvider);
// Check if user is a collaborator with write permissions
const { data: permission } =
await octokit.rest.repos.getCollaboratorPermissionLevel({
owner,
repo,
username,
});
// Allow only users with 'write', 'admin', or 'maintain' permissions
// Currently exists Read, Triage, Write, Maintain, Admin
const allowedPermissions = ["write", "admin", "maintain"];
const hasWriteAccess = allowedPermissions.includes(permission.permission);
return {
hasWriteAccess,
permission: permission.permission,
};
} catch (error) {
// If user is not a collaborator, GitHub API returns 404
console.warn(
`User ${username} is not a collaborator of ${owner}/${repo}:`,
error,
);
return {
hasWriteAccess: false,
permission: null,
};
}
};
export const haveGithubRequirements = (githubProvider: Github) => { export const haveGithubRequirements = (githubProvider: Github) => {
return !!( return !!(
githubProvider?.githubAppId && githubProvider?.githubAppId &&

View File

@@ -1,6 +1,6 @@
import fs, { writeFileSync } from "node:fs"; import fs, { writeFileSync } from "node:fs";
import path from "node:path";
import { createReadStream } from "node:fs"; import { createReadStream } from "node:fs";
import path from "node:path";
import { createInterface } from "node:readline"; import { createInterface } from "node:readline";
import { paths } from "@dokploy/server/constants"; import { paths } from "@dokploy/server/constants";
import type { Domain } from "@dokploy/server/services/domain"; import type { Domain } from "@dokploy/server/services/domain";
@@ -237,7 +237,6 @@ export const writeTraefikConfigInPath = async (
} else { } else {
fs.writeFileSync(configPath, traefikConfig, "utf8"); fs.writeFileSync(configPath, traefikConfig, "utf8");
} }
fs.writeFileSync(configPath, traefikConfig, "utf8");
} catch (e) { } catch (e) {
console.error("Error saving the YAML config file:", e); console.error("Error saving the YAML config file:", e);
} }

View File

@@ -1,12 +1,12 @@
import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
import { paths } from "@dokploy/server/constants"; import { paths } from "@dokploy/server/constants";
import type { Domain } from "@dokploy/server/services/domain";
import { dump, load } from "js-yaml"; import { dump, load } from "js-yaml";
import type { ApplicationNested } from "../builders"; import type { ApplicationNested } from "../builders";
import { execAsyncRemote } from "../process/execAsync"; import { execAsyncRemote } from "../process/execAsync";
import { writeTraefikConfigRemote } from "./application"; import { writeTraefikConfigRemote } from "./application";
import type { FileConfig } from "./file-types"; import type { FileConfig } from "./file-types";
import type { Domain } from "@dokploy/server/services/domain";
export const addMiddleware = (config: FileConfig, middlewareName: string) => { export const addMiddleware = (config: FileConfig, middlewareName: string) => {
if (config.http?.routers) { if (config.http?.routers) {

View File

@@ -1,9 +1,9 @@
import type { findVolumeBackupById } from "@dokploy/server/services/volume-backups";
import { normalizeS3Path } from "../backups/utils";
import { getS3Credentials } from "../backups/utils";
import path from "node:path"; import path from "node:path";
import { paths } from "@dokploy/server/constants"; import { paths } from "@dokploy/server/constants";
import { findComposeById } from "@dokploy/server/services/compose"; import { findComposeById } from "@dokploy/server/services/compose";
import type { findVolumeBackupById } from "@dokploy/server/services/volume-backups";
import { normalizeS3Path } from "../backups/utils";
import { getS3Credentials } from "../backups/utils";
export const backupVolume = async ( export const backupVolume = async (
volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>, volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>,

View File

@@ -1,3 +1,4 @@
import path from "node:path";
import { import {
findApplicationById, findApplicationById,
findComposeById, findComposeById,
@@ -5,7 +6,6 @@ import {
getS3Credentials, getS3Credentials,
paths, paths,
} from "../.."; } from "../..";
import path from "node:path";
export const restoreVolume = async ( export const restoreVolume = async (
id: string, id: string,

View File

@@ -1,4 +1,5 @@
import { findVolumeBackupById } from "@dokploy/server/services/volume-backups"; import { findVolumeBackupById } from "@dokploy/server/services/volume-backups";
import { scheduleJob, scheduledJobs } from "node-schedule";
import { import {
createDeploymentVolumeBackup, createDeploymentVolumeBackup,
execAsync, execAsync,
@@ -6,7 +7,6 @@ import {
updateDeploymentStatus, updateDeploymentStatus,
} from "../.."; } from "../..";
import { backupVolume } from "./backup"; import { backupVolume } from "./backup";
import { scheduleJob, scheduledJobs } from "node-schedule";
export const scheduleVolumeBackup = async (volumeBackupId: string) => { export const scheduleVolumeBackup = async (volumeBackupId: string) => {
const volumeBackup = await findVolumeBackupById(volumeBackupId); const volumeBackup = await findVolumeBackupById(volumeBackupId);

74
pnpm-lock.yaml generated
View File

@@ -13,8 +13,8 @@ importers:
.: .:
devDependencies: devDependencies:
'@biomejs/biome': '@biomejs/biome':
specifier: 1.9.4 specifier: 2.1.1
version: 1.9.4 version: 2.1.1
'@commitlint/cli': '@commitlint/cli':
specifier: ^19.8.1 specifier: ^19.8.1
version: 19.8.1(@types/node@18.19.104)(typescript@5.8.3) version: 19.8.1(@types/node@18.19.104)(typescript@5.8.3)
@@ -933,55 +933,55 @@ packages:
'@better-fetch/fetch@1.1.18': '@better-fetch/fetch@1.1.18':
resolution: {integrity: sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==} resolution: {integrity: sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==}
'@biomejs/biome@1.9.4': '@biomejs/biome@2.1.1':
resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} resolution: {integrity: sha512-HFGYkxG714KzG+8tvtXCJ1t1qXQMzgWzfvQaUjxN6UeKv+KvMEuliInnbZLJm6DXFXwqVi6446EGI0sGBLIYng==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
hasBin: true hasBin: true
'@biomejs/cli-darwin-arm64@1.9.4': '@biomejs/cli-darwin-arm64@2.1.1':
resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==} resolution: {integrity: sha512-2Muinu5ok4tWxq4nu5l19el48cwCY/vzvI7Vjbkf3CYIQkjxZLyj0Ad37Jv2OtlXYaLvv+Sfu1hFeXt/JwRRXQ==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@biomejs/cli-darwin-x64@1.9.4': '@biomejs/cli-darwin-x64@2.1.1':
resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==} resolution: {integrity: sha512-cC8HM5lrgKQXLAK+6Iz2FrYW5A62pAAX6KAnRlEyLb+Q3+Kr6ur/sSuoIacqlp1yvmjHJqjYfZjPvHWnqxoEIA==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@biomejs/cli-linux-arm64-musl@1.9.4': '@biomejs/cli-linux-arm64-musl@2.1.1':
resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==} resolution: {integrity: sha512-/7FBLnTswu4jgV9ttI3AMIdDGqVEPIZd8I5u2D4tfCoj8rl9dnjrEQbAIDlWhUXdyWlFSz8JypH3swU9h9P+2A==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@biomejs/cli-linux-arm64@1.9.4': '@biomejs/cli-linux-arm64@2.1.1':
resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} resolution: {integrity: sha512-tw4BEbhAUkWPe4WBr6IX04DJo+2jz5qpPzpW/SWvqMjb9QuHY8+J0M23V8EPY/zWU4IG8Ui0XESapR1CB49Q7g==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@biomejs/cli-linux-x64-musl@1.9.4': '@biomejs/cli-linux-x64-musl@2.1.1':
resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} resolution: {integrity: sha512-kUu+loNI3OCD2c12cUt7M5yaaSjDnGIksZwKnueubX6c/HWUyi/0mPbTBHR49Me3F0KKjWiKM+ZOjsmC+lUt9g==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@biomejs/cli-linux-x64@1.9.4': '@biomejs/cli-linux-x64@2.1.1':
resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} resolution: {integrity: sha512-3WJ1GKjU7NzZb6RTbwLB59v9cTIlzjbiFLDB0z4376TkDqoNYilJaC37IomCr/aXwuU8QKkrYoHrgpSq5ffJ4Q==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@biomejs/cli-win32-arm64@1.9.4': '@biomejs/cli-win32-arm64@2.1.1':
resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} resolution: {integrity: sha512-vEHK0v0oW+E6RUWLoxb2isI3rZo57OX9ZNyyGH701fZPj6Il0Rn1f5DMNyCmyflMwTnIQstEbs7n2BxYSqQx4Q==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@biomejs/cli-win32-x64@1.9.4': '@biomejs/cli-win32-x64@2.1.1':
resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==} resolution: {integrity: sha512-i2PKdn70kY++KEF/zkQFvQfX1e8SkA8hq4BgC+yE9dZqyLzB/XStY2MvwI3qswlRgnGpgncgqe0QYKVS1blksg==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
@@ -7281,39 +7281,39 @@ snapshots:
'@better-fetch/fetch@1.1.18': {} '@better-fetch/fetch@1.1.18': {}
'@biomejs/biome@1.9.4': '@biomejs/biome@2.1.1':
optionalDependencies: optionalDependencies:
'@biomejs/cli-darwin-arm64': 1.9.4 '@biomejs/cli-darwin-arm64': 2.1.1
'@biomejs/cli-darwin-x64': 1.9.4 '@biomejs/cli-darwin-x64': 2.1.1
'@biomejs/cli-linux-arm64': 1.9.4 '@biomejs/cli-linux-arm64': 2.1.1
'@biomejs/cli-linux-arm64-musl': 1.9.4 '@biomejs/cli-linux-arm64-musl': 2.1.1
'@biomejs/cli-linux-x64': 1.9.4 '@biomejs/cli-linux-x64': 2.1.1
'@biomejs/cli-linux-x64-musl': 1.9.4 '@biomejs/cli-linux-x64-musl': 2.1.1
'@biomejs/cli-win32-arm64': 1.9.4 '@biomejs/cli-win32-arm64': 2.1.1
'@biomejs/cli-win32-x64': 1.9.4 '@biomejs/cli-win32-x64': 2.1.1
'@biomejs/cli-darwin-arm64@1.9.4': '@biomejs/cli-darwin-arm64@2.1.1':
optional: true optional: true
'@biomejs/cli-darwin-x64@1.9.4': '@biomejs/cli-darwin-x64@2.1.1':
optional: true optional: true
'@biomejs/cli-linux-arm64-musl@1.9.4': '@biomejs/cli-linux-arm64-musl@2.1.1':
optional: true optional: true
'@biomejs/cli-linux-arm64@1.9.4': '@biomejs/cli-linux-arm64@2.1.1':
optional: true optional: true
'@biomejs/cli-linux-x64-musl@1.9.4': '@biomejs/cli-linux-x64-musl@2.1.1':
optional: true optional: true
'@biomejs/cli-linux-x64@1.9.4': '@biomejs/cli-linux-x64@2.1.1':
optional: true optional: true
'@biomejs/cli-win32-arm64@1.9.4': '@biomejs/cli-win32-arm64@2.1.1':
optional: true optional: true
'@biomejs/cli-win32-x64@1.9.4': '@biomejs/cli-win32-x64@2.1.1':
optional: true optional: true
'@codemirror/autocomplete@6.18.6': '@codemirror/autocomplete@6.18.6':

View File

@@ -1,6 +1,5 @@
packages: packages:
- "apps/api" - "apps/api"
- "apps/dokploy" - "apps/dokploy"
- "apps/monitoring"
- "apps/schedules" - "apps/schedules"
- "packages/server" - "packages/server"