Merge pull request #4372 from Dokploy/canary

🚀 Release v0.29.3
This commit is contained in:
Mauricio Siu
2026-05-11 11:57:12 -06:00
committed by GitHub
126 changed files with 9122 additions and 383 deletions

View File

@@ -4,5 +4,8 @@
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit", "source.fixAll.biome": "explicit",
"source.organizeImports.biome": "explicit" "source.organizeImports.biome": "explicit"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
} }
} }

View File

@@ -66,7 +66,7 @@ COPY --from=buildpacksio/pack:0.39.1 /usr/local/bin/pack /usr/local/bin/pack
EXPOSE 3000 EXPOSE 3000
HEALTHCHECK --interval=10s --timeout=3s --retries=10 \ HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=5 \
CMD curl -fs http://localhost:3000/api/trpc/settings.health || exit 1 CMD curl -fs http://localhost:3000/api/trpc/settings.health || exit 1
CMD ["sh", "-c", "pnpm run wait-for-postgres && exec pnpm start"] CMD ["sh", "-c", "pnpm run wait-for-postgres && exec pnpm start"]

View File

@@ -494,4 +494,49 @@ describe("processTemplate", () => {
expect(result.mounts).toHaveLength(1); expect(result.mounts).toHaveLength(1);
}); });
}); });
describe("isolated deployment config", () => {
it("should default to isolated=true when not specified", () => {
const template: CompleteTemplate = {
metadata: {} as any,
variables: {},
config: {
domains: [],
env: {},
},
};
expect(template.config.isolated).toBeUndefined();
// undefined !== false => isolatedDeployment = true
expect(template.config.isolated !== false).toBe(true);
});
it("should be isolated when isolated=true is explicitly set", () => {
const template: CompleteTemplate = {
metadata: {} as any,
variables: {},
config: {
isolated: true,
domains: [],
env: {},
},
};
expect(template.config.isolated !== false).toBe(true);
});
it("should disable isolated deployment when isolated=false", () => {
const template: CompleteTemplate = {
metadata: {} as any,
variables: {},
config: {
isolated: false,
domains: [],
env: {},
},
};
expect(template.config.isolated !== false).toBe(false);
});
});
}); });

View File

@@ -30,9 +30,7 @@ describe("helpers functions", () => {
const domain = processValue("${domain}", {}, mockSchema); const domain = processValue("${domain}", {}, mockSchema);
expect(domain.startsWith(`${mockSchema.projectName}-`)).toBeTruthy(); expect(domain.startsWith(`${mockSchema.projectName}-`)).toBeTruthy();
expect( expect(
domain.endsWith( domain.endsWith(`${mockSchema.serverIp.replaceAll(".", "-")}.sslip.io`),
`${mockSchema.serverIp.replaceAll(".", "-")}.traefik.me`,
),
).toBeTruthy(); ).toBeTruthy();
}); });
}); });

View File

