diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 015095aa6..52fd7f2f8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,7 +52,7 @@ feat: add new feature Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch. -We use Node v20.9.0 +We use Node v20.9.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.9.0 && nvm use` in the root directory. ```bash git clone https://github.com/dokploy/dokploy.git @@ -87,6 +87,8 @@ pnpm run dokploy:dev Go to http://localhost:3000 to see the development server +Note: this project uses Biome. If your editor is configured to use another formatter such as Prettier, it's recommended to either change it to use Biome or turn it off. + ## Build ```bash diff --git a/apps/dokploy/__test__/traefik/server/update-server-config.test.ts b/apps/dokploy/__test__/traefik/server/update-server-config.test.ts index f33b37fd1..201aee1ed 100644 --- a/apps/dokploy/__test__/traefik/server/update-server-config.test.ts +++ b/apps/dokploy/__test__/traefik/server/update-server-config.test.ts @@ -14,6 +14,7 @@ import { import { beforeEach, expect, test, vi } from "vitest"; const baseAdmin: User = { + https: false, enablePaidFeatures: false, metricsConfig: { containers: { @@ -73,7 +74,6 @@ beforeEach(() => { test("Should read the configuration file", () => { const config: FileConfig = loadOrCreateConfig("dokploy"); - expect(config.http?.routers?.["dokploy-router-app"]?.service).toBe( "dokploy-service-app", ); @@ -83,6 +83,7 @@ test("Should apply redirect-to-https", () => { updateServerTraefik( { ...baseAdmin, + https: true, certificateType: "letsencrypt", }, "example.com", diff --git a/apps/dokploy/__test__/utils/backups.test.ts b/apps/dokploy/__test__/utils/backups.test.ts new file mode 100644 index 000000000..c7bc310cf --- /dev/null +++ b/apps/dokploy/__test__/utils/backups.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, test } from "vitest"; +import { normalizeS3Path } from "@dokploy/server/utils/backups/utils"; + +describe("normalizeS3Path", () => { + test("should handle empty and whitespace-only prefix", () => { + expect(normalizeS3Path("")).toBe(""); + expect(normalizeS3Path("/")).toBe(""); + expect(normalizeS3Path(" ")).toBe(""); + expect(normalizeS3Path("\t")).toBe(""); + expect(normalizeS3Path("\n")).toBe(""); + expect(normalizeS3Path(" \n \t ")).toBe(""); + }); + + test("should trim whitespace from prefix", () => { + expect(normalizeS3Path(" prefix")).toBe("prefix/"); + expect(normalizeS3Path("prefix ")).toBe("prefix/"); + expect(normalizeS3Path(" prefix ")).toBe("prefix/"); + expect(normalizeS3Path("\tprefix\t")).toBe("prefix/"); + expect(normalizeS3Path(" prefix/nested ")).toBe("prefix/nested/"); + }); + + test("should remove leading slashes", () => { + expect(normalizeS3Path("/prefix")).toBe("prefix/"); + expect(normalizeS3Path("///prefix")).toBe("prefix/"); + }); + + test("should remove trailing slashes", () => { + expect(normalizeS3Path("prefix/")).toBe("prefix/"); + expect(normalizeS3Path("prefix///")).toBe("prefix/"); + }); + + test("should remove both leading and trailing slashes", () => { + expect(normalizeS3Path("/prefix/")).toBe("prefix/"); + expect(normalizeS3Path("///prefix///")).toBe("prefix/"); + }); + + test("should handle nested paths", () => { + expect(normalizeS3Path("prefix/nested")).toBe("prefix/nested/"); + expect(normalizeS3Path("/prefix/nested/")).toBe("prefix/nested/"); + expect(normalizeS3Path("///prefix/nested///")).toBe("prefix/nested/"); + }); + + test("should preserve middle slashes", () => { + expect(normalizeS3Path("prefix/nested/deep")).toBe("prefix/nested/deep/"); + expect(normalizeS3Path("/prefix/nested/deep/")).toBe("prefix/nested/deep/"); + }); + + test("should handle special characters", () => { + expect(normalizeS3Path("prefix-with-dashes")).toBe("prefix-with-dashes/"); + expect(normalizeS3Path("prefix_with_underscores")).toBe( + "prefix_with_underscores/", + ); + expect(normalizeS3Path("prefix.with.dots")).toBe("prefix.with.dots/"); + }); + + test("should handle the cases from the bug report", () => { + expect(normalizeS3Path("instance-backups/")).toBe("instance-backups/"); + expect(normalizeS3Path("/instance-backups/")).toBe("instance-backups/"); + expect(normalizeS3Path("instance-backups")).toBe("instance-backups/"); + }); +}); diff --git a/apps/dokploy/components/dashboard/application/general/generic/show.tsx b/apps/dokploy/components/dashboard/application/general/generic/show.tsx index 3f8854888..9b9a0ba05 100644 --- a/apps/dokploy/components/dashboard/application/general/generic/show.tsx +++ b/apps/dokploy/components/dashboard/application/general/generic/show.tsx @@ -65,7 +65,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => { setSab(e as TabState); }} > -
+
{ }) .then(() => { refetch(); - toast.success("Preview deployments enabled"); + toast.success( + checked + ? "Preview deployments enabled" + : "Preview deployments disabled", + ); }) .catch((error) => { toast.error(error.message); diff --git a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx index 10ebbe083..797e1ca81 100644 --- a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx @@ -84,6 +84,7 @@ export const RestoreBackup = ({ }: Props) => { const [isOpen, setIsOpen] = useState(false); const [search, setSearch] = useState(""); + const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(""); const { data: destinations = [] } = api.destination.all.useQuery(); @@ -99,13 +100,18 @@ export const RestoreBackup = ({ const destionationId = form.watch("destinationId"); const debouncedSetSearch = debounce((value: string) => { + setDebouncedSearchTerm(value); + }, 150); + + const handleSearchChange = (value: string) => { setSearch(value); - }, 300); + debouncedSetSearch(value); + }; const { data: files = [], isLoading } = api.backup.listBackupFiles.useQuery( { destinationId: destionationId, - search, + search: debouncedSearchTerm, serverId: serverId ?? "", }, { @@ -284,7 +290,8 @@ export const RestoreBackup = ({ {isLoading ? ( @@ -308,6 +315,8 @@ export const RestoreBackup = ({ key={file} onSelect={() => { form.setValue("backupFile", file); + setSearch(file); + setDebouncedSearchTerm(file); }} >
diff --git a/apps/dokploy/components/dashboard/project/add-template.tsx b/apps/dokploy/components/dashboard/project/add-template.tsx index 5dbbcd1da..8e9de54d9 100644 --- a/apps/dokploy/components/dashboard/project/add-template.tsx +++ b/apps/dokploy/components/dashboard/project/add-template.tsx @@ -307,7 +307,7 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => { > {templates?.map((template) => (
{ )} > - {template.version} + {template?.version}
{ )} > {template.name}
- {template.name} + {template?.name} {viewMode === "detailed" && - template.tags.length > 0 && ( + template?.tags?.length > 0 && (
- {template.tags.map((tag) => ( + {template?.tags?.map((tag) => ( { {viewMode === "detailed" && (
- {template.description} + {template?.description}
)} @@ -372,25 +372,27 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => { > {viewMode === "detailed" && (
- - - - {template.links.website && ( + {template?.links?.github && ( + + + )} + {template?.links?.website && ( + )} - {template.links.docs && ( + {template?.links?.docs && ( @@ -419,7 +421,7 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => { This will create an application from the{" "} - {template.name} template and add it to your + {template?.name} template and add it to your project. diff --git a/apps/dokploy/components/dashboard/projects/handle-project.tsx b/apps/dokploy/components/dashboard/projects/handle-project.tsx index 85b8aea9a..dcb812419 100644 --- a/apps/dokploy/components/dashboard/projects/handle-project.tsx +++ b/apps/dokploy/components/dashboard/projects/handle-project.tsx @@ -31,9 +31,14 @@ import { toast } from "sonner"; import { z } from "zod"; const AddProjectSchema = z.object({ - name: z.string().min(1, { - message: "Name is required", - }), + name: z + .string() + .min(1, { + message: "Name is required", + }) + .regex(/^[a-zA-Z]/, { + message: "Project name cannot start with a number", + }), description: z.string().optional(), }); @@ -97,18 +102,6 @@ export const HandleProject = ({ projectId }: Props) => { ); }); }; - // useEffect(() => { - // const getUsers = async () => { - // const users = await authClient.admin.listUsers({ - // query: { - // limit: 100, - // }, - // }); - // console.log(users); - // }; - - // getUsers(); - // }); return ( diff --git a/apps/dokploy/components/dashboard/projects/show.tsx b/apps/dokploy/components/dashboard/projects/show.tsx index 31ba80c8f..03ebe7a85 100644 --- a/apps/dokploy/components/dashboard/projects/show.tsx +++ b/apps/dokploy/components/dashboard/projects/show.tsx @@ -115,7 +115,7 @@ export const ShowProjects = () => {
)} -
+
{filteredProjects?.map((project) => { const emptyServices = project?.mariadb.length === 0 && diff --git a/apps/dokploy/components/dashboard/settings/ai-form.tsx b/apps/dokploy/components/dashboard/settings/ai-form.tsx index 05ab93a4f..b1923918e 100644 --- a/apps/dokploy/components/dashboard/settings/ai-form.tsx +++ b/apps/dokploy/components/dashboard/settings/ai-form.tsx @@ -55,7 +55,7 @@ export const AiForm = () => { key={config.aiId} className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" > -
+
{config.name} diff --git a/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx b/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx index 6aaa25630..b80c7b549 100644 --- a/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx +++ b/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx @@ -70,7 +70,7 @@ export const ShowCertificates = () => { key={certificate.certificateId} className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" > -
+
diff --git a/apps/dokploy/components/dashboard/settings/cluster/registry/show-registry.tsx b/apps/dokploy/components/dashboard/settings/cluster/registry/show-registry.tsx index 08cb03813..9ae595d6f 100644 --- a/apps/dokploy/components/dashboard/settings/cluster/registry/show-registry.tsx +++ b/apps/dokploy/components/dashboard/settings/cluster/registry/show-registry.tsx @@ -54,7 +54,7 @@ export const ShowRegistry = () => { key={registry.registryId} className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" > -
+
diff --git a/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx b/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx index 0639b0f75..014596ce3 100644 --- a/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx +++ b/apps/dokploy/components/dashboard/settings/destination/show-destinations.tsx @@ -55,7 +55,7 @@ export const ShowDestinations = () => { key={destination.destinationId} className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" > -
+
{index + 1}. {destination.name} diff --git a/apps/dokploy/components/dashboard/settings/git/gitlab/add-gitlab-provider.tsx b/apps/dokploy/components/dashboard/settings/git/gitlab/add-gitlab-provider.tsx index 4dd7da93c..023e46ed2 100644 --- a/apps/dokploy/components/dashboard/settings/git/gitlab/add-gitlab-provider.tsx +++ b/apps/dokploy/components/dashboard/settings/git/gitlab/add-gitlab-provider.tsx @@ -248,7 +248,9 @@ export const AddGitlabProvider = () => { name="groupName" render={({ field }) => ( - Group Name (Optional) + + Group Name (Optional, Comma-Separated List) + { name="groupName" render={({ field }) => ( - Group Name (Optional) + + Group Name (Optional, Comma-Separated List) + { key={notification.notificationId} className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" > -
+
{notification.notificationType === "slack" && (
diff --git a/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx b/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx index 6cf2c6a53..afc859f41 100644 --- a/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx +++ b/apps/dokploy/components/dashboard/settings/profile/enable-2fa.tsx @@ -36,6 +36,7 @@ const PasswordSchema = z.object({ password: z.string().min(8, { message: "Password is required", }), + issuer: z.string().optional(), }); const PinSchema = z.object({ @@ -60,12 +61,86 @@ export const Enable2FA = () => { const [isDialogOpen, setIsDialogOpen] = useState(false); const [step, setStep] = useState<"password" | "verify">("password"); const [isPasswordLoading, setIsPasswordLoading] = useState(false); + const [otpValue, setOtpValue] = useState(""); + + const handleVerifySubmit = async (e: React.FormEvent) => { + e.preventDefault(); + try { + const result = await authClient.twoFactor.verifyTotp({ + code: otpValue, + }); + + if (result.error) { + if (result.error.code === "INVALID_TWO_FACTOR_AUTHENTICATION") { + toast.error("Invalid verification code"); + return; + } + + throw result.error; + } + + if (!result.data) { + throw new Error("No response received from server"); + } + + toast.success("2FA configured successfully"); + utils.user.get.invalidate(); + setIsDialogOpen(false); + } catch (error) { + if (error instanceof Error) { + const errorMessage = + error.message === "Failed to fetch" + ? "Connection error. Please check your internet connection." + : error.message; + + toast.error(errorMessage); + } else { + toast.error("Error verifying 2FA code", { + description: error instanceof Error ? error.message : "Unknown error", + }); + } + } + }; + + const passwordForm = useForm({ + resolver: zodResolver(PasswordSchema), + defaultValues: { + password: "", + }, + }); + + const pinForm = useForm({ + resolver: zodResolver(PinSchema), + defaultValues: { + pin: "", + }, + }); + + useEffect(() => { + if (!isDialogOpen) { + setStep("password"); + setData(null); + setBackupCodes([]); + setOtpValue(""); + passwordForm.reset({ + password: "", + issuer: "", + }); + } + }, [isDialogOpen, passwordForm]); + + useEffect(() => { + if (step === "verify") { + setOtpValue(""); + } + }, [step]); const handlePasswordSubmit = async (formData: PasswordForm) => { setIsPasswordLoading(true); try { const { data: enableData, error } = await authClient.twoFactor.enable({ password: formData.password, + issuer: formData.issuer, }); if (!enableData) { @@ -103,75 +178,6 @@ export const Enable2FA = () => { } }; - const handleVerifySubmit = async (formData: PinForm) => { - try { - const result = await authClient.twoFactor.verifyTotp({ - code: formData.pin, - }); - - if (result.error) { - if (result.error.code === "INVALID_TWO_FACTOR_AUTHENTICATION") { - pinForm.setError("pin", { - message: "Invalid code. Please try again.", - }); - toast.error("Invalid verification code"); - return; - } - - throw result.error; - } - - if (!result.data) { - throw new Error("No response received from server"); - } - - toast.success("2FA configured successfully"); - utils.user.get.invalidate(); - setIsDialogOpen(false); - } catch (error) { - if (error instanceof Error) { - const errorMessage = - error.message === "Failed to fetch" - ? "Connection error. Please check your internet connection." - : error.message; - - pinForm.setError("pin", { - message: errorMessage, - }); - toast.error(errorMessage); - } else { - pinForm.setError("pin", { - message: "Error verifying code", - }); - toast.error("Error verifying 2FA code"); - } - } - }; - - const passwordForm = useForm({ - resolver: zodResolver(PasswordSchema), - defaultValues: { - password: "", - }, - }); - - const pinForm = useForm({ - resolver: zodResolver(PinSchema), - defaultValues: { - pin: "", - }, - }); - - useEffect(() => { - if (!isDialogOpen) { - setStep("password"); - setData(null); - setBackupCodes([]); - passwordForm.reset(); - pinForm.reset(); - } - }, [isDialogOpen, passwordForm, pinForm]); - return ( @@ -217,6 +223,27 @@ export const Enable2FA = () => { )} /> + ( + + Issuer + + + + + Use a custom issuer to identify the service you're + authenticating with. + + + + )} + /> diff --git a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx index 32179378a..9532b7d63 100644 --- a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx +++ b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx @@ -56,6 +56,7 @@ const randomImages = [ export const ProfileForm = () => { const _utils = api.useUtils(); const { data, refetch, isLoading } = api.user.get.useQuery(); + const { mutateAsync, isLoading: isUpdating, @@ -84,12 +85,17 @@ export const ProfileForm = () => { useEffect(() => { if (data) { - form.reset({ - email: data?.user?.email || "", - password: "", - image: data?.user?.image || "", - currentPassword: "", - }); + form.reset( + { + email: data?.user?.email || "", + password: form.getValues("password") || "", + image: data?.user?.image || "", + currentPassword: form.getValues("currentPassword") || "", + }, + { + keepValues: true, + }, + ); if (data.user.email) { generateSHA256Hash(data.user.email).then((hash) => { @@ -97,8 +103,7 @@ export const ProfileForm = () => { }); } } - form.reset(); - }, [form, form.reset, data]); + }, [form, data]); const onSubmit = async (values: Profile) => { await mutateAsync({ @@ -110,7 +115,12 @@ export const ProfileForm = () => { .then(async () => { await refetch(); toast.success("Profile Updated"); - form.reset(); + form.reset({ + email: values.email, + password: "", + image: values.image, + currentPassword: "", + }); }) .catch(() => { toast.error("Error updating the profile"); diff --git a/apps/dokploy/components/dashboard/settings/ssh-keys/show-ssh-keys.tsx b/apps/dokploy/components/dashboard/settings/ssh-keys/show-ssh-keys.tsx index 5842457ba..00d685a8d 100644 --- a/apps/dokploy/components/dashboard/settings/ssh-keys/show-ssh-keys.tsx +++ b/apps/dokploy/components/dashboard/settings/ssh-keys/show-ssh-keys.tsx @@ -56,7 +56,7 @@ export const ShowDestinations = () => { key={sshKey.sshKeyId} className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg" > -
+
diff --git a/apps/dokploy/components/dashboard/settings/web-domain.tsx b/apps/dokploy/components/dashboard/settings/web-domain.tsx index a579df397..d35dae35b 100644 --- a/apps/dokploy/components/dashboard/settings/web-domain.tsx +++ b/apps/dokploy/components/dashboard/settings/web-domain.tsx @@ -9,6 +9,7 @@ import { import { Form, FormControl, + FormDescription, FormField, FormItem, FormLabel, @@ -22,6 +23,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; import { GlobeIcon } from "lucide-react"; @@ -33,11 +35,19 @@ import { z } from "zod"; const addServerDomain = z .object({ - domain: z.string().min(1, { message: "URL is required" }), + domain: z.string(), letsEncryptEmail: z.string(), + https: z.boolean().optional(), certificateType: z.enum(["letsencrypt", "none", "custom"]), }) .superRefine((data, ctx) => { + if (data.https && !data.certificateType) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["certificateType"], + message: "Required", + }); + } if (data.certificateType === "letsencrypt" && !data.letsEncryptEmail) { ctx.addIssue({ code: z.ZodIssueCode.custom, @@ -61,15 +71,18 @@ export const WebDomain = () => { domain: "", certificateType: "none", letsEncryptEmail: "", + https: false, }, resolver: zodResolver(addServerDomain), }); + const https = form.watch("https"); useEffect(() => { if (data) { form.reset({ domain: data?.user?.host || "", certificateType: data?.user?.certificateType, letsEncryptEmail: data?.user?.letsEncryptEmail || "", + https: data?.user?.https || false, }); } }, [form, form.reset, data]); @@ -79,6 +92,7 @@ export const WebDomain = () => { host: data.domain, letsEncryptEmail: data.letsEncryptEmail, certificateType: data.certificateType, + https: data.https, }) .then(async () => { await refetch(); @@ -155,44 +169,67 @@ export const WebDomain = () => { /> { - return ( - - - {t("settings.server.domain.form.certificate.label")} - - + name="https" + render={({ field }) => ( + +
+ HTTPS + + Automatically provision SSL Certificate. + - - ); - }} +
+ + + +
+ )} /> + {https && ( + { + return ( + + + {t("settings.server.domain.form.certificate.label")} + + + + + ); + }} + /> + )}
- - - - -
- -
- ); -}; - -export default Page; - -Page.getLayout = (page: ReactElement) => { - return {page}; -}; -export async function getServerSideProps( - ctx: GetServerSidePropsContext<{ serviceId: string }>, -) { - const { req, res } = ctx; - const { user, session } = await validateRequest(ctx.req); - if (!user) { - return { - redirect: { - permanent: true, - destination: "/", - }, - }; - } - if (user.role === "member") { - return { - redirect: { - permanent: true, - destination: "/dashboard/settings/profile", - }, - }; - } - - const helpers = createServerSideHelpers({ - router: appRouter, - ctx: { - req: req as any, - res: res as any, - db: null as any, - session: session as any, - user: user as any, - }, - transformer: superjson, - }); - await helpers.user.get.prefetch(); - - return { - props: { - trpcState: helpers.dehydrate(), - }, - }; -} diff --git a/apps/dokploy/public/locales/nl/common.json b/apps/dokploy/public/locales/nl/common.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/apps/dokploy/public/locales/nl/common.json @@ -0,0 +1 @@ +{} diff --git a/apps/dokploy/public/locales/nl/settings.json b/apps/dokploy/public/locales/nl/settings.json new file mode 100644 index 000000000..c76d9bb9b --- /dev/null +++ b/apps/dokploy/public/locales/nl/settings.json @@ -0,0 +1,58 @@ +{ + "settings.common.save": "Opslaan", + "settings.common.enterTerminal": "Terminal", + "settings.server.domain.title": "Server Domein", + "settings.server.domain.description": "Voeg een domein toe aan jouw server applicatie.", + "settings.server.domain.form.domain": "Domein", + "settings.server.domain.form.letsEncryptEmail": "Let's Encrypt Email", + "settings.server.domain.form.certificate.label": "Certificaat Aanbieder", + "settings.server.domain.form.certificate.placeholder": "Select een certificaat", + "settings.server.domain.form.certificateOptions.none": "Geen", + "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt", + + "settings.server.webServer.title": "Web Server", + "settings.server.webServer.description": "Herlaad of maak de web server schoon.", + "settings.server.webServer.actions": "Acties", + "settings.server.webServer.reload": "Herladen", + "settings.server.webServer.watchLogs": "Bekijk Logs", + "settings.server.webServer.updateServerIp": "Update de Server IP", + "settings.server.webServer.server.label": "Server", + "settings.server.webServer.traefik.label": "Traefik", + "settings.server.webServer.traefik.modifyEnv": "Bewerk Omgeving", + "settings.server.webServer.traefik.managePorts": "Extra Poort Mappings", + "settings.server.webServer.traefik.managePortsDescription": "Bewerk extra Poorten voor Traefik", + "settings.server.webServer.traefik.targetPort": "Doel Poort", + "settings.server.webServer.traefik.publishedPort": "Gepubliceerde Poort", + "settings.server.webServer.traefik.addPort": "Voeg Poort toe", + "settings.server.webServer.traefik.portsUpdated": "Poorten succesvol aangepast", + "settings.server.webServer.traefik.portsUpdateError": "Poorten niet succesvol aangepast", + "settings.server.webServer.traefik.publishMode": "Publiceer Mode", + "settings.server.webServer.storage.label": "Opslag", + "settings.server.webServer.storage.cleanUnusedImages": "Maak ongebruikte images schoon", + "settings.server.webServer.storage.cleanUnusedVolumes": "Maak ongebruikte volumes schoon", + "settings.server.webServer.storage.cleanStoppedContainers": "Maak gestopte containers schoon", + "settings.server.webServer.storage.cleanDockerBuilder": "Maak Docker Builder & Systeem schoon", + "settings.server.webServer.storage.cleanMonitoring": "Maak monitoor schoon", + "settings.server.webServer.storage.cleanAll": "Maak alles schoon", + + "settings.profile.title": "Account", + "settings.profile.description": "Veramder details van account.", + "settings.profile.email": "Email", + "settings.profile.password": "Wachtwoord", + "settings.profile.avatar": "Profiel Icoon", + + "settings.appearance.title": "Uiterlijk", + "settings.appearance.description": "Verander het thema van je dashboard.", + "settings.appearance.theme": "Thema", + "settings.appearance.themeDescription": "Selecteer een thema voor je dashboard.", + "settings.appearance.themes.light": "Licht", + "settings.appearance.themes.dark": "Donker", + "settings.appearance.themes.system": "Systeem", + "settings.appearance.language": "Taal", + "settings.appearance.languageDescription": "Selecteer een taal voor je dashboard.", + + "settings.terminal.connectionSettings": "Verbindings instellingen", + "settings.terminal.ipAddress": "IP Address", + "settings.terminal.port": "Poort", + "settings.terminal.username": "Gebruikersnaam" +} diff --git a/apps/dokploy/public/locales/zh-Hans/common.json b/apps/dokploy/public/locales/zh-Hans/common.json index 0967ef424..91af07ff2 100644 --- a/apps/dokploy/public/locales/zh-Hans/common.json +++ b/apps/dokploy/public/locales/zh-Hans/common.json @@ -1 +1,78 @@ -{} +{ + "dashboard.title": "仪表盘", + "dashboard.overview": "概览", + "dashboard.projects": "项目", + "dashboard.servers": "服务器", + "dashboard.docker": "Docker", + "dashboard.monitoring": "监控", + "dashboard.settings": "设置", + "dashboard.logout": "退出登录", + "dashboard.profile": "个人资料", + "dashboard.terminal": "终端", + "dashboard.containers": "容器", + "dashboard.images": "镜像", + "dashboard.volumes": "卷", + "dashboard.networks": "网络", + "button.create": "创建", + "button.edit": "编辑", + "button.delete": "删除", + "button.cancel": "取消", + "button.save": "保存", + "button.confirm": "确认", + "button.back": "返回", + "button.next": "下一步", + "button.finish": "完成", + "status.running": "运行中", + "status.stopped": "已停止", + "status.error": "错误", + "status.pending": "等待中", + "status.success": "成功", + "status.failed": "失败", + "form.required": "必填", + "form.invalid": "无效", + "form.submit": "提交", + "form.reset": "重置", + "notification.success": "操作成功", + "notification.error": "操作失败", + "notification.warning": "警告", + "notification.info": "信息", + "time.now": "刚刚", + "time.minutes": "分钟前", + "time.hours": "小时前", + "time.days": "天前", + "filter.all": "全部", + "filter.active": "活跃", + "filter.inactive": "不活跃", + "sort.asc": "升序", + "sort.desc": "降序", + "search.placeholder": "搜索...", + "search.noResults": "无结果", + "pagination.prev": "上一页", + "pagination.next": "下一页", + "pagination.of": "共 {0} 页", + "error.notFound": "未找到", + "error.serverError": "服务器错误", + "error.unauthorized": "未授权", + "error.forbidden": "禁止访问", + "loading": "加载中...", + "empty": "暂无数据", + "more": "更多", + "less": "收起", + "project.create": "创建项目", + "project.edit": "编辑项目", + "project.delete": "删除项目", + "project.name": "项目名称", + "project.description": "项目描述", + "service.create": "创建服务", + "service.edit": "编辑服务", + "service.delete": "删除服务", + "service.name": "服务名称", + "service.type": "服务类型", + "domain.add": "添加域名", + "domain.remove": "移除域名", + "environment.variables": "环境变量", + "environment.add": "添加环境变量", + "environment.edit": "编辑环境变量", + "environment.name": "变量名", + "environment.value": "变量值" +} diff --git a/apps/dokploy/public/locales/zh-Hans/settings.json b/apps/dokploy/public/locales/zh-Hans/settings.json index c74fb21f8..d70676d6a 100644 --- a/apps/dokploy/public/locales/zh-Hans/settings.json +++ b/apps/dokploy/public/locales/zh-Hans/settings.json @@ -1,17 +1,16 @@ { "settings.common.save": "保存", - "settings.common.enterTerminal": "进入终端", - "settings.server.domain.title": "域名设置", - "settings.server.domain.description": "添加域名到服务器", + "settings.common.enterTerminal": "终端", + "settings.server.domain.title": "服务器域名", + "settings.server.domain.description": "为您的服务器应用添加域名。", "settings.server.domain.form.domain": "域名", "settings.server.domain.form.letsEncryptEmail": "Let's Encrypt 邮箱", - "settings.server.domain.form.certificate.label": "证书", - "settings.server.domain.form.certificate.placeholder": "选择一个证书", + "settings.server.domain.form.certificate.label": "证书提供商", + "settings.server.domain.form.certificate.placeholder": "选择证书", "settings.server.domain.form.certificateOptions.none": "无", "settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt", - - "settings.server.webServer.title": "服务器设置", - "settings.server.webServer.description": "管理服务器", + "settings.server.webServer.title": "Web 服务器", + "settings.server.webServer.description": "重载或清理 Web 服务器。", "settings.server.webServer.actions": "操作", "settings.server.webServer.reload": "重新加载", "settings.server.webServer.watchLogs": "查看日志", @@ -19,40 +18,50 @@ "settings.server.webServer.server.label": "服务器", "settings.server.webServer.traefik.label": "Traefik", "settings.server.webServer.traefik.modifyEnv": "修改环境变量", - "settings.server.webServer.traefik.managePorts": "端口转发", - "settings.server.webServer.traefik.managePortsDescription": "添加或删除 Traefik 的其他端口", + "settings.server.webServer.traefik.managePorts": "额外端口映射", + "settings.server.webServer.traefik.managePortsDescription": "为 Traefik 添加或删除额外端口", "settings.server.webServer.traefik.targetPort": "目标端口", - "settings.server.webServer.traefik.publishedPort": "对外端口", + "settings.server.webServer.traefik.publishedPort": "发布端口", "settings.server.webServer.traefik.addPort": "添加端口", "settings.server.webServer.traefik.portsUpdated": "端口更新成功", "settings.server.webServer.traefik.portsUpdateError": "端口更新失败", - "settings.server.webServer.traefik.publishMode": "端口映射", + "settings.server.webServer.traefik.publishMode": "发布模式", "settings.server.webServer.storage.label": "存储空间", "settings.server.webServer.storage.cleanUnusedImages": "清理未使用的镜像", "settings.server.webServer.storage.cleanUnusedVolumes": "清理未使用的卷", "settings.server.webServer.storage.cleanStoppedContainers": "清理已停止的容器", - "settings.server.webServer.storage.cleanDockerBuilder": "清理 Docker Builder 与 系统缓存", + "settings.server.webServer.storage.cleanDockerBuilder": "清理 Docker Builder 和系统", "settings.server.webServer.storage.cleanMonitoring": "清理监控数据", "settings.server.webServer.storage.cleanAll": "清理所有内容", - "settings.profile.title": "账户", - "settings.profile.description": "更改您的个人资料", + "settings.profile.description": "在此更改您的个人资料详情。", "settings.profile.email": "邮箱", "settings.profile.password": "密码", "settings.profile.avatar": "头像", - "settings.appearance.title": "外观", - "settings.appearance.description": "自定义面板主题", + "settings.appearance.description": "自定义您的仪表盘主题。", "settings.appearance.theme": "主题", - "settings.appearance.themeDescription": "选择面板主题", + "settings.appearance.themeDescription": "为您的仪表盘选择主题", "settings.appearance.themes.light": "明亮", - "settings.appearance.themes.dark": "黑暗", - "settings.appearance.themes.system": "系统主题", + "settings.appearance.themes.dark": "暗黑", + "settings.appearance.themes.system": "跟随系统", "settings.appearance.language": "语言", - "settings.appearance.languageDescription": "选择面板语言", - - "settings.terminal.connectionSettings": "终端设置", - "settings.terminal.ipAddress": "IP", + "settings.appearance.languageDescription": "为您的仪表盘选择语言", + "settings.terminal.connectionSettings": "连接设置", + "settings.terminal.ipAddress": "IP 地址", "settings.terminal.port": "端口", - "settings.terminal.username": "用户名" + "settings.terminal.username": "用户名", + "settings.settings": "设置", + "settings.general": "通用设置", + "settings.security": "安全", + "settings.users": "用户管理", + "settings.roles": "角色管理", + "settings.permissions": "权限", + "settings.api": "API设置", + "settings.certificates": "证书管理", + "settings.ssh": "SSH密钥", + "settings.backups": "备份", + "settings.logs": "日志", + "settings.updates": "更新", + "settings.network": "网络" } diff --git a/apps/dokploy/server/api/routers/application.ts b/apps/dokploy/server/api/routers/application.ts index 2397e4ca3..9d73b590b 100644 --- a/apps/dokploy/server/api/routers/application.ts +++ b/apps/dokploy/server/api/routers/application.ts @@ -33,6 +33,7 @@ import { findApplicationById, findProjectById, getApplicationStats, + mechanizeDockerContainer, readConfig, readRemoteConfig, removeDeployments, @@ -132,28 +133,36 @@ export const applicationRouter = createTRPCRouter({ .input(apiReloadApplication) .mutation(async ({ input, ctx }) => { const application = await findApplicationById(input.applicationId); - if ( - application.project.organizationId !== ctx.session.activeOrganizationId - ) { + + try { + if ( + application.project.organizationId !== + ctx.session.activeOrganizationId + ) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to reload this application", + }); + } + + if (application.serverId) { + await stopServiceRemote(application.serverId, input.appName); + } else { + await stopService(input.appName); + } + + await updateApplicationStatus(input.applicationId, "idle"); + await mechanizeDockerContainer(application); + await updateApplicationStatus(input.applicationId, "done"); + return true; + } catch (error) { + await updateApplicationStatus(input.applicationId, "error"); throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You are not authorized to reload this application", + code: "INTERNAL_SERVER_ERROR", + message: "Error reloading application", + cause: error, }); } - if (application.serverId) { - await stopServiceRemote(application.serverId, input.appName); - } else { - await stopService(input.appName); - } - await updateApplicationStatus(input.applicationId, "idle"); - - if (application.serverId) { - await startServiceRemote(application.serverId, input.appName); - } else { - await startService(input.appName); - } - await updateApplicationStatus(input.applicationId, "done"); - return true; }), delete: protectedProcedure diff --git a/apps/dokploy/server/api/routers/backup.ts b/apps/dokploy/server/api/routers/backup.ts index c691a4064..d4a787d0f 100644 --- a/apps/dokploy/server/api/routers/backup.ts +++ b/apps/dokploy/server/api/routers/backup.ts @@ -31,7 +31,10 @@ import { } from "@dokploy/server"; import { findDestinationById } from "@dokploy/server/services/destination"; -import { getS3Credentials } from "@dokploy/server/utils/backups/utils"; +import { + getS3Credentials, + normalizeS3Path, +} from "@dokploy/server/utils/backups/utils"; import { execAsync, execAsyncRemote, @@ -257,7 +260,7 @@ export const backupRouter = createTRPCRouter({ const lastSlashIndex = input.search.lastIndexOf("/"); const baseDir = lastSlashIndex !== -1 - ? input.search.slice(0, lastSlashIndex + 1) + ? normalizeS3Path(input.search.slice(0, lastSlashIndex + 1)) : ""; const searchTerm = lastSlashIndex !== -1 @@ -270,7 +273,7 @@ export const backupRouter = createTRPCRouter({ let stdout = ""; if (input.serverId) { - const result = await execAsyncRemote(listCommand, input.serverId); + const result = await execAsyncRemote(input.serverId, listCommand); stdout = result.stdout; } else { const result = await execAsync(listCommand); diff --git a/apps/dokploy/server/api/routers/registry.ts b/apps/dokploy/server/api/routers/registry.ts index a9a6be891..5486f37cd 100644 --- a/apps/dokploy/server/api/routers/registry.ts +++ b/apps/dokploy/server/api/routers/registry.ts @@ -10,8 +10,8 @@ import { import { IS_CLOUD, createRegistry, - execAsync, execAsyncRemote, + execFileAsync, findRegistryById, removeRegistry, updateRegistry, @@ -83,7 +83,13 @@ export const registryRouter = createTRPCRouter({ .input(apiTestRegistry) .mutation(async ({ input }) => { try { - const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`; + const args = [ + "login", + input.registryUrl, + "--username", + input.username, + "--password-stdin", + ]; if (IS_CLOUD && !input.serverId) { throw new TRPCError({ @@ -93,9 +99,14 @@ export const registryRouter = createTRPCRouter({ } if (input.serverId && input.serverId !== "none") { - await execAsyncRemote(input.serverId, loginCommand); + await execAsyncRemote( + input.serverId, + `echo ${input.password} | docker ${args.join(" ")}`, + ); } else { - await execAsync(loginCommand); + await execFileAsync("docker", args, { + input: Buffer.from(input.password).toString(), + }); } return true; diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index 70f14ec39..277f9ec6f 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -184,6 +184,7 @@ export const settingsRouter = createTRPCRouter({ letsEncryptEmail: input.letsEncryptEmail, }), certificateType: input.certificateType, + https: input.https, }); if (!user) { diff --git a/apps/dokploy/server/server.ts b/apps/dokploy/server/server.ts index 921724cad..8ec533ffa 100644 --- a/apps/dokploy/server/server.ts +++ b/apps/dokploy/server/server.ts @@ -46,8 +46,8 @@ void app.prepare().then(async () => { await initializeNetwork(); createDefaultTraefikConfig(); createDefaultServerTraefikConfig(); - initCronJobs(); await migration(); + await initCronJobs(); await sendDokployRestartNotifications(); } diff --git a/apps/dokploy/server/wss/drawer-logs.ts b/apps/dokploy/server/wss/drawer-logs.ts index 404dfeee5..0202ae521 100644 --- a/apps/dokploy/server/wss/drawer-logs.ts +++ b/apps/dokploy/server/wss/drawer-logs.ts @@ -1,9 +1,9 @@ import type http from "node:http"; -import { validateRequest } from "@dokploy/server/index"; import { applyWSSHandler } from "@trpc/server/adapters/ws"; import { WebSocketServer } from "ws"; import { appRouter } from "../api/root"; import { createTRPCContext } from "../api/trpc"; +import { validateRequest } from "@dokploy/server/lib/auth"; export const setupDrawerLogsWebSocketServer = ( server: http.Server, @@ -13,11 +13,13 @@ export const setupDrawerLogsWebSocketServer = ( path: "/drawer-logs", }); + // Set up tRPC WebSocket handler applyWSSHandler({ wss: wssTerm, router: appRouter, createContext: createTRPCContext as any, }); + server.on("upgrade", (req, socket, head) => { const { pathname } = new URL(req.url || "", `http://${req.headers.host}`); diff --git a/packages/server/package.json b/packages/server/package.json index 1ac0c8a7e..a02d7c219 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -36,11 +36,11 @@ "@ai-sdk/mistral": "^1.0.6", "@ai-sdk/openai": "^1.0.12", "@ai-sdk/openai-compatible": "^0.0.13", - "@better-auth/utils": "0.2.3", + "@better-auth/utils": "0.2.4", "@oslojs/encoding": "1.1.0", "@oslojs/crypto": "1.0.1", "drizzle-dbml-generator": "0.10.0", - "better-auth": "1.2.4", + "better-auth": "1.2.6", "@faker-js/faker": "^8.4.1", "@octokit/auth-app": "^6.0.4", "@react-email/components": "^0.0.21", diff --git a/packages/server/src/db/schema/user.ts b/packages/server/src/db/schema/user.ts index 246983805..9f4a5482b 100644 --- a/packages/server/src/db/schema/user.ts +++ b/packages/server/src/db/schema/user.ts @@ -50,6 +50,7 @@ export const users_temp = pgTable("user_temp", { // Admin serverIp: text("serverIp"), certificateType: certificateType("certificateType").notNull().default("none"), + https: boolean("https").notNull().default(false), host: text("host"), letsEncryptEmail: text("letsEncryptEmail"), sshPrivateKey: text("sshPrivateKey"), @@ -202,10 +203,12 @@ export const apiAssignDomain = createSchema host: true, certificateType: true, letsEncryptEmail: true, + https: true, }) .required() .partial({ letsEncryptEmail: true, + https: true, }); export const apiUpdateDockerCleanup = createSchema diff --git a/packages/server/src/templates/processors.ts b/packages/server/src/templates/processors.ts index 86d3cdf74..31e7861ad 100644 --- a/packages/server/src/templates/processors.ts +++ b/packages/server/src/templates/processors.ts @@ -1,3 +1,4 @@ +import { faker } from "@faker-js/faker"; import type { Schema } from "./index"; import { generateBase64, @@ -70,7 +71,7 @@ function processValue( schema: Schema, ): string { // First replace utility functions - let processedValue = value.replace(/\${([^}]+)}/g, (match, varName) => { + let processedValue = value?.replace(/\${([^}]+)}/g, (match, varName) => { // Handle utility functions if (varName === "domain") { return generateRandomDomain(schema); @@ -117,6 +118,14 @@ function processValue( return generateJwt(length); } + if (varName === "username") { + return faker.internet.userName().toLowerCase(); + } + + if (varName === "email") { + return faker.internet.email().toLowerCase(); + } + // If not a utility function, try to get from variables return variables[varName] || match; }); @@ -177,7 +186,14 @@ export function processDomains( variables: Record, schema: Schema, ): Template["domains"] { - if (!template?.config?.domains) return []; + if ( + !template?.config?.domains || + template.config.domains.length === 0 || + template.config.domains.every((domain) => !domain.serviceName) + ) { + return []; + } + return template?.config?.domains?.map((domain: DomainConfig) => ({ ...domain, host: domain.host @@ -194,7 +210,9 @@ export function processEnvVars( variables: Record, schema: Schema, ): Template["envs"] { - if (!template?.config?.env) return []; + if (!template?.config?.env || Object.keys(template.config.env).length === 0) { + return []; + } // Handle array of env vars if (Array.isArray(template.config.env)) { @@ -233,7 +251,13 @@ export function processMounts( variables: Record, schema: Schema, ): Template["mounts"] { - if (!template?.config?.mounts) return []; + if ( + !template?.config?.mounts || + template.config.mounts.length === 0 || + template.config.mounts.every((mount) => !mount.filePath && !mount.content) + ) { + return []; + } return template?.config?.mounts?.map((mount: MountConfig) => ({ filePath: processValue(mount.filePath, variables, schema), diff --git a/packages/server/src/utils/backups/index.ts b/packages/server/src/utils/backups/index.ts index d412df90d..6c9404064 100644 --- a/packages/server/src/utils/backups/index.ts +++ b/packages/server/src/utils/backups/index.ts @@ -2,7 +2,6 @@ import path from "node:path"; import { getAllServers } from "@dokploy/server/services/server"; import { scheduleJob } from "node-schedule"; import { db } from "../../db/index"; -import { findAdmin } from "../../services/admin"; import { cleanUpDockerBuilder, cleanUpSystemPrune, @@ -14,13 +13,24 @@ import { getS3Credentials, scheduleBackup } from "./utils"; import type { BackupSchedule } from "@dokploy/server/services/backup"; import { startLogCleanup } from "../access-log/handler"; +import { member } from "@dokploy/server/db/schema"; +import { eq } from "drizzle-orm"; export const initCronJobs = async () => { console.log("Setting up cron jobs...."); - const admin = await findAdmin(); + const admin = await db.query.member.findFirst({ + where: eq(member.role, "owner"), + with: { + user: true, + }, + }); - if (admin?.user.enableDockerCleanup) { + if (!admin) { + return; + } + + if (admin.user.enableDockerCleanup) { scheduleJob("docker-cleanup", "0 0 * * *", async () => { console.log( `Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`, @@ -96,8 +106,8 @@ export const keepLatestNBackups = async ( backup.prefix, ); - // --include "*.sql.gz" ensures nothing else other than the db backup files are touched by rclone - const rcloneList = `rclone lsf ${rcloneFlags.join(" ")} --include "*.sql.gz" ${backupFilesPath}`; + // --include "*.sql.gz" or "*.zip" ensures nothing else other than the dokploy backup files are touched by rclone + const rcloneList = `rclone lsf ${rcloneFlags.join(" ")} --include "*${backup.databaseType === "web-server" ? ".zip" : ".sql.gz"}" ${backupFilesPath}`; // when we pipe the above command with this one, we only get the list of files we want to delete const sortAndPickUnwantedBackups = `sort -r | tail -n +$((${backup.keepLatestCount}+1)) | xargs -I{}`; // this command deletes the files diff --git a/packages/server/src/utils/backups/mariadb.ts b/packages/server/src/utils/backups/mariadb.ts index 56c2919c4..776c5ff41 100644 --- a/packages/server/src/utils/backups/mariadb.ts +++ b/packages/server/src/utils/backups/mariadb.ts @@ -1,4 +1,3 @@ -import path from "node:path"; import type { BackupSchedule } from "@dokploy/server/services/backup"; import type { Mariadb } from "@dokploy/server/services/mariadb"; import { findProjectById } from "@dokploy/server/services/project"; @@ -8,7 +7,7 @@ import { } from "../docker/utils"; import { sendDatabaseBackupNotifications } from "../notifications/database-backup"; import { execAsync, execAsyncRemote } from "../process/execAsync"; -import { getS3Credentials } from "./utils"; +import { getS3Credentials, normalizeS3Path } from "./utils"; export const runMariadbBackup = async ( mariadb: Mariadb, @@ -19,7 +18,7 @@ export const runMariadbBackup = async ( const { prefix, database } = backup; const destination = backup.destination; const backupFileName = `${new Date().toISOString()}.sql.gz`; - const bucketDestination = path.join(prefix, backupFileName); + const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`; try { const rcloneFlags = getS3Credentials(destination); diff --git a/packages/server/src/utils/backups/mongo.ts b/packages/server/src/utils/backups/mongo.ts index a40ec4f47..a043a5a72 100644 --- a/packages/server/src/utils/backups/mongo.ts +++ b/packages/server/src/utils/backups/mongo.ts @@ -1,4 +1,3 @@ -import path from "node:path"; import type { BackupSchedule } from "@dokploy/server/services/backup"; import type { Mongo } from "@dokploy/server/services/mongo"; import { findProjectById } from "@dokploy/server/services/project"; @@ -8,7 +7,7 @@ import { } from "../docker/utils"; import { sendDatabaseBackupNotifications } from "../notifications/database-backup"; import { execAsync, execAsyncRemote } from "../process/execAsync"; -import { getS3Credentials } from "./utils"; +import { getS3Credentials, normalizeS3Path } from "./utils"; // mongodb://mongo:Bqh7AQl-PRbnBu@localhost:27017/?tls=false&directConnection=true export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => { @@ -17,7 +16,7 @@ export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => { const { prefix, database } = backup; const destination = backup.destination; const backupFileName = `${new Date().toISOString()}.dump.gz`; - const bucketDestination = path.join(prefix, backupFileName); + const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`; try { const rcloneFlags = getS3Credentials(destination); diff --git a/packages/server/src/utils/backups/mysql.ts b/packages/server/src/utils/backups/mysql.ts index 1272fc3ed..d98a8ecc2 100644 --- a/packages/server/src/utils/backups/mysql.ts +++ b/packages/server/src/utils/backups/mysql.ts @@ -1,4 +1,3 @@ -import path from "node:path"; import type { BackupSchedule } from "@dokploy/server/services/backup"; import type { MySql } from "@dokploy/server/services/mysql"; import { findProjectById } from "@dokploy/server/services/project"; @@ -8,7 +7,7 @@ import { } from "../docker/utils"; import { sendDatabaseBackupNotifications } from "../notifications/database-backup"; import { execAsync, execAsyncRemote } from "../process/execAsync"; -import { getS3Credentials } from "./utils"; +import { getS3Credentials, normalizeS3Path } from "./utils"; export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => { const { appName, databaseRootPassword, projectId, name } = mysql; @@ -16,7 +15,7 @@ export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => { const { prefix, database } = backup; const destination = backup.destination; const backupFileName = `${new Date().toISOString()}.sql.gz`; - const bucketDestination = path.join(prefix, backupFileName); + const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`; try { const rcloneFlags = getS3Credentials(destination); diff --git a/packages/server/src/utils/backups/postgres.ts b/packages/server/src/utils/backups/postgres.ts index 5ada2aa9d..cac582f7a 100644 --- a/packages/server/src/utils/backups/postgres.ts +++ b/packages/server/src/utils/backups/postgres.ts @@ -1,4 +1,3 @@ -import path from "node:path"; import type { BackupSchedule } from "@dokploy/server/services/backup"; import type { Postgres } from "@dokploy/server/services/postgres"; import { findProjectById } from "@dokploy/server/services/project"; @@ -8,7 +7,7 @@ import { } from "../docker/utils"; import { sendDatabaseBackupNotifications } from "../notifications/database-backup"; import { execAsync, execAsyncRemote } from "../process/execAsync"; -import { getS3Credentials } from "./utils"; +import { getS3Credentials, normalizeS3Path } from "./utils"; export const runPostgresBackup = async ( postgres: Postgres, @@ -20,7 +19,7 @@ export const runPostgresBackup = async ( const { prefix, database } = backup; const destination = backup.destination; const backupFileName = `${new Date().toISOString()}.sql.gz`; - const bucketDestination = path.join(prefix, backupFileName); + const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`; try { const rcloneFlags = getS3Credentials(destination); const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`; diff --git a/packages/server/src/utils/backups/utils.ts b/packages/server/src/utils/backups/utils.ts index 1abf7be0c..df3c83395 100644 --- a/packages/server/src/utils/backups/utils.ts +++ b/packages/server/src/utils/backups/utils.ts @@ -36,6 +36,13 @@ export const removeScheduleBackup = (backupId: string) => { currentJob?.cancel(); }; +export const normalizeS3Path = (prefix: string) => { + // Trim whitespace and remove leading/trailing slashes + const normalizedPrefix = prefix.trim().replace(/^\/+|\/+$/g, ""); + // Return empty string if prefix is empty, otherwise append trailing slash + return normalizedPrefix ? `${normalizedPrefix}/` : ""; +}; + export const getS3Credentials = (destination: Destination) => { const { accessKey, secretAccessKey, region, endpoint, provider } = destination; diff --git a/packages/server/src/utils/backups/web-server.ts b/packages/server/src/utils/backups/web-server.ts index 264ff764c..ef2249d0a 100644 --- a/packages/server/src/utils/backups/web-server.ts +++ b/packages/server/src/utils/backups/web-server.ts @@ -1,6 +1,6 @@ import type { BackupSchedule } from "@dokploy/server/services/backup"; import { execAsync } from "../process/execAsync"; -import { getS3Credentials } from "./utils"; +import { getS3Credentials, normalizeS3Path } from "./utils"; import { findDestinationById } from "@dokploy/server/services/destination"; import { IS_CLOUD, paths } from "@dokploy/server/constants"; import { mkdtemp } from "node:fs/promises"; @@ -18,18 +18,28 @@ export const runWebServerBackup = async (backup: BackupSchedule) => { const { BASE_PATH } = paths(); const tempDir = await mkdtemp(join(tmpdir(), "dokploy-backup-")); const backupFileName = `webserver-backup-${timestamp}.zip`; - const s3Path = `:s3:${destination.bucket}/${backup.prefix}${backupFileName}`; + const s3Path = `:s3:${destination.bucket}/${normalizeS3Path(backup.prefix)}${backupFileName}`; try { await execAsync(`mkdir -p ${tempDir}/filesystem`); - const postgresCommand = `docker exec $(docker ps --filter "name=dokploy-postgres" -q) pg_dump -v -Fc -U dokploy -d dokploy > ${tempDir}/database.sql`; + // First get the container ID + const { stdout: containerId } = await execAsync( + "docker ps --filter 'name=dokploy-postgres' -q", + ); + + if (!containerId) { + throw new Error("PostgreSQL container not found"); + } + + // Then run pg_dump with the container ID + const postgresCommand = `docker exec ${containerId.trim()} pg_dump -v -Fc -U dokploy -d dokploy > '${tempDir}/database.sql'`; await execAsync(postgresCommand); await execAsync(`cp -r ${BASE_PATH}/* ${tempDir}/filesystem/`); await execAsync( - `cd ${tempDir} && zip -r ${backupFileName} database.sql filesystem/`, + `cd ${tempDir} && zip -r ${backupFileName} database.sql filesystem/ > /dev/null 2>&1`, ); const uploadCommand = `rclone copyto ${rcloneFlags.join(" ")} "${tempDir}/${backupFileName}" "${s3Path}"`; diff --git a/packages/server/src/utils/builders/railpack.ts b/packages/server/src/utils/builders/railpack.ts index 612e02cf6..55fd40496 100644 --- a/packages/server/src/utils/builders/railpack.ts +++ b/packages/server/src/utils/builders/railpack.ts @@ -84,7 +84,7 @@ export const buildRailpack = async ( for (const envVar of envVariables) { const [key, value] = envVar.split("="); if (key && value) { - buildArgs.push("--secret", `id=${key},env=${key}`); + buildArgs.push("--secret", `id=${key},env='${key}'`); env[key] = value; } } @@ -132,7 +132,7 @@ export const getRailpackCommand = ( ]; for (const env of envVariables) { - prepareArgs.push("--env", env); + prepareArgs.push("--env", `'${env}'`); } // Calculate secrets hash for layer invalidation @@ -164,7 +164,7 @@ export const getRailpackCommand = ( for (const envVar of envVariables) { const [key, value] = envVar.split("="); if (key && value) { - buildArgs.push("--secret", `id=${key},env=${key}`); + buildArgs.push("--secret", `id=${key},env='${key}'`); exportEnvs.push(`export ${key}=${value}`); } } diff --git a/packages/server/src/utils/builders/static.ts b/packages/server/src/utils/builders/static.ts index c46bdf2e8..f7fc87cac 100644 --- a/packages/server/src/utils/builders/static.ts +++ b/packages/server/src/utils/builders/static.ts @@ -25,6 +25,12 @@ export const buildStatic = async ( ].join("\n"), ); + createFile( + buildAppDirectory, + ".dockerignore", + [".git", ".env", "Dockerfile", ".dockerignore"].join("\n"), + ); + await buildCustomDocker( { ...application, diff --git a/packages/server/src/utils/docker/domain.ts b/packages/server/src/utils/docker/domain.ts index 5a68146a6..4f0083979 100644 --- a/packages/server/src/utils/docker/domain.ts +++ b/packages/server/src/utils/docker/domain.ts @@ -249,6 +249,11 @@ export const addDomainToCompose = async ( labels.unshift("traefik.enable=true"); } labels.unshift(...httpLabels); + if (!compose.isolatedDeployment) { + if (!labels.includes("traefik.docker.network=dokploy-network")) { + labels.unshift("traefik.docker.network=dokploy-network"); + } + } } if (!compose.isolatedDeployment) { diff --git a/packages/server/src/utils/process/execAsync.ts b/packages/server/src/utils/process/execAsync.ts index aee1e821a..c3e409078 100644 --- a/packages/server/src/utils/process/execAsync.ts +++ b/packages/server/src/utils/process/execAsync.ts @@ -1,9 +1,48 @@ -import { exec } from "node:child_process"; +import { exec, execFile } from "node:child_process"; import util from "node:util"; import { findServerById } from "@dokploy/server/services/server"; import { Client } from "ssh2"; + export const execAsync = util.promisify(exec); +export const execFileAsync = async ( + command: string, + args: string[], + options: { input?: string } = {}, +): Promise<{ stdout: string; stderr: string }> => { + const child = execFile(command, args); + + if (options.input && child.stdin) { + child.stdin.write(options.input); + child.stdin.end(); + } + + return new Promise((resolve, reject) => { + let stdout = ""; + let stderr = ""; + + child.stdout?.on("data", (data) => { + stdout += data.toString(); + }); + + child.stderr?.on("data", (data) => { + stderr += data.toString(); + }); + + child.on("close", (code) => { + if (code === 0) { + resolve({ stdout, stderr }); + } else { + reject( + new Error(`Command failed with code ${code}. Stderr: ${stderr}`), + ); + } + }); + + child.on("error", reject); + }); +}; + export const execAsyncRemote = async ( serverId: string | null, command: string, diff --git a/packages/server/src/utils/providers/gitlab.ts b/packages/server/src/utils/providers/gitlab.ts index a1c5a39fb..1d8c3edca 100644 --- a/packages/server/src/utils/providers/gitlab.ts +++ b/packages/server/src/utils/providers/gitlab.ts @@ -270,7 +270,11 @@ export const getGitlabRepositories = async (gitlabId?: string) => { const groupName = gitlabProvider.groupName?.toLowerCase(); if (groupName) { - return full_path.toLowerCase().includes(groupName) && kind === "group"; + const isIncluded = groupName + .split(",") + .some((name) => full_path.toLowerCase().includes(name)); + + return isIncluded && kind === "group"; } return kind === "user"; }); @@ -453,7 +457,9 @@ export const testGitlabConnection = async ( const { full_path, kind } = repo.namespace; if (groupName) { - return full_path.toLowerCase().includes(groupName) && kind === "group"; + return groupName + .split(",") + .some((name) => full_path.toLowerCase().includes(name)); } return kind === "user"; }); diff --git a/packages/server/src/utils/restore/web-server.ts b/packages/server/src/utils/restore/web-server.ts index fb810a473..46aa92395 100644 --- a/packages/server/src/utils/restore/web-server.ts +++ b/packages/server/src/utils/restore/web-server.ts @@ -45,7 +45,7 @@ export const restoreWebServerBackup = async ( // Extract backup emit("Extracting backup..."); - await execAsync(`cd ${tempDir} && unzip ${backupFile}`); + await execAsync(`cd ${tempDir} && unzip ${backupFile} > /dev/null 2>&1`); // Restore filesystem first emit("Restoring filesystem..."); diff --git a/packages/server/src/utils/traefik/web-server.ts b/packages/server/src/utils/traefik/web-server.ts index 78046c673..1534e2f1c 100644 --- a/packages/server/src/utils/traefik/web-server.ts +++ b/packages/server/src/utils/traefik/web-server.ts @@ -3,7 +3,11 @@ import { join } from "node:path"; import { paths } from "@dokploy/server/constants"; import type { User } from "@dokploy/server/services/user"; import { dump, load } from "js-yaml"; -import { loadOrCreateConfig, writeTraefikConfig } from "./application"; +import { + loadOrCreateConfig, + removeTraefikConfig, + writeTraefikConfig, +} from "./application"; import type { FileConfig } from "./file-types"; import type { MainTraefikConfig } from "./types"; @@ -11,32 +15,62 @@ export const updateServerTraefik = ( user: User | null, newHost: string | null, ) => { + const { https, certificateType } = user || {}; const appName = "dokploy"; const config: FileConfig = loadOrCreateConfig(appName); config.http = config.http || { routers: {}, services: {} }; config.http.routers = config.http.routers || {}; + config.http.services = config.http.services || {}; - const currentRouterConfig = config.http.routers[`${appName}-router-app`]; + const currentRouterConfig = config.http.routers[`${appName}-router-app`] || { + rule: `Host(\`${newHost}\`)`, + service: `${appName}-service-app`, + entryPoints: ["web"], + }; + config.http.routers[`${appName}-router-app`] = currentRouterConfig; - if (currentRouterConfig && newHost) { - currentRouterConfig.rule = `Host(\`${newHost}\`)`; + config.http.services = { + ...config.http.services, + [`${appName}-service-app`]: { + loadBalancer: { + servers: [ + { + url: `http://dokploy:${process.env.PORT || 3000}`, + }, + ], + passHostHeader: true, + }, + }, + }; - if (user?.certificateType === "letsencrypt") { + if (https) { + currentRouterConfig.middlewares = ["redirect-to-https"]; + + if (certificateType === "letsencrypt") { config.http.routers[`${appName}-router-app-secure`] = { - ...currentRouterConfig, + rule: `Host(\`${newHost}\`)`, + service: `${appName}-service-app`, entryPoints: ["websecure"], tls: { certResolver: "letsencrypt" }, }; - - currentRouterConfig.middlewares = ["redirect-to-https"]; } else { - delete config.http.routers[`${appName}-router-app-secure`]; - currentRouterConfig.middlewares = []; + config.http.routers[`${appName}-router-app-secure`] = { + rule: `Host(\`${newHost}\`)`, + service: `${appName}-service-app`, + entryPoints: ["websecure"], + }; } + } else { + delete config.http.routers[`${appName}-router-app-secure`]; + currentRouterConfig.middlewares = []; } - writeTraefikConfig(config, appName); + if (newHost) { + writeTraefikConfig(config, appName); + } else { + removeTraefikConfig(appName); + } }; export const updateLetsEncryptEmail = (newEmail: string | null) => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 282994f8a..21dc828b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,7 +17,7 @@ importers: version: 1.9.4 '@commitlint/cli': specifier: ^19.3.0 - version: 19.3.0(@types/node@18.19.42)(typescript@5.7.2) + version: 19.3.0(@types/node@18.19.42)(typescript@5.8.3) '@commitlint/config-conventional': specifier: ^19.2.2 version: 19.2.2 @@ -266,8 +266,8 @@ importers: specifier: 5.1.1 version: 5.1.1(encoding@0.1.13) better-auth: - specifier: 1.2.4 - version: 1.2.4(typescript@5.5.3) + specifier: 1.2.6 + version: 1.2.6 bl: specifier: 6.0.11 version: 6.0.11 @@ -607,8 +607,8 @@ importers: specifier: ^0.0.13 version: 0.0.13(zod@3.23.8) '@better-auth/utils': - specifier: 0.2.3 - version: 0.2.3 + specifier: 0.2.4 + version: 0.2.4 '@faker-js/faker': specifier: ^8.4.1 version: 8.4.1 @@ -640,8 +640,8 @@ importers: specifier: 5.1.1 version: 5.1.1(encoding@0.1.13) better-auth: - specifier: 1.2.4 - version: 1.2.4(typescript@5.5.3) + specifier: 1.2.6 + version: 1.2.6 bl: specifier: 6.0.11 version: 6.0.11 @@ -924,11 +924,11 @@ packages: '@balena/dockerignore@1.0.2': resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} - '@better-auth/utils@0.2.3': - resolution: {integrity: sha512-Ap1GaSmo6JYhJhxJOpUB0HobkKPTNzfta+bLV89HfpyCAHN7p8ntCrmNFHNAVD0F6v0mywFVEUg1FUhNCc81Rw==} + '@better-auth/utils@0.2.4': + resolution: {integrity: sha512-ayiX87Xd5sCHEplAdeMgwkA0FgnXsEZBgDn890XHHwSWNqqRZDYOq3uj2Ei2leTv1I2KbG5HHn60Ah1i2JWZjQ==} - '@better-fetch/fetch@1.1.15': - resolution: {integrity: sha512-0Bl8YYj1f8qCTNHeSn5+1DWv2hy7rLBrQ8rS8Y9XYloiwZEfc3k4yspIG0llRxafxqhGCwlGRg+F8q1HZRCMXA==} + '@better-fetch/fetch@1.1.18': + resolution: {integrity: sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==} '@biomejs/biome@1.9.4': resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} @@ -3836,11 +3836,11 @@ packages: before-after-hook@2.2.3: resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} - better-auth@1.2.4: - resolution: {integrity: sha512-/ZK2jbUjm8JwdeCLFrUWUBmexPyI9PkaLVXWLWtN60sMDHTY8B5G72wcHglo1QMFBaw4G0qFkP5ayl9k6XfDaA==} + better-auth@1.2.6: + resolution: {integrity: sha512-RVy6nfNCXpohx49zP2ChUO3zN0nvz5UXuETJIhWU+dshBKpFMk4P4hAQauM3xqTJdd9hfeB5y+segmG1oYGTJQ==} - better-call@1.0.3: - resolution: {integrity: sha512-DUKImKoDIy5UtCvQbHTg0wuBRse6gu1Yvznn7+1B3I5TeY8sclRPFce0HI+4WF2bcb+9PqmkET8nXZubrHQh9A==} + better-call@1.0.7: + resolution: {integrity: sha512-p5kEthErx3HsW9dCCvvEx+uuEdncn0ZrlqrOG3TkR1aVYgynpwYbTVU90nY8/UwfMhROzqZWs8vryainSQxrNg==} binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} @@ -7093,8 +7093,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - typescript@5.7.2: - resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} engines: {node: '>=14.17'} hasBin: true @@ -7210,14 +7210,6 @@ packages: v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - valibot@1.0.0-beta.15: - resolution: {integrity: sha512-BKy8XosZkDHWmYC+cJG74LBzP++Gfntwi33pP3D3RKztz2XV9jmFWnkOi21GoqARP8wAWARwhV6eTr1JcWzjGw==} - peerDependencies: - typescript: '>=5' - peerDependenciesMeta: - typescript: - optional: true - vfile-message@4.0.2: resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} @@ -7567,11 +7559,12 @@ snapshots: '@balena/dockerignore@1.0.2': {} - '@better-auth/utils@0.2.3': + '@better-auth/utils@0.2.4': dependencies: + typescript: 5.8.3 uncrypto: 0.1.3 - '@better-fetch/fetch@1.1.15': {} + '@better-fetch/fetch@1.1.18': {} '@biomejs/biome@1.9.4': optionalDependencies: @@ -7678,11 +7671,11 @@ snapshots: style-mod: 4.1.2 w3c-keyname: 2.2.8 - '@commitlint/cli@19.3.0(@types/node@18.19.42)(typescript@5.7.2)': + '@commitlint/cli@19.3.0(@types/node@18.19.42)(typescript@5.8.3)': dependencies: '@commitlint/format': 19.3.0 '@commitlint/lint': 19.2.2 - '@commitlint/load': 19.2.0(@types/node@18.19.42)(typescript@5.7.2) + '@commitlint/load': 19.2.0(@types/node@18.19.42)(typescript@5.8.3) '@commitlint/read': 19.2.1 '@commitlint/types': 19.0.3 execa: 8.0.1 @@ -7729,15 +7722,15 @@ snapshots: '@commitlint/rules': 19.0.3 '@commitlint/types': 19.0.3 - '@commitlint/load@19.2.0(@types/node@18.19.42)(typescript@5.7.2)': + '@commitlint/load@19.2.0(@types/node@18.19.42)(typescript@5.8.3)': dependencies: '@commitlint/config-validator': 19.0.3 '@commitlint/execute-rule': 19.0.0 '@commitlint/resolve-extends': 19.1.0 '@commitlint/types': 19.0.3 chalk: 5.3.0 - cosmiconfig: 9.0.0(typescript@5.7.2) - cosmiconfig-typescript-loader: 5.0.0(@types/node@18.19.42)(cosmiconfig@9.0.0(typescript@5.7.2))(typescript@5.7.2) + cosmiconfig: 9.0.0(typescript@5.8.3) + cosmiconfig-typescript-loader: 5.0.0(@types/node@18.19.42)(cosmiconfig@9.0.0(typescript@5.8.3))(typescript@5.8.3) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 @@ -10547,27 +10540,24 @@ snapshots: before-after-hook@2.2.3: {} - better-auth@1.2.4(typescript@5.5.3): + better-auth@1.2.6: dependencies: - '@better-auth/utils': 0.2.3 - '@better-fetch/fetch': 1.1.15 + '@better-auth/utils': 0.2.4 + '@better-fetch/fetch': 1.1.18 '@noble/ciphers': 0.6.0 '@noble/hashes': 1.7.1 '@simplewebauthn/browser': 13.1.0 '@simplewebauthn/server': 13.1.1 - better-call: 1.0.3 + better-call: 1.0.7 defu: 6.1.4 jose: 5.9.6 kysely: 0.27.6 nanostores: 0.11.3 - valibot: 1.0.0-beta.15(typescript@5.5.3) zod: 3.24.1 - transitivePeerDependencies: - - typescript - better-call@1.0.3: + better-call@1.0.7: dependencies: - '@better-fetch/fetch': 1.1.15 + '@better-fetch/fetch': 1.1.18 rou3: 0.5.1 set-cookie-parser: 2.7.1 uncrypto: 0.1.3 @@ -10942,21 +10932,21 @@ snapshots: core-js@3.39.0: {} - cosmiconfig-typescript-loader@5.0.0(@types/node@18.19.42)(cosmiconfig@9.0.0(typescript@5.7.2))(typescript@5.7.2): + cosmiconfig-typescript-loader@5.0.0(@types/node@18.19.42)(cosmiconfig@9.0.0(typescript@5.8.3))(typescript@5.8.3): dependencies: '@types/node': 18.19.42 - cosmiconfig: 9.0.0(typescript@5.7.2) + cosmiconfig: 9.0.0(typescript@5.8.3) jiti: 1.21.6 - typescript: 5.7.2 + typescript: 5.8.3 - cosmiconfig@9.0.0(typescript@5.7.2): + cosmiconfig@9.0.0(typescript@5.8.3): dependencies: env-paths: 2.2.1 import-fresh: 3.3.0 js-yaml: 4.1.0 parse-json: 5.2.0 optionalDependencies: - typescript: 5.7.2 + typescript: 5.8.3 cpu-features@0.0.10: dependencies: @@ -14171,7 +14161,7 @@ snapshots: typescript@5.5.3: {} - typescript@5.7.2: {} + typescript@5.8.3: {} ufo@1.5.4: {} @@ -14292,10 +14282,6 @@ snapshots: v8-compile-cache-lib@3.0.1: optional: true - valibot@1.0.0-beta.15(typescript@5.5.3): - optionalDependencies: - typescript: 5.5.3 - vfile-message@4.0.2: dependencies: '@types/unist': 3.0.3