Merge branch 'canary' into fix/docker-terminal-dropdown-containers

This commit is contained in:
Mauricio Siu
2025-10-05 00:43:48 -06:00
58 changed files with 27589 additions and 345 deletions

View File

@@ -133,6 +133,7 @@ const baseApp: ApplicationNested = {
username: null, username: null,
dockerContextPath: null, dockerContextPath: null,
rollbackActive: false, rollbackActive: false,
stopGracePeriodSwarm: null,
}; };
describe("unzipDrop using real zip files", () => { describe("unzipDrop using real zip files", () => {

View File

@@ -0,0 +1,102 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ApplicationNested } from "@dokploy/server/utils/builders";
import { mechanizeDockerContainer } from "@dokploy/server/utils/builders";
type MockCreateServiceOptions = {
StopGracePeriod?: number;
[key: string]: unknown;
};
const { inspectMock, getServiceMock, createServiceMock, getRemoteDockerMock } =
vi.hoisted(() => {
const inspect = vi.fn<[], Promise<never>>();
const getService = vi.fn(() => ({ inspect }));
const createService = vi.fn<[MockCreateServiceOptions], Promise<void>>(
async () => undefined,
);
const getRemoteDocker = vi.fn(async () => ({
getService,
createService,
}));
return {
inspectMock: inspect,
getServiceMock: getService,
createServiceMock: createService,
getRemoteDockerMock: getRemoteDocker,
};
});
vi.mock("@dokploy/server/utils/servers/remote-docker", () => ({
getRemoteDocker: getRemoteDockerMock,
}));
const createApplication = (
overrides: Partial<ApplicationNested> = {},
): ApplicationNested =>
({
appName: "test-app",
buildType: "dockerfile",
env: null,
mounts: [],
cpuLimit: null,
memoryLimit: null,
memoryReservation: null,
cpuReservation: null,
command: null,
ports: [],
sourceType: "docker",
dockerImage: "example:latest",
registry: null,
environment: {
project: { env: null },
env: null,
},
replicas: 1,
stopGracePeriodSwarm: 0n,
serverId: "server-id",
...overrides,
}) as unknown as ApplicationNested;
describe("mechanizeDockerContainer", () => {
beforeEach(() => {
inspectMock.mockReset();
inspectMock.mockRejectedValue(new Error("service not found"));
getServiceMock.mockClear();
createServiceMock.mockClear();
getRemoteDockerMock.mockClear();
getRemoteDockerMock.mockResolvedValue({
getService: getServiceMock,
createService: createServiceMock,
});
});
it("converts bigint stopGracePeriodSwarm to a number and keeps zero values", async () => {
const application = createApplication({ stopGracePeriodSwarm: 0n });
await mechanizeDockerContainer(application);
expect(createServiceMock).toHaveBeenCalledTimes(1);
const call = createServiceMock.mock.calls[0];
if (!call) {
throw new Error("createServiceMock should have been called once");
}
const [settings] = call;
expect(settings.StopGracePeriod).toBe(0);
expect(typeof settings.StopGracePeriod).toBe("number");
});
it("omits StopGracePeriod when stopGracePeriodSwarm is null", async () => {
const application = createApplication({ stopGracePeriodSwarm: null });
await mechanizeDockerContainer(application);
expect(createServiceMock).toHaveBeenCalledTimes(1);
const call = createServiceMock.mock.calls[0];
if (!call) {
throw new Error("createServiceMock should have been called once");
}
const [settings] = call;
expect(settings).not.toHaveProperty("StopGracePeriod");
});
});

View File

@@ -111,6 +111,7 @@ const baseApp: ApplicationNested = {
updateConfigSwarm: null, updateConfigSwarm: null,
username: null, username: null,
dockerContextPath: null, dockerContextPath: null,
stopGracePeriodSwarm: null,
}; };
const baseDomain: Domain = { const baseDomain: Domain = {

View File

@@ -25,6 +25,7 @@ import {
FormLabel, FormLabel,
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -176,10 +177,18 @@ const addSwarmSettings = z.object({
modeSwarm: createStringToJSONSchema(ServiceModeSwarmSchema).nullable(), modeSwarm: createStringToJSONSchema(ServiceModeSwarmSchema).nullable(),
labelsSwarm: createStringToJSONSchema(LabelsSwarmSchema).nullable(), labelsSwarm: createStringToJSONSchema(LabelsSwarmSchema).nullable(),
networkSwarm: createStringToJSONSchema(NetworkSwarmSchema).nullable(), networkSwarm: createStringToJSONSchema(NetworkSwarmSchema).nullable(),
stopGracePeriodSwarm: z.bigint().nullable(),
}); });
type AddSwarmSettings = z.infer<typeof addSwarmSettings>; type AddSwarmSettings = z.infer<typeof addSwarmSettings>;
const hasStopGracePeriodSwarm = (
value: unknown,
): value is { stopGracePeriodSwarm: bigint | number | string | null } =>
typeof value === "object" &&
value !== null &&
"stopGracePeriodSwarm" in value;
interface Props { interface Props {
id: string; id: string;
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application"; type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
@@ -224,12 +233,22 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
modeSwarm: null, modeSwarm: null,
labelsSwarm: null, labelsSwarm: null,
networkSwarm: null, networkSwarm: null,
stopGracePeriodSwarm: null,
}, },
resolver: zodResolver(addSwarmSettings), resolver: zodResolver(addSwarmSettings),
}); });
useEffect(() => { useEffect(() => {
if (data) { if (data) {
const stopGracePeriodValue = hasStopGracePeriodSwarm(data)
? data.stopGracePeriodSwarm
: null;
const normalizedStopGracePeriod =
stopGracePeriodValue === null || stopGracePeriodValue === undefined
? null
: typeof stopGracePeriodValue === "bigint"
? stopGracePeriodValue
: BigInt(stopGracePeriodValue);
form.reset({ form.reset({
healthCheckSwarm: data.healthCheckSwarm healthCheckSwarm: data.healthCheckSwarm
? JSON.stringify(data.healthCheckSwarm, null, 2) ? JSON.stringify(data.healthCheckSwarm, null, 2)
@@ -255,6 +274,7 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
networkSwarm: data.networkSwarm networkSwarm: data.networkSwarm
? JSON.stringify(data.networkSwarm, null, 2) ? JSON.stringify(data.networkSwarm, null, 2)
: null, : null,
stopGracePeriodSwarm: normalizedStopGracePeriod,
}); });
} }
}, [form, form.reset, data]); }, [form, form.reset, data]);
@@ -275,6 +295,7 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
modeSwarm: data.modeSwarm, modeSwarm: data.modeSwarm,
labelsSwarm: data.labelsSwarm, labelsSwarm: data.labelsSwarm,
networkSwarm: data.networkSwarm, networkSwarm: data.networkSwarm,
stopGracePeriodSwarm: data.stopGracePeriodSwarm ?? null,
}) })
.then(async () => { .then(async () => {
toast.success("Swarm settings updated"); toast.success("Swarm settings updated");
@@ -352,9 +373,9 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
language="json" language="json"
placeholder={`{ placeholder={`{
"Test" : ["CMD-SHELL", "curl -f http://localhost:3000/health"], "Test" : ["CMD-SHELL", "curl -f http://localhost:3000/health"],
"Interval" : 10000, "Interval" : 10000000000,
"Timeout" : 10000, "Timeout" : 10000000000,
"StartPeriod" : 10000, "StartPeriod" : 10000000000,
"Retries" : 10 "Retries" : 10
}`} }`}
className="h-[12rem] font-mono" className="h-[12rem] font-mono"
@@ -407,9 +428,9 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
language="json" language="json"
placeholder={`{ placeholder={`{
"Condition" : "on-failure", "Condition" : "on-failure",
"Delay" : 10000, "Delay" : 10000000000,
"MaxAttempts" : 10, "MaxAttempts" : 10,
"Window" : 10000 "Window" : 10000000000
} `} } `}
className="h-[12rem] font-mono" className="h-[12rem] font-mono"
{...field} {...field}
@@ -529,9 +550,9 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
language="json" language="json"
placeholder={`{ placeholder={`{
"Parallelism" : 1, "Parallelism" : 1,
"Delay" : 10000, "Delay" : 10000000000,
"FailureAction" : "continue", "FailureAction" : "continue",
"Monitor" : 10000, "Monitor" : 10000000000,
"MaxFailureRatio" : 10, "MaxFailureRatio" : 10,
"Order" : "start-first" "Order" : "start-first"
}`} }`}
@@ -587,9 +608,9 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
language="json" language="json"
placeholder={`{ placeholder={`{
"Parallelism" : 1, "Parallelism" : 1,
"Delay" : 10000, "Delay" : 10000000000,
"FailureAction" : "continue", "FailureAction" : "continue",
"Monitor" : 10000, "Monitor" : 10000000000,
"MaxFailureRatio" : 10, "MaxFailureRatio" : 10,
"Order" : "start-first" "Order" : "start-first"
}`} }`}
@@ -774,7 +795,57 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="stopGracePeriodSwarm"
render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pl-6 ">
<FormLabel>Stop Grace Period (nanoseconds)</FormLabel>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<FormDescription className="break-all w-fit flex flex-row gap-1 items-center">
Duration in nanoseconds
<HelpCircle className="size-4 text-muted-foreground" />
</FormDescription>
</TooltipTrigger>
<TooltipContent
className="w-full z-[999]"
align="start"
side="bottom"
>
<code>
<pre>
{`Enter duration in nanoseconds:
• 30000000000 - 30 seconds
• 120000000000 - 2 minutes
• 3600000000000 - 1 hour
• 0 - no grace period`}
</pre>
</code>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<FormControl>
<Input
type="number"
placeholder="30000000000"
className="font-mono"
{...field}
value={field?.value?.toString() || ""}
onChange={(e) =>
field.onChange(
e.target.value ? BigInt(e.target.value) : null,
)
}
/>
</FormControl>
<pre>
<FormMessage />
</pre>
</FormItem>
)}
/>
<DialogFooter className="flex w-full flex-row justify-end md:col-span-2 m-0 sticky bottom-0 right-0 bg-muted border"> <DialogFooter className="flex w-full flex-row justify-end md:col-span-2 m-0 sticky bottom-0 right-0 bg-muted border">
<Button <Button
isLoading={isLoading} isLoading={isLoading}

View File

@@ -59,7 +59,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
const router = useRouter(); const router = useRouter();
const { mutateAsync, isLoading } = const { mutateAsync, isLoading } =
api.application.saveGitProdiver.useMutation(); api.application.saveGitProvider.useMutation();
const form = useForm<GitProvider>({ const form = useForm<GitProvider>({
defaultValues: { defaultValues: {

View File

@@ -7,7 +7,7 @@ import {
RefreshCw, RefreshCw,
} from "lucide-react"; } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { type Control, useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
@@ -57,6 +57,7 @@ export const commonCronExpressions = [
{ label: "Every month on the 1st at midnight", value: "0 0 1 * *" }, { label: "Every month on the 1st at midnight", value: "0 0 1 * *" },
{ label: "Every 15 minutes", value: "*/15 * * * *" }, { label: "Every 15 minutes", value: "*/15 * * * *" },
{ label: "Every weekday at midnight", value: "0 0 * * 1-5" }, { label: "Every weekday at midnight", value: "0 0 * * 1-5" },
{ label: "Custom", value: "custom" },
]; ];
const formSchema = z const formSchema = z
@@ -115,10 +116,91 @@ interface Props {
scheduleType?: "application" | "compose" | "server" | "dokploy-server"; scheduleType?: "application" | "compose" | "server" | "dokploy-server";
} }
export const ScheduleFormField = ({
name,
formControl,
}: {
name: string;
formControl: Control<any>;
}) => {
const [selectedOption, setSelectedOption] = useState("");
return (
<FormField
control={formControl}
name={name}
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2">
Schedule
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>Cron expression format: minute hour day month weekday</p>
<p>Example: 0 0 * * * (daily at midnight)</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<div className="flex flex-col gap-2">
<Select
value={selectedOption}
onValueChange={(value) => {
setSelectedOption(value);
field.onChange(value === "custom" ? "" : value);
}}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a predefined schedule" />
</SelectTrigger>
</FormControl>
<SelectContent>
{commonCronExpressions.map((expr) => (
<SelectItem key={expr.value} value={expr.value}>
{expr.label}
{expr.value !== "custom" && ` (${expr.value})`}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="relative">
<FormControl>
<Input
placeholder="Custom cron expression (e.g., 0 0 * * *)"
{...field}
onChange={(e) => {
const value = e.target.value;
const commonExpression = commonCronExpressions.find(
(expression) => expression.value === value,
);
if (commonExpression) {
setSelectedOption(commonExpression.value);
} else {
setSelectedOption("custom");
}
field.onChange(e);
}}
/>
</FormControl>
</div>
</div>
<FormDescription>
Choose a predefined schedule or enter a custom cron expression
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
);
};
export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => { export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [cacheType, setCacheType] = useState<CacheType>("cache"); const [cacheType, setCacheType] = useState<CacheType>("cache");
const utils = api.useUtils(); const utils = api.useUtils();
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
@@ -377,63 +459,9 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
)} )}
/> />
<FormField <ScheduleFormField
control={form.control}
name="cronExpression" name="cronExpression"
render={({ field }) => ( formControl={form.control}
<FormItem>
<FormLabel className="flex items-center gap-2">
Schedule
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>
Cron expression format: minute hour day month
weekday
</p>
<p>Example: 0 0 * * * (daily at midnight)</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<div className="flex flex-col gap-2">
<Select
onValueChange={(value) => {
field.onChange(value);
}}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a predefined schedule" />
</SelectTrigger>
</FormControl>
<SelectContent>
{commonCronExpressions.map((expr) => (
<SelectItem key={expr.value} value={expr.value}>
{expr.label} ({expr.value})
</SelectItem>
))}
</SelectContent>
</Select>
<div className="relative">
<FormControl>
<Input
placeholder="Custom cron expression (e.g., 0 0 * * *)"
{...field}
/>
</FormControl>
</div>
</div>
<FormDescription>
Choose a predefined schedule or enter a custom cron
expression
</FormDescription>
<FormMessage />
</FormItem>
)}
/> />
{(scheduleTypeForm === "application" || {(scheduleTypeForm === "application" ||

View File

@@ -1,11 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { import { DatabaseZap, PenBoxIcon, PlusCircle, RefreshCw } from "lucide-react";
DatabaseZap,
Info,
PenBoxIcon,
PlusCircle,
RefreshCw,
} from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -47,7 +41,7 @@ import {
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import type { CacheType } from "../domains/handle-domain"; import type { CacheType } from "../domains/handle-domain";
import { commonCronExpressions } from "../schedules/handle-schedules"; import { ScheduleFormField } from "../schedules/handle-schedules";
const formSchema = z const formSchema = z
.object({ .object({
@@ -306,64 +300,9 @@ export const HandleVolumeBackups = ({
</FormItem> </FormItem>
)} )}
/> />
<ScheduleFormField
<FormField
control={form.control}
name="cronExpression" name="cronExpression"
render={({ field }) => ( formControl={form.control}
<FormItem>
<FormLabel className="flex items-center gap-2">
Schedule
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>
Cron expression format: minute hour day month
weekday
</p>
<p>Example: 0 0 * * * (daily at midnight)</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<div className="flex flex-col gap-2">
<Select
onValueChange={(value) => {
field.onChange(value);
}}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a predefined schedule" />
</SelectTrigger>
</FormControl>
<SelectContent>
{commonCronExpressions.map((expr) => (
<SelectItem key={expr.value} value={expr.value}>
{expr.label} ({expr.value})
</SelectItem>
))}
</SelectContent>
</Select>
<div className="relative">
<FormControl>
<Input
placeholder="Custom cron expression (e.g., 0 0 * * *)"
{...field}
/>
</FormControl>
</div>
</div>
<FormDescription>
Choose a predefined schedule or enter a custom cron
expression
</FormDescription>
<FormMessage />
</FormItem>
)}
/> />
<FormField <FormField

View File

@@ -1,5 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
@@ -35,6 +35,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
); );
const { mutateAsync, isLoading } = api.compose.update.useMutation(); const { mutateAsync, isLoading } = api.compose.update.useMutation();
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const form = useForm<AddComposeFile>({ const form = useForm<AddComposeFile>({
defaultValues: { defaultValues: {
@@ -53,6 +54,12 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
} }
}, [form, form.reset, data]); }, [form, form.reset, data]);
useEffect(() => {
if (data?.composeFile !== undefined) {
setHasUnsavedChanges(composeFile !== data.composeFile);
}
}, [composeFile, data?.composeFile]);
const onSubmit = async (data: AddComposeFile) => { const onSubmit = async (data: AddComposeFile) => {
const { valid, error } = validateAndFormatYAML(data.composeFile); const { valid, error } = validateAndFormatYAML(data.composeFile);
if (!valid) { if (!valid) {
@@ -71,6 +78,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
}) })
.then(async () => { .then(async () => {
toast.success("Compose config Updated"); toast.success("Compose config Updated");
setHasUnsavedChanges(false);
refetch(); refetch();
await utils.compose.getConvertedCompose.invalidate({ await utils.compose.getConvertedCompose.invalidate({
composeId, composeId,
@@ -99,6 +107,19 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
return ( return (
<> <>
<div className="w-full flex flex-col gap-4 "> <div className="w-full flex flex-col gap-4 ">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-medium">Compose File</h3>
<p className="text-sm text-muted-foreground">
Configure your Docker Compose file for this service.
{hasUnsavedChanges && (
<span className="text-yellow-500 ml-2">
(You have unsaved changes)
</span>
)}
</p>
</div>
</div>
<Form {...form}> <Form {...form}>
<form <form
id="hook-form-save-compose-file" id="hook-form-save-compose-file"

View File

@@ -3,7 +3,6 @@ import {
CheckIcon, CheckIcon,
ChevronsUpDown, ChevronsUpDown,
DatabaseZap, DatabaseZap,
Info,
PenBoxIcon, PenBoxIcon,
PlusIcon, PlusIcon,
RefreshCw, RefreshCw,
@@ -62,7 +61,7 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { commonCronExpressions } from "../../application/schedules/handle-schedules"; import { ScheduleFormField } from "../../application/schedules/handle-schedules";
type CacheType = "cache" | "fetch"; type CacheType = "cache" | "fetch";
@@ -579,66 +578,9 @@ export const HandleBackup = ({
); );
}} }}
/> />
<FormField
control={form.control} <ScheduleFormField name="schedule" formControl={form.control} />
name="schedule"
render={({ field }) => {
return (
<FormItem>
<FormLabel className="flex items-center gap-2">
Schedule
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>
Cron expression format: minute hour day month
weekday
</p>
<p>Example: 0 0 * * * (daily at midnight)</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<div className="flex flex-col gap-2">
<Select
onValueChange={(value) => {
field.onChange(value);
}}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a predefined schedule" />
</SelectTrigger>
</FormControl>
<SelectContent>
{commonCronExpressions.map((expr) => (
<SelectItem key={expr.value} value={expr.value}>
{expr.label} ({expr.value})
</SelectItem>
))}
</SelectContent>
</Select>
<div className="relative">
<FormControl>
<Input
placeholder="Custom cron expression (e.g., 0 0 * * *)"
{...field}
/>
</FormControl>
</div>
</div>
<FormDescription>
Choose a predefined schedule or enter a custom cron
expression
</FormDescription>
<FormMessage />
</FormItem>
);
}}
/>
<FormField <FormField
control={form.control} control={form.control}
name="prefix" name="prefix"

View File

@@ -63,13 +63,20 @@ export const AdvancedEnvironmentSelector = ({
const [name, setName] = useState(""); const [name, setName] = useState("");
const [description, setDescription] = useState(""); const [description, setDescription] = useState("");
// API mutations // Get current user's permissions
const { data: environment } = api.environment.one.useQuery( const { data: currentUser } = api.user.get.useQuery();
{ environmentId: currentEnvironmentId || "" },
{ // Check if user can create environments
enabled: !!currentEnvironmentId, const canCreateEnvironments =
}, currentUser?.role === "owner" ||
); currentUser?.role === "admin" ||
currentUser?.canCreateEnvironments === true;
// Check if user can delete environments
const canDeleteEnvironments =
currentUser?.role === "owner" ||
currentUser?.role === "admin" ||
currentUser?.canDeleteEnvironments === true;
const haveServices = const haveServices =
selectedEnvironment && selectedEnvironment &&
@@ -267,17 +274,19 @@ export const AdvancedEnvironmentSelector = ({
<PencilIcon className="h-3 w-3" /> <PencilIcon className="h-3 w-3" />
</Button> </Button>
<Button {canDeleteEnvironments && (
variant="ghost" <Button
size="sm" variant="ghost"
className="h-6 w-6 p-0 text-red-600 hover:text-red-700" size="sm"
onClick={(e) => { className="h-6 w-6 p-0 text-red-600 hover:text-red-700"
e.stopPropagation(); onClick={(e) => {
openDeleteDialog(environment); e.stopPropagation();
}} openDeleteDialog(environment);
> }}
<TrashIcon className="h-3 w-3" /> >
</Button> <TrashIcon className="h-3 w-3" />
</Button>
)}
</div> </div>
)} )}
</div> </div>
@@ -285,13 +294,15 @@ export const AdvancedEnvironmentSelector = ({
})} })}
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem {canCreateEnvironments && (
className="cursor-pointer" <DropdownMenuItem
onClick={() => setIsCreateDialogOpen(true)} className="cursor-pointer"
> onClick={() => setIsCreateDialogOpen(true)}
<PlusIcon className="h-4 w-4 mr-2" /> >
Create Environment <PlusIcon className="h-4 w-4 mr-2" />
</DropdownMenuItem> Create Environment
</DropdownMenuItem>
)}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>

View File

@@ -96,8 +96,30 @@ export const ShowProjects = () => {
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
break; break;
case "services": { case "services": {
const aTotalServices = a.environments.length; const aTotalServices = a.environments.reduce((total, env) => {
const bTotalServices = b.environments.length; return (
total +
(env.applications?.length || 0) +
(env.mariadb?.length || 0) +
(env.mongo?.length || 0) +
(env.mysql?.length || 0) +
(env.postgres?.length || 0) +
(env.redis?.length || 0) +
(env.compose?.length || 0)
);
}, 0);
const bTotalServices = b.environments.reduce((total, env) => {
return (
total +
(env.applications?.length || 0) +
(env.mariadb?.length || 0) +
(env.mongo?.length || 0) +
(env.mysql?.length || 0) +
(env.postgres?.length || 0) +
(env.redis?.length || 0) +
(env.compose?.length || 0)
);
}, 0);
comparison = aTotalServices - bTotalServices; comparison = aTotalServices - bTotalServices;
break; break;
} }

View File

@@ -12,6 +12,8 @@ import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { import {
DiscordIcon, DiscordIcon,
GotifyIcon,
NtfyIcon,
SlackIcon, SlackIcon,
TelegramIcon, TelegramIcon,
} from "@/components/icons/notification-icons"; } from "@/components/icons/notification-icons";
@@ -130,11 +132,11 @@ export const notificationsMap = {
label: "Email", label: "Email",
}, },
gotify: { gotify: {
icon: <MessageCircleMore size={29} className="text-muted-foreground" />, icon: <GotifyIcon />,
label: "Gotify", label: "Gotify",
}, },
ntfy: { ntfy: {
icon: <MessageCircleMore size={29} className="text-muted-foreground" />, icon: <NtfyIcon />,
label: "ntfy", label: "ntfy",
}, },
}; };

View File

@@ -2,6 +2,8 @@ import { Bell, Loader2, Mail, MessageCircleMore, Trash2 } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { import {
DiscordIcon, DiscordIcon,
GotifyIcon,
NtfyIcon,
SlackIcon, SlackIcon,
TelegramIcon, TelegramIcon,
} from "@/components/icons/notification-icons"; } from "@/components/icons/notification-icons";
@@ -85,12 +87,12 @@ export const ShowNotifications = () => {
)} )}
{notification.notificationType === "gotify" && ( {notification.notificationType === "gotify" && (
<div className="flex items-center justify-center rounded-lg "> <div className="flex items-center justify-center rounded-lg ">
<MessageCircleMore className="size-6 text-muted-foreground" /> <GotifyIcon className="size-6" />
</div> </div>
)} )}
{notification.notificationType === "ntfy" && ( {notification.notificationType === "ntfy" && (
<div className="flex items-center justify-center rounded-lg "> <div className="flex items-center justify-center rounded-lg ">
<MessageCircleMore className="size-6 text-muted-foreground" /> <NtfyIcon className="size-6" />
</div> </div>
)} )}

View File

@@ -257,8 +257,16 @@ export const ProfileForm = () => {
onValueChange={(e) => { onValueChange={(e) => {
field.onChange(e); field.onChange(e);
}} }}
defaultValue={field.value} defaultValue={
value={field.value} field.value?.startsWith("data:")
? "upload"
: field.value
}
value={
field.value?.startsWith("data:")
? "upload"
: field.value
}
className="flex flex-row flex-wrap gap-2 max-xl:justify-center" className="flex flex-row flex-wrap gap-2 max-xl:justify-center"
> >
<FormItem key="no-avatar"> <FormItem key="no-avatar">
@@ -279,6 +287,71 @@ export const ProfileForm = () => {
</Avatar> </Avatar>
</FormLabel> </FormLabel>
</FormItem> </FormItem>
<FormItem key="custom-upload">
<FormLabel className="[&:has([data-state=checked])>.upload-avatar]:border-primary [&:has([data-state=checked])>.upload-avatar]:border-1 [&:has([data-state=checked])>.upload-avatar]:p-px cursor-pointer">
<FormControl>
<RadioGroupItem
value="upload"
className="sr-only"
/>
</FormControl>
<div
className="upload-avatar h-12 w-12 rounded-full border border-dashed border-muted-foreground hover:border-primary transition-colors flex items-center justify-center bg-muted/50 hover:bg-muted overflow-hidden"
onClick={() =>
document
.getElementById("avatar-upload")
?.click()
}
>
{field.value?.startsWith("data:") ? (
<img
src={field.value}
alt="Custom avatar"
className="h-full w-full object-cover rounded-full"
/>
) : (
<svg
className="h-5 w-5 text-muted-foreground"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 4v16m8-8H4"
/>
</svg>
)}
</div>
<input
id="avatar-upload"
type="file"
accept="image/*"
className="hidden"
onChange={async (e) => {
const file = e.target.files?.[0];
if (file) {
// max file size 2mb
if (file.size > 2 * 1024 * 1024) {
toast.error(
"Image size must be less than 2MB",
);
return;
}
const reader = new FileReader();
reader.onload = (event) => {
const result = event.target
?.result as string;
field.onChange(result);
};
reader.readAsDataURL(file);
}
}}
/>
</FormLabel>
</FormItem>
{availableAvatars.map((image) => ( {availableAvatars.map((image) => (
<FormItem key={image}> <FormItem key={image}>
<FormLabel className="[&:has([data-state=checked])>img]:border-primary [&:has([data-state=checked])>img]:border-1 [&:has([data-state=checked])>img]:p-px cursor-pointer"> <FormLabel className="[&:has([data-state=checked])>img]:border-primary [&:has([data-state=checked])>img]:border-1 [&:has([data-state=checked])>img]:p-px cursor-pointer">

View File

@@ -1,6 +1,6 @@
import type { findEnvironmentById } from "@dokploy/server/index"; import type { findEnvironmentById } from "@dokploy/server/index";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react"; import { useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
@@ -161,11 +161,13 @@ const addPermissions = z.object({
canCreateServices: z.boolean().optional().default(false), canCreateServices: z.boolean().optional().default(false),
canDeleteProjects: z.boolean().optional().default(false), canDeleteProjects: z.boolean().optional().default(false),
canDeleteServices: z.boolean().optional().default(false), canDeleteServices: z.boolean().optional().default(false),
canDeleteEnvironments: z.boolean().optional().default(false),
canAccessToTraefikFiles: z.boolean().optional().default(false), canAccessToTraefikFiles: z.boolean().optional().default(false),
canAccessToDocker: z.boolean().optional().default(false), canAccessToDocker: z.boolean().optional().default(false),
canAccessToAPI: z.boolean().optional().default(false), canAccessToAPI: z.boolean().optional().default(false),
canAccessToSSHKeys: z.boolean().optional().default(false), canAccessToSSHKeys: z.boolean().optional().default(false),
canAccessToGitProviders: z.boolean().optional().default(false), canAccessToGitProviders: z.boolean().optional().default(false),
canCreateEnvironments: z.boolean().optional().default(false),
}); });
type AddPermissions = z.infer<typeof addPermissions>; type AddPermissions = z.infer<typeof addPermissions>;
@@ -175,6 +177,7 @@ interface Props {
} }
export const AddUserPermissions = ({ userId }: Props) => { export const AddUserPermissions = ({ userId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { data: projects } = api.project.all.useQuery(); const { data: projects } = api.project.all.useQuery();
const { data, refetch } = api.user.one.useQuery( const { data, refetch } = api.user.one.useQuery(
@@ -192,13 +195,25 @@ export const AddUserPermissions = ({ userId }: Props) => {
const form = useForm<AddPermissions>({ const form = useForm<AddPermissions>({
defaultValues: { defaultValues: {
accessedProjects: [], accessedProjects: [],
accessedEnvironments: [],
accessedServices: [], accessedServices: [],
canDeleteEnvironments: false,
canCreateProjects: false,
canCreateServices: false,
canDeleteProjects: false,
canDeleteServices: false,
canAccessToTraefikFiles: false,
canAccessToDocker: false,
canAccessToAPI: false,
canAccessToSSHKeys: false,
canAccessToGitProviders: false,
canCreateEnvironments: false,
}, },
resolver: zodResolver(addPermissions), resolver: zodResolver(addPermissions),
}); });
useEffect(() => { useEffect(() => {
if (data) { if (data && isOpen) {
form.reset({ form.reset({
accessedProjects: data.accessedProjects || [], accessedProjects: data.accessedProjects || [],
accessedEnvironments: data.accessedEnvironments || [], accessedEnvironments: data.accessedEnvironments || [],
@@ -207,14 +222,16 @@ export const AddUserPermissions = ({ userId }: Props) => {
canCreateServices: data.canCreateServices, canCreateServices: data.canCreateServices,
canDeleteProjects: data.canDeleteProjects, canDeleteProjects: data.canDeleteProjects,
canDeleteServices: data.canDeleteServices, canDeleteServices: data.canDeleteServices,
canDeleteEnvironments: data.canDeleteEnvironments || false,
canAccessToTraefikFiles: data.canAccessToTraefikFiles, canAccessToTraefikFiles: data.canAccessToTraefikFiles,
canAccessToDocker: data.canAccessToDocker, canAccessToDocker: data.canAccessToDocker,
canAccessToAPI: data.canAccessToAPI, canAccessToAPI: data.canAccessToAPI,
canAccessToSSHKeys: data.canAccessToSSHKeys, canAccessToSSHKeys: data.canAccessToSSHKeys,
canAccessToGitProviders: data.canAccessToGitProviders, canAccessToGitProviders: data.canAccessToGitProviders,
canCreateEnvironments: data.canCreateEnvironments,
}); });
} }
}, [form, form.formState.isSubmitSuccessful, form.reset, data]); }, [form, form.reset, data, isOpen]);
const onSubmit = async (data: AddPermissions) => { const onSubmit = async (data: AddPermissions) => {
await mutateAsync({ await mutateAsync({
@@ -223,6 +240,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
canCreateProjects: data.canCreateProjects, canCreateProjects: data.canCreateProjects,
canDeleteServices: data.canDeleteServices, canDeleteServices: data.canDeleteServices,
canDeleteProjects: data.canDeleteProjects, canDeleteProjects: data.canDeleteProjects,
canDeleteEnvironments: data.canDeleteEnvironments,
canAccessToTraefikFiles: data.canAccessToTraefikFiles, canAccessToTraefikFiles: data.canAccessToTraefikFiles,
accessedProjects: data.accessedProjects || [], accessedProjects: data.accessedProjects || [],
accessedEnvironments: data.accessedEnvironments || [], accessedEnvironments: data.accessedEnvironments || [],
@@ -231,17 +249,19 @@ export const AddUserPermissions = ({ userId }: Props) => {
canAccessToAPI: data.canAccessToAPI, canAccessToAPI: data.canAccessToAPI,
canAccessToSSHKeys: data.canAccessToSSHKeys, canAccessToSSHKeys: data.canAccessToSSHKeys,
canAccessToGitProviders: data.canAccessToGitProviders, canAccessToGitProviders: data.canAccessToGitProviders,
canCreateEnvironments: data.canCreateEnvironments,
}) })
.then(async () => { .then(async () => {
toast.success("Permissions updated"); toast.success("Permissions updated");
refetch(); refetch();
setIsOpen(false);
}) })
.catch(() => { .catch(() => {
toast.error("Error updating the permissions"); toast.error("Error updating the permissions");
}); });
}; };
return ( return (
<Dialog> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger className="" asChild> <DialogTrigger className="" asChild>
<DropdownMenuItem <DropdownMenuItem
className="w-full cursor-pointer" className="w-full cursor-pointer"
@@ -343,6 +363,46 @@ export const AddUserPermissions = ({ userId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="canCreateEnvironments"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>Create Environments</FormLabel>
<FormDescription>
Allow the user to create environments
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="canDeleteEnvironments"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>Delete Environments</FormLabel>
<FormDescription>
Allow the user to delete environments
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="canAccessToTraefikFiles" name="canAccessToTraefikFiles"

View File

@@ -5,6 +5,7 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Card, Card,
@@ -76,6 +77,9 @@ export const WebDomain = () => {
resolver: zodResolver(addServerDomain), resolver: zodResolver(addServerDomain),
}); });
const https = form.watch("https"); const https = form.watch("https");
const domain = form.watch("domain") || "";
const host = data?.user?.host || "";
const hasChanged = domain !== host;
useEffect(() => { useEffect(() => {
if (data) { if (data) {
form.reset({ form.reset({
@@ -119,6 +123,19 @@ export const WebDomain = () => {
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-2 py-6 border-t"> <CardContent className="space-y-2 py-6 border-t">
{/* Warning for GitHub webhook URL changes */}
{hasChanged && (
<AlertBlock type="warning">
<div className="space-y-2">
<p className="font-medium"> Important: URL Change Impact</p>
<p>
If you change the Dokploy Server URL make sure to update
your Github Apps to keep the auto-deploy working and preview
deployments working.
</p>
</div>
</AlertBlock>
)}
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}

View File

@@ -88,3 +88,121 @@ export const DiscordIcon = ({ className }: Props) => {
</svg> </svg>
); );
}; };
export const GotifyIcon = ({ className }: Props) => {
return (
<svg
viewBox="0 0 500 500"
className={cn("size-8", className)}
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<style>
{`
.gotify-st0{fill:#DDCBA2;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;}
.gotify-st1{fill:#71CAEE;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;}
.gotify-st2{fill:#FFFFFF;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;}
.gotify-st3{fill:#888E93;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;}
.gotify-st4{fill:#F0F0F0;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;}
.gotify-st5{fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;}
.gotify-st8{fill:#FFFFFF;}
`}
</style>
<linearGradient
id="gotify-gradient"
x1="265"
y1="280"
x2="275"
y2="302"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stopColor="#71CAEE" />
<stop offset="0.04" stopColor="#83CAE2" />
<stop offset="0.12" stopColor="#9FCACE" />
<stop offset="0.21" stopColor="#B6CBBE" />
<stop offset="0.31" stopColor="#C7CBB1" />
<stop offset="0.44" stopColor="#D4CBA8" />
<stop offset="0.61" stopColor="#DBCBA3" />
<stop offset="1" stopColor="#DDCBA2" />
</linearGradient>
</defs>
<g transform="matrix(2.33,0,0,2.33,-432,-323)">
<g transform="translate(-25,26)">
<path
className="gotify-st1"
d="m258.9,119.7c-3,-0.9-6,-1.8-9,-2.7-4.6,-1.4-9.2,-2.8-14,-2.5-2.8,0.2-6.1,1.3-6.9,4-0.6,2-1.6,7.3-1.3,7.9 1.5,3.4 13.9,6.7 18.3,6.7"
/>
<path d="m392.6,177.9c-1.4,1.4-2.2,3.5-2.5,5.5-0.2,1.4-0.1,3 0.5,4.3 0.6,1.3 1.8,2.3 3.1,3 1.3,0.6 2.8,0.9 4.3,0.9 1.1,0 2.3,-0.1 3.1,-0.9 0.6,-0.7 0.8,-1.6 0.9,-2.5 0.2,-2.3-0.1,-4.7-0.9,-6.9-0.4,-1.1-0.9,-2.3-1.8,-3.1-1.7,-1.8-4.5,-2.2-6.4,-0.5-0.1,0-0.2,0.1-0.3,0.2z" />
<path
className="gotify-st2"
d="m358.5,164.2c-1,-1 0,-2.7 1,-3.7 5.8,-5.2 15.1,-4.6 21.8,-0.6 10.9,6.6 15.6,19.9 17.2,32.5 0.6,5.2 0.9,10.6-0.5,15.7-1.4,5.1-4.6,9.9-9.3,12.1-1.1,0.5-2.3,0.9-3.4,0.5-1.1,-0.4-1.9,-1.8-1.2,-2.8-9.4,-13.6-19,-26.8-20.9,-43.2-0.5,-4.1-1.8,-7.4-4.7,-10.5z"
/>
<path
className="gotify-st1"
d="m220.1,133c34.6,-18 79.3,-19.6 112.2,-8.7 23.7,7.9 41.3,26.7 49.5,50 7.1,20.6 7.1,43.6 3,65.7-7.5,40.2-26.2,77.9-49,112.6-12.6,19-24.6,36-44.2,48.5-38.7,24.6-88.9,22.1-129.3,11.5-19.5,-5.1-38.4,-17.3-44.3,-37.3-3.8,-12.8-2.1,-27.6 4.6,-40 13.5,-24.8 46.2,-38.4 50.8,-67.9 1.4,-8.7-0.3,-17.3-1.6,-25.7-3.8,-23.4-5.4,-45.8 6.7,-68.7 9.5,-17.7 24.3,-31 41.7,-40z"
/>
<path
className="gotify-st2"
d="m264.5,174.9c-0.5,0.5-0.9,1-1.3,1.6-9,11.6-12,27.9-9.3,42.1 1.7,9 5.9,17.9 13.2,23.4 19.3,14.6 51.5,13.5 68.4,-1.5 24.4,-21.7 13,-67.6-14,-78.8-17.6,-7.2-43.7,-1.6-57,13.2z"
/>
<path
className="gotify-st2"
d="m382.1,237.1c1.4,-0.1 2.9,-0.1 4.3,0.1 0.3,0 0.7,0.1 1,0.4 0.2,0.3 0.4,0.7 0.5,1.1 1,3.9 0.5,8.2 0.1,12.4-0.1,0.9-0.2,1.8-0.6,2.6-1,2.1-3.1,2.7-4.7,2.7-0.1,0-0.2,0-0.3,-0.1-0.3,-0.2-0.3,-0.7-0.2,-1.2 0.3,-5.9-0.1,-11.9-0.1,-18z"
/>
<path
className="gotify-st2"
d="m378.7,236.8c-1.4,0.4-2.5,2-2.8,4.4-0.5,4.4-0.7,8.9-0.5,13.4 0,0.9 0.1,1.9 0.5,2.4 0.2,0.3 0.5,0.4 0.8,0.4 1.6,0.3 4.1,-0.6 5.6,-1 0,0 0,-5.2-0.1,-8-0.1,-2.8-0.1,-6.1-0.2,-8.9 0,-0.6 0,-1.5 0,-2.2 0.1,-0.7-2.6,-0.7-3.3,-0.5z"
/>
<path
className="gotify-st0"
d="m358.3,231.8c-0.3,2.2 0.1,4.7 1.7,7.4 2.6,4.4 7,6.1 11.9,5.8 8.9,-0.6 25.3,-5.4 27.5,-15.7 0.6,-3-0.3,-6.1-2.2,-8.5-6.2,-7.8-17.8,-5.7-25.6,-2-5.9,2.7-12.4,7-13.3,13z"
/>
<path
className="gotify-st3"
d="m386.4,208.6c2.2,1.4 3.7,3.8 4,7 0.3,3.6-1.4,7.5-5,8.8-2.9,1.1-6.2,0.6-9.1,-0.4-2.9,-1-5.8,-2.8-6.8,-5.7-0.7,-2-0.3,-4.3 0.7,-6.1 1.1,-1.8 2.8,-3.2 4.7,-4.1 3.9,-1.8 8.4,-1.6 11.5,0.5z"
/>
<path
className="gotify-st0"
d="m414.7,262.6c2.4,0.6 4.8,2.1 5.6,4.4 0.8,2.3 0.1,4.9-1.6,6.7-1.7,1.8-4.2,2.5-6.6,2.5-0.8,0-1.7,-0.1-2.4,-0.5-2.5,-1.1-3.5,-4-4.2,-6.6-1.8,-6.8 3.6,-7.8 9.2,-6.5z"
/>
<path
className="gotify-st4"
d="m267.1,284.7c2.3,-4.5 141.3,-36.2 144.7,-31.6 3.4,4.5 15.8,88.2 9,90.4-6.8,2.3-119.8,37.3-126.6,35-6.8,-2.3-29.4,-89.3-27.1,-93.8z"
/>
<path
className="gotify-st5"
d="m294.2,378.5c0,0 54.3,-74.6 59.9,-76.9 5.7,-2.3 67.3,41.3 67.3,41.3"
/>
<path
className="gotify-st4"
d="m267,287.7c0,0 86,38.8 91.6,36.6 5.7,-2.3 53.1,-71.2 53.1,-71.2"
/>
<path
fill="url(#gotify-gradient)"
d="m261.9,283.5c-0.1,4.2 4.3,7.3 8.4,7.6 4.1,0.3 8.2,-1.3 12.2,-2.6 1.4,-0.4 2.9,-0.8 4.2,-0.2 1.8,0.9 2.7,4.1 1.8,5.9-0.9,1.8-3.4,3.5-5.3,4.4-6.5,3-12.9,3.6-19.9,2-5.3,-1.2-11.3,-4.3-13,-13.5"
/>
<path d="m318.4,198.4c-2,-0.3-4.1,0.1-5.9,1.3-3.2,2.1-4.7,6.2-4.7,9.9 0,1.9 0.4,3.8 1.4,5.3 1.2,1.7 3.1,2.9 5.2,3.4 3.4,0.8 8.2,0.7 10.5,-2.5 1,-1.5 1.4,-3.3 1.5,-5.1 0.5,-5.7-1.8,-11.4-8,-12.3z" />
<path
className="gotify-st8"
d="m320.4,203.3c0.9,0.3 1.7,0.8 2.1,1.7 0.4,0.8 0.4,1.7 0.3,2.5-0.1,1-0.6,2-1.5,2.7-0.7,0.5-1.7,0.7-2.6,0.5-0.9,-0.2-1.7,-0.8-2.2,-1.6-1.1,-1.6-0.9,-4.4 0.9,-5.5 0.9,-0.4 2,-0.6 3,-0.3z"
/>
</g>
</g>
</svg>
);
};
export const NtfyIcon = ({ className }: Props) => {
return (
<svg
viewBox="0 0 24 24"
className={cn("size-8", className)}
xmlns="http://www.w3.org/2000/svg"
>
<path
fill="currentColor"
d="M12.597 13.693v2.156h6.205v-2.156ZM5.183 6.549v2.363l3.591 1.901 0.023 0.01 -0.023 0.009 -3.591 1.901v2.35l0.386 -0.211 5.456 -2.969V9.729ZM3.659 2.037C1.915 2.037 0.42 3.41 0.42 5.154v0.002L0.438 18.73 0 21.963l5.956 -1.583h14.806c1.744 0 3.238 -1.374 3.238 -3.118V5.154c0 -1.744 -1.493 -3.116 -3.237 -3.117h-0.001zm0 2.2h17.104c0.613 0.001 1.037 0.447 1.037 0.917v12.108c0 0.47 -0.424 0.916 -1.038 0.916H5.633l-3.026 0.915 0.031 -0.179 -0.017 -13.76c0 -0.47 0.424 -0.917 1.038 -0.917z"
/>
</svg>
);
};

View File

@@ -7,7 +7,14 @@ import {
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
interface Props { interface Props {
status: "running" | "error" | "done" | "idle" | undefined | null; status:
| "running"
| "error"
| "done"
| "idle"
| "cancelled"
| undefined
| null;
className?: string; className?: string;
} }
@@ -34,6 +41,14 @@ export const StatusTooltip = ({ status, className }: Props) => {
className={cn("size-3.5 rounded-full bg-green-500", className)} className={cn("size-3.5 rounded-full bg-green-500", className)}
/> />
)} )}
{status === "cancelled" && (
<div
className={cn(
"size-3.5 rounded-full bg-muted-foreground",
className,
)}
/>
)}
{status === "running" && ( {status === "running" && (
<div <div
className={cn("size-3.5 rounded-full bg-yellow-500", className)} className={cn("size-3.5 rounded-full bg-yellow-500", className)}
@@ -46,6 +61,7 @@ export const StatusTooltip = ({ status, className }: Props) => {
{status === "error" && "Error"} {status === "error" && "Error"}
{status === "done" && "Done"} {status === "done" && "Done"}
{status === "running" && "Running"} {status === "running" && "Running"}
{status === "cancelled" && "Cancelled"}
</span> </span>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>

View File

@@ -0,0 +1 @@
ALTER TYPE "public"."deploymentStatus" ADD VALUE 'cancelled';

View File

@@ -0,0 +1,6 @@
ALTER TABLE "application" ADD COLUMN "stopGracePeriodSwarm" bigint;--> statement-breakpoint
ALTER TABLE "mariadb" ADD COLUMN "stopGracePeriodSwarm" bigint;--> statement-breakpoint
ALTER TABLE "mongo" ADD COLUMN "stopGracePeriodSwarm" bigint;--> statement-breakpoint
ALTER TABLE "mysql" ADD COLUMN "stopGracePeriodSwarm" bigint;--> statement-breakpoint
ALTER TABLE "postgres" ADD COLUMN "stopGracePeriodSwarm" bigint;--> statement-breakpoint
ALTER TABLE "redis" ADD COLUMN "stopGracePeriodSwarm" bigint;

View File

@@ -0,0 +1 @@
ALTER TABLE "member" ADD COLUMN "canCreateEnvironments" boolean DEFAULT false NOT NULL;

View File

@@ -0,0 +1 @@
ALTER TABLE "member" ADD COLUMN "canDeleteEnvironments" boolean DEFAULT false NOT NULL;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -792,6 +792,34 @@
"when": 1758483520214, "when": 1758483520214,
"tag": "0112_freezing_skrulls", "tag": "0112_freezing_skrulls",
"breakpoints": true "breakpoints": true
},
{
"idx": 113,
"version": "7",
"when": 1758960816504,
"tag": "0113_complete_rafael_vega",
"breakpoints": true
},
{
"idx": 114,
"version": "7",
"when": 1759643172958,
"tag": "0114_dry_black_tom",
"breakpoints": true
},
{
"idx": 115,
"version": "7",
"when": 1759644540829,
"tag": "0115_serious_black_bird",
"breakpoints": true
},
{
"idx": 116,
"version": "7",
"when": 1759645163834,
"tag": "0116_amusing_firedrake",
"breakpoints": true
} }
] ]
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.25.3", "version": "v0.25.5",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",

View File

@@ -226,6 +226,7 @@ const Service = (
<TabsTrigger value="general">General</TabsTrigger> <TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="environment">Environment</TabsTrigger> <TabsTrigger value="environment">Environment</TabsTrigger>
<TabsTrigger value="domains">Domains</TabsTrigger> <TabsTrigger value="domains">Domains</TabsTrigger>
<TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="preview-deployments"> <TabsTrigger value="preview-deployments">
Preview Deployments Preview Deployments
</TabsTrigger> </TabsTrigger>
@@ -233,7 +234,6 @@ const Service = (
<TabsTrigger value="volume-backups"> <TabsTrigger value="volume-backups">
Volume Backups Volume Backups
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="logs">Logs</TabsTrigger> <TabsTrigger value="logs">Logs</TabsTrigger>
{((data?.serverId && isCloud) || !data?.server) && ( {((data?.serverId && isCloud) || !data?.server) && (
<TabsTrigger value="monitoring">Monitoring</TabsTrigger> <TabsTrigger value="monitoring">Monitoring</TabsTrigger>

View File

@@ -524,7 +524,7 @@ export const applicationRouter = createTRPCRouter({
return true; return true;
}), }),
saveGitProdiver: protectedProcedure saveGitProvider: protectedProcedure
.input(apiSaveGitProvider) .input(apiSaveGitProvider)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const application = await findApplicationById(input.applicationId); const application = await findApplicationById(input.applicationId);

View File

@@ -1,6 +1,8 @@
import { import {
addNewEnvironment, addNewEnvironment,
checkEnvironmentAccess, checkEnvironmentAccess,
checkEnvironmentCreationPermission,
checkEnvironmentDeletionPermission,
createEnvironment, createEnvironment,
deleteEnvironment, deleteEnvironment,
duplicateEnvironment, duplicateEnvironment,
@@ -54,9 +56,12 @@ export const environmentRouter = createTRPCRouter({
.input(apiCreateEnvironment) .input(apiCreateEnvironment)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
// Check if user has access to the project // Check if user has permission to create environments
// This would typically involve checking project ownership/membership await checkEnvironmentCreationPermission(
// For now, we'll use a basic organization check ctx.user.id,
input.projectId,
ctx.session.activeOrganizationId,
);
if (input.name === "production") { if (input.name === "production") {
throw new TRPCError({ throw new TRPCError({
@@ -76,6 +81,9 @@ export const environmentRouter = createTRPCRouter({
} }
return environment; return environment;
} catch (error) { } catch (error) {
if (error instanceof TRPCError) {
throw error;
}
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: `Error creating the environment: ${error instanceof Error ? error.message : error}`, message: `Error creating the environment: ${error instanceof Error ? error.message : error}`,
@@ -187,14 +195,6 @@ export const environmentRouter = createTRPCRouter({
.input(apiRemoveEnvironment) .input(apiRemoveEnvironment)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
try { try {
if (ctx.user.role === "member") {
await checkEnvironmentAccess(
ctx.user.id,
input.environmentId,
ctx.session.activeOrganizationId,
"access",
);
}
const environment = await findEnvironmentById(input.environmentId); const environment = await findEnvironmentById(input.environmentId);
if ( if (
environment.project.organizationId !== environment.project.organizationId !==
@@ -206,27 +206,33 @@ export const environmentRouter = createTRPCRouter({
}); });
} }
// Check environment access for members // Check environment deletion permission
if (ctx.user.role === "member") { await checkEnvironmentDeletionPermission(
const { accessedEnvironments } = await findMemberById( ctx.user.id,
ctx.user.id, environment.projectId,
ctx.session.activeOrganizationId, ctx.session.activeOrganizationId,
); );
if (!accessedEnvironments.includes(environment.environmentId)) { // Additional check for environment access for members
throw new TRPCError({ if (ctx.user.role === "member") {
code: "FORBIDDEN", await checkEnvironmentAccess(
message: "You are not allowed to delete this environment", ctx.user.id,
}); input.environmentId,
} ctx.session.activeOrganizationId,
"access",
);
} }
const deletedEnvironment = await deleteEnvironment(input.environmentId); const deletedEnvironment = await deleteEnvironment(input.environmentId);
return deletedEnvironment; return deletedEnvironment;
} catch (error) { } catch (error) {
if (error instanceof TRPCError) {
throw error;
}
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: `Error deleting the environment: ${error instanceof Error ? error.message : error}`, message: `Error deleting the environment: ${error instanceof Error ? error.message : error}`,
cause: error,
}); });
} }
}), }),

View File

@@ -1,4 +1,5 @@
import { import {
addNewEnvironment,
addNewProject, addNewProject,
checkProjectAccess, checkProjectAccess,
createApplication, createApplication,
@@ -85,6 +86,12 @@ export const projectRouter = createTRPCRouter({
project.project.projectId, project.project.projectId,
ctx.session.activeOrganizationId, ctx.session.activeOrganizationId,
); );
await addNewEnvironment(
ctx.user.id,
project?.environment?.environmentId || "",
ctx.session.activeOrganizationId,
);
} }
return project; return project;

View File

@@ -8,6 +8,7 @@ import {
initializeNetwork, initializeNetwork,
initSchedules, initSchedules,
initVolumeBackupsCronJobs, initVolumeBackupsCronJobs,
initCancelDeployments,
sendDokployRestartNotifications, sendDokployRestartNotifications,
setupDirectories, setupDirectories,
} from "@dokploy/server"; } from "@dokploy/server";
@@ -52,6 +53,7 @@ void app.prepare().then(async () => {
await migration(); await migration();
await initCronJobs(); await initCronJobs();
await initSchedules(); await initSchedules();
await initCancelDeployments();
await initVolumeBackupsCronJobs(); await initVolumeBackupsCronJobs();
await sendDokployRestartNotifications(); await sendDokployRestartNotifications();
} }

View File

@@ -108,6 +108,12 @@ export const member = pgTable("member", {
canAccessToTraefikFiles: boolean("canAccessToTraefikFiles") canAccessToTraefikFiles: boolean("canAccessToTraefikFiles")
.notNull() .notNull()
.default(false), .default(false),
canDeleteEnvironments: boolean("canDeleteEnvironments")
.notNull()
.default(false),
canCreateEnvironments: boolean("canCreateEnvironments")
.notNull()
.default(false),
accessedProjects: text("accesedProjects") accessedProjects: text("accesedProjects")
.array() .array()
.notNull() .notNull()

View File

@@ -1,5 +1,6 @@
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { import {
bigint,
boolean, boolean,
integer, integer,
json, json,
@@ -20,7 +21,6 @@ import { gitlab } from "./gitlab";
import { mounts } from "./mount"; import { mounts } from "./mount";
import { ports } from "./port"; import { ports } from "./port";
import { previewDeployments } from "./preview-deployments"; import { previewDeployments } from "./preview-deployments";
import { projects } from "./project";
import { redirects } from "./redirects"; import { redirects } from "./redirects";
import { registry } from "./registry"; import { registry } from "./registry";
import { security } from "./security"; import { security } from "./security";
@@ -164,6 +164,7 @@ export const applications = pgTable("application", {
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(), modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(), labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(), networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
// //
replicas: integer("replicas").default(1).notNull(), replicas: integer("replicas").default(1).notNull(),
applicationStatus: applicationStatus("applicationStatus") applicationStatus: applicationStatus("applicationStatus")
@@ -312,6 +313,7 @@ const createSchema = createInsertSchema(applications, {
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
previewLabels: z.array(z.string()).optional(), previewLabels: z.array(z.string()).optional(),
cleanCache: z.boolean().optional(), cleanCache: z.boolean().optional(),
stopGracePeriodSwarm: z.bigint().nullable(),
}); });
export const apiCreateApplication = createSchema.pick({ export const apiCreateApplication = createSchema.pick({

View File

@@ -21,6 +21,7 @@ export const deploymentStatus = pgEnum("deploymentStatus", [
"running", "running",
"done", "done",
"error", "error",
"cancelled",
]); ]);
export const deployments = pgTable("deployment", { export const deployments = pgTable("deployment", {

View File

@@ -1,5 +1,5 @@
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { integer, json, pgTable, text } from "drizzle-orm/pg-core"; import { bigint, integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
@@ -62,6 +62,7 @@ export const mariadb = pgTable("mariadb", {
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(), modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(), labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(), networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
replicas: integer("replicas").default(1).notNull(), replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt") createdAt: text("createdAt")
.notNull() .notNull()
@@ -128,6 +129,7 @@ const createSchema = createInsertSchema(mariadb, {
modeSwarm: ServiceModeSwarmSchema.nullable(), modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(), labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(), networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(),
}); });
export const apiCreateMariaDB = createSchema export const apiCreateMariaDB = createSchema

View File

@@ -1,5 +1,12 @@
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { boolean, integer, json, pgTable, text } from "drizzle-orm/pg-core"; import {
bigint,
boolean,
integer,
json,
pgTable,
text,
} from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
@@ -58,6 +65,7 @@ export const mongo = pgTable("mongo", {
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(), modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(), labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(), networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
replicas: integer("replicas").default(1).notNull(), replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt") createdAt: text("createdAt")
.notNull() .notNull()
@@ -118,6 +126,7 @@ const createSchema = createInsertSchema(mongo, {
modeSwarm: ServiceModeSwarmSchema.nullable(), modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(), labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(), networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(),
}); });
export const apiCreateMongo = createSchema export const apiCreateMongo = createSchema

View File

@@ -1,5 +1,5 @@
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { integer, json, pgTable, text } from "drizzle-orm/pg-core"; import { bigint, integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
@@ -60,6 +60,7 @@ export const mysql = pgTable("mysql", {
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(), modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(), labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(), networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
replicas: integer("replicas").default(1).notNull(), replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt") createdAt: text("createdAt")
.notNull() .notNull()
@@ -125,6 +126,7 @@ const createSchema = createInsertSchema(mysql, {
modeSwarm: ServiceModeSwarmSchema.nullable(), modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(), labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(), networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(),
}); });
export const apiCreateMySql = createSchema export const apiCreateMySql = createSchema

View File

@@ -1,5 +1,5 @@
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { integer, json, pgTable, text } from "drizzle-orm/pg-core"; import { bigint, integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
@@ -60,6 +60,7 @@ export const postgres = pgTable("postgres", {
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(), modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(), labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(), networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
replicas: integer("replicas").default(1).notNull(), replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt") createdAt: text("createdAt")
.notNull() .notNull()
@@ -118,6 +119,7 @@ const createSchema = createInsertSchema(postgres, {
modeSwarm: ServiceModeSwarmSchema.nullable(), modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(), labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(), networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(),
}); });
export const apiCreatePostgres = createSchema export const apiCreatePostgres = createSchema

View File

@@ -1,5 +1,5 @@
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { integer, json, pgTable, text } from "drizzle-orm/pg-core"; import { bigint, integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod"; import { z } from "zod";
@@ -60,6 +60,7 @@ export const redis = pgTable("redis", {
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(), modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(), labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(), networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
stopGracePeriodSwarm: bigint("stopGracePeriodSwarm", { mode: "bigint" }),
replicas: integer("replicas").default(1).notNull(), replicas: integer("replicas").default(1).notNull(),
environmentId: text("environmentId") environmentId: text("environmentId")
@@ -108,6 +109,7 @@ const createSchema = createInsertSchema(redis, {
modeSwarm: ServiceModeSwarmSchema.nullable(), modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(), labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(), networkSwarm: NetworkSwarmSchema.nullable(),
stopGracePeriodSwarm: z.bigint().nullable(),
}); });
export const apiCreateRedis = createSchema export const apiCreateRedis = createSchema

View File

@@ -186,6 +186,8 @@ export const apiAssignPermissions = createSchema
canAccessToAPI: z.boolean().optional(), canAccessToAPI: z.boolean().optional(),
canAccessToSSHKeys: z.boolean().optional(), canAccessToSSHKeys: z.boolean().optional(),
canAccessToGitProviders: z.boolean().optional(), canAccessToGitProviders: z.boolean().optional(),
canDeleteEnvironments: z.boolean().optional(),
canCreateEnvironments: z.boolean().optional(),
}) })
.required(); .required();

View File

@@ -68,6 +68,7 @@ export * from "./utils/backups/postgres";
export * from "./utils/backups/utils"; export * from "./utils/backups/utils";
export * from "./utils/backups/web-server"; export * from "./utils/backups/web-server";
export * from "./utils/builders/compose"; export * from "./utils/builders/compose";
export * from "./utils/startup/cancell-deployments";
export * from "./utils/builders/docker-file"; export * from "./utils/builders/docker-file";
export * from "./utils/builders/drop"; export * from "./utils/builders/drop";
export * from "./utils/builders/heroku"; export * from "./utils/builders/heroku";

View File

@@ -603,6 +603,21 @@ const BUNNY_CDN_IPS = new Set([
"89.187.184.176", "89.187.184.176",
]); ]);
// Arvancloud IP ranges
// https://www.arvancloud.ir/fa/ips.txt
const ARVANCLOUD_IP_RANGES = [
"185.143.232.0/22",
"188.229.116.16/29",
"94.101.182.0/27",
"2.144.3.128/28",
"89.45.48.64/28",
"37.32.16.0/27",
"37.32.17.0/27",
"37.32.18.0/27",
"37.32.19.0/27",
"185.215.232.0/22",
];
const CDN_PROVIDERS: CDNProvider[] = [ const CDN_PROVIDERS: CDNProvider[] = [
{ {
name: "cloudflare", name: "cloudflare",
@@ -627,6 +642,14 @@ const CDN_PROVIDERS: CDNProvider[] = [
warningMessage: warningMessage:
"Domain is behind Fastly - actual IP is masked by CDN proxy", "Domain is behind Fastly - actual IP is masked by CDN proxy",
}, },
{
name: "arvancloud",
displayName: "Arvancloud",
checkIp: (ip: string) =>
ARVANCLOUD_IP_RANGES.some((range) => isIPInCIDR(ip, range)),
warningMessage:
"Domain is behind Arvancloud - actual IP is masked by CDN proxy",
},
]; ];
export const detectCDNProvider = (ip: string): CDNProvider | null => { export const detectCDNProvider = (ip: string): CDNProvider | null => {

View File

@@ -227,7 +227,7 @@ export const deployCompose = async ({
const buildLink = `${await getDokployUrl()}/dashboard/project/${ const buildLink = `${await getDokployUrl()}/dashboard/project/${
compose.environment.projectId compose.environment.projectId
}/services/compose/${compose.composeId}?tab=deployments`; }/environment/${compose.environmentId}/services/compose/${compose.composeId}?tab=deployments`;
const deployment = await createDeploymentCompose({ const deployment = await createDeploymentCompose({
composeId: composeId, composeId: composeId,
title: titleLog, title: titleLog,
@@ -335,7 +335,7 @@ export const deployRemoteCompose = async ({
const buildLink = `${await getDokployUrl()}/dashboard/project/${ const buildLink = `${await getDokployUrl()}/dashboard/project/${
compose.environment.projectId compose.environment.projectId
}/services/compose/${compose.composeId}?tab=deployments`; }/environment/${compose.environmentId}/services/compose/${compose.composeId}?tab=deployments`;
const deployment = await createDeploymentCompose({ const deployment = await createDeploymentCompose({
composeId: composeId, composeId: composeId,
title: titleLog, title: titleLog,

View File

@@ -163,6 +163,24 @@ export const canPerformAccessEnvironment = async (
return false; return false;
}; };
export const canPerformDeleteEnvironment = async (
userId: string,
projectId: string,
organizationId: string,
) => {
const { accessedProjects, canDeleteEnvironments } = await findMemberById(
userId,
organizationId,
);
const haveAccessToProject = accessedProjects.includes(projectId);
if (canDeleteEnvironments && haveAccessToProject) {
return true;
}
return false;
};
export const canAccessToTraefikFiles = async ( export const canAccessToTraefikFiles = async (
userId: string, userId: string,
organizationId: string, organizationId: string,
@@ -240,6 +258,42 @@ export const checkEnvironmentAccess = async (
} }
}; };
export const checkEnvironmentDeletionPermission = async (
userId: string,
projectId: string,
organizationId: string,
) => {
const member = await findMemberById(userId, organizationId);
if (!member) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "User not found in organization",
});
}
if (member.role === "owner" || member.role === "admin") {
return true;
}
if (!member.canDeleteEnvironments) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You don't have permission to delete environments",
});
}
const hasProjectAccess = member.accessedProjects.includes(projectId);
if (!hasProjectAccess) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You don't have access to this project",
});
}
return true;
};
export const checkProjectAccess = async ( export const checkProjectAccess = async (
authId: string, authId: string,
action: "create" | "delete" | "access", action: "create" | "delete" | "access",
@@ -272,6 +326,46 @@ export const checkProjectAccess = async (
} }
}; };
export const checkEnvironmentCreationPermission = async (
userId: string,
projectId: string,
organizationId: string,
) => {
// Get user's member record
const member = await findMemberById(userId, organizationId);
if (!member) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "User not found in organization",
});
}
// Owners and admins can always create environments
if (member.role === "owner" || member.role === "admin") {
return true;
}
// Check if user has canCreateEnvironments permission
if (!member.canCreateEnvironments) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You don't have permission to create environments",
});
}
// Check if user has access to the project
const hasProjectAccess = member.accessedProjects.includes(projectId);
if (!hasProjectAccess) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You don't have access to this project",
});
}
return true;
};
export const findMemberById = async ( export const findMemberById = async (
userId: string, userId: string,
organizationId: string, organizationId: string,

View File

@@ -142,6 +142,7 @@ export const mechanizeDockerContainer = async (
RollbackConfig, RollbackConfig,
UpdateConfig, UpdateConfig,
Networks, Networks,
StopGracePeriod,
} = generateConfigContainer(application); } = generateConfigContainer(application);
const bindsMount = generateBindMounts(mounts); const bindsMount = generateBindMounts(mounts);
@@ -191,6 +192,8 @@ export const mechanizeDockerContainer = async (
})), })),
}, },
UpdateConfig, UpdateConfig,
...(StopGracePeriod !== undefined &&
StopGracePeriod !== null && { StopGracePeriod }),
}; };
try { try {

View File

@@ -45,6 +45,7 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
RollbackConfig, RollbackConfig,
UpdateConfig, UpdateConfig,
Networks, Networks,
StopGracePeriod,
} = generateConfigContainer(mariadb); } = generateConfigContainer(mariadb);
const resources = calculateResources({ const resources = calculateResources({
memoryLimit, memoryLimit,
@@ -102,6 +103,8 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
: [], : [],
}, },
UpdateConfig, UpdateConfig,
...(StopGracePeriod !== undefined &&
StopGracePeriod !== null && { StopGracePeriod }),
}; };
try { try {
const service = docker.getService(appName); const service = docker.getService(appName);

View File

@@ -91,6 +91,7 @@ ${command ?? "wait $MONGOD_PID"}`;
RollbackConfig, RollbackConfig,
UpdateConfig, UpdateConfig,
Networks, Networks,
StopGracePeriod,
} = generateConfigContainer(mongo); } = generateConfigContainer(mongo);
const resources = calculateResources({ const resources = calculateResources({
@@ -155,6 +156,8 @@ ${command ?? "wait $MONGOD_PID"}`;
: [], : [],
}, },
UpdateConfig, UpdateConfig,
...(StopGracePeriod !== undefined &&
StopGracePeriod !== null && { StopGracePeriod }),
}; };
try { try {

View File

@@ -51,6 +51,7 @@ export const buildMysql = async (mysql: MysqlNested) => {
RollbackConfig, RollbackConfig,
UpdateConfig, UpdateConfig,
Networks, Networks,
StopGracePeriod,
} = generateConfigContainer(mysql); } = generateConfigContainer(mysql);
const resources = calculateResources({ const resources = calculateResources({
memoryLimit, memoryLimit,
@@ -108,6 +109,8 @@ export const buildMysql = async (mysql: MysqlNested) => {
: [], : [],
}, },
UpdateConfig, UpdateConfig,
...(StopGracePeriod !== undefined &&
StopGracePeriod !== null && { StopGracePeriod }),
}; };
try { try {
const service = docker.getService(appName); const service = docker.getService(appName);

View File

@@ -44,6 +44,7 @@ export const buildPostgres = async (postgres: PostgresNested) => {
RollbackConfig, RollbackConfig,
UpdateConfig, UpdateConfig,
Networks, Networks,
StopGracePeriod,
} = generateConfigContainer(postgres); } = generateConfigContainer(postgres);
const resources = calculateResources({ const resources = calculateResources({
memoryLimit, memoryLimit,
@@ -101,6 +102,8 @@ export const buildPostgres = async (postgres: PostgresNested) => {
: [], : [],
}, },
UpdateConfig, UpdateConfig,
...(StopGracePeriod !== undefined &&
StopGracePeriod !== null && { StopGracePeriod }),
}; };
try { try {
const service = docker.getService(appName); const service = docker.getService(appName);

View File

@@ -42,6 +42,7 @@ export const buildRedis = async (redis: RedisNested) => {
RollbackConfig, RollbackConfig,
UpdateConfig, UpdateConfig,
Networks, Networks,
StopGracePeriod,
} = generateConfigContainer(redis); } = generateConfigContainer(redis);
const resources = calculateResources({ const resources = calculateResources({
memoryLimit, memoryLimit,
@@ -98,6 +99,8 @@ export const buildRedis = async (redis: RedisNested) => {
: [], : [],
}, },
UpdateConfig, UpdateConfig,
...(StopGracePeriod !== undefined &&
StopGracePeriod !== null && { StopGracePeriod }),
}; };
try { try {

View File

@@ -394,8 +394,14 @@ export const generateConfigContainer = (
replicas, replicas,
mounts, mounts,
networkSwarm, networkSwarm,
stopGracePeriodSwarm,
} = application; } = application;
const sanitizedStopGracePeriodSwarm =
typeof stopGracePeriodSwarm === "bigint"
? Number(stopGracePeriodSwarm)
: stopGracePeriodSwarm;
const haveMounts = mounts && mounts.length > 0; const haveMounts = mounts && mounts.length > 0;
return { return {
@@ -444,6 +450,10 @@ export const generateConfigContainer = (
Order: "start-first", Order: "start-first",
}, },
}), }),
...(sanitizedStopGracePeriodSwarm !== null &&
sanitizedStopGracePeriodSwarm !== undefined && {
StopGracePeriod: sanitizedStopGracePeriodSwarm,
}),
...(networkSwarm ...(networkSwarm
? { ? {
Networks: networkSwarm, Networks: networkSwarm,

View File

@@ -33,6 +33,7 @@ export const sendEmailNotification = async (
to: toAddresses.join(", "), to: toAddresses.join(", "),
subject, subject,
html: htmlContent, html: htmlContent,
textEncoding: "base64",
}); });
} catch (err) { } catch (err) {
console.log(err); console.log(err);

View File

@@ -31,29 +31,51 @@ export const getBitbucketCloneUrl = (
apiToken?: string | null; apiToken?: string | null;
bitbucketUsername?: string | null; bitbucketUsername?: string | null;
appPassword?: string | null; appPassword?: string | null;
bitbucketEmail?: string | null;
bitbucketWorkspaceName?: string | null;
} | null, } | null,
repoClone: string, repoClone: string,
) => { ) => {
if (!bitbucketProvider) { if (!bitbucketProvider) {
throw new Error("Bitbucket provider is required"); throw new Error("Bitbucket provider is required");
} }
return bitbucketProvider.apiToken
? `https://x-token-auth:${bitbucketProvider.apiToken}@${repoClone}` if (bitbucketProvider.apiToken) {
: `https://${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}@${repoClone}`; return `https://x-bitbucket-api-token-auth:${bitbucketProvider.apiToken}@${repoClone}`;
}
// For app passwords, use username:app_password format
if (!bitbucketProvider.bitbucketUsername || !bitbucketProvider.appPassword) {
throw new Error(
"Username and app password are required when not using API token",
);
}
return `https://${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}@${repoClone}`;
}; };
export const getBitbucketHeaders = (bitbucketProvider: Bitbucket) => { export const getBitbucketHeaders = (bitbucketProvider: Bitbucket) => {
if (bitbucketProvider.apiToken) { if (bitbucketProvider.apiToken) {
// For API tokens, use HTTP Basic auth with email and token // According to Bitbucket official docs, for API calls with API tokens:
// According to Bitbucket docs: email:token for API calls // "You will need both your Atlassian account email and an API token"
const email = // Use: {atlassian_account_email}:{api_token}
bitbucketProvider.bitbucketEmail || bitbucketProvider.bitbucketUsername;
if (!bitbucketProvider.bitbucketEmail) {
throw new Error(
"Atlassian account email is required when using API token for API calls",
);
}
return { return {
Authorization: `Basic ${Buffer.from(`${email}:${bitbucketProvider.apiToken}`).toString("base64")}`, Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketEmail}:${bitbucketProvider.apiToken}`).toString("base64")}`,
}; };
} }
// For app passwords, use HTTP Basic auth with username and app password // For app passwords, use HTTP Basic auth with username and app password
if (!bitbucketProvider.bitbucketUsername || !bitbucketProvider.appPassword) {
throw new Error(
"Username and app password are required when not using API token",
);
}
return { return {
Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`, Authorization: `Basic ${Buffer.from(`${bitbucketProvider.bitbucketUsername}:${bitbucketProvider.appPassword}`).toString("base64")}`,
}; };

View File

@@ -99,6 +99,19 @@ export const refreshGiteaToken = async (giteaProviderId: string) => {
} }
}; };
const buildGiteaCloneUrl = (
giteaUrl: string,
accessToken: string,
owner: string,
repository: string,
) => {
const protocol = giteaUrl.startsWith("http://") ? "http" : "https";
const baseUrl = giteaUrl.replace(/^https?:\/\//, "");
const repoClone = `${owner}/${repository}.git`;
const cloneUrl = `${protocol}://oauth2:${accessToken}@${baseUrl}/${repoClone}`;
return cloneUrl;
};
export type ApplicationWithGitea = InferResultType< export type ApplicationWithGitea = InferResultType<
"applications", "applications",
{ gitea: true } { gitea: true }
@@ -148,9 +161,13 @@ export const getGiteaCloneCommand = async (
const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH; const basePath = isCompose ? COMPOSE_PATH : APPLICATIONS_PATH;
const outputPath = join(basePath, appName, "code"); const outputPath = join(basePath, appName, "code");
const baseUrl = gitea?.giteaUrl.replace(/^https?:\/\//, "");
const repoClone = `${giteaOwner}/${giteaRepository}.git`; const repoClone = `${giteaOwner}/${giteaRepository}.git`;
const cloneUrl = `https://oauth2:${gitea?.accessToken}@${baseUrl}/${repoClone}`; const cloneUrl = buildGiteaCloneUrl(
gitea?.giteaUrl!,
gitea?.accessToken!,
giteaOwner!,
giteaRepository!,
);
const cloneCommand = ` const cloneCommand = `
rm -rf ${outputPath}; rm -rf ${outputPath};
@@ -205,8 +222,12 @@ export const cloneGiteaRepository = async (
await recreateDirectory(outputPath); await recreateDirectory(outputPath);
const repoClone = `${giteaOwner}/${giteaRepository}.git`; const repoClone = `${giteaOwner}/${giteaRepository}.git`;
const baseUrl = giteaProvider.giteaUrl.replace(/^https?:\/\//, ""); const cloneUrl = buildGiteaCloneUrl(
const cloneUrl = `https://oauth2:${giteaProvider.accessToken}@${baseUrl}/${repoClone}`; giteaProvider.giteaUrl,
giteaProvider.accessToken!,
giteaOwner!,
giteaRepository!,
);
writeStream.write(`\nCloning Repo ${repoClone} to ${outputPath}...\n`); writeStream.write(`\nCloning Repo ${repoClone} to ${outputPath}...\n`);
@@ -269,9 +290,12 @@ export const cloneRawGiteaRepository = async (entity: Compose) => {
const outputPath = join(basePath, appName, "code"); const outputPath = join(basePath, appName, "code");
await recreateDirectory(outputPath); await recreateDirectory(outputPath);
const repoClone = `${giteaOwner}/${giteaRepository}.git`; const cloneUrl = buildGiteaCloneUrl(
const baseUrl = giteaProvider.giteaUrl.replace(/^https?:\/\//, ""); giteaProvider.giteaUrl,
const cloneUrl = `https://oauth2:${giteaProvider.accessToken}@${baseUrl}/${repoClone}`; giteaProvider.accessToken!,
giteaOwner!,
giteaRepository!,
);
try { try {
await spawnAsync("git", [ await spawnAsync("git", [
@@ -317,9 +341,13 @@ export const cloneRawGiteaRepositoryRemote = async (compose: Compose) => {
const giteaProvider = await findGiteaById(giteaId); const giteaProvider = await findGiteaById(giteaId);
const basePath = COMPOSE_PATH; const basePath = COMPOSE_PATH;
const outputPath = join(basePath, appName, "code"); const outputPath = join(basePath, appName, "code");
const repoClone = `${giteaOwner}/${giteaRepository}.git`; const cloneUrl = buildGiteaCloneUrl(
const baseUrl = giteaProvider.giteaUrl.replace(/^https?:\/\//, ""); giteaProvider.giteaUrl,
const cloneUrl = `https://oauth2:${giteaProvider.accessToken}@${baseUrl}/${repoClone}`; giteaProvider.accessToken!,
giteaOwner!,
giteaRepository!,
);
try { try {
const command = ` const command = `
rm -rf ${outputPath}; rm -rf ${outputPath};

View File

@@ -0,0 +1,21 @@
import { deployments } from "@dokploy/server/db/schema";
import { eq } from "drizzle-orm";
import { db } from "../../db/index";
export const initCancelDeployments = async () => {
try {
console.log("Setting up cancel deployments....");
const result = await db
.update(deployments)
.set({
status: "cancelled",
})
.where(eq(deployments.status, "running"))
.returning();
console.log(`Cancelled ${result.length} deployments`);
} catch (error) {
console.error(error);
}
};

219
pnpm-lock.yaml generated
View File

@@ -306,10 +306,10 @@ importers:
version: 16.4.5 version: 16.4.5
drizzle-orm: drizzle-orm:
specifier: ^0.39.3 specifier: ^0.39.3
version: 0.39.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(kysely@0.28.2)(postgres@3.4.4) version: 0.39.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(kysely@0.28.7)(postgres@3.4.4)
drizzle-zod: drizzle-zod:
specifier: 0.5.1 specifier: 0.5.1
version: 0.5.1(drizzle-orm@0.39.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(kysely@0.28.2)(postgres@3.4.4))(zod@3.25.32) version: 0.5.1(drizzle-orm@0.39.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(kysely@0.28.7)(postgres@3.4.4))(zod@3.25.32)
fancy-ansi: fancy-ansi:
specifier: ^0.1.3 specifier: ^0.1.3
version: 0.1.3 version: 0.1.3
@@ -550,7 +550,7 @@ importers:
version: 16.4.5 version: 16.4.5
drizzle-orm: drizzle-orm:
specifier: ^0.39.3 specifier: ^0.39.3
version: 0.39.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(kysely@0.28.2)(postgres@3.4.4) version: 0.39.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(kysely@0.28.7)(postgres@3.4.4)
hono: hono:
specifier: ^4.7.10 specifier: ^4.7.10
version: 4.7.10 version: 4.7.10
@@ -668,13 +668,13 @@ importers:
version: 16.4.5 version: 16.4.5
drizzle-dbml-generator: drizzle-dbml-generator:
specifier: 0.10.0 specifier: 0.10.0
version: 0.10.0(drizzle-orm@0.39.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(kysely@0.28.2)(postgres@3.4.4)) version: 0.10.0(drizzle-orm@0.39.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(kysely@0.28.7)(postgres@3.4.4))
drizzle-orm: drizzle-orm:
specifier: ^0.39.3 specifier: ^0.39.3
version: 0.39.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(kysely@0.28.2)(postgres@3.4.4) version: 0.39.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(kysely@0.28.7)(postgres@3.4.4)
drizzle-zod: drizzle-zod:
specifier: 0.5.1 specifier: 0.5.1
version: 0.5.1(drizzle-orm@0.39.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(kysely@0.28.2)(postgres@3.4.4))(zod@3.25.32) version: 0.5.1(drizzle-orm@0.39.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(kysely@0.28.7)(postgres@3.4.4))(zod@3.25.32)
hi-base32: hi-base32:
specifier: ^0.5.1 specifier: ^0.5.1
version: 0.5.1 version: 0.5.1
@@ -904,6 +904,9 @@ packages:
'@better-auth/utils@0.2.5': '@better-auth/utils@0.2.5':
resolution: {integrity: sha512-uI2+/8h/zVsH8RrYdG8eUErbuGBk16rZKQfz8CjxQOyCE6v7BqFYEbFwvOkvl1KbUdxhqOnXp78+uE5h8qVEgQ==} resolution: {integrity: sha512-uI2+/8h/zVsH8RrYdG8eUErbuGBk16rZKQfz8CjxQOyCE6v7BqFYEbFwvOkvl1KbUdxhqOnXp78+uE5h8qVEgQ==}
'@better-auth/utils@0.3.0':
resolution: {integrity: sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==}
'@better-fetch/fetch@1.1.18': '@better-fetch/fetch@1.1.18':
resolution: {integrity: sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==} resolution: {integrity: sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==}
@@ -1987,10 +1990,6 @@ packages:
resolution: {integrity: sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==} resolution: {integrity: sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==}
engines: {node: ^14.21.3 || >=16} engines: {node: ^14.21.3 || >=16}
'@noble/hashes@1.8.0':
resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
engines: {node: ^14.21.3 || >=16}
'@nodelib/fs.scandir@2.1.5': '@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -2629,17 +2628,38 @@ packages:
'@peculiar/asn1-android@2.3.16': '@peculiar/asn1-android@2.3.16':
resolution: {integrity: sha512-a1viIv3bIahXNssrOIkXZIlI2ePpZaNmR30d4aBL99mu2rO+mT9D6zBsp7H6eROWGtmwv0Ionp5olJurIo09dw==} resolution: {integrity: sha512-a1viIv3bIahXNssrOIkXZIlI2ePpZaNmR30d4aBL99mu2rO+mT9D6zBsp7H6eROWGtmwv0Ionp5olJurIo09dw==}
'@peculiar/asn1-ecc@2.3.15': '@peculiar/asn1-cms@2.5.0':
resolution: {integrity: sha512-/HtR91dvgog7z/WhCVdxZJ/jitJuIu8iTqiyWVgRE9Ac5imt2sT/E4obqIVGKQw7PIy+X6i8lVBoT6wC73XUgA==} resolution: {integrity: sha512-p0SjJ3TuuleIvjPM4aYfvYw8Fk1Hn/zAVyPJZTtZ2eE9/MIer6/18ROxX6N/e6edVSfvuZBqhxAj3YgsmSjQ/A==}
'@peculiar/asn1-rsa@2.3.15': '@peculiar/asn1-csr@2.5.0':
resolution: {integrity: sha512-p6hsanvPhexRtYSOHihLvUUgrJ8y0FtOM97N5UEpC+VifFYyZa0iZ5cXjTkZoDwxJ/TTJ1IJo3HVTB2JJTpXvg==} resolution: {integrity: sha512-ioigvA6WSYN9h/YssMmmoIwgl3RvZlAYx4A/9jD2qaqXZwGcNlAxaw54eSx2QG1Yu7YyBC5Rku3nNoHrQ16YsQ==}
'@peculiar/asn1-schema@2.3.15': '@peculiar/asn1-ecc@2.5.0':
resolution: {integrity: sha512-QPeD8UA8axQREpgR5UTAfu2mqQmm97oUqahDtNdBcfj3qAnoXzFdQW+aNf/tD2WVXF8Fhmftxoj0eMIT++gX2w==} resolution: {integrity: sha512-t4eYGNhXtLRxaP50h3sfO6aJebUCDGQACoeexcelL4roMFRRVgB20yBIu2LxsPh/tdW9I282gNgMOyg3ywg/mg==}
'@peculiar/asn1-x509@2.3.15': '@peculiar/asn1-pfx@2.5.0':
resolution: {integrity: sha512-0dK5xqTqSLaxv1FHXIcd4Q/BZNuopg+u1l23hT9rOmQ1g4dNtw0g/RnEi+TboB0gOwGtrWn269v27cMgchFIIg==} resolution: {integrity: sha512-Vj0d0wxJZA+Ztqfb7W+/iu8Uasw6hhKtCdLKXLG/P3kEPIQpqGI4P4YXlROfl7gOCqFIbgsj1HzFIFwQ5s20ug==}
'@peculiar/asn1-pkcs8@2.5.0':
resolution: {integrity: sha512-L7599HTI2SLlitlpEP8oAPaJgYssByI4eCwQq2C9eC90otFpm8MRn66PpbKviweAlhinWQ3ZjDD2KIVtx7PaVw==}
'@peculiar/asn1-pkcs9@2.5.0':
resolution: {integrity: sha512-UgqSMBLNLR5TzEZ5ZzxR45Nk6VJrammxd60WMSkofyNzd3DQLSNycGWSK5Xg3UTYbXcDFyG8pA/7/y/ztVCa6A==}
'@peculiar/asn1-rsa@2.5.0':
resolution: {integrity: sha512-qMZ/vweiTHy9syrkkqWFvbT3eLoedvamcUdnnvwyyUNv5FgFXA3KP8td+ATibnlZ0EANW5PYRm8E6MJzEB/72Q==}
'@peculiar/asn1-schema@2.5.0':
resolution: {integrity: sha512-YM/nFfskFJSlHqv59ed6dZlLZqtZQwjRVJ4bBAiWV08Oc+1rSd5lDZcBEx0lGDHfSoH3UziI2pXt2UM33KerPQ==}
'@peculiar/asn1-x509-attr@2.5.0':
resolution: {integrity: sha512-9f0hPOxiJDoG/bfNLAFven+Bd4gwz/VzrCIIWc1025LEI4BXO0U5fOCTNDPbbp2ll+UzqKsZ3g61mpBp74gk9A==}
'@peculiar/asn1-x509@2.5.0':
resolution: {integrity: sha512-CpwtMCTJvfvYTFMuiME5IH+8qmDe3yEWzKHe7OOADbGfq7ohxeLaXwQo0q4du3qs0AII3UbLCvb9NF/6q0oTKQ==}
'@peculiar/x509@1.14.0':
resolution: {integrity: sha512-Yc4PDxN3OrxUPiXgU63c+ZRXKGE8YKF2McTciYhUHFtHVB0KMnjeFSU0qpztGhsp4P0uKix4+J2xEpIEDu8oXg==}
'@petamoriken/float16@3.9.2': '@petamoriken/float16@3.9.2':
resolution: {integrity: sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog==} resolution: {integrity: sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog==}
@@ -3673,11 +3693,11 @@ packages:
'@selderee/plugin-htmlparser2@0.11.0': '@selderee/plugin-htmlparser2@0.11.0':
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
'@simplewebauthn/browser@13.1.0': '@simplewebauthn/browser@13.2.2':
resolution: {integrity: sha512-WuHZ/PYvyPJ9nxSzgHtOEjogBhwJfC8xzYkPC+rR/+8chl/ft4ngjiK8kSU5HtRJfczupyOh33b25TjYbvwAcg==} resolution: {integrity: sha512-FNW1oLQpTJyqG5kkDg5ZsotvWgmBaC6jCHR7Ej0qUNep36Wl9tj2eZu7J5rP+uhXgHaLk+QQ3lqcw2vS5MX1IA==}
'@simplewebauthn/server@13.1.1': '@simplewebauthn/server@13.2.2':
resolution: {integrity: sha512-1hsLpRHfSuMB9ee2aAdh0Htza/X3f4djhYISrggqGe3xopNjOcePiSDkDDoPzDYaaMCrbqGP1H2TYU7bgL9PmA==} resolution: {integrity: sha512-HcWLW28yTMGXpwE9VLx9J+N2KEUaELadLrkPEEI9tpI5la70xNEVEsu/C+m3u7uoq4FulLqZQhgBCzR9IZhFpA==}
engines: {node: '>=20.0.0'} engines: {node: '>=20.0.0'}
'@sinclair/typebox@0.27.8': '@sinclair/typebox@0.27.8':
@@ -4306,8 +4326,8 @@ packages:
better-auth@1.2.8-beta.7: better-auth@1.2.8-beta.7:
resolution: {integrity: sha512-gVApvvhnPVqMCYYLMhxUfbTi5fJYfp9rcsoJSjjTOMV+CIc7KVlYN6Qo8E7ju1JeRU5ae1Wl1NdXrolRJHjmaQ==} resolution: {integrity: sha512-gVApvvhnPVqMCYYLMhxUfbTi5fJYfp9rcsoJSjjTOMV+CIc7KVlYN6Qo8E7ju1JeRU5ae1Wl1NdXrolRJHjmaQ==}
better-call@1.0.9: better-call@1.0.19:
resolution: {integrity: sha512-Qfm0gjk0XQz0oI7qvTK1hbqTsBY4xV2hsHAxF8LZfUYl3RaECCIifXuVqtPpZJWvlCCMlQSvkvhhyuApGUba6g==} resolution: {integrity: sha512-sI3GcA1SCVa3H+CDHl8W8qzhlrckwXOTKhqq3OOPXjgn5aTOMIqGY34zLY/pHA6tRRMjTUC3lz5Mi7EbDA24Kw==}
bignumber.js@9.3.1: bignumber.js@9.3.1:
resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==}
@@ -5754,9 +5774,9 @@ packages:
keyv@4.5.4: keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
kysely@0.28.2: kysely@0.28.7:
resolution: {integrity: sha512-4YAVLoF0Sf0UTqlhgQMFU9iQECdah7n+13ANkiuVfRvlK+uI0Etbgd7bVP36dKlG+NXWbhGua8vnGt+sdhvT7A==} resolution: {integrity: sha512-u/cAuTL4DRIiO2/g4vNGRgklEKNIj5Q3CG7RoUB5DV5SfEC2hMvPxKi0GWPmnzwL2ryIeud2VTcEEmqzTzEPNw==}
engines: {node: '>=18.0.0'} engines: {node: '>=20.0.0'}
leac@0.6.0: leac@0.6.0:
resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==}
@@ -6926,6 +6946,9 @@ packages:
redux@5.0.1: redux@5.0.1:
resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==}
reflect-metadata@0.2.2:
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
refractor@3.6.0: refractor@3.6.0:
resolution: {integrity: sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==} resolution: {integrity: sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==}
@@ -7446,6 +7469,9 @@ packages:
typescript: typescript:
optional: true optional: true
tslib@1.14.1:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
tslib@2.8.1: tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
@@ -7454,6 +7480,10 @@ packages:
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
hasBin: true hasBin: true
tsyringe@4.10.0:
resolution: {integrity: sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==}
engines: {node: '>= 6.0.0'}
tweetnacl@0.14.5: tweetnacl@0.14.5:
resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==}
@@ -7913,6 +7943,8 @@ snapshots:
typescript: 5.8.3 typescript: 5.8.3
uncrypto: 0.1.3 uncrypto: 0.1.3
'@better-auth/utils@0.3.0': {}
'@better-fetch/fetch@1.1.18': {} '@better-fetch/fetch@1.1.18': {}
'@biomejs/biome@2.1.1': '@biomejs/biome@2.1.1':
@@ -8733,8 +8765,6 @@ snapshots:
'@noble/hashes@1.7.1': {} '@noble/hashes@1.7.1': {}
'@noble/hashes@1.8.0': {}
'@nodelib/fs.scandir@2.1.5': '@nodelib/fs.scandir@2.1.5':
dependencies: dependencies:
'@nodelib/fs.stat': 2.0.5 '@nodelib/fs.stat': 2.0.5
@@ -9637,37 +9667,100 @@ snapshots:
'@peculiar/asn1-android@2.3.16': '@peculiar/asn1-android@2.3.16':
dependencies: dependencies:
'@peculiar/asn1-schema': 2.3.15 '@peculiar/asn1-schema': 2.5.0
asn1js: 3.0.6 asn1js: 3.0.6
tslib: 2.8.1 tslib: 2.8.1
'@peculiar/asn1-ecc@2.3.15': '@peculiar/asn1-cms@2.5.0':
dependencies: dependencies:
'@peculiar/asn1-schema': 2.3.15 '@peculiar/asn1-schema': 2.5.0
'@peculiar/asn1-x509': 2.3.15 '@peculiar/asn1-x509': 2.5.0
'@peculiar/asn1-x509-attr': 2.5.0
asn1js: 3.0.6 asn1js: 3.0.6
tslib: 2.8.1 tslib: 2.8.1
'@peculiar/asn1-rsa@2.3.15': '@peculiar/asn1-csr@2.5.0':
dependencies: dependencies:
'@peculiar/asn1-schema': 2.3.15 '@peculiar/asn1-schema': 2.5.0
'@peculiar/asn1-x509': 2.3.15 '@peculiar/asn1-x509': 2.5.0
asn1js: 3.0.6 asn1js: 3.0.6
tslib: 2.8.1 tslib: 2.8.1
'@peculiar/asn1-schema@2.3.15': '@peculiar/asn1-ecc@2.5.0':
dependencies:
'@peculiar/asn1-schema': 2.5.0
'@peculiar/asn1-x509': 2.5.0
asn1js: 3.0.6
tslib: 2.8.1
'@peculiar/asn1-pfx@2.5.0':
dependencies:
'@peculiar/asn1-cms': 2.5.0
'@peculiar/asn1-pkcs8': 2.5.0
'@peculiar/asn1-rsa': 2.5.0
'@peculiar/asn1-schema': 2.5.0
asn1js: 3.0.6
tslib: 2.8.1
'@peculiar/asn1-pkcs8@2.5.0':
dependencies:
'@peculiar/asn1-schema': 2.5.0
'@peculiar/asn1-x509': 2.5.0
asn1js: 3.0.6
tslib: 2.8.1
'@peculiar/asn1-pkcs9@2.5.0':
dependencies:
'@peculiar/asn1-cms': 2.5.0
'@peculiar/asn1-pfx': 2.5.0
'@peculiar/asn1-pkcs8': 2.5.0
'@peculiar/asn1-schema': 2.5.0
'@peculiar/asn1-x509': 2.5.0
'@peculiar/asn1-x509-attr': 2.5.0
asn1js: 3.0.6
tslib: 2.8.1
'@peculiar/asn1-rsa@2.5.0':
dependencies:
'@peculiar/asn1-schema': 2.5.0
'@peculiar/asn1-x509': 2.5.0
asn1js: 3.0.6
tslib: 2.8.1
'@peculiar/asn1-schema@2.5.0':
dependencies: dependencies:
asn1js: 3.0.6 asn1js: 3.0.6
pvtsutils: 1.3.6 pvtsutils: 1.3.6
tslib: 2.8.1 tslib: 2.8.1
'@peculiar/asn1-x509@2.3.15': '@peculiar/asn1-x509-attr@2.5.0':
dependencies: dependencies:
'@peculiar/asn1-schema': 2.3.15 '@peculiar/asn1-schema': 2.5.0
'@peculiar/asn1-x509': 2.5.0
asn1js: 3.0.6
tslib: 2.8.1
'@peculiar/asn1-x509@2.5.0':
dependencies:
'@peculiar/asn1-schema': 2.5.0
asn1js: 3.0.6 asn1js: 3.0.6
pvtsutils: 1.3.6 pvtsutils: 1.3.6
tslib: 2.8.1 tslib: 2.8.1
'@peculiar/x509@1.14.0':
dependencies:
'@peculiar/asn1-cms': 2.5.0
'@peculiar/asn1-csr': 2.5.0
'@peculiar/asn1-ecc': 2.5.0
'@peculiar/asn1-pkcs9': 2.5.0
'@peculiar/asn1-rsa': 2.5.0
'@peculiar/asn1-schema': 2.5.0
'@peculiar/asn1-x509': 2.5.0
pvtsutils: 1.3.6
reflect-metadata: 0.2.2
tslib: 2.8.1
tsyringe: 4.10.0
'@petamoriken/float16@3.9.2': {} '@petamoriken/float16@3.9.2': {}
'@pkgjs/parseargs@0.11.0': '@pkgjs/parseargs@0.11.0':
@@ -10678,17 +10771,18 @@ snapshots:
domhandler: 5.0.3 domhandler: 5.0.3
selderee: 0.11.0 selderee: 0.11.0
'@simplewebauthn/browser@13.1.0': {} '@simplewebauthn/browser@13.2.2': {}
'@simplewebauthn/server@13.1.1': '@simplewebauthn/server@13.2.2':
dependencies: dependencies:
'@hexagon/base64': 1.1.28 '@hexagon/base64': 1.1.28
'@levischuck/tiny-cbor': 0.2.11 '@levischuck/tiny-cbor': 0.2.11
'@peculiar/asn1-android': 2.3.16 '@peculiar/asn1-android': 2.3.16
'@peculiar/asn1-ecc': 2.3.15 '@peculiar/asn1-ecc': 2.5.0
'@peculiar/asn1-rsa': 2.3.15 '@peculiar/asn1-rsa': 2.5.0
'@peculiar/asn1-schema': 2.3.15 '@peculiar/asn1-schema': 2.5.0
'@peculiar/asn1-x509': 2.3.15 '@peculiar/asn1-x509': 2.5.0
'@peculiar/x509': 1.14.0
'@sinclair/typebox@0.27.8': {} '@sinclair/typebox@0.27.8': {}
@@ -11602,18 +11696,19 @@ snapshots:
'@better-auth/utils': 0.2.5 '@better-auth/utils': 0.2.5
'@better-fetch/fetch': 1.1.18 '@better-fetch/fetch': 1.1.18
'@noble/ciphers': 0.6.0 '@noble/ciphers': 0.6.0
'@noble/hashes': 1.8.0 '@noble/hashes': 1.7.1
'@simplewebauthn/browser': 13.1.0 '@simplewebauthn/browser': 13.2.2
'@simplewebauthn/server': 13.1.1 '@simplewebauthn/server': 13.2.2
better-call: 1.0.9 better-call: 1.0.19
defu: 6.1.4 defu: 6.1.4
jose: 5.10.0 jose: 5.10.0
kysely: 0.28.2 kysely: 0.28.7
nanostores: 0.11.4 nanostores: 0.11.4
zod: 3.25.32 zod: 3.25.32
better-call@1.0.9: better-call@1.0.19:
dependencies: dependencies:
'@better-auth/utils': 0.3.0
'@better-fetch/fetch': 1.1.18 '@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
@@ -12181,9 +12276,9 @@ snapshots:
drange@1.1.1: {} drange@1.1.1: {}
drizzle-dbml-generator@0.10.0(drizzle-orm@0.39.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(kysely@0.28.2)(postgres@3.4.4)): drizzle-dbml-generator@0.10.0(drizzle-orm@0.39.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(kysely@0.28.7)(postgres@3.4.4)):
dependencies: dependencies:
drizzle-orm: 0.39.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(kysely@0.28.2)(postgres@3.4.4) drizzle-orm: 0.39.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(kysely@0.28.7)(postgres@3.4.4)
drizzle-kit@0.30.6: drizzle-kit@0.30.6:
dependencies: dependencies:
@@ -12195,16 +12290,16 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
drizzle-orm@0.39.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(kysely@0.28.2)(postgres@3.4.4): drizzle-orm@0.39.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(kysely@0.28.7)(postgres@3.4.4):
optionalDependencies: optionalDependencies:
'@opentelemetry/api': 1.9.0 '@opentelemetry/api': 1.9.0
'@types/pg': 8.6.1 '@types/pg': 8.6.1
kysely: 0.28.2 kysely: 0.28.7
postgres: 3.4.4 postgres: 3.4.4
drizzle-zod@0.5.1(drizzle-orm@0.39.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(kysely@0.28.2)(postgres@3.4.4))(zod@3.25.32): drizzle-zod@0.5.1(drizzle-orm@0.39.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(kysely@0.28.7)(postgres@3.4.4))(zod@3.25.32):
dependencies: dependencies:
drizzle-orm: 0.39.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(kysely@0.28.2)(postgres@3.4.4) drizzle-orm: 0.39.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(kysely@0.28.7)(postgres@3.4.4)
zod: 3.25.32 zod: 3.25.32
dunder-proto@1.0.1: dunder-proto@1.0.1:
@@ -13138,7 +13233,7 @@ snapshots:
dependencies: dependencies:
json-buffer: 3.0.1 json-buffer: 3.0.1
kysely@0.28.2: {} kysely@0.28.7: {}
leac@0.6.0: {} leac@0.6.0: {}
@@ -14424,6 +14519,8 @@ snapshots:
redux@5.0.1: {} redux@5.0.1: {}
reflect-metadata@0.2.2: {}
refractor@3.6.0: refractor@3.6.0:
dependencies: dependencies:
hastscript: 6.0.0 hastscript: 6.0.0
@@ -15031,6 +15128,8 @@ snapshots:
optionalDependencies: optionalDependencies:
typescript: 5.8.3 typescript: 5.8.3
tslib@1.14.1: {}
tslib@2.8.1: {} tslib@2.8.1: {}
tsx@4.16.2: tsx@4.16.2:
@@ -15040,6 +15139,10 @@ snapshots:
optionalDependencies: optionalDependencies:
fsevents: 2.3.3 fsevents: 2.3.3
tsyringe@4.10.0:
dependencies:
tslib: 1.14.1
tweetnacl@0.14.5: {} tweetnacl@0.14.5: {}
type-detect@4.1.0: {} type-detect@4.1.0: {}