mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge branch 'Dokploy:canary' into canary
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
|||||||
import { beforeEach, expect, test, vi } from "vitest";
|
import { beforeEach, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
const baseAdmin: User = {
|
const baseAdmin: User = {
|
||||||
|
https: false,
|
||||||
enablePaidFeatures: false,
|
enablePaidFeatures: false,
|
||||||
metricsConfig: {
|
metricsConfig: {
|
||||||
containers: {
|
containers: {
|
||||||
@@ -73,7 +74,6 @@ beforeEach(() => {
|
|||||||
|
|
||||||
test("Should read the configuration file", () => {
|
test("Should read the configuration file", () => {
|
||||||
const config: FileConfig = loadOrCreateConfig("dokploy");
|
const config: FileConfig = loadOrCreateConfig("dokploy");
|
||||||
|
|
||||||
expect(config.http?.routers?.["dokploy-router-app"]?.service).toBe(
|
expect(config.http?.routers?.["dokploy-router-app"]?.service).toBe(
|
||||||
"dokploy-service-app",
|
"dokploy-service-app",
|
||||||
);
|
);
|
||||||
@@ -83,6 +83,7 @@ test("Should apply redirect-to-https", () => {
|
|||||||
updateServerTraefik(
|
updateServerTraefik(
|
||||||
{
|
{
|
||||||
...baseAdmin,
|
...baseAdmin,
|
||||||
|
https: true,
|
||||||
certificateType: "letsencrypt",
|
certificateType: "letsencrypt",
|
||||||
},
|
},
|
||||||
"example.com",
|
"example.com",
|
||||||
|
|||||||
61
apps/dokploy/__test__/utils/backups.test.ts
Normal file
61
apps/dokploy/__test__/utils/backups.test.ts
Normal file
@@ -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/");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -298,7 +298,11 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
refetch();
|
refetch();
|
||||||
toast.success("Preview deployments enabled");
|
toast.success(
|
||||||
|
checked
|
||||||
|
? "Preview deployments enabled"
|
||||||
|
: "Preview deployments disabled",
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
toast.error(error.message);
|
toast.error(error.message);
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ export const RestoreBackup = ({
|
|||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
|
||||||
|
|
||||||
const { data: destinations = [] } = api.destination.all.useQuery();
|
const { data: destinations = [] } = api.destination.all.useQuery();
|
||||||
|
|
||||||
@@ -99,13 +100,18 @@ export const RestoreBackup = ({
|
|||||||
const destionationId = form.watch("destinationId");
|
const destionationId = form.watch("destinationId");
|
||||||
|
|
||||||
const debouncedSetSearch = debounce((value: string) => {
|
const debouncedSetSearch = debounce((value: string) => {
|
||||||
|
setDebouncedSearchTerm(value);
|
||||||
|
}, 150);
|
||||||
|
|
||||||
|
const handleSearchChange = (value: string) => {
|
||||||
setSearch(value);
|
setSearch(value);
|
||||||
}, 300);
|
debouncedSetSearch(value);
|
||||||
|
};
|
||||||
|
|
||||||
const { data: files = [], isLoading } = api.backup.listBackupFiles.useQuery(
|
const { data: files = [], isLoading } = api.backup.listBackupFiles.useQuery(
|
||||||
{
|
{
|
||||||
destinationId: destionationId,
|
destinationId: destionationId,
|
||||||
search,
|
search: debouncedSearchTerm,
|
||||||
serverId: serverId ?? "",
|
serverId: serverId ?? "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -284,7 +290,8 @@ export const RestoreBackup = ({
|
|||||||
<Command>
|
<Command>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="Search backup files..."
|
placeholder="Search backup files..."
|
||||||
onValueChange={debouncedSetSearch}
|
value={search}
|
||||||
|
onValueChange={handleSearchChange}
|
||||||
className="h-9"
|
className="h-9"
|
||||||
/>
|
/>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
@@ -308,6 +315,8 @@ export const RestoreBackup = ({
|
|||||||
key={file}
|
key={file}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
form.setValue("backupFile", file);
|
form.setValue("backupFile", file);
|
||||||
|
setSearch(file);
|
||||||
|
setDebouncedSearchTerm(file);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex w-full justify-between">
|
<div className="flex w-full justify-between">
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const PasswordSchema = z.object({
|
|||||||
password: z.string().min(8, {
|
password: z.string().min(8, {
|
||||||
message: "Password is required",
|
message: "Password is required",
|
||||||
}),
|
}),
|
||||||
|
issuer: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const PinSchema = z.object({
|
const PinSchema = z.object({
|
||||||
@@ -66,6 +67,7 @@ export const Enable2FA = () => {
|
|||||||
try {
|
try {
|
||||||
const { data: enableData, error } = await authClient.twoFactor.enable({
|
const { data: enableData, error } = await authClient.twoFactor.enable({
|
||||||
password: formData.password,
|
password: formData.password,
|
||||||
|
issuer: formData.issuer,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!enableData) {
|
if (!enableData) {
|
||||||
@@ -217,6 +219,26 @@ export const Enable2FA = () => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={passwordForm.control}
|
||||||
|
name="issuer"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Issuer</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter your issuer"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Enter your password to enable 2FA
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ const randomImages = [
|
|||||||
export const ProfileForm = () => {
|
export const ProfileForm = () => {
|
||||||
const _utils = api.useUtils();
|
const _utils = api.useUtils();
|
||||||
const { data, refetch, isLoading } = api.user.get.useQuery();
|
const { data, refetch, isLoading } = api.user.get.useQuery();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
mutateAsync,
|
mutateAsync,
|
||||||
isLoading: isUpdating,
|
isLoading: isUpdating,
|
||||||
@@ -84,12 +85,17 @@ export const ProfileForm = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
form.reset({
|
form.reset(
|
||||||
email: data?.user?.email || "",
|
{
|
||||||
password: "",
|
email: data?.user?.email || "",
|
||||||
image: data?.user?.image || "",
|
password: form.getValues("password") || "",
|
||||||
currentPassword: "",
|
image: data?.user?.image || "",
|
||||||
});
|
currentPassword: form.getValues("currentPassword") || "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keepValues: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (data.user.email) {
|
if (data.user.email) {
|
||||||
generateSHA256Hash(data.user.email).then((hash) => {
|
generateSHA256Hash(data.user.email).then((hash) => {
|
||||||
@@ -97,8 +103,7 @@ export const ProfileForm = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
form.reset();
|
}, [form, data]);
|
||||||
}, [form, form.reset, data]);
|
|
||||||
|
|
||||||
const onSubmit = async (values: Profile) => {
|
const onSubmit = async (values: Profile) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
@@ -110,7 +115,12 @@ export const ProfileForm = () => {
|
|||||||
.then(async () => {
|
.then(async () => {
|
||||||
await refetch();
|
await refetch();
|
||||||
toast.success("Profile Updated");
|
toast.success("Profile Updated");
|
||||||
form.reset();
|
form.reset({
|
||||||
|
email: values.email,
|
||||||
|
password: "",
|
||||||
|
image: values.image,
|
||||||
|
currentPassword: "",
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error updating the profile");
|
toast.error("Error updating the profile");
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { GlobeIcon } from "lucide-react";
|
import { GlobeIcon } from "lucide-react";
|
||||||
@@ -33,11 +35,19 @@ import { z } from "zod";
|
|||||||
|
|
||||||
const addServerDomain = z
|
const addServerDomain = z
|
||||||
.object({
|
.object({
|
||||||
domain: z.string().min(1, { message: "URL is required" }),
|
domain: z.string(),
|
||||||
letsEncryptEmail: z.string(),
|
letsEncryptEmail: z.string(),
|
||||||
|
https: z.boolean().optional(),
|
||||||
certificateType: z.enum(["letsencrypt", "none", "custom"]),
|
certificateType: z.enum(["letsencrypt", "none", "custom"]),
|
||||||
})
|
})
|
||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
|
if (data.https && !data.certificateType) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
path: ["certificateType"],
|
||||||
|
message: "Required",
|
||||||
|
});
|
||||||
|
}
|
||||||
if (data.certificateType === "letsencrypt" && !data.letsEncryptEmail) {
|
if (data.certificateType === "letsencrypt" && !data.letsEncryptEmail) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
@@ -61,15 +71,18 @@ export const WebDomain = () => {
|
|||||||
domain: "",
|
domain: "",
|
||||||
certificateType: "none",
|
certificateType: "none",
|
||||||
letsEncryptEmail: "",
|
letsEncryptEmail: "",
|
||||||
|
https: false,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(addServerDomain),
|
resolver: zodResolver(addServerDomain),
|
||||||
});
|
});
|
||||||
|
const https = form.watch("https");
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
form.reset({
|
form.reset({
|
||||||
domain: data?.user?.host || "",
|
domain: data?.user?.host || "",
|
||||||
certificateType: data?.user?.certificateType,
|
certificateType: data?.user?.certificateType,
|
||||||
letsEncryptEmail: data?.user?.letsEncryptEmail || "",
|
letsEncryptEmail: data?.user?.letsEncryptEmail || "",
|
||||||
|
https: data?.user?.https || false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form, form.reset, data]);
|
}, [form, form.reset, data]);
|
||||||
@@ -79,6 +92,7 @@ export const WebDomain = () => {
|
|||||||
host: data.domain,
|
host: data.domain,
|
||||||
letsEncryptEmail: data.letsEncryptEmail,
|
letsEncryptEmail: data.letsEncryptEmail,
|
||||||
certificateType: data.certificateType,
|
certificateType: data.certificateType,
|
||||||
|
https: data.https,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
await refetch();
|
await refetch();
|
||||||
@@ -155,44 +169,67 @@ export const WebDomain = () => {
|
|||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="certificateType"
|
name="https"
|
||||||
render={({ field }) => {
|
render={({ field }) => (
|
||||||
return (
|
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm w-full col-span-2">
|
||||||
<FormItem className="md:col-span-2">
|
<div className="space-y-0.5">
|
||||||
<FormLabel>
|
<FormLabel>HTTPS</FormLabel>
|
||||||
{t("settings.server.domain.form.certificate.label")}
|
<FormDescription>
|
||||||
</FormLabel>
|
Automatically provision SSL Certificate.
|
||||||
<Select
|
</FormDescription>
|
||||||
onValueChange={field.onChange}
|
|
||||||
value={field.value}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue
|
|
||||||
placeholder={t(
|
|
||||||
"settings.server.domain.form.certificate.placeholder",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value={"none"}>
|
|
||||||
{t(
|
|
||||||
"settings.server.domain.form.certificateOptions.none",
|
|
||||||
)}
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value={"letsencrypt"}>
|
|
||||||
{t(
|
|
||||||
"settings.server.domain.form.certificateOptions.letsencrypt",
|
|
||||||
)}
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</div>
|
||||||
);
|
<FormControl>
|
||||||
}}
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
|
{https && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="certificateType"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem className="md:col-span-2">
|
||||||
|
<FormLabel>
|
||||||
|
{t("settings.server.domain.form.certificate.label")}
|
||||||
|
</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
value={field.value}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue
|
||||||
|
placeholder={t(
|
||||||
|
"settings.server.domain.form.certificate.placeholder",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={"none"}>
|
||||||
|
{t(
|
||||||
|
"settings.server.domain.form.certificateOptions.none",
|
||||||
|
)}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value={"letsencrypt"}>
|
||||||
|
{t(
|
||||||
|
"settings.server.domain.form.certificateOptions.letsencrypt",
|
||||||
|
)}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex w-full justify-end col-span-2">
|
<div className="flex w-full justify-end col-span-2">
|
||||||
<Button isLoading={isLoading} type="submit">
|
<Button isLoading={isLoading} type="submit">
|
||||||
|
|||||||
@@ -120,17 +120,6 @@ export const UserNav = () => {
|
|||||||
Docker
|
Docker
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{data?.role === "owner" && (
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => {
|
|
||||||
router.push("/dashboard/settings");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Settings
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|||||||
1
apps/dokploy/drizzle/0084_thin_iron_lad.sql
Normal file
1
apps/dokploy/drizzle/0084_thin_iron_lad.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "user_temp" ADD COLUMN "https" boolean DEFAULT false NOT NULL;
|
||||||
5369
apps/dokploy/drizzle/meta/0084_snapshot.json
Normal file
5369
apps/dokploy/drizzle/meta/0084_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -589,6 +589,13 @@
|
|||||||
"when": 1743288371413,
|
"when": 1743288371413,
|
||||||
"tag": "0083_parallel_stranger",
|
"tag": "0083_parallel_stranger",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 84,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1743923992280,
|
||||||
|
"tag": "0084_thin_iron_lad",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.21.3",
|
"version": "v0.21.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
"adm-zip": "^0.5.14",
|
"adm-zip": "^0.5.14",
|
||||||
"ai": "^4.0.23",
|
"ai": "^4.0.23",
|
||||||
"bcrypt": "5.1.1",
|
"bcrypt": "5.1.1",
|
||||||
"better-auth": "1.2.4",
|
"better-auth": "1.2.6",
|
||||||
"bl": "6.0.11",
|
"bl": "6.0.11",
|
||||||
"boxen": "^7.1.1",
|
"boxen": "^7.1.1",
|
||||||
"bullmq": "5.4.2",
|
"bullmq": "5.4.2",
|
||||||
|
|||||||
@@ -314,31 +314,43 @@ const Project = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const applicationActions = {
|
const applicationActions = {
|
||||||
|
start: api.application.start.useMutation(),
|
||||||
|
stop: api.application.stop.useMutation(),
|
||||||
move: api.application.move.useMutation(),
|
move: api.application.move.useMutation(),
|
||||||
delete: api.application.delete.useMutation(),
|
delete: api.application.delete.useMutation(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const postgresActions = {
|
const postgresActions = {
|
||||||
|
start: api.postgres.start.useMutation(),
|
||||||
|
stop: api.postgres.stop.useMutation(),
|
||||||
move: api.postgres.move.useMutation(),
|
move: api.postgres.move.useMutation(),
|
||||||
delete: api.postgres.remove.useMutation(),
|
delete: api.postgres.remove.useMutation(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mysqlActions = {
|
const mysqlActions = {
|
||||||
|
start: api.mysql.start.useMutation(),
|
||||||
|
stop: api.mysql.stop.useMutation(),
|
||||||
move: api.mysql.move.useMutation(),
|
move: api.mysql.move.useMutation(),
|
||||||
delete: api.mysql.remove.useMutation(),
|
delete: api.mysql.remove.useMutation(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mariadbActions = {
|
const mariadbActions = {
|
||||||
|
start: api.mariadb.start.useMutation(),
|
||||||
|
stop: api.mariadb.stop.useMutation(),
|
||||||
move: api.mariadb.move.useMutation(),
|
move: api.mariadb.move.useMutation(),
|
||||||
delete: api.mariadb.remove.useMutation(),
|
delete: api.mariadb.remove.useMutation(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const redisActions = {
|
const redisActions = {
|
||||||
|
start: api.redis.start.useMutation(),
|
||||||
|
stop: api.redis.stop.useMutation(),
|
||||||
move: api.redis.move.useMutation(),
|
move: api.redis.move.useMutation(),
|
||||||
delete: api.redis.remove.useMutation(),
|
delete: api.redis.remove.useMutation(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mongoActions = {
|
const mongoActions = {
|
||||||
|
start: api.mongo.start.useMutation(),
|
||||||
|
stop: api.mongo.stop.useMutation(),
|
||||||
move: api.mongo.move.useMutation(),
|
move: api.mongo.move.useMutation(),
|
||||||
delete: api.mongo.remove.useMutation(),
|
delete: api.mongo.remove.useMutation(),
|
||||||
};
|
};
|
||||||
@@ -348,7 +360,34 @@ const Project = (
|
|||||||
setIsBulkActionLoading(true);
|
setIsBulkActionLoading(true);
|
||||||
for (const serviceId of selectedServices) {
|
for (const serviceId of selectedServices) {
|
||||||
try {
|
try {
|
||||||
await composeActions.start.mutateAsync({ composeId: serviceId });
|
const service = filteredServices.find((s) => s.id === serviceId);
|
||||||
|
if (!service) continue;
|
||||||
|
|
||||||
|
switch (service.type) {
|
||||||
|
case "application":
|
||||||
|
await applicationActions.start.mutateAsync({
|
||||||
|
applicationId: serviceId,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "compose":
|
||||||
|
await composeActions.start.mutateAsync({ composeId: serviceId });
|
||||||
|
break;
|
||||||
|
case "postgres":
|
||||||
|
await postgresActions.start.mutateAsync({ postgresId: serviceId });
|
||||||
|
break;
|
||||||
|
case "mysql":
|
||||||
|
await mysqlActions.start.mutateAsync({ mysqlId: serviceId });
|
||||||
|
break;
|
||||||
|
case "mariadb":
|
||||||
|
await mariadbActions.start.mutateAsync({ mariadbId: serviceId });
|
||||||
|
break;
|
||||||
|
case "redis":
|
||||||
|
await redisActions.start.mutateAsync({ redisId: serviceId });
|
||||||
|
break;
|
||||||
|
case "mongo":
|
||||||
|
await mongoActions.start.mutateAsync({ mongoId: serviceId });
|
||||||
|
break;
|
||||||
|
}
|
||||||
success++;
|
success++;
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
toast.error(`Error starting service ${serviceId}`);
|
toast.error(`Error starting service ${serviceId}`);
|
||||||
@@ -368,7 +407,34 @@ const Project = (
|
|||||||
setIsBulkActionLoading(true);
|
setIsBulkActionLoading(true);
|
||||||
for (const serviceId of selectedServices) {
|
for (const serviceId of selectedServices) {
|
||||||
try {
|
try {
|
||||||
await composeActions.stop.mutateAsync({ composeId: serviceId });
|
const service = filteredServices.find((s) => s.id === serviceId);
|
||||||
|
if (!service) continue;
|
||||||
|
|
||||||
|
switch (service.type) {
|
||||||
|
case "application":
|
||||||
|
await applicationActions.stop.mutateAsync({
|
||||||
|
applicationId: serviceId,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "compose":
|
||||||
|
await composeActions.stop.mutateAsync({ composeId: serviceId });
|
||||||
|
break;
|
||||||
|
case "postgres":
|
||||||
|
await postgresActions.stop.mutateAsync({ postgresId: serviceId });
|
||||||
|
break;
|
||||||
|
case "mysql":
|
||||||
|
await mysqlActions.stop.mutateAsync({ mysqlId: serviceId });
|
||||||
|
break;
|
||||||
|
case "mariadb":
|
||||||
|
await mariadbActions.stop.mutateAsync({ mariadbId: serviceId });
|
||||||
|
break;
|
||||||
|
case "redis":
|
||||||
|
await redisActions.stop.mutateAsync({ redisId: serviceId });
|
||||||
|
break;
|
||||||
|
case "mongo":
|
||||||
|
await mongoActions.stop.mutateAsync({ mongoId: serviceId });
|
||||||
|
break;
|
||||||
|
}
|
||||||
success++;
|
success++;
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
toast.error(`Error stopping service ${serviceId}`);
|
toast.error(`Error stopping service ${serviceId}`);
|
||||||
|
|||||||
@@ -1,219 +0,0 @@
|
|||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
|
||||||
|
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { DialogFooter } from "@/components/ui/dialog";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { appRouter } from "@/server/api/root";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { validateRequest } from "@dokploy/server";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
|
||||||
import { Settings } from "lucide-react";
|
|
||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
import { type ReactElement, useEffect } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import superjson from "superjson";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const settings = z.object({
|
|
||||||
cleanCacheOnApplications: z.boolean(),
|
|
||||||
cleanCacheOnCompose: z.boolean(),
|
|
||||||
cleanCacheOnPreviews: z.boolean(),
|
|
||||||
});
|
|
||||||
|
|
||||||
type SettingsType = z.infer<typeof settings>;
|
|
||||||
|
|
||||||
const Page = () => {
|
|
||||||
const { data, refetch } = api.user.get.useQuery();
|
|
||||||
const { mutateAsync, isLoading, isError, error } =
|
|
||||||
api.user.update.useMutation();
|
|
||||||
const form = useForm<SettingsType>({
|
|
||||||
defaultValues: {
|
|
||||||
cleanCacheOnApplications: false,
|
|
||||||
cleanCacheOnCompose: false,
|
|
||||||
cleanCacheOnPreviews: false,
|
|
||||||
},
|
|
||||||
resolver: zodResolver(settings),
|
|
||||||
});
|
|
||||||
useEffect(() => {
|
|
||||||
form.reset({
|
|
||||||
cleanCacheOnApplications: data?.user.cleanupCacheApplications || false,
|
|
||||||
cleanCacheOnCompose: data?.user.cleanupCacheOnCompose || false,
|
|
||||||
cleanCacheOnPreviews: data?.user.cleanupCacheOnPreviews || false,
|
|
||||||
});
|
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
|
|
||||||
|
|
||||||
const onSubmit = async (values: SettingsType) => {
|
|
||||||
await mutateAsync({
|
|
||||||
cleanupCacheApplications: values.cleanCacheOnApplications,
|
|
||||||
cleanupCacheOnCompose: values.cleanCacheOnCompose,
|
|
||||||
cleanupCacheOnPreviews: values.cleanCacheOnPreviews,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Settings updated");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Something went wrong");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div className="w-full">
|
|
||||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
|
||||||
<div className="rounded-xl bg-background shadow-md ">
|
|
||||||
<CardHeader className="">
|
|
||||||
<CardTitle className="text-xl flex flex-row gap-2">
|
|
||||||
<Settings className="size-6 text-muted-foreground self-center" />
|
|
||||||
Settings
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>Manage your Dokploy settings</CardDescription>
|
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-2 py-8 border-t">
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
id="hook-form-add-security"
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
className="grid w-full gap-2"
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="cleanCacheOnApplications"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>Clean Cache on Applications</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Clean the cache after every application deployment
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="cleanCacheOnPreviews"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>Clean Cache on Previews</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Clean the cache after every preview deployment
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="cleanCacheOnCompose"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>Clean Cache on Compose</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Clean the cache after every compose deployment
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
isLoading={isLoading}
|
|
||||||
form="hook-form-add-security"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</CardContent>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Page;
|
|
||||||
|
|
||||||
Page.getLayout = (page: ReactElement) => {
|
|
||||||
return <DashboardLayout metaName="Server">{page}</DashboardLayout>;
|
|
||||||
};
|
|
||||||
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(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
1
apps/dokploy/public/locales/nl/common.json
Normal file
1
apps/dokploy/public/locales/nl/common.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
58
apps/dokploy/public/locales/nl/settings.json
Normal file
58
apps/dokploy/public/locales/nl/settings.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
findApplicationById,
|
findApplicationById,
|
||||||
findProjectById,
|
findProjectById,
|
||||||
getApplicationStats,
|
getApplicationStats,
|
||||||
|
mechanizeDockerContainer,
|
||||||
readConfig,
|
readConfig,
|
||||||
readRemoteConfig,
|
readRemoteConfig,
|
||||||
removeDeployments,
|
removeDeployments,
|
||||||
@@ -132,28 +133,36 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
.input(apiReloadApplication)
|
.input(apiReloadApplication)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const application = await findApplicationById(input.applicationId);
|
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({
|
throw new TRPCError({
|
||||||
code: "UNAUTHORIZED",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "You are not authorized to reload this application",
|
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
|
delete: protectedProcedure
|
||||||
|
|||||||
@@ -31,7 +31,10 @@ import {
|
|||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
|
|
||||||
import { findDestinationById } from "@dokploy/server/services/destination";
|
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 {
|
import {
|
||||||
execAsync,
|
execAsync,
|
||||||
execAsyncRemote,
|
execAsyncRemote,
|
||||||
@@ -257,7 +260,7 @@ export const backupRouter = createTRPCRouter({
|
|||||||
const lastSlashIndex = input.search.lastIndexOf("/");
|
const lastSlashIndex = input.search.lastIndexOf("/");
|
||||||
const baseDir =
|
const baseDir =
|
||||||
lastSlashIndex !== -1
|
lastSlashIndex !== -1
|
||||||
? input.search.slice(0, lastSlashIndex + 1)
|
? normalizeS3Path(input.search.slice(0, lastSlashIndex + 1))
|
||||||
: "";
|
: "";
|
||||||
const searchTerm =
|
const searchTerm =
|
||||||
lastSlashIndex !== -1
|
lastSlashIndex !== -1
|
||||||
@@ -270,7 +273,7 @@ export const backupRouter = createTRPCRouter({
|
|||||||
let stdout = "";
|
let stdout = "";
|
||||||
|
|
||||||
if (input.serverId) {
|
if (input.serverId) {
|
||||||
const result = await execAsyncRemote(listCommand, input.serverId);
|
const result = await execAsyncRemote(input.serverId, listCommand);
|
||||||
stdout = result.stdout;
|
stdout = result.stdout;
|
||||||
} else {
|
} else {
|
||||||
const result = await execAsync(listCommand);
|
const result = await execAsync(listCommand);
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
createRegistry,
|
createRegistry,
|
||||||
execAsync,
|
|
||||||
execAsyncRemote,
|
execAsyncRemote,
|
||||||
|
execFileAsync,
|
||||||
findRegistryById,
|
findRegistryById,
|
||||||
removeRegistry,
|
removeRegistry,
|
||||||
updateRegistry,
|
updateRegistry,
|
||||||
@@ -83,7 +83,13 @@ export const registryRouter = createTRPCRouter({
|
|||||||
.input(apiTestRegistry)
|
.input(apiTestRegistry)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
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) {
|
if (IS_CLOUD && !input.serverId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -93,9 +99,14 @@ export const registryRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (input.serverId && input.serverId !== "none") {
|
if (input.serverId && input.serverId !== "none") {
|
||||||
await execAsyncRemote(input.serverId, loginCommand);
|
await execAsyncRemote(
|
||||||
|
input.serverId,
|
||||||
|
`echo ${input.password} | docker ${args.join(" ")}`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
await execAsync(loginCommand);
|
await execFileAsync("docker", args, {
|
||||||
|
input: Buffer.from(input.password).toString(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
letsEncryptEmail: input.letsEncryptEmail,
|
letsEncryptEmail: input.letsEncryptEmail,
|
||||||
}),
|
}),
|
||||||
certificateType: input.certificateType,
|
certificateType: input.certificateType,
|
||||||
|
https: input.https,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { applyWSSHandler } from "@trpc/server/adapters/ws";
|
|||||||
import { WebSocketServer } from "ws";
|
import { WebSocketServer } from "ws";
|
||||||
import { appRouter } from "../api/root";
|
import { appRouter } from "../api/root";
|
||||||
import { createTRPCContext } from "../api/trpc";
|
import { createTRPCContext } from "../api/trpc";
|
||||||
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
|
|
||||||
export const setupDrawerLogsWebSocketServer = (
|
export const setupDrawerLogsWebSocketServer = (
|
||||||
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
||||||
@@ -32,8 +33,13 @@ export const setupDrawerLogsWebSocketServer = (
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Return cleanup function
|
wssTerm.on("connection", async (ws, req) => {
|
||||||
return () => {
|
const _url = new URL(req.url || "", `http://${req.headers.host}`);
|
||||||
wssTerm.close();
|
const { user, session } = await validateRequest(req);
|
||||||
};
|
|
||||||
|
if (!user || !session) {
|
||||||
|
ws.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,28 +27,15 @@ const getWsUrl = () => {
|
|||||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
const host = window.location.host;
|
const host = window.location.host;
|
||||||
|
|
||||||
// Use the base URL for all tRPC WebSocket connections
|
|
||||||
return `${protocol}${host}/drawer-logs`;
|
return `${protocol}${host}/drawer-logs`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Singleton WebSocket client instance
|
const wsClient =
|
||||||
let wsClientInstance: ReturnType<typeof createWSClient> | null = null;
|
typeof window !== "undefined"
|
||||||
|
? createWSClient({
|
||||||
const getWsClient = () => {
|
url: getWsUrl() || "",
|
||||||
if (typeof window === "undefined") return null;
|
})
|
||||||
|
: null;
|
||||||
if (!wsClientInstance) {
|
|
||||||
wsClientInstance = createWSClient({
|
|
||||||
url: getWsUrl() || "",
|
|
||||||
onClose: () => {
|
|
||||||
// Reset the instance when connection closes so it can be recreated
|
|
||||||
wsClientInstance = null;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return wsClientInstance;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** A set of type-safe react-query hooks for your tRPC API. */
|
/** A set of type-safe react-query hooks for your tRPC API. */
|
||||||
export const api = createTRPCNext<AppRouter>({
|
export const api = createTRPCNext<AppRouter>({
|
||||||
@@ -70,7 +57,7 @@ export const api = createTRPCNext<AppRouter>({
|
|||||||
splitLink({
|
splitLink({
|
||||||
condition: (op) => op.type === "subscription",
|
condition: (op) => op.type === "subscription",
|
||||||
true: wsLink({
|
true: wsLink({
|
||||||
client: getWsClient()!,
|
client: wsClient!,
|
||||||
}),
|
}),
|
||||||
false: splitLink({
|
false: splitLink({
|
||||||
condition: (op) => op.input instanceof FormData,
|
condition: (op) => op.input instanceof FormData,
|
||||||
|
|||||||
@@ -36,11 +36,11 @@
|
|||||||
"@ai-sdk/mistral": "^1.0.6",
|
"@ai-sdk/mistral": "^1.0.6",
|
||||||
"@ai-sdk/openai": "^1.0.12",
|
"@ai-sdk/openai": "^1.0.12",
|
||||||
"@ai-sdk/openai-compatible": "^0.0.13",
|
"@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/encoding": "1.1.0",
|
||||||
"@oslojs/crypto": "1.0.1",
|
"@oslojs/crypto": "1.0.1",
|
||||||
"drizzle-dbml-generator": "0.10.0",
|
"drizzle-dbml-generator": "0.10.0",
|
||||||
"better-auth": "1.2.4",
|
"better-auth": "1.2.6",
|
||||||
"@faker-js/faker": "^8.4.1",
|
"@faker-js/faker": "^8.4.1",
|
||||||
"@octokit/auth-app": "^6.0.4",
|
"@octokit/auth-app": "^6.0.4",
|
||||||
"@react-email/components": "^0.0.21",
|
"@react-email/components": "^0.0.21",
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export const users_temp = pgTable("user_temp", {
|
|||||||
// Admin
|
// Admin
|
||||||
serverIp: text("serverIp"),
|
serverIp: text("serverIp"),
|
||||||
certificateType: certificateType("certificateType").notNull().default("none"),
|
certificateType: certificateType("certificateType").notNull().default("none"),
|
||||||
|
https: boolean("https").notNull().default(false),
|
||||||
host: text("host"),
|
host: text("host"),
|
||||||
letsEncryptEmail: text("letsEncryptEmail"),
|
letsEncryptEmail: text("letsEncryptEmail"),
|
||||||
sshPrivateKey: text("sshPrivateKey"),
|
sshPrivateKey: text("sshPrivateKey"),
|
||||||
@@ -202,10 +203,12 @@ export const apiAssignDomain = createSchema
|
|||||||
host: true,
|
host: true,
|
||||||
certificateType: true,
|
certificateType: true,
|
||||||
letsEncryptEmail: true,
|
letsEncryptEmail: true,
|
||||||
|
https: true,
|
||||||
})
|
})
|
||||||
.required()
|
.required()
|
||||||
.partial({
|
.partial({
|
||||||
letsEncryptEmail: true,
|
letsEncryptEmail: true,
|
||||||
|
https: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiUpdateDockerCleanup = createSchema
|
export const apiUpdateDockerCleanup = createSchema
|
||||||
|
|||||||
@@ -106,8 +106,8 @@ export const keepLatestNBackups = async (
|
|||||||
backup.prefix,
|
backup.prefix,
|
||||||
);
|
);
|
||||||
|
|
||||||
// --include "*.sql.gz" ensures nothing else other than the db backup files are touched by rclone
|
// --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 "*.sql.gz" ${backupFilesPath}`;
|
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
|
// 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{}`;
|
const sortAndPickUnwantedBackups = `sort -r | tail -n +$((${backup.keepLatestCount}+1)) | xargs -I{}`;
|
||||||
// this command deletes the files
|
// this command deletes the files
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import path from "node:path";
|
|
||||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||||
import type { Mariadb } from "@dokploy/server/services/mariadb";
|
import type { Mariadb } from "@dokploy/server/services/mariadb";
|
||||||
import { findProjectById } from "@dokploy/server/services/project";
|
import { findProjectById } from "@dokploy/server/services/project";
|
||||||
@@ -8,7 +7,7 @@ import {
|
|||||||
} from "../docker/utils";
|
} from "../docker/utils";
|
||||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||||
import { getS3Credentials } from "./utils";
|
import { getS3Credentials, normalizeS3Path } from "./utils";
|
||||||
|
|
||||||
export const runMariadbBackup = async (
|
export const runMariadbBackup = async (
|
||||||
mariadb: Mariadb,
|
mariadb: Mariadb,
|
||||||
@@ -19,7 +18,7 @@ export const runMariadbBackup = async (
|
|||||||
const { prefix, database } = backup;
|
const { prefix, database } = backup;
|
||||||
const destination = backup.destination;
|
const destination = backup.destination;
|
||||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||||
const bucketDestination = path.join(prefix, backupFileName);
|
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const rcloneFlags = getS3Credentials(destination);
|
const rcloneFlags = getS3Credentials(destination);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import path from "node:path";
|
|
||||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||||
import type { Mongo } from "@dokploy/server/services/mongo";
|
import type { Mongo } from "@dokploy/server/services/mongo";
|
||||||
import { findProjectById } from "@dokploy/server/services/project";
|
import { findProjectById } from "@dokploy/server/services/project";
|
||||||
@@ -8,7 +7,7 @@ import {
|
|||||||
} from "../docker/utils";
|
} from "../docker/utils";
|
||||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
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
|
// mongodb://mongo:Bqh7AQl-PRbnBu@localhost:27017/?tls=false&directConnection=true
|
||||||
export const runMongoBackup = async (mongo: Mongo, backup: BackupSchedule) => {
|
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 { prefix, database } = backup;
|
||||||
const destination = backup.destination;
|
const destination = backup.destination;
|
||||||
const backupFileName = `${new Date().toISOString()}.dump.gz`;
|
const backupFileName = `${new Date().toISOString()}.dump.gz`;
|
||||||
const bucketDestination = path.join(prefix, backupFileName);
|
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const rcloneFlags = getS3Credentials(destination);
|
const rcloneFlags = getS3Credentials(destination);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import path from "node:path";
|
|
||||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||||
import type { MySql } from "@dokploy/server/services/mysql";
|
import type { MySql } from "@dokploy/server/services/mysql";
|
||||||
import { findProjectById } from "@dokploy/server/services/project";
|
import { findProjectById } from "@dokploy/server/services/project";
|
||||||
@@ -8,7 +7,7 @@ import {
|
|||||||
} from "../docker/utils";
|
} from "../docker/utils";
|
||||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||||
import { getS3Credentials } from "./utils";
|
import { getS3Credentials, normalizeS3Path } from "./utils";
|
||||||
|
|
||||||
export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
|
export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
|
||||||
const { appName, databaseRootPassword, projectId, name } = mysql;
|
const { appName, databaseRootPassword, projectId, name } = mysql;
|
||||||
@@ -16,7 +15,7 @@ export const runMySqlBackup = async (mysql: MySql, backup: BackupSchedule) => {
|
|||||||
const { prefix, database } = backup;
|
const { prefix, database } = backup;
|
||||||
const destination = backup.destination;
|
const destination = backup.destination;
|
||||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||||
const bucketDestination = path.join(prefix, backupFileName);
|
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const rcloneFlags = getS3Credentials(destination);
|
const rcloneFlags = getS3Credentials(destination);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import path from "node:path";
|
|
||||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||||
import type { Postgres } from "@dokploy/server/services/postgres";
|
import type { Postgres } from "@dokploy/server/services/postgres";
|
||||||
import { findProjectById } from "@dokploy/server/services/project";
|
import { findProjectById } from "@dokploy/server/services/project";
|
||||||
@@ -8,7 +7,7 @@ import {
|
|||||||
} from "../docker/utils";
|
} from "../docker/utils";
|
||||||
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
import { sendDatabaseBackupNotifications } from "../notifications/database-backup";
|
||||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||||
import { getS3Credentials } from "./utils";
|
import { getS3Credentials, normalizeS3Path } from "./utils";
|
||||||
|
|
||||||
export const runPostgresBackup = async (
|
export const runPostgresBackup = async (
|
||||||
postgres: Postgres,
|
postgres: Postgres,
|
||||||
@@ -20,7 +19,7 @@ export const runPostgresBackup = async (
|
|||||||
const { prefix, database } = backup;
|
const { prefix, database } = backup;
|
||||||
const destination = backup.destination;
|
const destination = backup.destination;
|
||||||
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
const backupFileName = `${new Date().toISOString()}.sql.gz`;
|
||||||
const bucketDestination = path.join(prefix, backupFileName);
|
const bucketDestination = `${normalizeS3Path(prefix)}${backupFileName}`;
|
||||||
try {
|
try {
|
||||||
const rcloneFlags = getS3Credentials(destination);
|
const rcloneFlags = getS3Credentials(destination);
|
||||||
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
|
const rcloneDestination = `:s3:${destination.bucket}/${bucketDestination}`;
|
||||||
|
|||||||
@@ -36,6 +36,13 @@ export const removeScheduleBackup = (backupId: string) => {
|
|||||||
currentJob?.cancel();
|
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) => {
|
export const getS3Credentials = (destination: Destination) => {
|
||||||
const { accessKey, secretAccessKey, region, endpoint, provider } =
|
const { accessKey, secretAccessKey, region, endpoint, provider } =
|
||||||
destination;
|
destination;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||||
import { execAsync } from "../process/execAsync";
|
import { execAsync } from "../process/execAsync";
|
||||||
import { getS3Credentials } from "./utils";
|
import { getS3Credentials, normalizeS3Path } from "./utils";
|
||||||
import { findDestinationById } from "@dokploy/server/services/destination";
|
import { findDestinationById } from "@dokploy/server/services/destination";
|
||||||
import { IS_CLOUD, paths } from "@dokploy/server/constants";
|
import { IS_CLOUD, paths } from "@dokploy/server/constants";
|
||||||
import { mkdtemp } from "node:fs/promises";
|
import { mkdtemp } from "node:fs/promises";
|
||||||
@@ -18,7 +18,7 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
|
|||||||
const { BASE_PATH } = paths();
|
const { BASE_PATH } = paths();
|
||||||
const tempDir = await mkdtemp(join(tmpdir(), "dokploy-backup-"));
|
const tempDir = await mkdtemp(join(tmpdir(), "dokploy-backup-"));
|
||||||
const backupFileName = `webserver-backup-${timestamp}.zip`;
|
const backupFileName = `webserver-backup-${timestamp}.zip`;
|
||||||
const s3Path = `:s3:${destination.bucket}/${backup.prefix}${backupFileName}`;
|
const s3Path = `:s3:${destination.bucket}/${normalizeS3Path(backup.prefix)}${backupFileName}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await execAsync(`mkdir -p ${tempDir}/filesystem`);
|
await execAsync(`mkdir -p ${tempDir}/filesystem`);
|
||||||
@@ -29,7 +29,7 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
|
|||||||
await execAsync(`cp -r ${BASE_PATH}/* ${tempDir}/filesystem/`);
|
await execAsync(`cp -r ${BASE_PATH}/* ${tempDir}/filesystem/`);
|
||||||
|
|
||||||
await execAsync(
|
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}"`;
|
const uploadCommand = `rclone copyto ${rcloneFlags.join(" ")} "${tempDir}/${backupFileName}" "${s3Path}"`;
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export const buildRailpack = async (
|
|||||||
for (const envVar of envVariables) {
|
for (const envVar of envVariables) {
|
||||||
const [key, value] = envVar.split("=");
|
const [key, value] = envVar.split("=");
|
||||||
if (key && value) {
|
if (key && value) {
|
||||||
buildArgs.push("--secret", `id=${key},env=${key}`);
|
buildArgs.push("--secret", `id=${key},env='${key}'`);
|
||||||
env[key] = value;
|
env[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,7 +132,7 @@ export const getRailpackCommand = (
|
|||||||
];
|
];
|
||||||
|
|
||||||
for (const env of envVariables) {
|
for (const env of envVariables) {
|
||||||
prepareArgs.push("--env", env);
|
prepareArgs.push("--env", `'${env}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate secrets hash for layer invalidation
|
// Calculate secrets hash for layer invalidation
|
||||||
@@ -164,7 +164,7 @@ export const getRailpackCommand = (
|
|||||||
for (const envVar of envVariables) {
|
for (const envVar of envVariables) {
|
||||||
const [key, value] = envVar.split("=");
|
const [key, value] = envVar.split("=");
|
||||||
if (key && value) {
|
if (key && value) {
|
||||||
buildArgs.push("--secret", `id=${key},env=${key}`);
|
buildArgs.push("--secret", `id=${key},env='${key}'`);
|
||||||
exportEnvs.push(`export ${key}=${value}`);
|
exportEnvs.push(`export ${key}=${value}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,12 @@ export const buildStatic = async (
|
|||||||
].join("\n"),
|
].join("\n"),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
createFile(
|
||||||
|
buildAppDirectory,
|
||||||
|
".dockerignore",
|
||||||
|
[".git", ".env", "Dockerfile", ".dockerignore"].join("\n"),
|
||||||
|
);
|
||||||
|
|
||||||
await buildCustomDocker(
|
await buildCustomDocker(
|
||||||
{
|
{
|
||||||
...application,
|
...application,
|
||||||
|
|||||||
@@ -249,6 +249,11 @@ export const addDomainToCompose = async (
|
|||||||
labels.unshift("traefik.enable=true");
|
labels.unshift("traefik.enable=true");
|
||||||
}
|
}
|
||||||
labels.unshift(...httpLabels);
|
labels.unshift(...httpLabels);
|
||||||
|
if (!compose.isolatedDeployment) {
|
||||||
|
if (!labels.includes("traefik.docker.network=dokploy-network")) {
|
||||||
|
labels.unshift("traefik.docker.network=dokploy-network");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!compose.isolatedDeployment) {
|
if (!compose.isolatedDeployment) {
|
||||||
|
|||||||
@@ -1,9 +1,48 @@
|
|||||||
import { exec } from "node:child_process";
|
import { exec, execFile } from "node:child_process";
|
||||||
import util from "node:util";
|
import util from "node:util";
|
||||||
import { findServerById } from "@dokploy/server/services/server";
|
import { findServerById } from "@dokploy/server/services/server";
|
||||||
import { Client } from "ssh2";
|
import { Client } from "ssh2";
|
||||||
|
|
||||||
export const execAsync = util.promisify(exec);
|
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 (
|
export const execAsyncRemote = async (
|
||||||
serverId: string | null,
|
serverId: string | null,
|
||||||
command: string,
|
command: string,
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const restoreWebServerBackup = async (
|
|||||||
|
|
||||||
// Extract backup
|
// Extract backup
|
||||||
emit("Extracting backup...");
|
emit("Extracting backup...");
|
||||||
await execAsync(`cd ${tempDir} && unzip ${backupFile}`);
|
await execAsync(`cd ${tempDir} && unzip ${backupFile} > /dev/null 2>&1`);
|
||||||
|
|
||||||
// Restore filesystem first
|
// Restore filesystem first
|
||||||
emit("Restoring filesystem...");
|
emit("Restoring filesystem...");
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ import { join } from "node:path";
|
|||||||
import { paths } from "@dokploy/server/constants";
|
import { paths } from "@dokploy/server/constants";
|
||||||
import type { User } from "@dokploy/server/services/user";
|
import type { User } from "@dokploy/server/services/user";
|
||||||
import { dump, load } from "js-yaml";
|
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 { FileConfig } from "./file-types";
|
||||||
import type { MainTraefikConfig } from "./types";
|
import type { MainTraefikConfig } from "./types";
|
||||||
|
|
||||||
@@ -11,32 +15,62 @@ export const updateServerTraefik = (
|
|||||||
user: User | null,
|
user: User | null,
|
||||||
newHost: string | null,
|
newHost: string | null,
|
||||||
) => {
|
) => {
|
||||||
|
const { https, certificateType } = user || {};
|
||||||
const appName = "dokploy";
|
const appName = "dokploy";
|
||||||
const config: FileConfig = loadOrCreateConfig(appName);
|
const config: FileConfig = loadOrCreateConfig(appName);
|
||||||
|
|
||||||
config.http = config.http || { routers: {}, services: {} };
|
config.http = config.http || { routers: {}, services: {} };
|
||||||
config.http.routers = config.http.routers || {};
|
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) {
|
config.http.services = {
|
||||||
currentRouterConfig.rule = `Host(\`${newHost}\`)`;
|
...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`] = {
|
config.http.routers[`${appName}-router-app-secure`] = {
|
||||||
...currentRouterConfig,
|
rule: `Host(\`${newHost}\`)`,
|
||||||
|
service: `${appName}-service-app`,
|
||||||
entryPoints: ["websecure"],
|
entryPoints: ["websecure"],
|
||||||
tls: { certResolver: "letsencrypt" },
|
tls: { certResolver: "letsencrypt" },
|
||||||
};
|
};
|
||||||
|
|
||||||
currentRouterConfig.middlewares = ["redirect-to-https"];
|
|
||||||
} else {
|
} else {
|
||||||
delete config.http.routers[`${appName}-router-app-secure`];
|
config.http.routers[`${appName}-router-app-secure`] = {
|
||||||
currentRouterConfig.middlewares = [];
|
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) => {
|
export const updateLetsEncryptEmail = (newEmail: string | null) => {
|
||||||
|
|||||||
88
pnpm-lock.yaml
generated
88
pnpm-lock.yaml
generated
@@ -17,7 +17,7 @@ importers:
|
|||||||
version: 1.9.4
|
version: 1.9.4
|
||||||
'@commitlint/cli':
|
'@commitlint/cli':
|
||||||
specifier: ^19.3.0
|
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':
|
'@commitlint/config-conventional':
|
||||||
specifier: ^19.2.2
|
specifier: ^19.2.2
|
||||||
version: 19.2.2
|
version: 19.2.2
|
||||||
@@ -266,8 +266,8 @@ importers:
|
|||||||
specifier: 5.1.1
|
specifier: 5.1.1
|
||||||
version: 5.1.1(encoding@0.1.13)
|
version: 5.1.1(encoding@0.1.13)
|
||||||
better-auth:
|
better-auth:
|
||||||
specifier: 1.2.4
|
specifier: 1.2.6
|
||||||
version: 1.2.4(typescript@5.5.3)
|
version: 1.2.6
|
||||||
bl:
|
bl:
|
||||||
specifier: 6.0.11
|
specifier: 6.0.11
|
||||||
version: 6.0.11
|
version: 6.0.11
|
||||||
@@ -607,8 +607,8 @@ importers:
|
|||||||
specifier: ^0.0.13
|
specifier: ^0.0.13
|
||||||
version: 0.0.13(zod@3.23.8)
|
version: 0.0.13(zod@3.23.8)
|
||||||
'@better-auth/utils':
|
'@better-auth/utils':
|
||||||
specifier: 0.2.3
|
specifier: 0.2.4
|
||||||
version: 0.2.3
|
version: 0.2.4
|
||||||
'@faker-js/faker':
|
'@faker-js/faker':
|
||||||
specifier: ^8.4.1
|
specifier: ^8.4.1
|
||||||
version: 8.4.1
|
version: 8.4.1
|
||||||
@@ -640,8 +640,8 @@ importers:
|
|||||||
specifier: 5.1.1
|
specifier: 5.1.1
|
||||||
version: 5.1.1(encoding@0.1.13)
|
version: 5.1.1(encoding@0.1.13)
|
||||||
better-auth:
|
better-auth:
|
||||||
specifier: 1.2.4
|
specifier: 1.2.6
|
||||||
version: 1.2.4(typescript@5.5.3)
|
version: 1.2.6
|
||||||
bl:
|
bl:
|
||||||
specifier: 6.0.11
|
specifier: 6.0.11
|
||||||
version: 6.0.11
|
version: 6.0.11
|
||||||
@@ -924,11 +924,11 @@ packages:
|
|||||||
'@balena/dockerignore@1.0.2':
|
'@balena/dockerignore@1.0.2':
|
||||||
resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==}
|
resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==}
|
||||||
|
|
||||||
'@better-auth/utils@0.2.3':
|
'@better-auth/utils@0.2.4':
|
||||||
resolution: {integrity: sha512-Ap1GaSmo6JYhJhxJOpUB0HobkKPTNzfta+bLV89HfpyCAHN7p8ntCrmNFHNAVD0F6v0mywFVEUg1FUhNCc81Rw==}
|
resolution: {integrity: sha512-ayiX87Xd5sCHEplAdeMgwkA0FgnXsEZBgDn890XHHwSWNqqRZDYOq3uj2Ei2leTv1I2KbG5HHn60Ah1i2JWZjQ==}
|
||||||
|
|
||||||
'@better-fetch/fetch@1.1.15':
|
'@better-fetch/fetch@1.1.18':
|
||||||
resolution: {integrity: sha512-0Bl8YYj1f8qCTNHeSn5+1DWv2hy7rLBrQ8rS8Y9XYloiwZEfc3k4yspIG0llRxafxqhGCwlGRg+F8q1HZRCMXA==}
|
resolution: {integrity: sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==}
|
||||||
|
|
||||||
'@biomejs/biome@1.9.4':
|
'@biomejs/biome@1.9.4':
|
||||||
resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==}
|
resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==}
|
||||||
@@ -3836,11 +3836,11 @@ packages:
|
|||||||
before-after-hook@2.2.3:
|
before-after-hook@2.2.3:
|
||||||
resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==}
|
resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==}
|
||||||
|
|
||||||
better-auth@1.2.4:
|
better-auth@1.2.6:
|
||||||
resolution: {integrity: sha512-/ZK2jbUjm8JwdeCLFrUWUBmexPyI9PkaLVXWLWtN60sMDHTY8B5G72wcHglo1QMFBaw4G0qFkP5ayl9k6XfDaA==}
|
resolution: {integrity: sha512-RVy6nfNCXpohx49zP2ChUO3zN0nvz5UXuETJIhWU+dshBKpFMk4P4hAQauM3xqTJdd9hfeB5y+segmG1oYGTJQ==}
|
||||||
|
|
||||||
better-call@1.0.3:
|
better-call@1.0.7:
|
||||||
resolution: {integrity: sha512-DUKImKoDIy5UtCvQbHTg0wuBRse6gu1Yvznn7+1B3I5TeY8sclRPFce0HI+4WF2bcb+9PqmkET8nXZubrHQh9A==}
|
resolution: {integrity: sha512-p5kEthErx3HsW9dCCvvEx+uuEdncn0ZrlqrOG3TkR1aVYgynpwYbTVU90nY8/UwfMhROzqZWs8vryainSQxrNg==}
|
||||||
|
|
||||||
binary-extensions@2.3.0:
|
binary-extensions@2.3.0:
|
||||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||||
@@ -7093,8 +7093,8 @@ packages:
|
|||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
typescript@5.7.2:
|
typescript@5.8.3:
|
||||||
resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==}
|
resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@@ -7210,14 +7210,6 @@ packages:
|
|||||||
v8-compile-cache-lib@3.0.1:
|
v8-compile-cache-lib@3.0.1:
|
||||||
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
|
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:
|
vfile-message@4.0.2:
|
||||||
resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==}
|
resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==}
|
||||||
|
|
||||||
@@ -7567,11 +7559,12 @@ snapshots:
|
|||||||
|
|
||||||
'@balena/dockerignore@1.0.2': {}
|
'@balena/dockerignore@1.0.2': {}
|
||||||
|
|
||||||
'@better-auth/utils@0.2.3':
|
'@better-auth/utils@0.2.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
|
typescript: 5.8.3
|
||||||
uncrypto: 0.1.3
|
uncrypto: 0.1.3
|
||||||
|
|
||||||
'@better-fetch/fetch@1.1.15': {}
|
'@better-fetch/fetch@1.1.18': {}
|
||||||
|
|
||||||
'@biomejs/biome@1.9.4':
|
'@biomejs/biome@1.9.4':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@@ -7678,11 +7671,11 @@ snapshots:
|
|||||||
style-mod: 4.1.2
|
style-mod: 4.1.2
|
||||||
w3c-keyname: 2.2.8
|
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:
|
dependencies:
|
||||||
'@commitlint/format': 19.3.0
|
'@commitlint/format': 19.3.0
|
||||||
'@commitlint/lint': 19.2.2
|
'@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/read': 19.2.1
|
||||||
'@commitlint/types': 19.0.3
|
'@commitlint/types': 19.0.3
|
||||||
execa: 8.0.1
|
execa: 8.0.1
|
||||||
@@ -7729,15 +7722,15 @@ snapshots:
|
|||||||
'@commitlint/rules': 19.0.3
|
'@commitlint/rules': 19.0.3
|
||||||
'@commitlint/types': 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:
|
dependencies:
|
||||||
'@commitlint/config-validator': 19.0.3
|
'@commitlint/config-validator': 19.0.3
|
||||||
'@commitlint/execute-rule': 19.0.0
|
'@commitlint/execute-rule': 19.0.0
|
||||||
'@commitlint/resolve-extends': 19.1.0
|
'@commitlint/resolve-extends': 19.1.0
|
||||||
'@commitlint/types': 19.0.3
|
'@commitlint/types': 19.0.3
|
||||||
chalk: 5.3.0
|
chalk: 5.3.0
|
||||||
cosmiconfig: 9.0.0(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.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)
|
||||||
lodash.isplainobject: 4.0.6
|
lodash.isplainobject: 4.0.6
|
||||||
lodash.merge: 4.6.2
|
lodash.merge: 4.6.2
|
||||||
lodash.uniq: 4.5.0
|
lodash.uniq: 4.5.0
|
||||||
@@ -10547,27 +10540,24 @@ snapshots:
|
|||||||
|
|
||||||
before-after-hook@2.2.3: {}
|
before-after-hook@2.2.3: {}
|
||||||
|
|
||||||
better-auth@1.2.4(typescript@5.5.3):
|
better-auth@1.2.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@better-auth/utils': 0.2.3
|
'@better-auth/utils': 0.2.4
|
||||||
'@better-fetch/fetch': 1.1.15
|
'@better-fetch/fetch': 1.1.18
|
||||||
'@noble/ciphers': 0.6.0
|
'@noble/ciphers': 0.6.0
|
||||||
'@noble/hashes': 1.7.1
|
'@noble/hashes': 1.7.1
|
||||||
'@simplewebauthn/browser': 13.1.0
|
'@simplewebauthn/browser': 13.1.0
|
||||||
'@simplewebauthn/server': 13.1.1
|
'@simplewebauthn/server': 13.1.1
|
||||||
better-call: 1.0.3
|
better-call: 1.0.7
|
||||||
defu: 6.1.4
|
defu: 6.1.4
|
||||||
jose: 5.9.6
|
jose: 5.9.6
|
||||||
kysely: 0.27.6
|
kysely: 0.27.6
|
||||||
nanostores: 0.11.3
|
nanostores: 0.11.3
|
||||||
valibot: 1.0.0-beta.15(typescript@5.5.3)
|
|
||||||
zod: 3.24.1
|
zod: 3.24.1
|
||||||
transitivePeerDependencies:
|
|
||||||
- typescript
|
|
||||||
|
|
||||||
better-call@1.0.3:
|
better-call@1.0.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@better-fetch/fetch': 1.1.15
|
'@better-fetch/fetch': 1.1.18
|
||||||
rou3: 0.5.1
|
rou3: 0.5.1
|
||||||
set-cookie-parser: 2.7.1
|
set-cookie-parser: 2.7.1
|
||||||
uncrypto: 0.1.3
|
uncrypto: 0.1.3
|
||||||
@@ -10942,21 +10932,21 @@ snapshots:
|
|||||||
|
|
||||||
core-js@3.39.0: {}
|
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:
|
dependencies:
|
||||||
'@types/node': 18.19.42
|
'@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
|
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:
|
dependencies:
|
||||||
env-paths: 2.2.1
|
env-paths: 2.2.1
|
||||||
import-fresh: 3.3.0
|
import-fresh: 3.3.0
|
||||||
js-yaml: 4.1.0
|
js-yaml: 4.1.0
|
||||||
parse-json: 5.2.0
|
parse-json: 5.2.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.7.2
|
typescript: 5.8.3
|
||||||
|
|
||||||
cpu-features@0.0.10:
|
cpu-features@0.0.10:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -14171,7 +14161,7 @@ snapshots:
|
|||||||
|
|
||||||
typescript@5.5.3: {}
|
typescript@5.5.3: {}
|
||||||
|
|
||||||
typescript@5.7.2: {}
|
typescript@5.8.3: {}
|
||||||
|
|
||||||
ufo@1.5.4: {}
|
ufo@1.5.4: {}
|
||||||
|
|
||||||
@@ -14292,10 +14282,6 @@ snapshots:
|
|||||||
v8-compile-cache-lib@3.0.1:
|
v8-compile-cache-lib@3.0.1:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
valibot@1.0.0-beta.15(typescript@5.5.3):
|
|
||||||
optionalDependencies:
|
|
||||||
typescript: 5.5.3
|
|
||||||
|
|
||||||
vfile-message@4.0.2:
|
vfile-message@4.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
|
|||||||
Reference in New Issue
Block a user