mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-19 06:05:25 +02:00
Compare commits
58 Commits
1365-creat
...
v0.24.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17e9154887 | ||
|
|
2442494096 | ||
|
|
bac2afb423 | ||
|
|
4e9630e976 | ||
|
|
558f6aecae | ||
|
|
c3e2b0d0f1 | ||
|
|
11d584316a | ||
|
|
f78dc555b2 | ||
|
|
5812b12a59 | ||
|
|
7301d15e8f | ||
|
|
f79796a6c8 | ||
|
|
4122b37abd | ||
|
|
79e9593663 | ||
|
|
def3fa0030 | ||
|
|
d561068bcd | ||
|
|
212c1b2d5f | ||
|
|
d3a54172b5 | ||
|
|
cda33eb291 | ||
|
|
c178234e53 | ||
|
|
329db1fd1a | ||
|
|
6efbf030a7 | ||
|
|
b95dfed8fc | ||
|
|
7fe3418d55 | ||
|
|
288d86c73b | ||
|
|
ffd5ccd386 | ||
|
|
98ddd096e5 | ||
|
|
da6cc9fe72 | ||
|
|
22d0af269e | ||
|
|
f0fdc46de5 | ||
|
|
9aea24115d | ||
|
|
a9ee6c2393 | ||
|
|
349717044c | ||
|
|
f94f32695f | ||
|
|
37b78ea09c | ||
|
|
9b89b4631f | ||
|
|
7100095f2b | ||
|
|
a36ab65aa6 | ||
|
|
bf81ba20ff | ||
|
|
658a4a9b99 | ||
|
|
47cb096cf3 | ||
|
|
f3856722da | ||
|
|
a67c3eb979 | ||
|
|
aaa205f104 | ||
|
|
cadea7ff28 | ||
|
|
9ab937f726 | ||
|
|
d0af517eb7 | ||
|
|
66bdf9bf0a | ||
|
|
d4a3af475a | ||
|
|
e92a8d7c98 | ||
|
|
c4fec8cee5 | ||
|
|
55f75bce53 | ||
|
|
fdc524d79d | ||
|
|
93d6662466 | ||
|
|
1977235d31 | ||
|
|
1dd713a1d1 | ||
|
|
18b65f28f2 | ||
|
|
666db23b8e | ||
|
|
2ca5321fdc |
3
.github/workflows/deploy.yml
vendored
3
.github/workflows/deploy.yml
vendored
@@ -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:
|
||||||
|
|||||||
3
.github/workflows/dokploy.yml
vendored
3
.github/workflows/dokploy.yml
vendored
@@ -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
|
||||||
|
|||||||
6
.github/workflows/format.yml
vendored
6
.github/workflows/format.yml
vendored
@@ -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
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["biomejs.biome"]
|
||||||
|
}
|
||||||
8
.vscode/settings.json
vendored
Normal file
8
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "biomejs.biome",
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.biome": "explicit",
|
||||||
|
"source.organizeImports.biome": "explicit"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const baseApp: ApplicationNested = {
|
|||||||
herokuVersion: "",
|
herokuVersion: "",
|
||||||
giteaBranch: "",
|
giteaBranch: "",
|
||||||
giteaBuildPath: "",
|
giteaBuildPath: "",
|
||||||
|
previewRequireCollaboratorPermissions: false,
|
||||||
giteaId: "",
|
giteaId: "",
|
||||||
giteaOwner: "",
|
giteaOwner: "",
|
||||||
giteaRepository: "",
|
giteaRepository: "",
|
||||||
|
|||||||
@@ -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: "",
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,68 +157,84 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
{hasServers && (
|
||||||
control={form.control}
|
<FormField
|
||||||
name="serverId"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="serverId"
|
||||||
<FormItem>
|
render={({ field }) => (
|
||||||
<TooltipProvider delayDuration={0}>
|
<FormItem>
|
||||||
<Tooltip>
|
<TooltipProvider delayDuration={0}>
|
||||||
<TooltipTrigger asChild>
|
<Tooltip>
|
||||||
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
|
<TooltipTrigger asChild>
|
||||||
Select a Server {!isCloud ? "(Optional)" : ""}
|
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
|
||||||
<HelpCircle className="size-4 text-muted-foreground" />
|
Select a Server {!isCloud ? "(Optional)" : ""}
|
||||||
</FormLabel>
|
<HelpCircle className="size-4 text-muted-foreground" />
|
||||||
</TooltipTrigger>
|
</FormLabel>
|
||||||
<TooltipContent
|
</TooltipTrigger>
|
||||||
className="z-[999] w-[300px]"
|
<TooltipContent
|
||||||
align="start"
|
className="z-[999] w-[300px]"
|
||||||
side="top"
|
align="start"
|
||||||
>
|
side="top"
|
||||||
<span>
|
>
|
||||||
If no server is selected, the application will be
|
<span>
|
||||||
deployed on the server where the user is logged in.
|
If no server is selected, the application will be
|
||||||
</span>
|
deployed on the server where the user is logged in.
|
||||||
</TooltipContent>
|
</span>
|
||||||
</Tooltip>
|
</TooltipContent>
|
||||||
</TooltipProvider>
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
defaultValue={field.value}
|
defaultValue={field.value}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select a Server" />
|
<SelectValue placeholder="Select a Server" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
{servers?.map((server) => (
|
{servers?.map((server) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={server.serverId}
|
key={server.serverId}
|
||||||
value={server.serverId}
|
value={server.serverId}
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2 justify-between w-full">
|
<span className="flex items-center gap-2 justify-between w-full">
|
||||||
<span>{server.name}</span>
|
<span>{server.name}</span>
|
||||||
<span className="text-muted-foreground text-xs self-center">
|
<span className="text-muted-foreground text-xs self-center">
|
||||||
{server.ipAddress}
|
{server.ipAddress}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</SelectItem>
|
||||||
</SelectItem>
|
))}
|
||||||
))}
|
<SelectLabel>Servers ({servers?.length})</SelectLabel>
|
||||||
<SelectLabel>Servers ({servers?.length})</SelectLabel>
|
</SelectGroup>
|
||||||
</SelectGroup>
|
</SelectContent>
|
||||||
</SelectContent>
|
</Select>
|
||||||
</Select>
|
<FormMessage />
|
||||||
<FormMessage />
|
</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>
|
||||||
|
|||||||
@@ -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,62 +165,64 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<FormField
|
{hasServers && (
|
||||||
control={form.control}
|
<FormField
|
||||||
name="serverId"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="serverId"
|
||||||
<FormItem>
|
render={({ field }) => (
|
||||||
<TooltipProvider delayDuration={0}>
|
<FormItem>
|
||||||
<Tooltip>
|
<TooltipProvider delayDuration={0}>
|
||||||
<TooltipTrigger asChild>
|
<Tooltip>
|
||||||
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
|
<TooltipTrigger asChild>
|
||||||
Select a Server {!isCloud ? "(Optional)" : ""}
|
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
|
||||||
<HelpCircle className="size-4 text-muted-foreground" />
|
Select a Server {!isCloud ? "(Optional)" : ""}
|
||||||
</FormLabel>
|
<HelpCircle className="size-4 text-muted-foreground" />
|
||||||
</TooltipTrigger>
|
</FormLabel>
|
||||||
<TooltipContent
|
</TooltipTrigger>
|
||||||
className="z-[999] w-[300px]"
|
<TooltipContent
|
||||||
align="start"
|
className="z-[999] w-[300px]"
|
||||||
side="top"
|
align="start"
|
||||||
>
|
side="top"
|
||||||
<span>
|
>
|
||||||
If no server is selected, the application will be
|
<span>
|
||||||
deployed on the server where the user is logged in.
|
If no server is selected, the application will be
|
||||||
</span>
|
deployed on the server where the user is logged in.
|
||||||
</TooltipContent>
|
</span>
|
||||||
</Tooltip>
|
</TooltipContent>
|
||||||
</TooltipProvider>
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
defaultValue={field.value}
|
defaultValue={field.value}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select a Server" />
|
<SelectValue placeholder="Select a Server" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
{servers?.map((server) => (
|
{servers?.map((server) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={server.serverId}
|
key={server.serverId}
|
||||||
value={server.serverId}
|
value={server.serverId}
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2 justify-between w-full">
|
<span className="flex items-center gap-2 justify-between w-full">
|
||||||
<span>{server.name}</span>
|
<span>{server.name}</span>
|
||||||
<span className="text-muted-foreground text-xs self-center">
|
<span className="text-muted-foreground text-xs self-center">
|
||||||
{server.ipAddress}
|
{server.ipAddress}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</SelectItem>
|
||||||
</SelectItem>
|
))}
|
||||||
))}
|
<SelectLabel>Servers ({servers?.length})</SelectLabel>
|
||||||
<SelectLabel>Servers ({servers?.length})</SelectLabel>
|
</SelectGroup>
|
||||||
</SelectGroup>
|
</SelectContent>
|
||||||
</SelectContent>
|
</Select>
|
||||||
</Select>
|
<FormMessage />
|
||||||
<FormMessage />
|
</FormItem>
|
||||||
</FormItem>
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
)}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="appName"
|
name="appName"
|
||||||
|
|||||||
@@ -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,45 +382,62 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
{hasServers && (
|
||||||
control={form.control}
|
<FormField
|
||||||
name="serverId"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="serverId"
|
||||||
<FormItem>
|
render={({ field }) => (
|
||||||
<FormLabel>Select a Server</FormLabel>
|
<FormItem>
|
||||||
<Select
|
<FormLabel>Select a Server</FormLabel>
|
||||||
onValueChange={field.onChange}
|
<Select
|
||||||
defaultValue={field.value || ""}
|
onValueChange={field.onChange}
|
||||||
>
|
defaultValue={field.value || ""}
|
||||||
<SelectTrigger>
|
>
|
||||||
<SelectValue placeholder="Select a Server" />
|
<SelectTrigger>
|
||||||
</SelectTrigger>
|
<SelectValue placeholder="Select a Server" />
|
||||||
<SelectContent>
|
</SelectTrigger>
|
||||||
<SelectGroup>
|
<SelectContent>
|
||||||
{servers?.map((server) => (
|
<SelectGroup>
|
||||||
<SelectItem
|
{servers?.map((server) => (
|
||||||
key={server.serverId}
|
<SelectItem
|
||||||
value={server.serverId}
|
key={server.serverId}
|
||||||
>
|
value={server.serverId}
|
||||||
{server.name}
|
>
|
||||||
</SelectItem>
|
{server.name}
|
||||||
))}
|
</SelectItem>
|
||||||
<SelectLabel>
|
))}
|
||||||
Servers ({servers?.length})
|
<SelectLabel>
|
||||||
</SelectLabel>
|
Servers ({servers?.length})
|
||||||
</SelectGroup>
|
</SelectLabel>
|
||||||
</SelectContent>
|
</SelectGroup>
|
||||||
</Select>
|
</SelectContent>
|
||||||
<FormMessage />
|
</Select>
|
||||||
</FormItem>
|
<FormMessage />
|
||||||
)}
|
</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>
|
||||||
|
|||||||
@@ -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,60 +427,62 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
|
|||||||
project.
|
project.
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
|
|
||||||
<div>
|
{hasServers && (
|
||||||
<TooltipProvider delayDuration={0}>
|
<div>
|
||||||
<Tooltip>
|
<TooltipProvider delayDuration={0}>
|
||||||
<TooltipTrigger asChild>
|
<Tooltip>
|
||||||
<Label className="break-all w-fit flex flex-row gap-1 items-center pb-2 pt-3.5">
|
<TooltipTrigger asChild>
|
||||||
Select a Server{" "}
|
<Label className="break-all w-fit flex flex-row gap-1 items-center pb-2 pt-3.5">
|
||||||
{!isCloud ? "(Optional)" : ""}
|
Select a Server{" "}
|
||||||
<HelpCircle className="size-4 text-muted-foreground" />
|
{!isCloud ? "(Optional)" : ""}
|
||||||
</Label>
|
<HelpCircle className="size-4 text-muted-foreground" />
|
||||||
</TooltipTrigger>
|
</Label>
|
||||||
<TooltipContent
|
</TooltipTrigger>
|
||||||
className="z-[999] w-[300px]"
|
<TooltipContent
|
||||||
align="start"
|
className="z-[999] w-[300px]"
|
||||||
side="top"
|
align="start"
|
||||||
>
|
side="top"
|
||||||
<span>
|
>
|
||||||
If no server is selected, the application
|
<span>
|
||||||
will be deployed on the server where the
|
If no server is selected, the
|
||||||
user is logged in.
|
application will be deployed on the
|
||||||
</span>
|
server where the user is logged in.
|
||||||
</TooltipContent>
|
</span>
|
||||||
</Tooltip>
|
</TooltipContent>
|
||||||
</TooltipProvider>
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
onValueChange={(e) => {
|
onValueChange={(e) => {
|
||||||
setServerId(e);
|
setServerId(e);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select a Server" />
|
<SelectValue placeholder="Select a Server" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
{servers?.map((server) => (
|
{servers?.map((server) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={server.serverId}
|
key={server.serverId}
|
||||||
value={server.serverId}
|
value={server.serverId}
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2 justify-between w-full">
|
<span className="flex items-center gap-2 justify-between w-full">
|
||||||
<span>{server.name}</span>
|
<span>{server.name}</span>
|
||||||
<span className="text-muted-foreground text-xs self-center">
|
<span className="text-muted-foreground text-xs self-center">
|
||||||
{server.ipAddress}
|
{server.ipAddress}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</SelectItem>
|
||||||
</SelectItem>
|
))}
|
||||||
))}
|
<SelectLabel>
|
||||||
<SelectLabel>
|
Servers ({servers?.length})
|
||||||
Servers ({servers?.length})
|
</SelectLabel>
|
||||||
</SelectLabel>
|
</SelectGroup>
|
||||||
</SelectGroup>
|
</SelectContent>
|
||||||
</SelectContent>
|
</Select>
|
||||||
</Select>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
|||||||
@@ -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,37 +48,39 @@ export const StepOne = ({ setTemplateInfo, templateInfo }: any) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
{hasServers && (
|
||||||
<Label htmlFor="server-deploy">
|
<div className="space-y-2">
|
||||||
Select the server where you want to deploy (optional)
|
<Label htmlFor="server-deploy">
|
||||||
</Label>
|
Select the server where you want to deploy (optional)
|
||||||
<Select
|
</Label>
|
||||||
value={templateInfo.server?.serverId}
|
<Select
|
||||||
onValueChange={(value) => {
|
value={templateInfo.server?.serverId}
|
||||||
const server = servers?.find((s) => s.serverId === value);
|
onValueChange={(value) => {
|
||||||
if (server) {
|
const server = servers?.find((s) => s.serverId === value);
|
||||||
setTemplateInfo({
|
if (server) {
|
||||||
...templateInfo,
|
setTemplateInfo({
|
||||||
server: server,
|
...templateInfo,
|
||||||
});
|
server: server,
|
||||||
}
|
});
|
||||||
}}
|
}
|
||||||
>
|
}}
|
||||||
<SelectTrigger className="w-full">
|
>
|
||||||
<SelectValue placeholder="Select a server" />
|
<SelectTrigger className="w-full">
|
||||||
</SelectTrigger>
|
<SelectValue placeholder="Select a server" />
|
||||||
<SelectContent>
|
</SelectTrigger>
|
||||||
<SelectGroup>
|
<SelectContent>
|
||||||
{servers?.map((server) => (
|
<SelectGroup>
|
||||||
<SelectItem key={server.serverId} value={server.serverId}>
|
{servers?.map((server) => (
|
||||||
{server.name}
|
<SelectItem key={server.serverId} value={server.serverId}>
|
||||||
</SelectItem>
|
{server.name}
|
||||||
))}
|
</SelectItem>
|
||||||
<SelectLabel>Servers ({servers?.length})</SelectLabel>
|
))}
|
||||||
</SelectGroup>
|
<SelectLabel>Servers ({servers?.length})</SelectLabel>
|
||||||
</SelectContent>
|
</SelectGroup>
|
||||||
</Select>
|
</SelectContent>
|
||||||
</div>
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Examples:</Label>
|
<Label>Examples:</Label>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!destinationId,
|
enabled: !!destinationId,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -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 || "",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,89 +64,122 @@ 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"
|
||||||
|
>
|
||||||
<div className="flex flex-col gap-2 w-full border rounded-lg p-4">
|
Add SSH Key when creating server in your provider
|
||||||
<span className="text-base font-semibold text-primary">
|
</Label>
|
||||||
Option 1
|
</div>
|
||||||
</span>
|
</RadioGroup>
|
||||||
<ul>
|
|
||||||
<li className="items-center flex gap-1">
|
|
||||||
1. Login to your server{" "}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
2. When you are logged in run the following command
|
|
||||||
<div className="flex relative flex-col gap-4 w-full mt-2">
|
|
||||||
<CodeEditor
|
|
||||||
lineWrapping
|
|
||||||
language="properties"
|
|
||||||
value={`echo "${cloudSSHKey?.publicKey}" >> ~/.ssh/authorized_keys`}
|
|
||||||
readOnly
|
|
||||||
className="font-mono opacity-60"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="absolute right-2 top-2"
|
|
||||||
onClick={() => {
|
|
||||||
copy(
|
|
||||||
`echo "${cloudSSHKey?.publicKey}" >> ~/.ssh/authorized_keys`,
|
|
||||||
);
|
|
||||||
toast.success("Copied to clipboard");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CopyIcon className="size-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li className="mt-1">
|
|
||||||
3. You're done, follow the next step to insert the details
|
|
||||||
of your server.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2 w-full mt-2 border rounded-lg p-4">
|
|
||||||
<span className="text-base font-semibold text-primary">
|
{/* Content based on selected option */}
|
||||||
Option 2
|
{selectedOption === "manual" && (
|
||||||
</span>
|
<div className="flex flex-col gap-2 w-full border rounded-lg p-4">
|
||||||
<div className="flex flex-col gap-4 w-full overflow-auto">
|
<span className="text-base font-semibold text-primary">
|
||||||
<div className="flex relative flex-col gap-2 overflow-y-auto">
|
Manual Setup Instructions
|
||||||
<div className="text-sm text-primary flex flex-row gap-2 items-center">
|
</span>
|
||||||
Copy Public Key
|
<ul className="space-y-2">
|
||||||
<button
|
<li className="items-center flex gap-1">
|
||||||
type="button"
|
1. Login to your server
|
||||||
className="right-2 top-8"
|
</li>
|
||||||
onClick={() => {
|
<li>
|
||||||
copy(
|
2. When you are logged in run the following command
|
||||||
cloudSSHKey?.publicKey || "Generate a SSH Key",
|
<div className="flex relative flex-col gap-4 w-full mt-2">
|
||||||
);
|
<CodeEditor
|
||||||
toast.success("SSH Copied to clipboard");
|
lineWrapping
|
||||||
}}
|
language="properties"
|
||||||
>
|
value={`echo "${cloudSSHKey?.publicKey}" >> ~/.ssh/authorized_keys`}
|
||||||
<CopyIcon className="size-4 text-muted-foreground" />
|
readOnly
|
||||||
</button>
|
className="font-mono opacity-60"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="absolute right-2 top-2"
|
||||||
|
onClick={() => {
|
||||||
|
copy(
|
||||||
|
`echo "${cloudSSHKey?.publicKey}" >> ~/.ssh/authorized_keys`,
|
||||||
|
);
|
||||||
|
toast.success("Copied to clipboard");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CopyIcon className="size-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li className="mt-1">
|
||||||
|
3. You're done, follow the next step to insert the
|
||||||
|
details of your server.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedOption === "provider" && (
|
||||||
|
<div className="flex flex-col gap-2 w-full border rounded-lg p-4">
|
||||||
|
<span className="text-base font-semibold text-primary">
|
||||||
|
Provider Setup Instructions
|
||||||
|
</span>
|
||||||
|
<div className="flex flex-col gap-4 w-full overflow-auto">
|
||||||
|
<div className="flex relative flex-col gap-2 overflow-y-auto">
|
||||||
|
<div className="text-sm text-primary flex flex-row gap-2 items-center">
|
||||||
|
Copy Public Key
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="right-2 top-8"
|
||||||
|
onClick={() => {
|
||||||
|
copy(
|
||||||
|
cloudSSHKey?.publicKey || "Generate a SSH Key",
|
||||||
|
);
|
||||||
|
toast.success("SSH Copied to clipboard");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CopyIcon className="size-4 text-muted-foreground" />
|
||||||
|
</button>
|
||||||
|
</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
|
||||||
|
href="https://docs.dokploy.com/docs/core/multi-server/instructions#requirements"
|
||||||
|
target="_blank"
|
||||||
|
className="text-primary flex flex-row gap-2 mt-2"
|
||||||
|
>
|
||||||
|
View Tutorial <ExternalLinkIcon className="size-4" />
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
)}
|
||||||
href="https://docs.dokploy.com/docs/core/multi-server/instructions#requirements"
|
|
||||||
target="_blank"
|
|
||||||
className="text-primary flex flex-row gap-2"
|
|
||||||
>
|
|
||||||
View Tutorial <ExternalLinkIcon className="size-4" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
1
apps/dokploy/drizzle/0103_cultured_pestilence.sql
Normal file
1
apps/dokploy/drizzle/0103_cultured_pestilence.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "application" ADD COLUMN "previewRequireCollaboratorPermissions" boolean DEFAULT true;
|
||||||
6136
apps/dokploy/drizzle/meta/0103_snapshot.json
Normal file
6136
apps/dokploy/drizzle/meta/0103_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.\-_]+$/;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
5
apps/dokploy/types/chatwoot.d.ts
vendored
5
apps/dokploy/types/chatwoot.d.ts
vendored
@@ -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: (
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
34
biome.json
34
biome.json
@@ -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",
|
||||||
|
|||||||
15
package.json
15
package.json
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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", {
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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"
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -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 = (
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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 &&
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>>,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
74
pnpm-lock.yaml
generated
@@ -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':
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
packages:
|
packages:
|
||||||
- "apps/api"
|
- "apps/api"
|
||||||
- "apps/dokploy"
|
- "apps/dokploy"
|
||||||
- "apps/monitoring"
|
|
||||||
- "apps/schedules"
|
- "apps/schedules"
|
||||||
- "packages/server"
|
- "packages/server"
|
||||||
|
|||||||
Reference in New Issue
Block a user