@@ -21,9 +21,9 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import type { RouterOutputs } from "@/utils/api"; import type { RouterOutputs } from "@/utils/api";
import type { ValidationStates } from "./show-domains";
import { AddDomain } from "./handle-domain";
import { DnsHelperModal } from "./dns-helper-modal"; import { DnsHelperModal } from "./dns-helper-modal";
import { AddDomain } from "./handle-domain";
import type { ValidationStates } from "./show-domains";
export type Domain = export type Domain =
| RouterOutputs["domain"]["byApplicationId"][0] | RouterOutputs["domain"]["byApplicationId"][0]
@@ -168,7 +168,7 @@ export const createColumns = ({
{domain.certificateType} {domain.certificateType}
</Badge> </Badge>
)} )}
{!domain.host.includes("traefik.me") && ( {!domain.host.includes("sslip.io") && (
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
@@ -256,7 +256,7 @@ export const createColumns = ({
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{!domain.host.includes("traefik.me") && ( {!domain.host.includes("sslip.io") && (
<DnsHelperModal <DnsHelperModal
domain={{ domain={{
host: domain.host, host: domain.host,

View File

@@ -225,7 +225,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
const https = form.watch("https"); const https = form.watch("https");
const domainType = form.watch("domainType"); const domainType = form.watch("domainType");
const host = form.watch("host"); const host = form.watch("host");
const isTraefikMeDomain = host?.includes("traefik.me") || false; const isTraefikMeDomain = host?.includes("sslip.io") || false;
useEffect(() => { useEffect(() => {
if (data) { if (data) {
@@ -513,7 +513,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
{!canGenerateTraefikMeDomains && {!canGenerateTraefikMeDomains &&
field.value.includes("traefik.me") && ( field.value.includes("sslip.io") && (
<AlertBlock type="warning"> <AlertBlock type="warning">
You need to set an IP address in your{" "} You need to set an IP address in your{" "}
<Link <Link
@@ -524,12 +524,12 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
? "Remote Servers -> Server -> Edit Server -> Update IP Address" ? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"} : "Web Server -> Server -> Update Server IP"}
</Link>{" "} </Link>{" "}
to make your traefik.me domain work. to make your sslip.io domain work.
</AlertBlock> </AlertBlock>
)} )}
{isTraefikMeDomain && ( {isTraefikMeDomain && (
<AlertBlock type="info"> <AlertBlock type="info">
<strong>Note:</strong> traefik.me is a public HTTP <strong>Note:</strong> sslip.io is a public HTTP
service and does not support SSL/HTTPS. HTTPS and service and does not support SSL/HTTPS. HTTPS and
certificate options will not have any effect. certificate options will not have any effect.
</AlertBlock> </AlertBlock>
@@ -567,7 +567,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
sideOffset={5} sideOffset={5}
className="max-w-[10rem]" className="max-w-[10rem]"
> >
<p>Generate traefik.me domain</p> <p>Generate sslip.io domain</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>

View File

@@ -425,7 +425,7 @@ export const ShowDomains = ({ id, type }: Props) => {
</Badge> </Badge>
)} )}
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
{!item.host.includes("traefik.me") && ( {!item.host.includes("sslip.io") && (
<DnsHelperModal <DnsHelperModal
domain={{ domain={{
host: item.host, host: item.host,

View File

@@ -5,6 +5,7 @@ import { useEffect } 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 { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { BitbucketIcon } from "@/components/icons/data-tools-icons"; import { BitbucketIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -57,7 +58,10 @@ const BitbucketProviderSchema = z.object({
slug: z.string().optional(), slug: z.string().optional(),
}) })
.required(), .required(),
branch: z.string().min(1, "Branch is required"), branch: z
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
bitbucketId: z.string().min(1, "Bitbucket Provider is required"), bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().optional(), enableSubmodules: z.boolean().optional(),

View File

@@ -6,6 +6,7 @@ import { useEffect } 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 { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { GitIcon } from "@/components/icons/data-tools-icons"; import { GitIcon } from "@/components/icons/data-tools-icons";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -41,7 +42,10 @@ const GitProviderSchema = z.object({
repositoryURL: z.string().min(1, { repositoryURL: z.string().min(1, {
message: "Repository URL is required", message: "Repository URL is required",
}), }),
branch: z.string().min(1, "Branch required"), branch: z
.string()
.min(1, "Branch required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
sshKey: z.string().optional(), sshKey: z.string().optional(),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false), enableSubmodules: z.boolean().default(false),
@@ -107,110 +111,103 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
return ( return (
<Form {...form}> <Form {...form}>
<form <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
onSubmit={form.handleSubmit(onSubmit)} <div className="grid grid-cols-2 lg:grid-cols-4 gap-4 items-start">
className="flex flex-col gap-4" <FormField
> control={form.control}
<div className="grid md:grid-cols-2 gap-4"> name="repositoryURL"
<div className="flex items-end col-span-2 gap-4"> render={({ field }) => (
<div className="grow"> <FormItem className="col-span-2 lg:col-span-3">
<FormField <div className="flex items-center justify-between h-5">
control={form.control} <FormLabel>Repository URL</FormLabel>
name="repositoryURL" {field.value?.startsWith("https://") && (
render={({ field }) => ( <Link
<FormItem> href={field.value}
<div className="flex items-center justify-between"> target="_blank"
<FormLabel>Repository URL</FormLabel> rel="noopener noreferrer"
{field.value?.startsWith("https://") && ( className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
<Link >
href={field.value} <GitIcon className="h-4 w-4" />
target="_blank" <span>View Repository</span>
rel="noopener noreferrer" </Link>
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary" )}
> </div>
<GitIcon className="h-4 w-4" /> <FormControl>
<span>View Repository</span> <Input placeholder="Repository URL" {...field} />
</Link> </FormControl>
)} <FormMessage />
</div> </FormItem>
<FormControl>
<Input placeholder="Repository URL" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{sshKeys && sshKeys.length > 0 ? (
<FormField
control={form.control}
name="sshKey"
render={({ field }) => (
<FormItem className="basis-40">
<FormLabel className="w-full inline-flex justify-between">
SSH Key
<LockIcon className="size-4 text-muted-foreground" />
</FormLabel>
<FormControl>
<Select
key={field.value}
onValueChange={field.onChange}
defaultValue={field.value}
value={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a key" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{sshKeys?.map((sshKey) => (
<SelectItem
key={sshKey.sshKeyId}
value={sshKey.sshKeyId}
>
{sshKey.name}
</SelectItem>
))}
<SelectItem value="none">None</SelectItem>
<SelectLabel>Keys ({sshKeys?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
</FormItem>
)}
/>
) : (
<Button
variant="secondary"
onClick={() => router.push("/dashboard/settings/ssh-keys")}
type="button"
>
<KeyRoundIcon className="size-4" /> Add SSH Key
</Button>
)} )}
</div> />
<div className="space-y-4"> {sshKeys && sshKeys.length > 0 ? (
<FormField <FormField
control={form.control} control={form.control}
name="branch" name="sshKey"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem className="col-span-2 lg:col-span-1">
<FormLabel>Branch</FormLabel> <FormLabel className="w-full inline-flex justify-between">
SSH Key
<LockIcon className="size-4 text-muted-foreground" />
</FormLabel>
<FormControl> <FormControl>
<Input placeholder="Branch" {...field} /> <Select
key={field.value}
onValueChange={field.onChange}
defaultValue={field.value}
value={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a key" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{sshKeys?.map((sshKey) => (
<SelectItem
key={sshKey.sshKeyId}
value={sshKey.sshKeyId}
>
{sshKey.name}
</SelectItem>
))}
<SelectItem value="none">None</SelectItem>
<SelectLabel>Keys ({sshKeys?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
</FormControl> </FormControl>
<FormMessage />
</FormItem> </FormItem>
)} )}
/> />
</div> ) : (
<Button
variant="secondary"
onClick={() => router.push("/dashboard/settings/ssh-keys")}
type="button"
className="col-span-2 lg:col-span-1 lg:mt-7"
>
<KeyRoundIcon className="size-4" /> Add SSH Key
</Button>
)}
<FormField
control={form.control}
name="branch"
render={({ field }) => (
<FormItem className="col-span-2">
<FormLabel>Branch</FormLabel>
<FormControl>
<Input placeholder="Branch" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="buildPath" name="buildPath"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem className="col-span-2">
<FormLabel>Build Path</FormLabel> <FormLabel>Build Path</FormLabel>
<FormControl> <FormControl>
<Input placeholder="/" {...field} /> <Input placeholder="/" {...field} />
@@ -223,7 +220,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
control={form.control} control={form.control}
name="watchPaths" name="watchPaths"
render={({ field }) => ( render={({ field }) => (
<FormItem className="md:col-span-2"> <FormItem className="col-span-2 lg:col-span-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FormLabel>Watch Paths</FormLabel> <FormLabel>Watch Paths</FormLabel>
<TooltipProvider> <TooltipProvider>

View File

@@ -5,6 +5,7 @@ import { useEffect } 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 { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { GiteaIcon } from "@/components/icons/data-tools-icons"; import { GiteaIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -72,7 +73,10 @@ const GiteaProviderSchema = z.object({
owner: z.string().min(1, "Owner is required"), owner: z.string().min(1, "Owner is required"),
}) })
.required(), .required(),
branch: z.string().min(1, "Branch is required"), branch: z
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
giteaId: z.string().min(1, "Gitea Provider is required"), giteaId: z.string().min(1, "Gitea Provider is required"),
watchPaths: z.array(z.string()).default([]), watchPaths: z.array(z.string()).default([]),
enableSubmodules: z.boolean().optional(), enableSubmodules: z.boolean().optional(),

View File

@@ -5,6 +5,7 @@ import { useEffect } 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 { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { GithubIcon } from "@/components/icons/data-tools-icons"; import { GithubIcon } from "@/components/icons/data-tools-icons";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -55,7 +56,10 @@ const GithubProviderSchema = z.object({
owner: z.string().min(1, "Owner is required"), owner: z.string().min(1, "Owner is required"),
}) })
.required(), .required(),
branch: z.string().min(1, "Branch is required"), branch: z
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
githubId: z.string().min(1, "Github Provider is required"), githubId: z.string().min(1, "Github Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
triggerType: z.enum(["push", "tag"]).default("push"), triggerType: z.enum(["push", "tag"]).default("push"),

View File

@@ -5,6 +5,7 @@ import { useEffect, useMemo } 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 { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { GitlabIcon } from "@/components/icons/data-tools-icons"; import { GitlabIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -58,7 +59,10 @@ const GitlabProviderSchema = z.object({
id: z.number().nullable(), id: z.number().nullable(),
}) })
.required(), .required(),
branch: z.string().min(1, "Branch is required"), branch: z
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
gitlabId: z.string().min(1, "Gitlab Provider is required"), gitlabId: z.string().min(1, "Gitlab Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false), enableSubmodules: z.boolean().default(false),

View File

@@ -58,7 +58,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
<CardHeader> <CardHeader>
<CardTitle className="text-xl">Deploy Settings</CardTitle> <CardTitle className="text-xl">Deploy Settings</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="flex flex-row gap-4 flex-wrap"> <CardContent className="grid grid-cols-2 lg:flex lg:flex-row lg:flex-wrap gap-4">
<TooltipProvider delayDuration={0} disableHoverableContent={false}> <TooltipProvider delayDuration={0} disableHoverableContent={false}>
{canDeploy && ( {canDeploy && (
<DialogAction <DialogAction
@@ -274,14 +274,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
> >
<Button <Button
variant="outline" variant="outline"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2" className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2 col-span-2"
> >
<Terminal className="size-4 mr-1" /> <Terminal className="size-4 mr-1" />
Open Terminal Open Terminal
</Button> </Button>
</DockerTerminalModal> </DockerTerminalModal>
{canUpdateService && ( {canUpdateService && (
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border"> <div className="flex flex-row items-center gap-2 justify-between rounded-md px-4 py-2 border col-span-2 md:col-span-1">
<span className="text-sm font-medium">Autodeploy</span> <span className="text-sm font-medium">Autodeploy</span>
<Switch <Switch
aria-label="Toggle autodeploy" aria-label="Toggle autodeploy"
@@ -305,7 +305,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
)} )}
{canUpdateService && ( {canUpdateService && (
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border"> <div className="flex flex-row items-center gap-2 justify-between rounded-md px-4 py-2 border col-span-2 md:col-span-1">
<span className="text-sm font-medium">Clean Cache</span> <span className="text-sm font-medium">Clean Cache</span>
<Switch <Switch
aria-label="Toggle clean cache" aria-label="Toggle clean cache"

View File

@@ -87,7 +87,7 @@ export const AddPreviewDomain = ({
}); });
const host = form.watch("host"); const host = form.watch("host");
const isTraefikMeDomain = host?.includes("traefik.me") || false; const isTraefikMeDomain = host?.includes("sslip.io") || false;
useEffect(() => { useEffect(() => {
if (data) { if (data) {
@@ -162,7 +162,7 @@ export const AddPreviewDomain = ({
<FormItem> <FormItem>
{isTraefikMeDomain && ( {isTraefikMeDomain && (
<AlertBlock type="info"> <AlertBlock type="info">
<strong>Note:</strong> traefik.me is a public HTTP <strong>Note:</strong> sslip.io is a public HTTP
service and does not support SSL/HTTPS. HTTPS and service and does not support SSL/HTTPS. HTTPS and
certificate options will not have any effect. certificate options will not have any effect.
</AlertBlock> </AlertBlock>
@@ -202,7 +202,7 @@ export const AddPreviewDomain = ({
sideOffset={5} sideOffset={5}
className="max-w-[10rem]" className="max-w-[10rem]"
> >
<p>Generate traefik.me domain</p> <p>Generate sslip.io domain</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>

View File

@@ -88,7 +88,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
const form = useForm<Schema>({ const form = useForm<Schema>({
defaultValues: { defaultValues: {
env: "", env: "",
wildcardDomain: "*.traefik.me", wildcardDomain: "*.sslip.io",
port: 3000, port: 3000,
previewLimit: 3, previewLimit: 3,
previewLabels: [], previewLabels: [],
@@ -102,7 +102,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
const previewHttps = form.watch("previewHttps"); const previewHttps = form.watch("previewHttps");
const wildcardDomain = form.watch("wildcardDomain"); const wildcardDomain = form.watch("wildcardDomain");
const isTraefikMeDomain = wildcardDomain?.includes("traefik.me") || false; const isTraefikMeDomain = wildcardDomain?.includes("sslip.io") || false;
useEffect(() => { useEffect(() => {
setIsEnabled(data?.isPreviewDeploymentsActive || false); setIsEnabled(data?.isPreviewDeploymentsActive || false);
@@ -114,7 +114,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
env: data.previewEnv || "", env: data.previewEnv || "",
buildArgs: data.previewBuildArgs || "", buildArgs: data.previewBuildArgs || "",
buildSecrets: data.previewBuildSecrets || "", buildSecrets: data.previewBuildSecrets || "",
wildcardDomain: data.previewWildcard || "*.traefik.me", wildcardDomain: data.previewWildcard || "*.sslip.io",
port: data.previewPort || 3000, port: data.previewPort || 3000,
previewLabels: data.previewLabels || [], previewLabels: data.previewLabels || [],
previewLimit: data.previewLimit || 3, previewLimit: data.previewLimit || 3,
@@ -173,7 +173,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
<div className="grid gap-4"> <div className="grid gap-4">
{isTraefikMeDomain && ( {isTraefikMeDomain && (
<AlertBlock type="info"> <AlertBlock type="info">
<strong>Note:</strong> traefik.me is a public HTTP service and <strong>Note:</strong> sslip.io is a public HTTP service and
does not support SSL/HTTPS. HTTPS and certificate options will does not support SSL/HTTPS. HTTPS and certificate options will
not have any effect. not have any effect.
</AlertBlock> </AlertBlock>
@@ -192,7 +192,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
<FormItem> <FormItem>
<FormLabel>Wildcard Domain</FormLabel> <FormLabel>Wildcard Domain</FormLabel>
<FormControl> <FormControl>
<Input placeholder="*.traefik.me" {...field} /> <Input placeholder="*.sslip.io" {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>

View File

@@ -80,6 +80,7 @@ export const commonCronExpressions = [
const formSchema = z const formSchema = z
.object({ .object({
name: z.string().min(1, "Name is required"), name: z.string().min(1, "Name is required"),
description: z.string().optional(),
cronExpression: z.string().min(1, "Cron expression is required"), cronExpression: z.string().min(1, "Cron expression is required"),
shellType: z.enum(["bash", "sh"]).default("bash"), shellType: z.enum(["bash", "sh"]).default("bash"),
command: z.string(), command: z.string(),
@@ -224,6 +225,7 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
resolver: standardSchemaResolver(formSchema), resolver: standardSchemaResolver(formSchema),
defaultValues: { defaultValues: {
name: "", name: "",
description: "",
cronExpression: "", cronExpression: "",
shellType: "bash", shellType: "bash",
command: "", command: "",
@@ -263,6 +265,7 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
if (scheduleId && schedule) { if (scheduleId && schedule) {
form.reset({ form.reset({
name: schedule.name, name: schedule.name,
description: schedule.description || "",
cronExpression: schedule.cronExpression, cronExpression: schedule.cronExpression,
shellType: schedule.shellType, shellType: schedule.shellType,
command: schedule.command, command: schedule.command,
@@ -479,6 +482,26 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
)} )}
/> />
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Input
placeholder="Backs up the database every day at midnight"
{...field}
/>
</FormControl>
<FormDescription>
Optional description of what this schedule does
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<ScheduleFormField <ScheduleFormField
name="cronExpression" name="cronExpression"
formControl={form.control} formControl={form.control}

View File

@@ -125,6 +125,11 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
{schedule.enabled ? "Enabled" : "Disabled"} {schedule.enabled ? "Enabled" : "Disabled"}
</Badge> </Badge>
</div> </div>
{schedule.description && (
<p className="text-xs text-muted-foreground/70 [overflow-wrap:anywhere] line-clamp-2">
{schedule.description}
</p>
)}
<div className="flex items-center gap-2 text-sm text-muted-foreground flex-wrap"> <div className="flex items-center gap-2 text-sm text-muted-foreground flex-wrap">
<Badge <Badge
variant="outline" variant="outline"

View File

@@ -2,6 +2,10 @@ import { Loader2, MoreHorizontal, RefreshCw } from "lucide-react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { useState } from "react"; import { useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { ShowContainerConfig } from "@/components/dashboard/docker/config/show-container-config";
import { ShowContainerMounts } from "@/components/dashboard/docker/mounts/show-container-mounts";
import { ShowContainerNetworks } from "@/components/dashboard/docker/networks/show-container-networks";
import { DockerTerminalModal } from "@/components/dashboard/docker/terminal/docker-terminal-modal";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -36,10 +40,6 @@ import {
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { ShowContainerConfig } from "@/components/dashboard/docker/config/show-container-config";
import { ShowContainerMounts } from "@/components/dashboard/docker/mounts/show-container-mounts";
import { ShowContainerNetworks } from "@/components/dashboard/docker/networks/show-container-networks";
import { DockerTerminalModal } from "@/components/dashboard/docker/terminal/docker-terminal-modal";
const DockerLogsId = dynamic( const DockerLogsId = dynamic(
() => () =>

View File

@@ -49,12 +49,12 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
const composeFile = form.watch("composeFile"); const composeFile = form.watch("composeFile");
useEffect(() => { useEffect(() => {
if (data && !composeFile) { if (data) {
form.reset({ form.reset({
composeFile: data.composeFile || "", composeFile: data.composeFile || "",
}); });
} }
}, [form, form.reset, data]); }, [form, data]);
useEffect(() => { useEffect(() => {
if (data?.composeFile !== undefined) { if (data?.composeFile !== undefined) {

View File

@@ -5,6 +5,7 @@ import { useEffect } 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 { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { BitbucketIcon } from "@/components/icons/data-tools-icons"; import { BitbucketIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -57,7 +58,10 @@ const BitbucketProviderSchema = z.object({
slug: z.string().optional(), slug: z.string().optional(),
}) })
.required(), .required(),
branch: z.string().min(1, "Branch is required"), branch: z
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
bitbucketId: z.string().min(1, "Bitbucket Provider is required"), bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false), enableSubmodules: z.boolean().default(false),

View File

@@ -6,6 +6,7 @@ import { useEffect } 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 { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { GitIcon } from "@/components/icons/data-tools-icons"; import { GitIcon } from "@/components/icons/data-tools-icons";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -41,7 +42,10 @@ const GitProviderSchema = z.object({
repositoryURL: z.string().min(1, { repositoryURL: z.string().min(1, {
message: "Repository URL is required", message: "Repository URL is required",
}), }),
branch: z.string().min(1, "Branch required"), branch: z
.string()
.min(1, "Branch required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
sshKey: z.string().optional(), sshKey: z.string().optional(),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false), enableSubmodules: z.boolean().default(false),

View File

@@ -1,10 +1,11 @@
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema"; import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, Plus, X, HelpCircle } from "lucide-react"; import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useEffect } from "react"; import { useEffect } 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 { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { GiteaIcon } from "@/components/icons/data-tools-icons"; import { GiteaIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -57,7 +58,10 @@ const GiteaProviderSchema = z.object({
owner: z.string().min(1, "Owner is required"), owner: z.string().min(1, "Owner is required"),
}) })
.required(), .required(),
branch: z.string().min(1, "Branch is required"), branch: z
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
giteaId: z.string().min(1, "Gitea Provider is required"), giteaId: z.string().min(1, "Gitea Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false), enableSubmodules: z.boolean().default(false),

View File

@@ -1,3 +1,4 @@
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema"; import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, HelpCircle, X } from "lucide-react"; import { CheckIcon, ChevronsUpDown, HelpCircle, X } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
@@ -55,7 +56,10 @@ const GithubProviderSchema = z.object({
owner: z.string().min(1, "Owner is required"), owner: z.string().min(1, "Owner is required"),
}) })
.required(), .required(),
branch: z.string().min(1, "Branch is required"), branch: z
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
githubId: z.string().min(1, "Github Provider is required"), githubId: z.string().min(1, "Github Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
triggerType: z.enum(["push", "tag"]).default("push"), triggerType: z.enum(["push", "tag"]).default("push"),

View File

@@ -5,6 +5,7 @@ import { useEffect, useMemo } 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 { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { GitlabIcon } from "@/components/icons/data-tools-icons"; import { GitlabIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -58,7 +59,10 @@ const GitlabProviderSchema = z.object({
gitlabPathNamespace: z.string().min(1), gitlabPathNamespace: z.string().min(1),
}) })
.required(), .required(),
branch: z.string().min(1, "Branch is required"), branch: z
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
gitlabId: z.string().min(1, "Gitlab Provider is required"), gitlabId: z.string().min(1, "Gitlab Provider is required"),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false), enableSubmodules: z.boolean().default(false),

View File

@@ -288,7 +288,6 @@ export const RestoreBackup = ({
toast.error("Please select a database type"); toast.error("Please select a database type");
return; return;
} }
console.log({ data });
setIsDeploying(true); setIsDeploying(true);
}; };

View File

@@ -1,5 +1,14 @@
"use client"; "use client";
import { Bot, Loader2, RotateCcw, Settings, X } from "lucide-react"; import copy from "copy-to-clipboard";
import {
Bot,
Check,
Copy,
Loader2,
RotateCcw,
Settings,
X,
} from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useState } from "react"; import { useState } from "react";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
@@ -30,6 +39,7 @@ const MAX_LOG_LINES = 200;
export function AnalyzeLogs({ logs, context }: Props) { export function AnalyzeLogs({ logs, context }: Props) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [aiId, setAiId] = useState<string>(""); const [aiId, setAiId] = useState<string>("");
const [copied, setCopied] = useState(false);
const { data: providers } = api.ai.getEnabledProviders.useQuery(undefined, { const { data: providers } = api.ai.getEnabledProviders.useQuery(undefined, {
enabled: open, enabled: open,
}); });
@@ -52,6 +62,15 @@ export function AnalyzeLogs({ logs, context }: Props) {
mutate({ aiId, logs: logsText, context }); mutate({ aiId, logs: logsText, context });
}; };
const handleCopy = () => {
if (!data?.analysis) return;
const success = copy(data.analysis);
if (success) {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
return ( return (
<Popover <Popover
open={open} open={open}
@@ -168,6 +187,18 @@ export function AnalyzeLogs({ logs, context }: Props) {
)} )}
Re-analyze Re-analyze
</Button> </Button>
<Button
size="sm"
variant="outline"
onClick={handleCopy}
title="Copy analysis to clipboard"
>
{copied ? (
<Check className="h-3.5 w-3.5" />
) : (
<Copy className="h-3.5 w-3.5" />
)}
</Button>
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"

View File

@@ -1,3 +1,4 @@
import { Badge } from "@/components/ui/badge";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -15,7 +16,6 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
interface Props { interface Props {

View File

@@ -1,3 +1,4 @@
import { Badge } from "@/components/ui/badge";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -15,7 +16,6 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
interface Props { interface Props {

View File

@@ -26,8 +26,8 @@ import {
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { import {
uploadFileToContainerSchema,
type UploadFileToContainer, type UploadFileToContainer,
uploadFileToContainerSchema,
} from "@/utils/schema"; } from "@/utils/schema";
interface Props { interface Props {

View File

@@ -1,10 +1,10 @@
import { toast } from "sonner";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { UpdateDatabasePassword } from "@/components/shared/update-database-password"; import { UpdateDatabasePassword } from "@/components/shared/update-database-password";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { toast } from "sonner";
interface Props { interface Props {
mariadbId: string; mariadbId: string;

View File

@@ -1,10 +1,10 @@
import { toast } from "sonner";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { UpdateDatabasePassword } from "@/components/shared/update-database-password"; import { UpdateDatabasePassword } from "@/components/shared/update-database-password";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { toast } from "sonner";
interface Props { interface Props {
mongoId: string; mongoId: string;

View File

@@ -1,10 +1,10 @@
import { toast } from "sonner";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { UpdateDatabasePassword } from "@/components/shared/update-database-password"; import { UpdateDatabasePassword } from "@/components/shared/update-database-password";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { toast } from "sonner";
interface Props { interface Props {
mysqlId: string; mysqlId: string;

View File

@@ -1,10 +1,10 @@
import { toast } from "sonner";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { UpdateDatabasePassword } from "@/components/shared/update-database-password"; import { UpdateDatabasePassword } from "@/components/shared/update-database-password";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { toast } from "sonner";
interface Props { interface Props {
postgresId: string; postgresId: string;

View File

@@ -632,7 +632,6 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
control={form.control} control={form.control}
name="enableNamespaces" name="enableNamespaces"
render={({ field }) => { render={({ field }) => {
console.log(field.value);
return ( return (
<FormItem> <FormItem>
<FormLabel>Enable Namespaces</FormLabel> <FormLabel>Enable Namespaces</FormLabel>

View File

@@ -1,6 +1,6 @@
import { import {
BookText,
Bookmark, Bookmark,
BookText,
CheckIcon, CheckIcon,
ChevronsUpDown, ChevronsUpDown,
Globe, Globe,

View File

@@ -344,7 +344,7 @@ export const ShowProjects = () => {
} }
}} }}
> >
<Card className="group relative w-full h-full bg-transparent transition-colors hover:bg-border"> <Card className="group relative w-full h-full bg-transparent transition-colors hover:bg-border flex flex-col">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center justify-between gap-2 overflow-clip"> <CardTitle className="flex items-center justify-between gap-2 overflow-clip">
<span className="flex flex-col gap-1.5 "> <span className="flex flex-col gap-1.5 ">
@@ -491,7 +491,7 @@ export const ShowProjects = () => {
</div> </div>
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardFooter className="pt-4"> <CardFooter className="pt-4 mt-auto">
<div className="space-y-1 text-xs flex flex-row justify-between max-sm:flex-wrap w-full gap-2 sm:gap-4"> <div className="space-y-1 text-xs flex flex-row justify-between max-sm:flex-wrap w-full gap-2 sm:gap-4">
<DateTooltip date={project.createdAt}> <DateTooltip date={project.createdAt}>
Created Created

View File

@@ -1,10 +1,10 @@
import { toast } from "sonner";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { UpdateDatabasePassword } from "@/components/shared/update-database-password"; import { UpdateDatabasePassword } from "@/components/shared/update-database-password";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { toast } from "sonner";
interface Props { interface Props {
redisId: string; redisId: string;

View File

@@ -25,7 +25,6 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { NumberInput } from "@/components/ui/input";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -34,6 +33,7 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { NumberInput } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";

View File

@@ -1,3 +1,5 @@
import { HelpCircle } from "lucide-react";
import { toast } from "sonner";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { import {
@@ -7,8 +9,6 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { HelpCircle } from "lucide-react";
import { toast } from "sonner";
interface Props { interface Props {
serverId?: string; serverId?: string;

View File

@@ -3,6 +3,7 @@ import { useEffect, 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 { EnterpriseFeatureLocked } from "@/components/proprietary/enterprise-feature-gate";
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 { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
@@ -26,7 +27,6 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { EnterpriseFeatureLocked } from "@/components/proprietary/enterprise-feature-gate";
import { api, type RouterOutputs } from "@/utils/api"; import { api, type RouterOutputs } from "@/utils/api";
/** Shape returned by project.allForPermissions (admin only). Used for the permissions UI. */ /** Shape returned by project.allForPermissions (admin only). Used for the permissions UI. */

View File

@@ -141,14 +141,14 @@ export const WebDomain = () => {
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 md:grid-cols-2" className="grid w-full gap-4 grid-cols-2"
> >
<FormField <FormField
control={form.control} control={form.control}
name="domain" name="domain"
render={({ field }) => { render={({ field }) => {
return ( return (
<FormItem> <FormItem className="col-span-2 md:col-span-1">
<FormLabel>Domain</FormLabel> <FormLabel>Domain</FormLabel>
<FormControl> <FormControl>
<Input <Input
@@ -168,7 +168,7 @@ export const WebDomain = () => {
name="letsEncryptEmail" name="letsEncryptEmail"
render={({ field }) => { render={({ field }) => {
return ( return (
<FormItem> <FormItem className="col-span-2 md:col-span-1">
<FormLabel>Let's Encrypt Email</FormLabel> <FormLabel>Let's Encrypt Email</FormLabel>
<FormControl> <FormControl>
<Input <Input
@@ -209,7 +209,7 @@ export const WebDomain = () => {
name="certificateType" name="certificateType"
render={({ field }) => { render={({ field }) => {
return ( return (
<FormItem className="md:col-span-2"> <FormItem className="col-span-2">
<FormLabel>Certificate Provider</FormLabel> <FormLabel>Certificate Provider</FormLabel>
<Select <Select
onValueChange={field.onChange} onValueChange={field.onChange}

View File

@@ -868,6 +868,19 @@ function SidebarLogo() {
); );
} }
function MobileCloser() {
const pathname = usePathname();
const { setOpenMobile, isMobile } = useSidebar();
useEffect(() => {
if (isMobile) {
setOpenMobile(false);
}
}, [pathname, isMobile, setOpenMobile]);
return null;
}
export default function Page({ children }: Props) { export default function Page({ children }: Props) {
const [defaultOpen, setDefaultOpen] = useState<boolean | undefined>( const [defaultOpen, setDefaultOpen] = useState<boolean | undefined>(
undefined, undefined,
@@ -933,6 +946,7 @@ export default function Page({ children }: Props) {
} as React.CSSProperties } as React.CSSProperties
} }
> >
<MobileCloser />
<Sidebar collapsible="icon" variant="floating"> <Sidebar collapsible="icon" variant="floating">
<SidebarHeader> <SidebarHeader>
{/* <SidebarMenuButton {/* <SidebarMenuButton

View File

@@ -342,7 +342,7 @@ export const AdvanceBreadcrumb = () => {
className="h-auto px-2 py-1.5 hover:bg-accent gap-2" className="h-auto px-2 py-1.5 hover:bg-accent gap-2"
> >
<FolderInput className="size-4 text-muted-foreground" /> <FolderInput className="size-4 text-muted-foreground" />
<span className="font-medium max-w-[150px] truncate"> <span className="font-medium max-w-[50px] md:max-w-[150px] truncate">
{currentProject?.name || "Select Project"} {currentProject?.name || "Select Project"}
</span> </span>
<ChevronDown className="size-4 text-muted-foreground" /> <ChevronDown className="size-4 text-muted-foreground" />
@@ -478,7 +478,7 @@ export const AdvanceBreadcrumb = () => {
aria-expanded={environmentOpen} aria-expanded={environmentOpen}
className="h-auto px-2 py-1.5 hover:bg-accent gap-2" className="h-auto px-2 py-1.5 hover:bg-accent gap-2"
> >
<span className="font-medium max-w-[150px] truncate"> <span className="font-medium max-w-[50px] md:max-w-[150px] truncate">
{currentEnvironment?.name || "production"} {currentEnvironment?.name || "production"}
</span> </span>
<ChevronDown className="size-4 text-muted-foreground" /> <ChevronDown className="size-4 text-muted-foreground" />
@@ -533,7 +533,7 @@ export const AdvanceBreadcrumb = () => {
)} )}
{projectEnvironments && projectEnvironments.length === 1 && ( {projectEnvironments && projectEnvironments.length === 1 && (
<p className="text-sm font-normal ml-1"> <p className="text-sm font-normal ml-1 max-w-[50px] md:max-w-[150px] truncate">
{currentEnvironment?.name || "production"} {currentEnvironment?.name || "production"}
</p> </p>
)} )}
@@ -551,7 +551,7 @@ export const AdvanceBreadcrumb = () => {
className="h-auto px-2 py-1.5 hover:bg-accent gap-2" className="h-auto px-2 py-1.5 hover:bg-accent gap-2"
> >
{getServiceIcon(currentService.type)} {getServiceIcon(currentService.type)}
<span className="font-medium max-w-[150px] truncate"> <span className="font-medium max-w-[50px] md:max-w-[150px] truncate">
{currentService.name} {currentService.name}
</span> </span>
<ChevronDown className="size-4 text-muted-foreground" /> <ChevronDown className="size-4 text-muted-foreground" />
@@ -617,7 +617,7 @@ export const AdvanceBreadcrumb = () => {
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="size-7 ml-1" className="size-7 ml-1 hidden md:flex"
onClick={() => { onClick={() => {
router.push( router.push(
`/dashboard/project/${projectId}/environment/${environmentId}`, `/dashboard/project/${projectId}/environment/${environmentId}`,

View File

@@ -63,6 +63,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
className={cn( className={cn(
buttonVariants({ variant, size, className }), buttonVariants({ variant, size, className }),
"flex gap-2", "flex gap-2",
className,
)} )}
ref={ref} ref={ref}
{...props} {...props}

View File

@@ -0,0 +1 @@
ALTER TABLE "schedule" ADD COLUMN "description" text;

File diff suppressed because it is too large Load Diff

View File

@@ -1163,6 +1163,13 @@
"when": 1775845419261, "when": 1775845419261,
"tag": "0165_abnormal_greymalkin", "tag": "0165_abnormal_greymalkin",
"breakpoints": true "breakpoints": true
},
{
"idx": 166,
"version": "7",
"when": 1778303519111,
"tag": "0166_nosy_slapstick",
"breakpoints": true
} }
] ]
} }

View File

@@ -28,6 +28,7 @@ try {
"wait-for-postgres": "wait-for-postgres.ts", "wait-for-postgres": "wait-for-postgres.ts",
"reset-password": "reset-password.ts", "reset-password": "reset-password.ts",
"reset-2fa": "reset-2fa.ts", "reset-2fa": "reset-2fa.ts",
"migrate-auth-secret": "scripts/migrate-auth-secret.ts",
}, },
bundle: true, bundle: true,
platform: "node", platform: "node",

View File

@@ -1,6 +1,6 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.29.2", "version": "v0.29.3",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
@@ -14,6 +14,7 @@
"wait-for-postgres-dev": "tsx -r dotenv/config wait-for-postgres.ts", "wait-for-postgres-dev": "tsx -r dotenv/config wait-for-postgres.ts",
"reset-password": "node -r dotenv/config dist/reset-password.mjs", "reset-password": "node -r dotenv/config dist/reset-password.mjs",
"reset-2fa": "node -r dotenv/config dist/reset-2fa.mjs", "reset-2fa": "node -r dotenv/config dist/reset-2fa.mjs",
"migrate-auth-secret": "node -r dotenv/config dist/migrate-auth-secret.mjs",
"dev": "tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ", "dev": "tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ",
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts", "studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
"migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts", "migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts",
@@ -126,7 +127,7 @@
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"nextjs-toploader": "^3.9.17", "nextjs-toploader": "^3.9.17",
"node-os-utils": "2.0.1", "node-os-utils": "2.0.1",
"node-pty": "1.0.0", "node-pty": "1.1.0",
"node-schedule": "2.1.1", "node-schedule": "2.1.1",
"nodemailer": "6.9.14", "nodemailer": "6.9.14",
"octokit": "3.1.2", "octokit": "3.1.2",

View File

@@ -28,6 +28,11 @@ export default async function handler(
res: NextApiResponse, res: NextApiResponse,
) { ) {
const signature = req.headers["x-hub-signature-256"]; const signature = req.headers["x-hub-signature-256"];
if (!signature) {
res.status(401).json({ message: "Missing signature header" });
return;
}
const githubBody = req.body; const githubBody = req.body;
if (!githubBody?.installation?.id) { if (!githubBody?.installation?.id) {

View File

@@ -84,7 +84,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -24,7 +24,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -95,7 +95,7 @@ export async function getServerSideProps(
if (IS_CLOUD) { if (IS_CLOUD) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/dashboard/home", destination: "/dashboard/home",
}, },
}; };
@@ -104,7 +104,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -1099,7 +1099,7 @@ const EnvironmentPage = (
</div> </div>
<CardContent className="space-y-2 py-8 border-t gap-4 flex flex-col min-h-[60vh]"> <CardContent className="space-y-2 py-8 border-t gap-4 flex flex-col min-h-[60vh]">
<> <>
<div className="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between"> <div className="flex flex-col gap-4 2xl:flex-row 2xl:items-center 2xl:justify-between">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Checkbox <Checkbox
@@ -1620,9 +1620,9 @@ const EnvironmentPage = (
<ContextMenuTrigger asChild> <ContextMenuTrigger asChild>
<Link <Link
href={`/dashboard/project/${projectId}/environment/${environmentId}/services/${service.type}/${service.id}`} href={`/dashboard/project/${projectId}/environment/${environmentId}/services/${service.type}/${service.id}`}
className="block" className="block h-full"
> >
<Card className="flex flex-col group relative cursor-pointer bg-transparent transition-colors hover:bg-border"> <Card className="flex flex-col h-full group relative cursor-pointer bg-transparent transition-colors hover:bg-border">
{service.serverId && ( {service.serverId && (
<div className="absolute -left-1 -top-2"> <div className="absolute -left-1 -top-2">
<ServerIcon className="size-4 text-muted-foreground" /> <ServerIcon className="size-4 text-muted-foreground" />
@@ -1827,7 +1827,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -451,7 +451,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -455,7 +455,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -307,7 +307,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -336,7 +336,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -340,7 +340,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -318,7 +318,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -324,7 +324,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -329,7 +329,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -56,7 +56,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -17,7 +17,7 @@ export async function getServerSideProps(
if (IS_CLOUD) { if (IS_CLOUD) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/dashboard/home", destination: "/dashboard/home",
}, },
}; };
@@ -26,7 +26,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -34,7 +34,7 @@ export async function getServerSideProps(
if (IS_CLOUD) { if (IS_CLOUD) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/dashboard/home", destination: "/dashboard/home",
}, },
}; };
@@ -43,7 +43,7 @@ export async function getServerSideProps(
if (!user || (user.role !== "owner" && user.role !== "admin")) { if (!user || (user.role !== "owner" && user.role !== "admin")) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -45,7 +45,7 @@ export async function getServerSideProps(
if (!user || user.role === "member") { if (!user || user.role === "member") {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -27,7 +27,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
if (!user) { if (!user) {
return { return {
redirect: { destination: "/", permanent: true }, redirect: { destination: "/", permanent: false },
}; };
} }

View File

@@ -23,7 +23,7 @@ export async function getServerSideProps(
if (!IS_CLOUD) { if (!IS_CLOUD) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/dashboard/home", destination: "/dashboard/home",
}, },
}; };
@@ -33,7 +33,7 @@ export async function getServerSideProps(
if (!user || user.role !== "owner") { if (!user || user.role !== "owner") {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -28,7 +28,7 @@ export async function getServerSideProps(
if (!user || user.role === "member") { if (!user || user.role === "member") {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -27,7 +27,7 @@ export async function getServerSideProps(
if (IS_CLOUD) { if (IS_CLOUD) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/dashboard/home", destination: "/dashboard/home",
}, },
}; };
@@ -36,7 +36,7 @@ export async function getServerSideProps(
if (!user || user.role === "member") { if (!user || user.role === "member") {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -28,7 +28,7 @@ export async function getServerSideProps(
if (!user || user.role === "member") { if (!user || user.role === "member") {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -27,7 +27,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };
@@ -53,7 +53,7 @@ export async function getServerSideProps(
if (!userPermissions?.gitProviders.read) { if (!userPermissions?.gitProviders.read) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -23,7 +23,7 @@ export async function getServerSideProps(
if (!IS_CLOUD) { if (!IS_CLOUD) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/dashboard/home", destination: "/dashboard/home",
}, },
}; };
@@ -33,7 +33,7 @@ export async function getServerSideProps(
if (!user || user.role !== "owner") { if (!user || user.role !== "owner") {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -38,7 +38,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };
@@ -46,7 +46,7 @@ export async function getServerSideProps(
if (user.role !== "owner") { if (user.role !== "owner") {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/dashboard/settings/profile", destination: "/dashboard/settings/profile",
}, },
}; };

View File

@@ -28,7 +28,7 @@ export async function getServerSideProps(
if (!user || user.role === "member") { if (!user || user.role === "member") {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -54,7 +54,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -28,7 +28,7 @@ export async function getServerSideProps(
if (!user || user.role === "member") { if (!user || user.role === "member") {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -44,7 +44,7 @@ export async function getServerSideProps(
if (IS_CLOUD) { if (IS_CLOUD) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/dashboard/home", destination: "/dashboard/home",
}, },
}; };
@@ -53,7 +53,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };
@@ -61,7 +61,7 @@ export async function getServerSideProps(
if (user.role === "member") { if (user.role === "member") {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/dashboard/settings/profile", destination: "/dashboard/settings/profile",
}, },
}; };

View File

@@ -28,7 +28,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };
@@ -36,7 +36,7 @@ export async function getServerSideProps(
if (user.role === "member") { if (user.role === "member") {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/dashboard/settings/profile", destination: "/dashboard/settings/profile",
}, },
}; };

View File

@@ -27,7 +27,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };
@@ -54,7 +54,7 @@ export async function getServerSideProps(
if (!userPermissions?.sshKeys.read) { if (!userPermissions?.sshKeys.read) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -46,7 +46,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };
@@ -54,7 +54,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
if (user.role === "member") { if (user.role === "member") {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/dashboard/settings/profile", destination: "/dashboard/settings/profile",
}, },
}; };

View File

@@ -47,7 +47,7 @@ export async function getServerSideProps(
if (!userPermissions?.tag.read) { if (!userPermissions?.tag.read) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -39,7 +39,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };
@@ -66,7 +66,7 @@ export async function getServerSideProps(
if (!userPermissions?.member.read) { if (!userPermissions?.member.read) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -46,7 +46,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };
@@ -54,7 +54,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
if (user.role !== "owner") { if (user.role !== "owner") {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/dashboard/settings/profile", destination: "/dashboard/settings/profile",
}, },
}; };

View File

@@ -4,8 +4,8 @@ import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import type { ReactElement } from "react"; import type { ReactElement } from "react";
import superjson from "superjson"; import superjson from "superjson";
import SwarmMonitorCard from "@/components/dashboard/swarm/monitoring-card";
import { ShowSwarmContainers } from "@/components/dashboard/swarm/containers/show-swarm-containers"; import { ShowSwarmContainers } from "@/components/dashboard/swarm/containers/show-swarm-containers";
import SwarmMonitorCard from "@/components/dashboard/swarm/monitoring-card";
import { DashboardLayout } from "@/components/layouts/dashboard-layout"; import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
@@ -45,7 +45,7 @@ export async function getServerSideProps(
if (IS_CLOUD) { if (IS_CLOUD) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/dashboard/home", destination: "/dashboard/home",
}, },
}; };
@@ -54,7 +54,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };
@@ -80,7 +80,7 @@ export async function getServerSideProps(
if (!userPermissions?.docker.read) { if (!userPermissions?.docker.read) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -23,7 +23,7 @@ export async function getServerSideProps(
if (IS_CLOUD) { if (IS_CLOUD) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/dashboard/home", destination: "/dashboard/home",
}, },
}; };
@@ -32,7 +32,7 @@ export async function getServerSideProps(
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };
@@ -58,7 +58,7 @@ export async function getServerSideProps(
if (!userPermissions?.traefikFiles.read) { if (!userPermissions?.traefikFiles.read) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -407,7 +407,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
if (user) { if (user) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/dashboard/home", destination: "/dashboard/home",
}, },
}; };
@@ -425,7 +425,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
if (!hasAdmin) { if (!hasAdmin) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/register", destination: "/register",
}, },
}; };
@@ -436,7 +436,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
if (user) { if (user) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/dashboard/home", destination: "/dashboard/home",
}, },
}; };

View File

@@ -333,7 +333,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
// if (IS_CLOUD) { // if (IS_CLOUD) {
// return { // return {
// redirect: { // redirect: {
// permanent: true, // permanent: false,
// destination: "/", // destination: "/",
// }, // },
// }; // };
@@ -342,7 +342,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
if (typeof token !== "string") { if (typeof token !== "string") {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };
@@ -365,7 +365,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
if (invitation.isExpired) { if (invitation.isExpired) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };
@@ -382,7 +382,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
console.log("error", error); console.log("error", error);
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -302,7 +302,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
if (user) { if (user) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/dashboard/home", destination: "/dashboard/home",
}, },
}; };

View File

@@ -190,7 +190,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
if (!IS_CLOUD) { if (!IS_CLOUD) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };
@@ -200,7 +200,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
if (typeof token !== "string") { if (typeof token !== "string") {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -30,6 +30,9 @@ const loginSchema = z.object({
.min(1, { .min(1, {
message: "Email is required", message: "Email is required",
}) })
.max(255, {
message: "Email must be at most 255 characters",
})
.email({ .email({
message: "Email must be a valid email", message: "Email must be a valid email",
}), }),
@@ -118,7 +121,11 @@ export default function Home() {
<FormItem> <FormItem>
<FormLabel>Email</FormLabel> <FormLabel>Email</FormLabel>
<FormControl> <FormControl>
<Input placeholder="Email" {...field} /> <Input
placeholder="Email"
maxLength={255}
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -161,7 +168,7 @@ export async function getServerSideProps(_context: GetServerSidePropsContext) {
if (!IS_CLOUD) { if (!IS_CLOUD) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -82,7 +82,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
if (!user) { if (!user) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };
@@ -103,7 +103,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
if (!userPermissions?.api.read) { if (!userPermissions?.api.read) {
return { return {
redirect: { redirect: {
permanent: true, permanent: false,
destination: "/", destination: "/",
}, },
}; };

View File

@@ -0,0 +1,97 @@
/**
* Use this command to automatically migrate the auth secret: curl -sSL https://dokploy.com/security/0.29.3.sh | bash
* Migration script: re-encrypt 2FA secrets after rotating BETTER_AUTH_SECRET.
*
* Usage:
* OLD_SECRET=<old_secret> NEW_SECRET=<new_secret> npx tsx apps/dokploy/scripts/migrate-auth-secret.ts
*
* Both OLD_SECRET and NEW_SECRET are required.
* Run this BEFORE restarting Dokploy with the new secret.
*/
import { db } from "@dokploy/server/db";
import { twoFactor } from "@dokploy/server/db/schema";
import { symmetricDecrypt, symmetricEncrypt } from "better-auth/crypto";
import { eq } from "drizzle-orm";
const OLD_SECRET = process.env.OLD_SECRET as string;
const NEW_SECRET = process.env.NEW_SECRET as string;
if (!OLD_SECRET || !NEW_SECRET) {
console.error(
"❌ OLD_SECRET and NEW_SECRET environment variables are required.",
);
console.error(
" Usage: OLD_SECRET=<old> NEW_SECRET=<new> npx tsx apps/dokploy/scripts/migrate-auth-secret.ts",
);
process.exit(1);
}
if (OLD_SECRET === NEW_SECRET) {
console.error("❌ OLD_SECRET and NEW_SECRET must be different.");
process.exit(1);
}
async function reEncrypt(
value: string,
oldSecret: string,
newSecret: string,
): Promise<string> {
const plaintext = await symmetricDecrypt({ key: oldSecret, data: value });
return symmetricEncrypt({ key: newSecret, data: plaintext });
}
async function main() {
console.log("🔍 Fetching 2FA records...");
const records = await db.select().from(twoFactor);
if (records.length === 0) {
console.log("✅ No 2FA records found, nothing to migrate.");
return;
}
console.log(`📦 Found ${records.length} 2FA record(s) to migrate.`);
let migrated = 0;
let failed = 0;
await db.transaction(async (tx) => {
for (const record of records) {
try {
const [newSecret, newBackupCodes] = await Promise.all([
reEncrypt(record.secret, OLD_SECRET, NEW_SECRET),
reEncrypt(record.backupCodes, OLD_SECRET, NEW_SECRET),
]);
await tx
.update(twoFactor)
.set({ secret: newSecret, backupCodes: newBackupCodes })
.where(eq(twoFactor.id, record.id));
migrated++;
} catch (err) {
console.error(
`❌ Failed to migrate record ${record.id} (userId: ${record.userId}):`,
err,
);
failed++;
throw err; // rollback the whole transaction
}
}
});
console.log(`✅ Migrated ${migrated} record(s) successfully.`);
if (failed > 0) {
console.error(
`${failed} record(s) failed — transaction was rolled back.`,
);
process.exit(1);
} else {
process.exit(0);
}
}
main().catch((err) => {
console.error("❌ Migration failed:", err);
process.exit(1);
});

View File

@@ -25,8 +25,8 @@ import { findProjectById } from "@dokploy/server/services/project";
import { import {
getProviderHeaders, getProviderHeaders,
getProviderName, getProviderName,
selectAIProvider,
type Model, type Model,
selectAIProvider,
} from "@dokploy/server/utils/ai/select-ai-provider"; } from "@dokploy/server/utils/ai/select-ai-provider";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { generateText } from "ai"; import { generateText } from "ai";

View File

@@ -640,7 +640,7 @@ export const composeRouter = createTRPCRouter({
name: input.id, name: input.id,
sourceType: "raw", sourceType: "raw",
appName: appName, appName: appName,
isolatedDeployment: true, isolatedDeployment: template.config.config?.isolated !== false,
}); });
await addNewService(ctx, compose.composeId); await addNewService(ctx, compose.composeId);
@@ -700,11 +700,14 @@ export const composeRouter = createTRPCRouter({
getTags: protectedProcedure getTags: protectedProcedure
.input(z.object({ baseUrl: z.string().optional() })) .input(z.object({ baseUrl: z.string().optional() }))
.query(async ({ input }) => { .query(async ({ input }) => {
const githubTemplates = await fetchTemplatesList(input.baseUrl); try {
const githubTemplates = await fetchTemplatesList(input.baseUrl);
const allTags = githubTemplates.flatMap((template) => template.tags); const allTags = githubTemplates.flatMap((template) => template.tags);
const uniqueTags = _.uniq(allTags); return _.uniq(allTags);
return uniqueTags; } catch (error) {
console.warn("Failed to fetch template tags:", error);
return [];
}
}), }),
disconnectGitProvider: protectedProcedure disconnectGitProvider: protectedProcedure
.input(apiFindCompose) .input(apiFindCompose)

View File

@@ -6,6 +6,7 @@ import {
findEnvironmentById, findEnvironmentById,
findLibsqlById, findLibsqlById,
findProjectById, findProjectById,
getAccessibleServerIds,
getContainerLogs, getContainerLogs,
IS_CLOUD, IS_CLOUD,
rebuildDatabase, rebuildDatabase,
@@ -16,7 +17,6 @@ import {
stopService, stopService,
stopServiceRemote, stopServiceRemote,
updateLibsqlById, updateLibsqlById,
getAccessibleServerIds,
} from "@dokploy/server"; } from "@dokploy/server";
import { import {
addNewService, addNewService,

View File

@@ -9,6 +9,7 @@ import {
findEnvironmentById, findEnvironmentById,
findMySqlById, findMySqlById,
findProjectById, findProjectById,
getAccessibleServerIds,
getContainerLogs, getContainerLogs,
getServiceContainerCommand, getServiceContainerCommand,
IS_CLOUD, IS_CLOUD,
@@ -20,7 +21,6 @@ import {
stopService, stopService,
stopServiceRemote, stopServiceRemote,
updateMySqlById, updateMySqlById,
getAccessibleServerIds,
} from "@dokploy/server"; } from "@dokploy/server";
import { db } from "@dokploy/server/db"; import { db } from "@dokploy/server/db";
import { import {

View File

@@ -9,6 +9,7 @@ import {
findEnvironmentById, findEnvironmentById,
findPostgresById, findPostgresById,
findProjectById, findProjectById,
getAccessibleServerIds,
getContainerLogs, getContainerLogs,
getMountPath, getMountPath,
getServiceContainerCommand, getServiceContainerCommand,
@@ -21,7 +22,6 @@ import {
stopService, stopService,
stopServiceRemote, stopServiceRemote,
updatePostgresById, updatePostgresById,
getAccessibleServerIds,
} from "@dokploy/server"; } from "@dokploy/server";
import { db } from "@dokploy/server/db"; import { db } from "@dokploy/server/db";
import { import {

View File

@@ -856,8 +856,6 @@ export const projectRouter = createTRPCRouter({
ctx.session.activeOrganizationId, ctx.session.activeOrganizationId,
).then((value) => value.environment); ).then((value) => value.environment);
console.log("targetProject", targetProject);
if (input.includeServices) { if (input.includeServices) {
const servicesToDuplicate = input.selectedServices || []; const servicesToDuplicate = input.selectedServices || [];

View File

@@ -8,6 +8,7 @@ import {
findEnvironmentById, findEnvironmentById,
findProjectById, findProjectById,
findRedisById, findRedisById,
getAccessibleServerIds,
getContainerLogs, getContainerLogs,
getServiceContainerCommand, getServiceContainerCommand,
IS_CLOUD, IS_CLOUD,
@@ -19,7 +20,6 @@ import {
stopService, stopService,
stopServiceRemote, stopServiceRemote,
updateRedisById, updateRedisById,
getAccessibleServerIds,
} from "@dokploy/server"; } from "@dokploy/server";
import { db } from "@dokploy/server/db"; import { db } from "@dokploy/server/db";
import { import {

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