mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-16 04:35:24 +02:00
Merge branch 'canary' into feature/add-custom-webhook-notification-provider
This commit is contained in:
@@ -16,11 +16,11 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server
|
||||
|
||||
|
||||
# Deploy only the dokploy app
|
||||
ARG NEXT_PUBLIC_UMAMI_HOST
|
||||
ENV NEXT_PUBLIC_UMAMI_HOST=$NEXT_PUBLIC_UMAMI_HOST
|
||||
# ARG NEXT_PUBLIC_UMAMI_HOST
|
||||
# ENV NEXT_PUBLIC_UMAMI_HOST=$NEXT_PUBLIC_UMAMI_HOST
|
||||
|
||||
ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID
|
||||
ENV NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID
|
||||
# ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID
|
||||
# ENV NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID
|
||||
|
||||
ARG NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
|
||||
ENV NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=$NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
|
||||
|
||||
215
apps/dokploy/__test__/compose/domain/host-rule-format.test.ts
Normal file
215
apps/dokploy/__test__/compose/domain/host-rule-format.test.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import type { Domain } from "@dokploy/server";
|
||||
import { createDomainLabels } from "@dokploy/server";
|
||||
import { parse, stringify } from "yaml";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
/**
|
||||
* Regression tests for Traefik Host rule label format.
|
||||
*
|
||||
* These tests verify that the Host rule is generated with the correct format:
|
||||
* - Host(`domain.com`) - with opening and closing parentheses
|
||||
* - Host(`domain.com`) && PathPrefix(`/path`) - for path-based routing
|
||||
*
|
||||
* Issue: https://github.com/Dokploy/dokploy/issues/3161
|
||||
* The bug caused Host rules to be malformed as Host`domain.com`)
|
||||
* (missing opening parenthesis) which broke all domain routing.
|
||||
*/
|
||||
describe("Host rule format regression tests", () => {
|
||||
const baseDomain: Domain = {
|
||||
host: "example.com",
|
||||
port: 8080,
|
||||
https: false,
|
||||
uniqueConfigKey: 1,
|
||||
customCertResolver: null,
|
||||
certificateType: "none",
|
||||
applicationId: "",
|
||||
composeId: "",
|
||||
domainType: "compose",
|
||||
serviceName: "test-app",
|
||||
domainId: "",
|
||||
path: "/",
|
||||
createdAt: "",
|
||||
previewDeploymentId: "",
|
||||
internalPath: "/",
|
||||
stripPath: false,
|
||||
};
|
||||
|
||||
describe("Host rule format validation", () => {
|
||||
it("should generate Host rule with correct parentheses format", async () => {
|
||||
const labels = await createDomainLabels("test-app", baseDomain, "web");
|
||||
const ruleLabel = labels.find((l) => l.includes(".rule="));
|
||||
|
||||
expect(ruleLabel).toBeDefined();
|
||||
// Verify exact format: Host(`domain`)
|
||||
expect(ruleLabel).toMatch(/Host\(`[^`]+`\)/);
|
||||
// Ensure opening parenthesis is present after Host
|
||||
expect(ruleLabel).toContain("Host(`example.com`)");
|
||||
// Ensure it does NOT have the malformed format
|
||||
expect(ruleLabel).not.toMatch(/Host`[^`]+`\)/);
|
||||
});
|
||||
|
||||
it("should generate PathPrefix with correct parentheses format", async () => {
|
||||
const labels = await createDomainLabels(
|
||||
"test-app",
|
||||
{ ...baseDomain, path: "/api" },
|
||||
"web",
|
||||
);
|
||||
const ruleLabel = labels.find((l) => l.includes(".rule="));
|
||||
|
||||
expect(ruleLabel).toBeDefined();
|
||||
// Verify PathPrefix format
|
||||
expect(ruleLabel).toMatch(/PathPrefix\(`[^`]+`\)/);
|
||||
expect(ruleLabel).toContain("PathPrefix(`/api`)");
|
||||
// Ensure opening parenthesis is present
|
||||
expect(ruleLabel).not.toMatch(/PathPrefix`[^`]+`\)/);
|
||||
});
|
||||
|
||||
it("should generate combined Host and PathPrefix with correct format", async () => {
|
||||
const labels = await createDomainLabels(
|
||||
"test-app",
|
||||
{ ...baseDomain, path: "/api/v1" },
|
||||
"websecure",
|
||||
);
|
||||
const ruleLabel = labels.find((l) => l.includes(".rule="));
|
||||
|
||||
expect(ruleLabel).toBeDefined();
|
||||
expect(ruleLabel).toBe(
|
||||
"traefik.http.routers.test-app-1-websecure.rule=Host(`example.com`) && PathPrefix(`/api/v1`)",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("YAML serialization preserves Host rule format", () => {
|
||||
it("should preserve Host rule format through YAML stringify/parse", async () => {
|
||||
const labels = await createDomainLabels("test-app", baseDomain, "web");
|
||||
const ruleLabel = labels.find((l) => l.includes(".rule="));
|
||||
|
||||
// Simulate compose file structure
|
||||
const composeSpec = {
|
||||
services: {
|
||||
myapp: {
|
||||
image: "nginx",
|
||||
labels: labels,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Stringify to YAML
|
||||
const yamlOutput = stringify(composeSpec, { lineWidth: 1000 });
|
||||
|
||||
// Parse back
|
||||
const parsed = parse(yamlOutput) as typeof composeSpec;
|
||||
const parsedRuleLabel = parsed.services.myapp.labels.find((l: string) =>
|
||||
l.includes(".rule="),
|
||||
);
|
||||
|
||||
// Verify format is preserved
|
||||
expect(parsedRuleLabel).toBe(ruleLabel);
|
||||
expect(parsedRuleLabel).toContain("Host(`example.com`)");
|
||||
expect(parsedRuleLabel).not.toMatch(/Host`[^`]+`\)/);
|
||||
});
|
||||
|
||||
it("should preserve complex rule format through YAML serialization", async () => {
|
||||
const labels = await createDomainLabels(
|
||||
"test-app",
|
||||
{ ...baseDomain, path: "/api", https: true },
|
||||
"websecure",
|
||||
);
|
||||
|
||||
const composeSpec = {
|
||||
services: {
|
||||
myapp: {
|
||||
labels: labels,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const yamlOutput = stringify(composeSpec, { lineWidth: 1000 });
|
||||
const parsed = parse(yamlOutput) as typeof composeSpec;
|
||||
const parsedRuleLabel = parsed.services.myapp.labels.find((l: string) =>
|
||||
l.includes(".rule="),
|
||||
);
|
||||
|
||||
expect(parsedRuleLabel).toContain(
|
||||
"Host(`example.com`) && PathPrefix(`/api`)",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edge cases for domain names", () => {
|
||||
const domainCases = [
|
||||
{ name: "simple domain", host: "example.com" },
|
||||
{ name: "subdomain", host: "app.example.com" },
|
||||
{ name: "deep subdomain", host: "api.v1.app.example.com" },
|
||||
{ name: "numeric domain", host: "123.example.com" },
|
||||
{ name: "hyphenated domain", host: "my-app.example-host.com" },
|
||||
{ name: "localhost", host: "localhost" },
|
||||
{ name: "IP address style", host: "192.168.1.100" },
|
||||
];
|
||||
|
||||
for (const { name, host } of domainCases) {
|
||||
it(`should generate correct Host rule for ${name}: ${host}`, async () => {
|
||||
const labels = await createDomainLabels(
|
||||
"test-app",
|
||||
{ ...baseDomain, host },
|
||||
"web",
|
||||
);
|
||||
const ruleLabel = labels.find((l) => l.includes(".rule="));
|
||||
|
||||
expect(ruleLabel).toBeDefined();
|
||||
expect(ruleLabel).toContain(`Host(\`${host}\`)`);
|
||||
// Verify parenthesis is present
|
||||
expect(ruleLabel).toMatch(
|
||||
new RegExp(`Host\\(\\\`${host.replace(/\./g, "\\.")}\\\`\\)`),
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("Multiple domains scenario", () => {
|
||||
it("should generate correct format for both web and websecure entrypoints", async () => {
|
||||
const webLabels = await createDomainLabels("test-app", baseDomain, "web");
|
||||
const websecureLabels = await createDomainLabels(
|
||||
"test-app",
|
||||
baseDomain,
|
||||
"websecure",
|
||||
);
|
||||
|
||||
const webRule = webLabels.find((l) => l.includes(".rule="));
|
||||
const websecureRule = websecureLabels.find((l) => l.includes(".rule="));
|
||||
|
||||
// Both should have correct format
|
||||
expect(webRule).toContain("Host(`example.com`)");
|
||||
expect(websecureRule).toContain("Host(`example.com`)");
|
||||
|
||||
// Neither should have malformed format
|
||||
expect(webRule).not.toMatch(/Host`[^`]+`\)/);
|
||||
expect(websecureRule).not.toMatch(/Host`[^`]+`\)/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Special characters in paths", () => {
|
||||
const pathCases = [
|
||||
{ name: "simple path", path: "/api" },
|
||||
{ name: "nested path", path: "/api/v1/users" },
|
||||
{ name: "path with hyphen", path: "/api-v1" },
|
||||
{ name: "path with underscore", path: "/api_v1" },
|
||||
];
|
||||
|
||||
for (const { name, path } of pathCases) {
|
||||
it(`should generate correct PathPrefix for ${name}: ${path}`, async () => {
|
||||
const labels = await createDomainLabels(
|
||||
"test-app",
|
||||
{ ...baseDomain, path },
|
||||
"web",
|
||||
);
|
||||
const ruleLabel = labels.find((l) => l.includes(".rule="));
|
||||
|
||||
expect(ruleLabel).toBeDefined();
|
||||
expect(ruleLabel).toContain(`PathPrefix(\`${path}\`)`);
|
||||
// Verify parenthesis is present
|
||||
expect(ruleLabel).not.toMatch(/PathPrefix`[^`]+`\)/);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -189,7 +189,7 @@ describe("deployApplication - Command Generation Tests", () => {
|
||||
|
||||
it("should verify nixpacks command is called with correct app", async () => {
|
||||
const mockNixpacksCommand = "nixpacks build /path/to/app --name test-app";
|
||||
vi.mocked(builders.getBuildCommand).mockReturnValue(mockNixpacksCommand);
|
||||
vi.mocked(builders.getBuildCommand).mockResolvedValue(mockNixpacksCommand);
|
||||
|
||||
await deployApplication({
|
||||
applicationId: "test-app-id",
|
||||
@@ -220,7 +220,7 @@ describe("deployApplication - Command Generation Tests", () => {
|
||||
);
|
||||
|
||||
const mockRailpackCommand = "railpack prepare /path/to/app";
|
||||
vi.mocked(builders.getBuildCommand).mockReturnValue(mockRailpackCommand);
|
||||
vi.mocked(builders.getBuildCommand).mockResolvedValue(mockRailpackCommand);
|
||||
|
||||
await deployApplication({
|
||||
applicationId: "test-app-id",
|
||||
@@ -241,7 +241,7 @@ describe("deployApplication - Command Generation Tests", () => {
|
||||
|
||||
it("should execute commands in correct order", async () => {
|
||||
const mockNixpacksCommand = "nixpacks build";
|
||||
vi.mocked(builders.getBuildCommand).mockReturnValue(mockNixpacksCommand);
|
||||
vi.mocked(builders.getBuildCommand).mockResolvedValue(mockNixpacksCommand);
|
||||
|
||||
await deployApplication({
|
||||
applicationId: "test-app-id",
|
||||
@@ -260,7 +260,7 @@ describe("deployApplication - Command Generation Tests", () => {
|
||||
|
||||
it("should include log redirection in command", async () => {
|
||||
const mockCommand = "nixpacks build";
|
||||
vi.mocked(builders.getBuildCommand).mockReturnValue(mockCommand);
|
||||
vi.mocked(builders.getBuildCommand).mockResolvedValue(mockCommand);
|
||||
|
||||
await deployApplication({
|
||||
applicationId: "test-app-id",
|
||||
|
||||
@@ -41,6 +41,9 @@ const baseApp: ApplicationNested = {
|
||||
giteaRepository: "",
|
||||
cleanCache: false,
|
||||
watchPaths: [],
|
||||
rollbackRegistryId: "",
|
||||
rollbackRegistry: null,
|
||||
deployments: [],
|
||||
enableSubmodules: false,
|
||||
applicationStatus: "done",
|
||||
triggerType: "push",
|
||||
|
||||
@@ -18,6 +18,8 @@ const baseAdmin: User = {
|
||||
enablePaidFeatures: false,
|
||||
allowImpersonation: false,
|
||||
role: "user",
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
metricsConfig: {
|
||||
containers: {
|
||||
refreshRate: 20,
|
||||
@@ -61,7 +63,6 @@ const baseAdmin: User = {
|
||||
expirationDate: "",
|
||||
id: "",
|
||||
isRegistered: false,
|
||||
name: "",
|
||||
createdAt2: new Date().toISOString(),
|
||||
emailVerified: false,
|
||||
image: "",
|
||||
|
||||
@@ -17,6 +17,9 @@ const baseApp: ApplicationNested = {
|
||||
giteaBuildPath: "",
|
||||
giteaId: "",
|
||||
args: [],
|
||||
rollbackRegistryId: "",
|
||||
rollbackRegistry: null,
|
||||
deployments: [],
|
||||
cleanCache: false,
|
||||
applicationStatus: "done",
|
||||
endpointSpecSwarm: null,
|
||||
|
||||
@@ -143,7 +143,7 @@ export const ShowDeployments = ({
|
||||
See the last 10 deployments for this {type}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<div className="flex flex-row items-center flex-wrap gap-2">
|
||||
{(type === "application" || type === "compose") && (
|
||||
<KillBuild id={id} type={type} />
|
||||
)}
|
||||
@@ -373,7 +373,19 @@ export const ShowDeployments = ({
|
||||
type === "application" && (
|
||||
<DialogAction
|
||||
title="Rollback to this deployment"
|
||||
description="Are you sure you want to rollback to this deployment?"
|
||||
description={
|
||||
<div className="flex flex-col gap-3">
|
||||
<p>
|
||||
Are you sure you want to rollback to this
|
||||
deployment?
|
||||
</p>
|
||||
<AlertBlock type="info" className="text-sm">
|
||||
Please wait a few seconds while the image is
|
||||
pulled from the registry. Your application
|
||||
should be running shortly.
|
||||
</AlertBlock>
|
||||
</div>
|
||||
}
|
||||
type="default"
|
||||
onClick={async () => {
|
||||
await rollback({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -20,13 +21,37 @@ import {
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const formSchema = z.object({
|
||||
rollbackActive: z.boolean(),
|
||||
});
|
||||
const formSchema = z
|
||||
.object({
|
||||
rollbackActive: z.boolean(),
|
||||
rollbackRegistryId: z.string().optional(),
|
||||
})
|
||||
.superRefine((values, ctx) => {
|
||||
if (
|
||||
values.rollbackActive &&
|
||||
(!values.rollbackRegistryId || values.rollbackRegistryId === "none")
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["rollbackRegistryId"],
|
||||
message: "Registry is required when rollbacks are enabled",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
@@ -49,17 +74,33 @@ export const ShowRollbackSettings = ({ applicationId, children }: Props) => {
|
||||
const { mutateAsync: updateApplication, isLoading } =
|
||||
api.application.update.useMutation();
|
||||
|
||||
const { data: registries } = api.registry.all.useQuery();
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
rollbackActive: application?.rollbackActive ?? false,
|
||||
rollbackRegistryId: application?.rollbackRegistryId || "",
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (application) {
|
||||
form.reset({
|
||||
rollbackActive: application.rollbackActive ?? false,
|
||||
rollbackRegistryId: application.rollbackRegistryId || "",
|
||||
});
|
||||
}
|
||||
}, [application, form]);
|
||||
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
await updateApplication({
|
||||
applicationId,
|
||||
rollbackActive: data.rollbackActive,
|
||||
rollbackRegistryId:
|
||||
data.rollbackRegistryId === "none" || !data.rollbackRegistryId
|
||||
? null
|
||||
: data.rollbackRegistryId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success("Rollback settings updated");
|
||||
@@ -112,6 +153,65 @@ export const ShowRollbackSettings = ({ applicationId, children }: Props) => {
|
||||
)}
|
||||
/>
|
||||
|
||||
{form.watch("rollbackActive") && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="rollbackRegistryId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Rollback Registry</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value || "none"}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a registry" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="none">
|
||||
<span className="flex items-center gap-2">
|
||||
<span>None</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
{registries?.map((registry) => (
|
||||
<SelectItem
|
||||
key={registry.registryId}
|
||||
value={registry.registryId}
|
||||
>
|
||||
{registry.registryName}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>
|
||||
Registries ({registries?.length || 0})
|
||||
</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{!registries || registries.length === 0 ? (
|
||||
<FormDescription className="text-amber-600 dark:text-amber-500">
|
||||
No registries available. Please{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/registry"
|
||||
className="underline font-medium hover:text-amber-700 dark:hover:text-amber-400"
|
||||
>
|
||||
configure a registry
|
||||
</Link>{" "}
|
||||
first to enable rollbacks.
|
||||
</FormDescription>
|
||||
) : (
|
||||
<FormDescription>
|
||||
Select a registry where rollback images will be stored.
|
||||
</FormDescription>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button type="submit" className="w-full" isLoading={isLoading}>
|
||||
Save Settings
|
||||
</Button>
|
||||
|
||||
@@ -103,7 +103,7 @@ export const ImpersonationBar = () => {
|
||||
setOpen(false);
|
||||
|
||||
toast.success("Successfully impersonating user", {
|
||||
description: `You are now viewing as ${selectedUser.name || selectedUser.email}`,
|
||||
description: `You are now viewing as ${`${selectedUser.name} ${selectedUser.lastName}`.trim() || selectedUser.email}`,
|
||||
});
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
@@ -195,7 +195,8 @@ export const ImpersonationBar = () => {
|
||||
<UserIcon className="mr-2 h-4 w-4 flex-shrink-0" />
|
||||
<span className="truncate flex flex-col items-start">
|
||||
<span className="text-sm font-medium">
|
||||
{selectedUser.name || ""}
|
||||
{`${selectedUser.name} ${selectedUser.lastName}`.trim() ||
|
||||
""}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{selectedUser.email}
|
||||
@@ -242,7 +243,8 @@ export const ImpersonationBar = () => {
|
||||
<UserIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<span className="flex flex-col items-start">
|
||||
<span className="text-sm font-medium">
|
||||
{user.name || ""}
|
||||
{`${user.name} ${user.lastName}`.trim() ||
|
||||
""}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{user.email} • {user.role}
|
||||
@@ -283,10 +285,14 @@ export const ImpersonationBar = () => {
|
||||
<AvatarImage
|
||||
className="object-cover"
|
||||
src={data?.user?.image || ""}
|
||||
alt={data?.user?.name || ""}
|
||||
alt={
|
||||
`${data?.user?.firstName} ${data?.user?.lastName}`.trim() ||
|
||||
""
|
||||
}
|
||||
/>
|
||||
<AvatarFallback>
|
||||
{data?.user?.name?.slice(0, 2).toUpperCase() || "U"}
|
||||
{`${data?.user?.firstName?.[0] || ""}${data?.user?.lastName?.[0] || ""}`.toUpperCase() ||
|
||||
"U"}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-col gap-1">
|
||||
@@ -299,7 +305,8 @@ export const ImpersonationBar = () => {
|
||||
Impersonating
|
||||
</Badge>
|
||||
<span className="font-medium">
|
||||
{data?.user?.name || ""}
|
||||
{`${data?.user?.firstName} ${data?.user?.lastName}`.trim() ||
|
||||
""}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-sm text-muted-foreground flex-wrap">
|
||||
|
||||
@@ -10,7 +10,7 @@ import { DockerNetworkChart } from "./docker-network-chart";
|
||||
|
||||
const defaultData = {
|
||||
cpu: {
|
||||
value: 0,
|
||||
value: "0%",
|
||||
time: "",
|
||||
},
|
||||
memory: {
|
||||
@@ -46,7 +46,7 @@ interface Props {
|
||||
}
|
||||
export interface DockerStats {
|
||||
cpu: {
|
||||
value: number;
|
||||
value: string;
|
||||
time: string;
|
||||
};
|
||||
memory: {
|
||||
@@ -220,7 +220,13 @@ export const ContainerFreeMonitoring = ({
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Used: {currentData.cpu.value}
|
||||
</span>
|
||||
<Progress value={currentData.cpu.value} className="w-[100%]" />
|
||||
<Progress
|
||||
value={Number.parseInt(
|
||||
currentData.cpu.value.replace("%", ""),
|
||||
10,
|
||||
)}
|
||||
className="w-[100%]"
|
||||
/>
|
||||
<DockerCpuChart acummulativeData={acummulativeData.cpu} />
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -65,6 +65,25 @@ export const ShowRequests = () => {
|
||||
to: Date | undefined;
|
||||
}>(getDefaultDateRange());
|
||||
|
||||
// Check if logs exist to determine if traefik has been reloaded
|
||||
// Only fetch when active to minimize network calls
|
||||
const { data: statsLogsCheck } = api.settings.readStatsLogs.useQuery(
|
||||
{
|
||||
page: {
|
||||
pageIndex: 0,
|
||||
pageSize: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
enabled: !!isActive,
|
||||
refetchInterval: 5000, // Check every 5 seconds when active
|
||||
},
|
||||
);
|
||||
|
||||
// Determine if warning should be shown
|
||||
// Show warning only if active but no logs exist yet
|
||||
const shouldShowWarning = isActive && (statsLogsCheck?.totalCount ?? 0) === 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (logCleanupStatus) {
|
||||
setCronExpression(logCleanupStatus.cronExpression || "0 0 * * *");
|
||||
@@ -85,16 +104,18 @@ export const ShowRequests = () => {
|
||||
See all the incoming requests that pass trough Traefik
|
||||
</CardDescription>
|
||||
|
||||
<AlertBlock type="warning">
|
||||
When you activate, you need to reload traefik to apply the
|
||||
changes, you can reload traefik in{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/server"
|
||||
className="text-primary"
|
||||
>
|
||||
Settings
|
||||
</Link>
|
||||
</AlertBlock>
|
||||
{shouldShowWarning && (
|
||||
<AlertBlock type="warning">
|
||||
When you activate, you need to reload traefik to apply the
|
||||
changes, you can reload traefik in{" "}
|
||||
<Link
|
||||
href="/dashboard/settings/server"
|
||||
className="text-primary"
|
||||
>
|
||||
Settings
|
||||
</Link>
|
||||
</AlertBlock>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 py-8 border-t">
|
||||
<div className="flex w-full gap-4 justify-end items-center">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -41,6 +41,7 @@ const profileSchema = z.object({
|
||||
currentPassword: z.string().nullable(),
|
||||
image: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
lastName: z.string().optional(),
|
||||
allowImpersonation: z.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
@@ -88,7 +89,8 @@ export const ProfileForm = () => {
|
||||
image: data?.user?.image || "",
|
||||
currentPassword: "",
|
||||
allowImpersonation: data?.user?.allowImpersonation || false,
|
||||
name: data?.user?.name || "",
|
||||
name: data?.user?.firstName || "",
|
||||
lastName: data?.user?.lastName || "",
|
||||
},
|
||||
resolver: zodResolver(profileSchema),
|
||||
});
|
||||
@@ -102,7 +104,8 @@ export const ProfileForm = () => {
|
||||
image: data?.user?.image || "",
|
||||
currentPassword: form.getValues("currentPassword") || "",
|
||||
allowImpersonation: data?.user?.allowImpersonation,
|
||||
name: data?.user?.name || "",
|
||||
name: data?.user?.firstName || "",
|
||||
lastName: data?.user?.lastName || "",
|
||||
},
|
||||
{
|
||||
keepValues: true,
|
||||
@@ -127,6 +130,7 @@ export const ProfileForm = () => {
|
||||
currentPassword: values.currentPassword || undefined,
|
||||
allowImpersonation: values.allowImpersonation,
|
||||
name: values.name || undefined,
|
||||
lastName: values.lastName || undefined,
|
||||
});
|
||||
await refetch();
|
||||
toast.success("Profile Updated");
|
||||
@@ -136,6 +140,7 @@ export const ProfileForm = () => {
|
||||
image: values.image,
|
||||
currentPassword: "",
|
||||
name: values.name || "",
|
||||
lastName: values.lastName || "",
|
||||
});
|
||||
} catch (error) {
|
||||
toast.error("Error updating the profile");
|
||||
@@ -180,9 +185,22 @@ export const ProfileForm = () => {
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormLabel>First Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Name" {...field} />
|
||||
<Input placeholder="John" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="lastName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Last Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Doe" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -280,7 +298,7 @@ export const ProfileForm = () => {
|
||||
<Avatar className="default-avatar h-12 w-12 rounded-full border hover:p-px hover:border-primary transition-transform">
|
||||
<AvatarFallback className="rounded-lg">
|
||||
{getFallbackAvatarInitials(
|
||||
data?.user?.name,
|
||||
`${data?.user?.firstName} ${data?.user?.lastName}`.trim(),
|
||||
)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -85,7 +87,26 @@ export const ShowTraefikActions = ({ serverId }: Props) => {
|
||||
</DropdownMenuItem>
|
||||
</EditTraefikEnv>
|
||||
|
||||
<DropdownMenuItem
|
||||
<DialogAction
|
||||
title={
|
||||
haveTraefikDashboardPortEnabled
|
||||
? "Disable Traefik Dashboard"
|
||||
: "Enable Traefik Dashboard"
|
||||
}
|
||||
description={
|
||||
<div className="space-y-4">
|
||||
<AlertBlock type="warning">
|
||||
The Traefik container will be recreated from scratch. This
|
||||
means the container will be deleted and created again, which
|
||||
may cause downtime in your applications.
|
||||
</AlertBlock>
|
||||
<p>
|
||||
Are you sure you want to{" "}
|
||||
{haveTraefikDashboardPortEnabled ? "disable" : "enable"} the
|
||||
Traefik dashboard?
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
onClick={async () => {
|
||||
await toggleDashboard({
|
||||
enableDashboard: !haveTraefikDashboardPortEnabled,
|
||||
@@ -97,14 +118,26 @@ export const ShowTraefikActions = ({ serverId }: Props) => {
|
||||
);
|
||||
refetchDashboard();
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch((error) => {
|
||||
const errorMessage =
|
||||
error?.message ||
|
||||
"Failed to toggle dashboard. Please check if port 8080 is available.";
|
||||
toast.error(errorMessage);
|
||||
});
|
||||
}}
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
disabled={toggleDashboardIsLoading}
|
||||
type="default"
|
||||
>
|
||||
<span>
|
||||
{haveTraefikDashboardPortEnabled ? "Disable" : "Enable"} Dashboard
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
>
|
||||
<span>
|
||||
{haveTraefikDashboardPortEnabled ? "Disable" : "Enable"}{" "}
|
||||
Dashboard
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
</DialogAction>
|
||||
<ManageTraefikPorts serverId={serverId}>
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
|
||||
@@ -158,6 +158,7 @@ export const AddInvitation = () => {
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="member">Member</SelectItem>
|
||||
<SelectItem value="admin">Admin</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
|
||||
159
apps/dokploy/components/dashboard/settings/users/change-role.tsx
Normal file
159
apps/dokploy/components/dashboard/settings/users/change-role.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const changeRoleSchema = z.object({
|
||||
role: z.enum(["admin", "member"]),
|
||||
});
|
||||
|
||||
type ChangeRoleSchema = z.infer<typeof changeRoleSchema>;
|
||||
|
||||
interface Props {
|
||||
memberId: string;
|
||||
currentRole: "admin" | "member";
|
||||
userEmail: string;
|
||||
}
|
||||
|
||||
export const ChangeRole = ({ memberId, currentRole, userEmail }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { mutateAsync, isError, error, isLoading } =
|
||||
api.organization.updateMemberRole.useMutation();
|
||||
|
||||
const form = useForm<ChangeRoleSchema>({
|
||||
defaultValues: {
|
||||
role: currentRole,
|
||||
},
|
||||
resolver: zodResolver(changeRoleSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
form.reset({
|
||||
role: currentRole,
|
||||
});
|
||||
}
|
||||
}, [form, currentRole, isOpen]);
|
||||
|
||||
const onSubmit = async (data: ChangeRoleSchema) => {
|
||||
await mutateAsync({
|
||||
memberId,
|
||||
role: data.role,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Role updated successfully");
|
||||
await utils.user.all.invalidate();
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error?.message || "Error updating role");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger className="" asChild>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Change Role
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-[85vh] sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Change User Role</DialogTitle>
|
||||
<DialogDescription>
|
||||
Change the role for <strong>{userEmail}</strong>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-change-role"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="w-full space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="role"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Role</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a role" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="admin">Admin</SelectItem>
|
||||
<SelectItem value="member">Member</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
<strong>Admin:</strong> Can manage users and settings.
|
||||
<br />
|
||||
<strong>Member:</strong> Limited permissions, can be
|
||||
customized.
|
||||
<br />
|
||||
<em className="text-muted-foreground text-xs">
|
||||
Note: Owner role is intransferible.
|
||||
</em>
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
form="hook-form-change-role"
|
||||
type="submit"
|
||||
>
|
||||
Update Role
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -29,12 +29,15 @@ import {
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { api } from "@/utils/api";
|
||||
import { AddUserPermissions } from "./add-permissions";
|
||||
import { ChangeRole } from "./change-role";
|
||||
|
||||
export const ShowUsers = () => {
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data, isLoading, refetch } = api.user.all.useQuery();
|
||||
const { mutateAsync } = api.user.remove.useMutation();
|
||||
|
||||
const utils = api.useUtils();
|
||||
const { data: session } = authClient.useSession();
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
@@ -81,6 +84,52 @@ export const ShowUsers = () => {
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data?.map((member) => {
|
||||
const currentUserRole = data?.find(
|
||||
(m) => m.user.id === session?.user?.id,
|
||||
)?.role;
|
||||
|
||||
// Owner never has "Edit Permissions" (they're absolute owner)
|
||||
// Other users can edit permissions if target is not themselves and target is a member
|
||||
const canEditPermissions =
|
||||
member.role !== "owner" &&
|
||||
member.role === "member" &&
|
||||
member.user.id !== session?.user?.id;
|
||||
|
||||
// Can change role based on hierarchy:
|
||||
// - Owner: Can change anyone's role (except themselves and other owners)
|
||||
// - Admin: Can only change member roles (not other admins or owners)
|
||||
// - Owner role is intransferible
|
||||
const canChangeRole =
|
||||
member.role !== "owner" &&
|
||||
member.user.id !== session?.user?.id &&
|
||||
(currentUserRole === "owner" ||
|
||||
(currentUserRole === "admin" &&
|
||||
member.role === "member"));
|
||||
|
||||
// Delete/Unlink follow same hierarchy as role changes
|
||||
// - Owner: Can delete/unlink anyone (except themselves and owner can't be deleted)
|
||||
// - Admin: Can only delete/unlink members (not other admins or owner)
|
||||
const canDelete =
|
||||
member.role !== "owner" &&
|
||||
!isCloud &&
|
||||
member.user.id !== session?.user?.id &&
|
||||
(currentUserRole === "owner" ||
|
||||
(currentUserRole === "admin" &&
|
||||
member.role === "member"));
|
||||
|
||||
const canUnlink =
|
||||
member.role !== "owner" &&
|
||||
member.user.id !== session?.user?.id &&
|
||||
(currentUserRole === "owner" ||
|
||||
(currentUserRole === "admin" &&
|
||||
member.role === "member"));
|
||||
|
||||
const hasAnyAction =
|
||||
canEditPermissions ||
|
||||
canChangeRole ||
|
||||
canDelete ||
|
||||
canUnlink;
|
||||
|
||||
return (
|
||||
<TableRow key={member.id}>
|
||||
<TableCell className="w-[100px]">
|
||||
@@ -109,7 +158,7 @@ export const ShowUsers = () => {
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-right flex justify-end">
|
||||
{member.role !== "owner" && (
|
||||
{hasAnyAction ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
@@ -127,11 +176,23 @@ export const ShowUsers = () => {
|
||||
Actions
|
||||
</DropdownMenuLabel>
|
||||
|
||||
<AddUserPermissions
|
||||
userId={member.user.id}
|
||||
/>
|
||||
{canChangeRole && (
|
||||
<ChangeRole
|
||||
memberId={member.id}
|
||||
currentRole={
|
||||
member.role as "admin" | "member"
|
||||
}
|
||||
userEmail={member.user.email}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isCloud && (
|
||||
{canEditPermissions && (
|
||||
<AddUserPermissions
|
||||
userId={member.user.id}
|
||||
/>
|
||||
)}
|
||||
|
||||
{canDelete && (
|
||||
<DialogAction
|
||||
title="Delete User"
|
||||
description="Are you sure you want to delete this user?"
|
||||
@@ -146,9 +207,10 @@ export const ShowUsers = () => {
|
||||
);
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((err) => {
|
||||
toast.error(
|
||||
"Error deleting destination",
|
||||
err?.message ||
|
||||
"Error deleting user",
|
||||
);
|
||||
});
|
||||
}}
|
||||
@@ -162,66 +224,79 @@ export const ShowUsers = () => {
|
||||
</DialogAction>
|
||||
)}
|
||||
|
||||
<DialogAction
|
||||
title="Unlink User"
|
||||
description="Are you sure you want to unlink this user?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
if (!isCloud) {
|
||||
const orgCount =
|
||||
await utils.user.checkUserOrganizations.fetch(
|
||||
{
|
||||
{canUnlink && (
|
||||
<DialogAction
|
||||
title="Unlink User"
|
||||
description="Are you sure you want to unlink this user?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
if (!isCloud) {
|
||||
const orgCount =
|
||||
await utils.user.checkUserOrganizations.fetch(
|
||||
{
|
||||
userId: member.user.id,
|
||||
},
|
||||
);
|
||||
|
||||
if (orgCount === 1) {
|
||||
await mutateAsync({
|
||||
userId: member.user.id,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(
|
||||
"User deleted successfully",
|
||||
);
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error deleting user",
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { error } =
|
||||
await authClient.organization.removeMember(
|
||||
{
|
||||
memberIdOrEmail: member.id,
|
||||
},
|
||||
);
|
||||
|
||||
console.log(orgCount);
|
||||
|
||||
if (orgCount === 1) {
|
||||
await mutateAsync({
|
||||
userId: member.user.id,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(
|
||||
"User deleted successfully",
|
||||
);
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
"Error deleting user",
|
||||
);
|
||||
});
|
||||
return;
|
||||
if (!error) {
|
||||
toast.success(
|
||||
"User unlinked successfully",
|
||||
);
|
||||
refetch();
|
||||
} else {
|
||||
toast.error(
|
||||
"Error unlinking user",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const { error } =
|
||||
await authClient.organization.removeMember(
|
||||
{
|
||||
memberIdOrEmail: member.id,
|
||||
},
|
||||
);
|
||||
|
||||
if (!error) {
|
||||
toast.success(
|
||||
"User unlinked successfully",
|
||||
);
|
||||
refetch();
|
||||
} else {
|
||||
toast.error("Error unlinking user");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
}}
|
||||
>
|
||||
Unlink User
|
||||
</DropdownMenuItem>
|
||||
</DialogAction>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Unlink User
|
||||
</DropdownMenuItem>
|
||||
</DialogAction>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0"
|
||||
disabled
|
||||
>
|
||||
<span className="sr-only">
|
||||
No actions available
|
||||
</span>
|
||||
<MoreHorizontal className="h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -105,7 +105,9 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
||||
});
|
||||
toast.success(t("settings.server.webServer.traefik.portsUpdated"));
|
||||
setOpen(false);
|
||||
} catch {}
|
||||
} catch (error) {
|
||||
toast.error((error as Error).message || "Error updating Traefik ports");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -156,11 +158,11 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<ScrollArea className="h-[400px] pr-4">
|
||||
<ScrollArea className="pr-4">
|
||||
<div className="grid gap-4">
|
||||
{fields.map((field, index) => (
|
||||
<Card key={field.id} className="bg-transparent">
|
||||
<CardContent className="grid grid-cols-[1fr_1fr_1fr_auto] gap-4 p-4 transparent">
|
||||
<CardContent className="grid grid-cols-4 gap-4 p-4 transparent">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`ports.${index}.targetPort`}
|
||||
@@ -303,6 +305,12 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
||||
</div>
|
||||
</AlertBlock>
|
||||
)}
|
||||
|
||||
<AlertBlock type="warning">
|
||||
The Traefik container will be recreated from scratch. This
|
||||
means the container will be deleted and created again, which
|
||||
may cause downtime in your applications.
|
||||
</AlertBlock>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
|
||||
@@ -158,7 +158,8 @@ const MENU: Menu = {
|
||||
url: "/dashboard/schedules",
|
||||
icon: Clock,
|
||||
// Only enabled in non-cloud environments
|
||||
isEnabled: ({ isCloud, auth }) => !isCloud && auth?.role === "owner",
|
||||
isEnabled: ({ isCloud, auth }) =>
|
||||
!isCloud && (auth?.role === "owner" || auth?.role === "admin"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -168,7 +169,9 @@ const MENU: Menu = {
|
||||
// Only enabled for admins and users with access to Traefik files in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!(
|
||||
(auth?.role === "owner" || auth?.canAccessToTraefikFiles) &&
|
||||
(auth?.role === "owner" ||
|
||||
auth?.role === "admin" ||
|
||||
auth?.canAccessToTraefikFiles) &&
|
||||
!isCloud
|
||||
),
|
||||
},
|
||||
@@ -179,7 +182,12 @@ const MENU: Menu = {
|
||||
icon: BlocksIcon,
|
||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!((auth?.role === "owner" || auth?.canAccessToDocker) && !isCloud),
|
||||
!!(
|
||||
(auth?.role === "owner" ||
|
||||
auth?.role === "admin" ||
|
||||
auth?.canAccessToDocker) &&
|
||||
!isCloud
|
||||
),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -188,7 +196,12 @@ const MENU: Menu = {
|
||||
icon: PieChart,
|
||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!((auth?.role === "owner" || auth?.canAccessToDocker) && !isCloud),
|
||||
!!(
|
||||
(auth?.role === "owner" ||
|
||||
auth?.role === "admin" ||
|
||||
auth?.canAccessToDocker) &&
|
||||
!isCloud
|
||||
),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -197,7 +210,12 @@ const MENU: Menu = {
|
||||
icon: Forward,
|
||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!((auth?.role === "owner" || auth?.canAccessToDocker) && !isCloud),
|
||||
!!(
|
||||
(auth?.role === "owner" ||
|
||||
auth?.role === "admin" ||
|
||||
auth?.canAccessToDocker) &&
|
||||
!isCloud
|
||||
),
|
||||
},
|
||||
|
||||
// Legacy unused menu, adjusted to the new structure
|
||||
@@ -264,7 +282,8 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/server",
|
||||
icon: Activity,
|
||||
// Only enabled for admins in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud),
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!((auth?.role === "owner" || auth?.role === "admin") && !isCloud),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -278,7 +297,8 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/servers",
|
||||
icon: Server,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
isEnabled: ({ auth }) =>
|
||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -286,7 +306,8 @@ const MENU: Menu = {
|
||||
icon: Users,
|
||||
url: "/dashboard/settings/users",
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
isEnabled: ({ auth }) =>
|
||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -295,14 +316,19 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/ssh-keys",
|
||||
// Only enabled for admins and users with access to SSH keys
|
||||
isEnabled: ({ auth }) =>
|
||||
!!(auth?.role === "owner" || auth?.canAccessToSSHKeys),
|
||||
!!(
|
||||
auth?.role === "owner" ||
|
||||
auth?.canAccessToSSHKeys ||
|
||||
auth?.role === "admin"
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "AI",
|
||||
icon: BotIcon,
|
||||
url: "/dashboard/settings/ai",
|
||||
isSingle: true,
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
isEnabled: ({ auth }) =>
|
||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -311,7 +337,11 @@ const MENU: Menu = {
|
||||
icon: GitBranch,
|
||||
// Only enabled for admins and users with access to Git providers
|
||||
isEnabled: ({ auth }) =>
|
||||
!!(auth?.role === "owner" || auth?.canAccessToGitProviders),
|
||||
!!(
|
||||
auth?.role === "owner" ||
|
||||
auth?.canAccessToGitProviders ||
|
||||
auth?.role === "admin"
|
||||
),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -319,7 +349,8 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/registry",
|
||||
icon: Package,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
isEnabled: ({ auth }) =>
|
||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -327,7 +358,8 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/destinations",
|
||||
icon: Database,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
isEnabled: ({ auth }) =>
|
||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||
},
|
||||
|
||||
{
|
||||
@@ -336,7 +368,8 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/certificates",
|
||||
icon: ShieldCheck,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
isEnabled: ({ auth }) =>
|
||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -344,7 +377,8 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/cluster",
|
||||
icon: Boxes,
|
||||
// Only enabled for admins in non-cloud environments
|
||||
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud),
|
||||
isEnabled: ({ auth, isCloud }) =>
|
||||
!!((auth?.role === "owner" || auth?.role === "admin") && !isCloud),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -352,7 +386,8 @@ const MENU: Menu = {
|
||||
url: "/dashboard/settings/notifications",
|
||||
icon: Bell,
|
||||
// Only enabled for admins
|
||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||
isEnabled: ({ auth }) =>
|
||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
||||
},
|
||||
{
|
||||
isSingle: true,
|
||||
@@ -718,7 +753,9 @@ function SidebarLogo() {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{(user?.role === "owner" || isCloud) && (
|
||||
{(user?.role === "owner" ||
|
||||
user?.role === "admin" ||
|
||||
isCloud) && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<AddOrganization />
|
||||
@@ -1082,7 +1119,7 @@ export default function Page({ children }: Props) {
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<SidebarMenu className="flex flex-col gap-2">
|
||||
{!isCloud && auth?.role === "owner" && (
|
||||
{!isCloud && (auth?.role === "owner" || auth?.role === "admin") && (
|
||||
<SidebarMenuItem>
|
||||
<UpdateServerButton />
|
||||
</SidebarMenuItem>
|
||||
|
||||
@@ -49,7 +49,9 @@ export const UserNav = () => {
|
||||
alt={data?.user?.image || ""}
|
||||
/>
|
||||
<AvatarFallback className="rounded-lg">
|
||||
{getFallbackAvatarInitials(data?.user?.name)}
|
||||
{getFallbackAvatarInitials(
|
||||
`${data?.user?.firstName} ${data?.user?.lastName}`.trim(),
|
||||
)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
@@ -102,7 +104,9 @@ export const UserNav = () => {
|
||||
>
|
||||
Monitoring
|
||||
</DropdownMenuItem>
|
||||
{(data?.role === "owner" || data?.canAccessToTraefikFiles) && (
|
||||
{(data?.role === "owner" ||
|
||||
data?.role === "admin" ||
|
||||
data?.canAccessToTraefikFiles) && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
@@ -112,7 +116,9 @@ export const UserNav = () => {
|
||||
Traefik
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{(data?.role === "owner" || data?.canAccessToDocker) && (
|
||||
{(data?.role === "owner" ||
|
||||
data?.role === "admin" ||
|
||||
data?.canAccessToDocker) && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
@@ -126,7 +132,7 @@ export const UserNav = () => {
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
data?.role === "owner" && (
|
||||
(data?.role === "owner" || data?.role === "admin") && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
|
||||
2
apps/dokploy/drizzle/0125_neat_the_phantom.sql
Normal file
2
apps/dokploy/drizzle/0125_neat_the_phantom.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "application" ADD COLUMN "rollbackRegistryId" text;--> statement-breakpoint
|
||||
ALTER TABLE "application" ADD CONSTRAINT "application_rollbackRegistryId_registry_registryId_fk" FOREIGN KEY ("rollbackRegistryId") REFERENCES "public"."registry"("registryId") ON DELETE set null ON UPDATE no action;
|
||||
1
apps/dokploy/drizzle/0126_nifty_monster_badoon.sql
Normal file
1
apps/dokploy/drizzle/0126_nifty_monster_badoon.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "user" ALTER COLUMN "enableDockerCleanup" SET DEFAULT true;
|
||||
1
apps/dokploy/drizzle/0127_superb_alice.sql
Normal file
1
apps/dokploy/drizzle/0127_superb_alice.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE "notification" ADD COLUMN "volumeBackup" boolean DEFAULT false NOT NULL;
|
||||
2
apps/dokploy/drizzle/0128_hard_falcon.sql
Normal file
2
apps/dokploy/drizzle/0128_hard_falcon.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "user" RENAME COLUMN "name" TO "firstName";--> statement-breakpoint
|
||||
ALTER TABLE "user" ADD COLUMN "lastName" text DEFAULT '' NOT NULL;
|
||||
6850
apps/dokploy/drizzle/meta/0125_snapshot.json
Normal file
6850
apps/dokploy/drizzle/meta/0125_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
6850
apps/dokploy/drizzle/meta/0126_snapshot.json
Normal file
6850
apps/dokploy/drizzle/meta/0126_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
6857
apps/dokploy/drizzle/meta/0127_snapshot.json
Normal file
6857
apps/dokploy/drizzle/meta/0127_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
6864
apps/dokploy/drizzle/meta/0128_snapshot.json
Normal file
6864
apps/dokploy/drizzle/meta/0128_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -876,6 +876,34 @@
|
||||
"when": 1764571454170,
|
||||
"tag": "0124_certain_cloak",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 125,
|
||||
"version": "7",
|
||||
"when": 1764573207555,
|
||||
"tag": "0125_neat_the_phantom",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 126,
|
||||
"version": "7",
|
||||
"when": 1765065295708,
|
||||
"tag": "0126_nifty_monster_badoon",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 127,
|
||||
"version": "7",
|
||||
"when": 1765095189368,
|
||||
"tag": "0127_superb_alice",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 128,
|
||||
"version": "7",
|
||||
"when": 1765101709413,
|
||||
"tag": "0128_hard_falcon",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
adminClient,
|
||||
apiKeyClient,
|
||||
inferAdditionalFields,
|
||||
organizationClient,
|
||||
twoFactorClient,
|
||||
} from "better-auth/client/plugins";
|
||||
@@ -13,5 +14,12 @@ export const authClient = createAuthClient({
|
||||
twoFactorClient(),
|
||||
apiKeyClient(),
|
||||
adminClient(),
|
||||
inferAdditionalFields({
|
||||
user: {
|
||||
lastName: {
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -6,9 +6,6 @@
|
||||
/** @type {import("next").NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.25.11",
|
||||
"version": "v0.26.0",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
@@ -118,7 +118,7 @@
|
||||
"lucide-react": "^0.469.0",
|
||||
"micromatch": "4.0.8",
|
||||
"nanoid": "3.3.11",
|
||||
"next": "^15.3.2",
|
||||
"next": "^16.0.7",
|
||||
"next-i18next": "^15.4.2",
|
||||
"next-themes": "^0.2.1",
|
||||
"nextjs-toploader": "^3.9.17",
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { NextPage } from "next";
|
||||
import type { AppProps } from "next/app";
|
||||
import { Inter } from "next/font/google";
|
||||
import Head from "next/head";
|
||||
import Script from "next/script";
|
||||
import { appWithTranslation } from "next-i18next";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import NextTopLoader from "nextjs-toploader";
|
||||
@@ -43,14 +42,6 @@ const MyApp = ({
|
||||
<Head>
|
||||
<title>Dokploy</title>
|
||||
</Head>
|
||||
{process.env.NEXT_PUBLIC_UMAMI_HOST &&
|
||||
process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID && (
|
||||
<Script
|
||||
src={process.env.NEXT_PUBLIC_UMAMI_HOST}
|
||||
data-website-id={process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
|
||||
@@ -40,7 +40,7 @@ export async function getServerSideProps(
|
||||
};
|
||||
}
|
||||
const { user } = await validateRequest(ctx.req);
|
||||
if (!user || user.role !== "owner") {
|
||||
if (!user || (user.role !== "owner" && user.role !== "admin")) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -30,7 +30,7 @@ export async function getServerSideProps(
|
||||
}
|
||||
const { req, res } = ctx;
|
||||
const { user, session } = await validateRequest(req);
|
||||
if (!user || user.role === "member") {
|
||||
if (!user || user.role !== "owner") {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
|
||||
@@ -18,7 +18,9 @@ const Page = () => {
|
||||
<div className="w-full">
|
||||
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
||||
<ProfileForm />
|
||||
{(data?.canAccessToAPI || data?.role === "owner") && <ShowApiKeys />}
|
||||
{(data?.canAccessToAPI ||
|
||||
data?.role === "owner" ||
|
||||
data?.role === "admin") && <ShowApiKeys />}
|
||||
|
||||
{/* {isCloud && <RemoveSelfAccount />} */}
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,10 @@ import { api } from "@/utils/api";
|
||||
const registerSchema = z
|
||||
.object({
|
||||
name: z.string().min(1, {
|
||||
message: "Name is required",
|
||||
message: "First name is required",
|
||||
}),
|
||||
lastName: z.string().min(1, {
|
||||
message: "Last name is required",
|
||||
}),
|
||||
email: z
|
||||
.string()
|
||||
@@ -92,6 +95,7 @@ const Invitation = ({
|
||||
const form = useForm<Register>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
@@ -115,6 +119,7 @@ const Invitation = ({
|
||||
email: values.email,
|
||||
password: values.password,
|
||||
name: values.name,
|
||||
lastName: values.lastName,
|
||||
fetchOptions: {
|
||||
headers: {
|
||||
"x-dokploy-token": token,
|
||||
@@ -197,12 +202,22 @@ const Invitation = ({
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormLabel>First Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Enter your name"
|
||||
{...field}
|
||||
/>
|
||||
<Input placeholder="John" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="lastName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Last Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Doe" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -27,7 +27,10 @@ import { authClient } from "@/lib/auth-client";
|
||||
const registerSchema = z
|
||||
.object({
|
||||
name: z.string().min(1, {
|
||||
message: "Name is required",
|
||||
message: "First name is required",
|
||||
}),
|
||||
lastName: z.string().min(1, {
|
||||
message: "Last name is required",
|
||||
}),
|
||||
email: z
|
||||
.string()
|
||||
@@ -79,6 +82,7 @@ const Register = ({ isCloud }: Props) => {
|
||||
const form = useForm<Register>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
@@ -95,6 +99,7 @@ const Register = ({ isCloud }: Props) => {
|
||||
email: values.email,
|
||||
password: values.password,
|
||||
name: values.name,
|
||||
lastName: values.lastName,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
@@ -158,9 +163,22 @@ const Register = ({ isCloud }: Props) => {
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormLabel>First Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="name" {...field} />
|
||||
<Input placeholder="John" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="lastName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Last Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Doe" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { findAdmin } from "@dokploy/server";
|
||||
import { findOwner } from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { user } from "@dokploy/server/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const result = await findAdmin();
|
||||
const result = await findOwner();
|
||||
|
||||
const update = await db
|
||||
.update(user)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { findAdmin, generateRandomPassword } from "@dokploy/server";
|
||||
import { findOwner, generateRandomPassword } from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { account } from "@dokploy/server/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
@@ -7,7 +7,7 @@ import { eq } from "drizzle-orm";
|
||||
try {
|
||||
const randomPassword = await generateRandomPassword();
|
||||
|
||||
const result = await findAdmin();
|
||||
const result = await findOwner();
|
||||
|
||||
const update = await db
|
||||
.update(account)
|
||||
|
||||
@@ -15,7 +15,7 @@ export const organizationRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (ctx.user.role !== "owner" && !IS_CLOUD) {
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin" && !IS_CLOUD) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "Only the organization owner can create an organization",
|
||||
@@ -96,7 +96,7 @@ export const organizationRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (ctx.user.role !== "owner" && !IS_CLOUD) {
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin" && !IS_CLOUD) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "Only the organization owner can update it",
|
||||
@@ -119,7 +119,7 @@ export const organizationRouter = createTRPCRouter({
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (ctx.user.role !== "owner" && !IS_CLOUD) {
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin" && !IS_CLOUD) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "Only the organization owner can delete it",
|
||||
@@ -194,6 +194,65 @@ export const organizationRouter = createTRPCRouter({
|
||||
.delete(invitation)
|
||||
.where(eq(invitation.id, input.invitationId));
|
||||
}),
|
||||
updateMemberRole: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
memberId: z.string(),
|
||||
role: z.enum(["admin", "member"]),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
// Fetch the target member
|
||||
const target = await db.query.member.findFirst({
|
||||
where: eq(member.id, input.memberId),
|
||||
with: { user: true },
|
||||
});
|
||||
|
||||
if (!target) {
|
||||
throw new TRPCError({ code: "NOT_FOUND", message: "Member not found" });
|
||||
}
|
||||
|
||||
if (target.organizationId !== ctx.session.activeOrganizationId) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You are not allowed to update this member's role",
|
||||
});
|
||||
}
|
||||
|
||||
// Prevent users from changing their own role
|
||||
if (target.userId === ctx.user.id) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You cannot change your own role",
|
||||
});
|
||||
}
|
||||
|
||||
// Owner role is intransferible - cannot change to or from owner
|
||||
if (target.role === "owner") {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "The owner role is intransferible",
|
||||
});
|
||||
}
|
||||
|
||||
// Only owners can change admin roles
|
||||
// Admins can only change member roles
|
||||
if (ctx.user.role === "admin" && target.role === "admin") {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message:
|
||||
"Only the organization owner can change admin roles. Admins can only modify member roles.",
|
||||
});
|
||||
}
|
||||
|
||||
// Update the target member's role
|
||||
await db
|
||||
.update(member)
|
||||
.set({ role: input.role })
|
||||
.where(eq(member.id, input.memberId));
|
||||
|
||||
return true;
|
||||
}),
|
||||
setDefault: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import {
|
||||
canAccessToTraefikFiles,
|
||||
checkGPUStatus,
|
||||
cleanStoppedContainers,
|
||||
cleanUpDockerBuilder,
|
||||
cleanUpSystemPrune,
|
||||
cleanUpUnusedImages,
|
||||
cleanUpUnusedVolumes,
|
||||
checkPortInUse,
|
||||
cleanupAll,
|
||||
cleanupBuilders,
|
||||
cleanupContainers,
|
||||
cleanupImages,
|
||||
cleanupSystem,
|
||||
cleanupVolumes,
|
||||
DEFAULT_UPDATE_DATA,
|
||||
execAsync,
|
||||
findServerById,
|
||||
@@ -130,6 +132,17 @@ export const settingsRouter = createTRPCRouter({
|
||||
let newPorts = ports;
|
||||
// If receive true, add 8080 to ports
|
||||
if (input.enableDashboard) {
|
||||
// Check if port 8080 is already in use before enabling dashboard
|
||||
const portCheck = await checkPortInUse(8080, input.serverId);
|
||||
if (portCheck.isInUse) {
|
||||
const conflictingContainer = portCheck.conflictingContainer
|
||||
? ` by container "${portCheck.conflictingContainer}"`
|
||||
: "";
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: `Port 8080 is already in use${conflictingContainer}. Please stop the conflicting service or use a different port for the Traefik dashboard.`,
|
||||
});
|
||||
}
|
||||
newPorts.push({
|
||||
targetPort: 8080,
|
||||
publishedPort: 8080,
|
||||
@@ -149,41 +162,38 @@ export const settingsRouter = createTRPCRouter({
|
||||
cleanUnusedImages: adminProcedure
|
||||
.input(apiServerSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
await cleanUpUnusedImages(input?.serverId);
|
||||
await cleanupImages(input?.serverId);
|
||||
return true;
|
||||
}),
|
||||
cleanUnusedVolumes: adminProcedure
|
||||
.input(apiServerSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
await cleanUpUnusedVolumes(input?.serverId);
|
||||
await cleanupVolumes(input?.serverId);
|
||||
return true;
|
||||
}),
|
||||
cleanStoppedContainers: adminProcedure
|
||||
.input(apiServerSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
await cleanStoppedContainers(input?.serverId);
|
||||
await cleanupContainers(input?.serverId);
|
||||
return true;
|
||||
}),
|
||||
cleanDockerBuilder: adminProcedure
|
||||
.input(apiServerSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
await cleanUpDockerBuilder(input?.serverId);
|
||||
await cleanupBuilders(input?.serverId);
|
||||
}),
|
||||
cleanDockerPrune: adminProcedure
|
||||
.input(apiServerSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
await cleanUpSystemPrune(input?.serverId);
|
||||
await cleanUpDockerBuilder(input?.serverId);
|
||||
await cleanupSystem(input?.serverId);
|
||||
await cleanupBuilders(input?.serverId);
|
||||
|
||||
return true;
|
||||
}),
|
||||
cleanAll: adminProcedure
|
||||
.input(apiServerSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
await cleanUpUnusedImages(input?.serverId);
|
||||
await cleanStoppedContainers(input?.serverId);
|
||||
await cleanUpDockerBuilder(input?.serverId);
|
||||
await cleanUpSystemPrune(input?.serverId);
|
||||
await cleanupAll(input?.serverId);
|
||||
|
||||
return true;
|
||||
}),
|
||||
@@ -201,7 +211,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
await updateUser(ctx.user.id, {
|
||||
await updateUser(ctx.user.ownerId, {
|
||||
sshPrivateKey: input.sshPrivateKey,
|
||||
});
|
||||
|
||||
@@ -213,7 +223,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
const user = await updateUser(ctx.user.id, {
|
||||
const user = await updateUser(ctx.user.ownerId, {
|
||||
host: input.host,
|
||||
...(input.letsEncryptEmail && {
|
||||
letsEncryptEmail: input.letsEncryptEmail,
|
||||
@@ -240,7 +250,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
await updateUser(ctx.user.id, {
|
||||
await updateUser(ctx.user.ownerId, {
|
||||
sshPrivateKey: null,
|
||||
});
|
||||
return true;
|
||||
@@ -281,9 +291,9 @@ export const settingsRouter = createTRPCRouter({
|
||||
console.log(
|
||||
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
|
||||
);
|
||||
await cleanUpUnusedImages(server.serverId);
|
||||
await cleanUpDockerBuilder(server.serverId);
|
||||
await cleanUpSystemPrune(server.serverId);
|
||||
|
||||
await cleanupAll(server.serverId);
|
||||
|
||||
await sendDockerCleanupNotifications(server.organizationId);
|
||||
});
|
||||
}
|
||||
@@ -300,7 +310,7 @@ export const settingsRouter = createTRPCRouter({
|
||||
}
|
||||
}
|
||||
} else if (!IS_CLOUD) {
|
||||
const userUpdated = await updateUser(ctx.user.id, {
|
||||
const userUpdated = await updateUser(ctx.user.ownerId, {
|
||||
enableDockerCleanup: input.enableDockerCleanup,
|
||||
});
|
||||
|
||||
@@ -309,9 +319,9 @@ export const settingsRouter = createTRPCRouter({
|
||||
console.log(
|
||||
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
|
||||
);
|
||||
await cleanUpUnusedImages();
|
||||
await cleanUpDockerBuilder();
|
||||
await cleanUpSystemPrune();
|
||||
|
||||
await cleanupAll();
|
||||
|
||||
await sendDockerCleanupNotifications(
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
@@ -810,6 +820,19 @@ export const settingsRouter = createTRPCRouter({
|
||||
"dokploy-traefik",
|
||||
input?.serverId,
|
||||
);
|
||||
|
||||
for (const port of input.additionalPorts) {
|
||||
const portCheck = await checkPortInUse(
|
||||
port.publishedPort,
|
||||
input.serverId,
|
||||
);
|
||||
if (portCheck.isInUse) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: `Port ${port.targetPort} is already in use by ${portCheck.conflictingContainer}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
const preparedEnv = prepareEnvironmentVariables(env);
|
||||
|
||||
await writeTraefikSetup({
|
||||
|
||||
@@ -56,15 +56,16 @@ export const stripeRouter = createTRPCRouter({
|
||||
});
|
||||
|
||||
const items = getStripeItems(input.serverQuantity, input.isAnnual);
|
||||
const user = await findUserById(ctx.user.id);
|
||||
// Always operate on the organization owner's Stripe customer
|
||||
const owner = await findUserById(ctx.user.ownerId);
|
||||
|
||||
let stripeCustomerId = user.stripeCustomerId;
|
||||
let stripeCustomerId = owner.stripeCustomerId;
|
||||
|
||||
if (stripeCustomerId) {
|
||||
const customer = await stripe.customers.retrieve(stripeCustomerId);
|
||||
|
||||
if (customer.deleted) {
|
||||
await updateUser(user.id, {
|
||||
await updateUser(owner.id, {
|
||||
stripeCustomerId: null,
|
||||
});
|
||||
stripeCustomerId = null;
|
||||
@@ -78,7 +79,7 @@ export const stripeRouter = createTRPCRouter({
|
||||
customer: stripeCustomerId,
|
||||
}),
|
||||
metadata: {
|
||||
adminId: user.id,
|
||||
adminId: owner.id,
|
||||
},
|
||||
allow_promotion_codes: true,
|
||||
success_url: `${WEBSITE_URL}/dashboard/settings/servers?success=true`,
|
||||
@@ -88,15 +89,16 @@ export const stripeRouter = createTRPCRouter({
|
||||
return { sessionId: session.id };
|
||||
}),
|
||||
createCustomerPortalSession: adminProcedure.mutation(async ({ ctx }) => {
|
||||
const user = await findUserById(ctx.user.id);
|
||||
// Use the organization's owner account for billing portal
|
||||
const owner = await findUserById(ctx.user.ownerId);
|
||||
|
||||
if (!user.stripeCustomerId) {
|
||||
if (!owner.stripeCustomerId) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Stripe Customer ID not found",
|
||||
});
|
||||
}
|
||||
const stripeCustomerId = user.stripeCustomerId;
|
||||
const stripeCustomerId = owner.stripeCustomerId;
|
||||
|
||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
|
||||
apiVersion: "2024-09-30.acacia",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
createApiKey,
|
||||
findAdmin,
|
||||
findNotificationById,
|
||||
findOrganizationById,
|
||||
findUserById,
|
||||
@@ -87,7 +86,11 @@ export const userRouter = createTRPCRouter({
|
||||
// Allow access if:
|
||||
// 1. User is requesting their own information
|
||||
// 2. User has owner role (admin permissions) AND user is in the same organization
|
||||
if (memberResult.userId !== ctx.user.id && ctx.user.role !== "owner") {
|
||||
if (
|
||||
memberResult.userId !== ctx.user.id &&
|
||||
ctx.user.role !== "owner" &&
|
||||
ctx.user.role !== "admin"
|
||||
) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to access this user",
|
||||
@@ -223,10 +226,61 @@ export const userRouter = createTRPCRouter({
|
||||
userId: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ensure the acting user has admin privileges in the active organization
|
||||
if (ctx.user.role !== "owner" && ctx.user.role !== "admin") {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "Only owners or admins can delete users",
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch target member within the active organization
|
||||
const targetMember = await db.query.member.findFirst({
|
||||
where: and(
|
||||
eq(member.userId, input.userId),
|
||||
eq(member.organizationId, ctx.session?.activeOrganizationId || ""),
|
||||
),
|
||||
});
|
||||
|
||||
if (!targetMember) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Target user is not a member of this organization",
|
||||
});
|
||||
}
|
||||
|
||||
// Never allow deleting the organization owner via this endpoint
|
||||
if (targetMember.role === "owner") {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You cannot delete the organization owner",
|
||||
});
|
||||
}
|
||||
|
||||
// Admin self-protection: an admin cannot delete themselves
|
||||
if (targetMember.role === "admin" && input.userId === ctx.user.id) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message:
|
||||
"Admins cannot delete themselves. Ask the owner or another admin.",
|
||||
});
|
||||
}
|
||||
|
||||
// Only owners can delete admins
|
||||
// Admins can only delete members
|
||||
if (ctx.user.role === "admin" && targetMember.role === "admin") {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message:
|
||||
"Only the organization owner can delete admins. Admins can only delete members.",
|
||||
});
|
||||
}
|
||||
|
||||
return await removeUserById(input.userId);
|
||||
}),
|
||||
assignPermissions: adminProcedure
|
||||
|
||||
@@ -183,7 +183,11 @@ export const uploadProcedure = async (opts: any) => {
|
||||
};
|
||||
|
||||
export const cliProcedure = t.procedure.use(({ ctx, next }) => {
|
||||
if (!ctx.session || !ctx.user || ctx.user.role !== "owner") {
|
||||
if (
|
||||
!ctx.session ||
|
||||
!ctx.user ||
|
||||
(ctx.user.role !== "owner" && ctx.user.role !== "admin")
|
||||
) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
return next({
|
||||
@@ -197,7 +201,11 @@ export const cliProcedure = t.procedure.use(({ ctx, next }) => {
|
||||
});
|
||||
|
||||
export const adminProcedure = t.procedure.use(({ ctx, next }) => {
|
||||
if (!ctx.session || !ctx.user || ctx.user.role !== "owner") {
|
||||
if (
|
||||
!ctx.session ||
|
||||
!ctx.user ||
|
||||
(ctx.user.role !== "owner" && ctx.user.role !== "admin")
|
||||
) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
return next({
|
||||
|
||||
@@ -26,6 +26,16 @@ config({ path: ".env" });
|
||||
const PORT = Number.parseInt(process.env.PORT || "3000", 10);
|
||||
const HOST = process.env.HOST || "0.0.0.0";
|
||||
const dev = process.env.NODE_ENV !== "production";
|
||||
|
||||
// Initialize critical directories and Traefik config BEFORE Next.js starts
|
||||
// This prevents race conditions with the install script
|
||||
if (process.env.NODE_ENV === "production" && !IS_CLOUD) {
|
||||
setupDirectories();
|
||||
createDefaultTraefikConfig();
|
||||
createDefaultServerTraefikConfig();
|
||||
console.log("✅ Critical initialization complete");
|
||||
}
|
||||
|
||||
const app = next({ dev, turbopack: process.env.TURBOPACK === "1" });
|
||||
const handle = app.getRequestHandler();
|
||||
void app.prepare().then(async () => {
|
||||
@@ -45,11 +55,8 @@ void app.prepare().then(async () => {
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === "production" && !IS_CLOUD) {
|
||||
setupDirectories();
|
||||
createDefaultMiddlewares();
|
||||
await initializeNetwork();
|
||||
createDefaultTraefikConfig();
|
||||
createDefaultServerTraefikConfig();
|
||||
await migration();
|
||||
await initCronJobs();
|
||||
await initSchedules();
|
||||
|
||||
@@ -71,7 +71,9 @@ export const setupDockerContainerLogsWebSocketServer = (
|
||||
const command = search
|
||||
? `${baseCommand} 2>&1 | grep --line-buffered -iF "${escapedSearch}"`
|
||||
: baseCommand;
|
||||
client.exec(command, (err, stream) => {
|
||||
// Use pty: true to ensure the remote process receives SIGHUP when SSH connection closes
|
||||
// This is crucial for terminating docker logs processes when the connection is closed
|
||||
client.exec(command, { pty: true }, (err, stream) => {
|
||||
if (err) {
|
||||
console.error("Execution error:", err);
|
||||
ws.close();
|
||||
|
||||
@@ -58,7 +58,12 @@ export const setupDockerContainerTerminalWebSocketServer = (
|
||||
`docker exec -it -w / ${containerId} ${activeWay}`,
|
||||
{ pty: true },
|
||||
(err, stream) => {
|
||||
if (err) throw err;
|
||||
if (err) {
|
||||
console.error("SSH exec error:", err);
|
||||
ws.close();
|
||||
conn.end();
|
||||
return;
|
||||
}
|
||||
|
||||
stream
|
||||
.on("close", (code: number, _signal: string) => {
|
||||
@@ -93,10 +98,20 @@ export const setupDockerContainerTerminalWebSocketServer = (
|
||||
|
||||
ws.on("close", () => {
|
||||
stream.end();
|
||||
// Ensure SSH connection is closed when WebSocket closes
|
||||
conn.end();
|
||||
});
|
||||
},
|
||||
);
|
||||
})
|
||||
.on("error", (err) => {
|
||||
console.error("SSH connection error:", err);
|
||||
if (ws.readyState === ws.OPEN) {
|
||||
ws.send(`SSH error: ${err.message}`);
|
||||
ws.close();
|
||||
}
|
||||
conn.end();
|
||||
})
|
||||
.connect({
|
||||
host: server.ipAddress,
|
||||
port: server.port,
|
||||
|
||||
@@ -31,8 +31,11 @@ export const setupDeploymentLogsWebSocketServer = (
|
||||
const serverId = url.searchParams.get("serverId");
|
||||
const { user, session } = await validateRequest(req);
|
||||
|
||||
// Generate unique connection ID for tracking
|
||||
const connectionId = `deployment-logs-${Date.now()}-${Math.random().toString(36).substring(7)}`;
|
||||
|
||||
if (!logPath) {
|
||||
console.log("logPath no provided");
|
||||
console.log(`[${connectionId}] logPath no provided`);
|
||||
ws.close(4000, "logPath no provided");
|
||||
return;
|
||||
}
|
||||
@@ -42,40 +45,55 @@ export const setupDeploymentLogsWebSocketServer = (
|
||||
return;
|
||||
}
|
||||
|
||||
let tailProcess: ReturnType<typeof spawn> | null = null;
|
||||
let sshClient: Client | null = null;
|
||||
|
||||
try {
|
||||
if (serverId) {
|
||||
const server = await findServerById(serverId);
|
||||
|
||||
if (!server.sshKeyId) return;
|
||||
const client = new Client();
|
||||
client
|
||||
if (!server.sshKeyId) {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
sshClient = new Client();
|
||||
sshClient
|
||||
.on("ready", () => {
|
||||
const command = `
|
||||
tail -n +1 -f ${logPath};
|
||||
`;
|
||||
client.exec(command, (err, stream) => {
|
||||
sshClient!.exec(command, (err, stream) => {
|
||||
if (err) {
|
||||
console.error("Execution error:", err);
|
||||
sshClient!.end();
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
stream
|
||||
.on("close", () => {
|
||||
client.end();
|
||||
sshClient!.end();
|
||||
ws.close();
|
||||
})
|
||||
.on("data", (data: string) => {
|
||||
ws.send(data.toString());
|
||||
if (ws.readyState === ws.OPEN) {
|
||||
ws.send(data.toString());
|
||||
}
|
||||
})
|
||||
.stderr.on("data", (data) => {
|
||||
ws.send(data.toString());
|
||||
if (ws.readyState === ws.OPEN) {
|
||||
ws.send(data.toString());
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
.on("error", (err) => {
|
||||
console.error("SSH connection error:", err);
|
||||
ws.send(`SSH error: ${err.message}`);
|
||||
ws.close(); // Cierra el WebSocket si hay un error con SSH
|
||||
if (ws.readyState === ws.OPEN) {
|
||||
ws.send(`SSH error: ${err.message}`);
|
||||
ws.close();
|
||||
}
|
||||
if (sshClient) {
|
||||
sshClient.end();
|
||||
}
|
||||
})
|
||||
.connect({
|
||||
host: server.ipAddress,
|
||||
@@ -85,26 +103,75 @@ export const setupDeploymentLogsWebSocketServer = (
|
||||
});
|
||||
|
||||
ws.on("close", () => {
|
||||
client.end();
|
||||
if (sshClient) {
|
||||
sshClient.end();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const tail = spawn("tail", ["-n", "+1", "-f", logPath]);
|
||||
tailProcess = spawn("tail", ["-n", "+1", "-f", logPath]);
|
||||
|
||||
tail.stdout.on("data", (data) => {
|
||||
ws.send(data.toString());
|
||||
});
|
||||
const stdout = tailProcess.stdout;
|
||||
const stderr = tailProcess.stderr;
|
||||
|
||||
tail.stderr.on("data", (data) => {
|
||||
ws.send(new Error(`tail error: ${data.toString()}`).message);
|
||||
});
|
||||
tail.on("close", () => {
|
||||
if (stdout) {
|
||||
stdout.on("data", (data) => {
|
||||
if (ws.readyState === ws.OPEN) {
|
||||
ws.send(data.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
stderr.on("data", (data) => {
|
||||
if (ws.readyState === ws.OPEN) {
|
||||
ws.send(new Error(`tail error: ${data.toString()}`).message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
tailProcess.on("close", () => {
|
||||
ws.close();
|
||||
});
|
||||
|
||||
tailProcess.on("error", () => {
|
||||
if (ws.readyState === ws.OPEN) {
|
||||
ws.close();
|
||||
}
|
||||
});
|
||||
|
||||
ws.on("close", () => {
|
||||
if (tailProcess && !tailProcess.killed) {
|
||||
tailProcess.kill("SIGTERM");
|
||||
// Force kill after a timeout if it doesn't terminate
|
||||
setTimeout(() => {
|
||||
if (tailProcess && !tailProcess.killed) {
|
||||
tailProcess.kill("SIGKILL");
|
||||
} else {
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Clean up resources on error
|
||||
if (tailProcess && !tailProcess.killed) {
|
||||
tailProcess.kill("SIGTERM");
|
||||
setTimeout(() => {
|
||||
if (tailProcess && !tailProcess.killed) {
|
||||
tailProcess.kill("SIGKILL");
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
if (sshClient) {
|
||||
sshClient.end();
|
||||
}
|
||||
if (ws.readyState === ws.OPEN) {
|
||||
// @ts-ignore
|
||||
const errorMessage = error?.message as unknown as string;
|
||||
ws.send(errorMessage || "An error occurred");
|
||||
ws.close();
|
||||
}
|
||||
} catch {
|
||||
// @ts-ignore
|
||||
// const errorMessage = error?.message as unknown as string;
|
||||
// ws.send(errorMessage);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -8,21 +8,22 @@
|
||||
"resolveJsonModule": true,
|
||||
"moduleDetection": "force",
|
||||
"isolatedModules": true,
|
||||
|
||||
/* Strictness */
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"checkJs": true,
|
||||
|
||||
/* Bundled projects */
|
||||
"lib": ["dom", "dom.iterable", "ES2022"],
|
||||
"noEmit": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"jsx": "preserve",
|
||||
"plugins": [{ "name": "next" }],
|
||||
"jsx": "react-jsx",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"incremental": true,
|
||||
|
||||
/* Path Aliases */
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
@@ -30,7 +31,6 @@
|
||||
"@dokploy/server/*": ["../../packages/server/src/*"]
|
||||
}
|
||||
},
|
||||
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import {
|
||||
cleanUpDockerBuilder,
|
||||
cleanUpSystemPrune,
|
||||
cleanUpUnusedImages,
|
||||
cleanupAll,
|
||||
findBackupById,
|
||||
findScheduleById,
|
||||
findServerById,
|
||||
@@ -91,9 +89,7 @@ export const runJobs = async (job: QueueJob) => {
|
||||
logger.info("Server is inactive");
|
||||
return;
|
||||
}
|
||||
await cleanUpUnusedImages(serverId);
|
||||
await cleanUpDockerBuilder(serverId);
|
||||
await cleanUpSystemPrune(serverId);
|
||||
await cleanupAll(serverId);
|
||||
} else if (job.type === "schedule") {
|
||||
const { scheduleId } = job;
|
||||
const schedule = await findScheduleById(scheduleId);
|
||||
|
||||
@@ -187,6 +187,12 @@ export const applications = pgTable("application", {
|
||||
registryId: text("registryId").references(() => registry.registryId, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
rollbackRegistryId: text("rollbackRegistryId").references(
|
||||
() => registry.registryId,
|
||||
{
|
||||
onDelete: "set null",
|
||||
},
|
||||
),
|
||||
environmentId: text("environmentId")
|
||||
.notNull()
|
||||
.references(() => environments.environmentId, { onDelete: "cascade" }),
|
||||
@@ -270,6 +276,11 @@ export const applicationsRelations = relations(
|
||||
relationName: "applicationBuildRegistry",
|
||||
}),
|
||||
previewDeployments: many(previewDeployments),
|
||||
rollbackRegistry: one(registry, {
|
||||
fields: [applications.rollbackRegistryId],
|
||||
references: [registry.registryId],
|
||||
relationName: "applicationRollbackRegistry",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ export const notifications = pgTable("notification", {
|
||||
appDeploy: boolean("appDeploy").notNull().default(false),
|
||||
appBuildError: boolean("appBuildError").notNull().default(false),
|
||||
databaseBackup: boolean("databaseBackup").notNull().default(false),
|
||||
volumeBackup: boolean("volumeBackup").notNull().default(false),
|
||||
dokployRestart: boolean("dokployRestart").notNull().default(false),
|
||||
dockerCleanup: boolean("dockerCleanup").notNull().default(false),
|
||||
serverThreshold: boolean("serverThreshold").notNull().default(false),
|
||||
@@ -186,6 +187,7 @@ export const apiCreateSlack = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
@@ -213,6 +215,7 @@ export const apiCreateTelegram = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
@@ -242,6 +245,7 @@ export const apiCreateDiscord = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
@@ -272,6 +276,7 @@ export const apiCreateEmail = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
@@ -307,6 +312,7 @@ export const apiCreateGotify = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
@@ -340,6 +346,7 @@ export const apiCreateNtfy = notificationsSchema
|
||||
.pick({
|
||||
appBuildError: true,
|
||||
databaseBackup: true,
|
||||
volumeBackup: true,
|
||||
dokployRestart: true,
|
||||
name: true,
|
||||
appDeploy: true,
|
||||
|
||||
@@ -39,6 +39,9 @@ export const registryRelations = relations(registry, ({ many }) => ({
|
||||
buildApplications: many(applications, {
|
||||
relationName: "applicationBuildRegistry",
|
||||
}),
|
||||
rollbackApplications: many(applications, {
|
||||
relationName: "applicationRollbackRegistry",
|
||||
}),
|
||||
}));
|
||||
|
||||
const createSchema = createInsertSchema(registry, {
|
||||
|
||||
@@ -31,7 +31,8 @@ export const user = pgTable("user", {
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
.$defaultFn(() => nanoid()),
|
||||
name: text("name").notNull().default(""),
|
||||
firstName: text("firstName").notNull().default(""),
|
||||
lastName: text("lastName").notNull().default(""),
|
||||
isRegistered: boolean("isRegistered").notNull().default(false),
|
||||
expirationDate: text("expirationDate")
|
||||
.notNull()
|
||||
@@ -56,7 +57,7 @@ export const user = pgTable("user", {
|
||||
host: text("host"),
|
||||
letsEncryptEmail: text("letsEncryptEmail"),
|
||||
sshPrivateKey: text("sshPrivateKey"),
|
||||
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
|
||||
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(true),
|
||||
logCleanupCron: text("logCleanupCron").default("0 0 * * *"),
|
||||
role: text("role").notNull().default("user"),
|
||||
// Metrics
|
||||
@@ -332,6 +333,7 @@ export const apiUpdateUser = createSchema.partial().extend({
|
||||
password: z.string().optional(),
|
||||
currentPassword: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
lastName: z.string().optional(),
|
||||
metricsConfig: z
|
||||
.object({
|
||||
server: z.object({
|
||||
|
||||
123
packages/server/src/emails/emails/volume-backup.tsx
Normal file
123
packages/server/src/emails/emails/volume-backup.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import {
|
||||
Body,
|
||||
Container,
|
||||
Head,
|
||||
Heading,
|
||||
Html,
|
||||
Img,
|
||||
Preview,
|
||||
Section,
|
||||
Tailwind,
|
||||
Text,
|
||||
} from "@react-email/components";
|
||||
|
||||
export type TemplateProps = {
|
||||
projectName: string;
|
||||
applicationName: string;
|
||||
volumeName: string;
|
||||
serviceType:
|
||||
| "application"
|
||||
| "postgres"
|
||||
| "mysql"
|
||||
| "mongodb"
|
||||
| "mariadb"
|
||||
| "redis"
|
||||
| "compose";
|
||||
type: "error" | "success";
|
||||
errorMessage?: string;
|
||||
backupSize?: string;
|
||||
date: string;
|
||||
};
|
||||
|
||||
export const VolumeBackupEmail = ({
|
||||
projectName = "dokploy",
|
||||
applicationName = "frontend",
|
||||
volumeName = "app-data",
|
||||
serviceType = "application",
|
||||
type = "success",
|
||||
errorMessage,
|
||||
backupSize,
|
||||
date = "2023-05-01T00:00:00.000Z",
|
||||
}: TemplateProps) => {
|
||||
const previewText = `Volume backup for ${applicationName} was ${type === "success" ? "successful ✅" : "failed ❌"}`;
|
||||
return (
|
||||
<Html>
|
||||
<Preview>{previewText}</Preview>
|
||||
<Tailwind
|
||||
config={{
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
brand: "#007291",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Head />
|
||||
|
||||
<Body className="bg-white my-auto mx-auto font-sans px-2">
|
||||
<Container className="border border-solid border-[#eaeaea] rounded-lg my-[40px] mx-auto p-[20px] max-w-[465px]">
|
||||
<Section className="mt-[32px]">
|
||||
<Img
|
||||
src={
|
||||
"https://raw.githubusercontent.com/Dokploy/dokploy/refs/heads/canary/apps/dokploy/logo.png"
|
||||
}
|
||||
width="100"
|
||||
height="50"
|
||||
alt="Dokploy"
|
||||
className="my-0 mx-auto"
|
||||
/>
|
||||
</Section>
|
||||
<Heading className="text-black text-[24px] font-normal text-center p-0 my-[30px] mx-0">
|
||||
Volume backup for <strong>{applicationName}</strong>
|
||||
</Heading>
|
||||
<Text className="text-black text-[14px] leading-[24px]">
|
||||
Hello,
|
||||
</Text>
|
||||
<Text className="text-black text-[14px] leading-[24px]">
|
||||
Your volume backup for <strong>{applicationName}</strong> was{" "}
|
||||
{type === "success"
|
||||
? "successful ✅"
|
||||
: "failed. Please check the error message below. ❌"}
|
||||
.
|
||||
</Text>
|
||||
<Section className="flex text-black text-[14px] leading-[24px] bg-[#F4F4F5] rounded-lg p-2">
|
||||
<Text className="!leading-3 font-bold">Details: </Text>
|
||||
<Text className="!leading-3">
|
||||
Project Name: <strong>{projectName}</strong>
|
||||
</Text>
|
||||
<Text className="!leading-3">
|
||||
Application Name: <strong>{applicationName}</strong>
|
||||
</Text>
|
||||
<Text className="!leading-3">
|
||||
Volume Name: <strong>{volumeName}</strong>
|
||||
</Text>
|
||||
<Text className="!leading-3">
|
||||
Service Type: <strong>{serviceType}</strong>
|
||||
</Text>
|
||||
{backupSize && (
|
||||
<Text className="!leading-3">
|
||||
Backup Size: <strong>{backupSize}</strong>
|
||||
</Text>
|
||||
)}
|
||||
<Text className="!leading-3">
|
||||
Date: <strong>{date}</strong>
|
||||
</Text>
|
||||
</Section>
|
||||
{type === "error" && errorMessage ? (
|
||||
<Section className="flex text-black text-[14px] mt-4 leading-[24px] bg-[#F4F4F5] rounded-lg p-2">
|
||||
<Text className="!leading-3 font-bold">Reason: </Text>
|
||||
<Text className="text-[12px] leading-[24px]">
|
||||
{errorMessage || "Error message not provided"}
|
||||
</Text>
|
||||
</Section>
|
||||
) : null}
|
||||
</Container>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
export default VolumeBackupEmail;
|
||||
@@ -127,16 +127,22 @@ const { handler, api } = betterAuth({
|
||||
});
|
||||
}
|
||||
|
||||
console.log(user);
|
||||
|
||||
if (IS_CLOUD) {
|
||||
try {
|
||||
const hutk = getHubSpotUTK(
|
||||
context?.request?.headers?.get("cookie") || undefined,
|
||||
);
|
||||
// Cast to include additional fields
|
||||
const userWithFields = user as typeof user & {
|
||||
lastName?: string;
|
||||
};
|
||||
const hubspotSuccess = await submitToHubSpot(
|
||||
{
|
||||
email: user.email,
|
||||
firstName: user.name,
|
||||
lastName: user.name,
|
||||
firstName: user.name || "", // name is mapped to firstName column
|
||||
lastName: userWithFields.lastName || "",
|
||||
},
|
||||
hutk,
|
||||
);
|
||||
@@ -204,6 +210,9 @@ const { handler, api } = betterAuth({
|
||||
},
|
||||
user: {
|
||||
modelName: "user",
|
||||
fields: {
|
||||
name: "firstName", // Map better-auth's default 'name' field to 'firstName' column
|
||||
},
|
||||
additionalFields: {
|
||||
role: {
|
||||
type: "string",
|
||||
@@ -220,6 +229,12 @@ const { handler, api } = betterAuth({
|
||||
type: "boolean",
|
||||
defaultValue: false,
|
||||
},
|
||||
lastName: {
|
||||
type: "string",
|
||||
required: false,
|
||||
input: true,
|
||||
defaultValue: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
@@ -316,16 +331,11 @@ export const validateRequest = async (request: IncomingMessage) => {
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
email,
|
||||
emailVerified,
|
||||
image,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
twoFactorEnabled,
|
||||
} = apiKeyRecord.user;
|
||||
// When accessing from DB, use actual column names
|
||||
const userFromDb = apiKeyRecord.user as typeof apiKeyRecord.user & {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
};
|
||||
|
||||
const mockSession = {
|
||||
session: {
|
||||
@@ -333,14 +343,14 @@ export const validateRequest = async (request: IncomingMessage) => {
|
||||
activeOrganizationId: organizationId || "",
|
||||
},
|
||||
user: {
|
||||
id,
|
||||
name,
|
||||
email,
|
||||
emailVerified,
|
||||
image,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
twoFactorEnabled,
|
||||
id: userFromDb.id,
|
||||
name: userFromDb.firstName, // Map firstName back to name for better-auth
|
||||
email: userFromDb.email,
|
||||
emailVerified: userFromDb.emailVerified,
|
||||
image: userFromDb.image,
|
||||
createdAt: userFromDb.createdAt,
|
||||
updatedAt: userFromDb.updatedAt,
|
||||
twoFactorEnabled: userFromDb.twoFactorEnabled,
|
||||
role: member?.role || "member",
|
||||
ownerId: member?.organization.ownerId || apiKeyRecord.user.id,
|
||||
},
|
||||
|
||||
@@ -46,7 +46,7 @@ export const isAdminPresent = async () => {
|
||||
return true;
|
||||
};
|
||||
|
||||
export const findAdmin = async () => {
|
||||
export const findOwner = async () => {
|
||||
const admin = await db.query.member.findFirst({
|
||||
where: eq(member.role, "owner"),
|
||||
with: {
|
||||
@@ -107,11 +107,11 @@ export const getDokployUrl = async () => {
|
||||
if (IS_CLOUD) {
|
||||
return "https://app.dokploy.com";
|
||||
}
|
||||
const admin = await findAdmin();
|
||||
const owner = await findOwner();
|
||||
|
||||
if (admin.user.host) {
|
||||
const protocol = admin.user.https ? "https" : "http";
|
||||
return `${protocol}://${admin.user.host}`;
|
||||
if (owner.user.host) {
|
||||
const protocol = owner.user.https ? "https" : "http";
|
||||
return `${protocol}://${owner.user.host}`;
|
||||
}
|
||||
return `http://${admin.user.serverIp}:${process.env.PORT}`;
|
||||
return `http://${owner.user.serverIp}:${process.env.PORT}`;
|
||||
};
|
||||
|
||||
@@ -104,14 +104,28 @@ export const suggestVariants = async ({
|
||||
),
|
||||
}),
|
||||
prompt: `
|
||||
Act as advanced DevOps engineer and generate a list of open source projects what can cover users needs(up to 3 items).
|
||||
Act as advanced DevOps engineer and analyze the user's request to determine the appropriate suggestions (up to 3 items).
|
||||
|
||||
CRITICAL - Read the user's request carefully and follow the appropriate strategy:
|
||||
|
||||
Strategy A - If the user specifies a PARTICULAR APPLICATION/SERVICE (e.g., "deploy Chatwoot", "install sendingtk/chatwoot:develop", "setup Bitwarden"):
|
||||
- Generate different deployment VARIANTS of that SAME application
|
||||
- Each variant should be a different configuration (minimal, full stack, with different databases, development vs production, etc.)
|
||||
- Example: For "Chatwoot" → "Chatwoot with PostgreSQL", "Chatwoot Development", "Chatwoot Full Stack"
|
||||
- The name MUST include the specific application name the user mentioned
|
||||
|
||||
Strategy B - If the user describes a GENERAL NEED or USE CASE (e.g., "personal blog", "project management tool", "chat application"):
|
||||
- Suggest different open source projects that fulfill that need
|
||||
- Each suggestion should be a different tool/platform that solves the same problem
|
||||
- Example: For "personal blog" → "WordPress", "Ghost", "Hugo with Nginx"
|
||||
- The name should be the actual project name
|
||||
|
||||
Return your response as a JSON object with the following structure:
|
||||
{
|
||||
"suggestions": [
|
||||
{
|
||||
"id": "project-slug",
|
||||
"name": "Project Name",
|
||||
"id": "project-or-variant-slug",
|
||||
"name": "Project Name or Variant Name",
|
||||
"shortDescription": "Brief one-line description",
|
||||
"description": "Detailed description"
|
||||
}
|
||||
@@ -120,10 +134,14 @@ export const suggestVariants = async ({
|
||||
|
||||
Important rules for the response:
|
||||
1. Use slug format for the id field (lowercase, hyphenated)
|
||||
2. The description field should ONLY contain a plain text description of the project, its features, and use cases
|
||||
3. Do NOT include any code snippets, configuration examples, or installation instructions in the description
|
||||
4. The shortDescription should be a single-line summary focusing on the main technologies
|
||||
5. All projects should be installable in docker and have docker compose support
|
||||
2. Determine which strategy to use based on whether the user specified a particular application or described a general need
|
||||
3. For Strategy A (specific app): The name must include the app name and describe the variant configuration
|
||||
4. For Strategy B (general need): The name should be the actual project/tool name that fulfills the need
|
||||
5. The description field should ONLY contain a plain text description of the project or variant, its features, and use cases
|
||||
6. Do NOT include any code snippets, configuration examples, or installation instructions in the description
|
||||
7. The shortDescription should be a single-line summary focusing on key technologies or differentiators
|
||||
8. All suggestions should be installable in docker and have docker compose support
|
||||
9. Provide variety in your suggestions - different complexity levels, tech stacks, or approaches
|
||||
|
||||
User wants to create a new project with the following details:
|
||||
|
||||
@@ -186,6 +204,24 @@ export const suggestVariants = async ({
|
||||
6. If a service depends on a database or other service, INCLUDE that service in the docker-compose
|
||||
7. Make sure all required services are defined in the docker-compose
|
||||
|
||||
Docker Image Rules (CRITICAL):
|
||||
1. ALWAYS use 'image:' field, NEVER use 'build:' field
|
||||
2. NEVER use 'build: .' or any build directive - we don't have local Dockerfiles
|
||||
3. Use images from Docker Hub or other public registries (e.g., docker.io, ghcr.io, quay.io)
|
||||
4. For dependencies (databases, redis, etc.), use official images (e.g., postgres:16, redis:7, etc.)
|
||||
5. Always specify image tags - avoid using 'latest' tag, use specific versions when possible
|
||||
6. Examples of correct image usage:
|
||||
- image: sendingtk/chatwoot:develop
|
||||
- image: postgres:16-alpine
|
||||
- image: redis:7-alpine
|
||||
- image: chatwoot/chatwoot:latest
|
||||
7. Examples of INCORRECT usage (DO NOT USE):
|
||||
- build: .
|
||||
- build: ./app
|
||||
- build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
|
||||
Volume Mounting and Configuration Rules:
|
||||
1. DO NOT create configuration files unless the service CANNOT work without them
|
||||
2. Most services can work with just environment variables - USE THEM FIRST
|
||||
@@ -214,6 +250,8 @@ export const suggestVariants = async ({
|
||||
- serviceName: the name of the service in the docker-compose
|
||||
2. Make sure the service is properly configured to work with the specified port
|
||||
|
||||
User's original request: ${input}
|
||||
|
||||
Project details:
|
||||
${suggestion?.description}
|
||||
`,
|
||||
|
||||
@@ -49,7 +49,6 @@ import {
|
||||
updatePreviewDeployment,
|
||||
} from "./preview-deployment";
|
||||
import { validUniqueServerAppName } from "./project";
|
||||
import { createRollback } from "./rollbacks";
|
||||
export type Application = typeof applications.$inferSelect;
|
||||
|
||||
export const createApplication = async (
|
||||
@@ -113,6 +112,7 @@ export const findApplicationById = async (applicationId: string) => {
|
||||
server: true,
|
||||
previewDeployments: true,
|
||||
buildRegistry: true,
|
||||
rollbackRegistry: true,
|
||||
},
|
||||
});
|
||||
if (!application) {
|
||||
@@ -198,7 +198,7 @@ export const deployApplication = async ({
|
||||
command += await buildRemoteDocker(application);
|
||||
}
|
||||
|
||||
command += getBuildCommand(application);
|
||||
command += await getBuildCommand(application);
|
||||
|
||||
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
||||
if (serverId) {
|
||||
@@ -211,17 +211,6 @@ export const deployApplication = async ({
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateApplicationStatus(applicationId, "done");
|
||||
|
||||
if (application.rollbackActive) {
|
||||
const tagImage =
|
||||
application.sourceType === "docker"
|
||||
? application.dockerImage
|
||||
: application.appName;
|
||||
await createRollback({
|
||||
appName: tagImage || "",
|
||||
deploymentId: deployment.deploymentId,
|
||||
});
|
||||
}
|
||||
|
||||
await sendBuildSuccessNotifications({
|
||||
projectName: application.environment.project.name,
|
||||
applicationName: application.name,
|
||||
@@ -299,7 +288,7 @@ export const rebuildApplication = async ({
|
||||
try {
|
||||
let command = "set -e;";
|
||||
// Check case for docker only
|
||||
command += getBuildCommand(application);
|
||||
command += await getBuildCommand(application);
|
||||
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, commandWithLog);
|
||||
@@ -310,17 +299,6 @@ export const rebuildApplication = async ({
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
await updateApplicationStatus(applicationId, "done");
|
||||
|
||||
if (application.rollbackActive) {
|
||||
const tagImage =
|
||||
application.sourceType === "docker"
|
||||
? application.dockerImage
|
||||
: application.appName;
|
||||
await createRollback({
|
||||
appName: tagImage || "",
|
||||
deploymentId: deployment.deploymentId,
|
||||
});
|
||||
}
|
||||
|
||||
await sendBuildSuccessNotifications({
|
||||
projectName: application.environment.project.name,
|
||||
applicationName: application.name,
|
||||
@@ -423,6 +401,10 @@ export const deployPreviewApplication = async ({
|
||||
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||
application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||
application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
|
||||
application.rollbackActive = false;
|
||||
application.buildRegistry = null;
|
||||
application.rollbackRegistry = null;
|
||||
application.registry = null;
|
||||
|
||||
let command = "set -e;";
|
||||
if (application.sourceType === "github") {
|
||||
@@ -431,7 +413,7 @@ export const deployPreviewApplication = async ({
|
||||
appName: previewDeployment.appName,
|
||||
branch: previewDeployment.branch,
|
||||
});
|
||||
command += getBuildCommand(application);
|
||||
command += await getBuildCommand(application);
|
||||
|
||||
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
|
||||
if (application.serverId) {
|
||||
|
||||
@@ -60,6 +60,7 @@ export const createSlackNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "slack",
|
||||
@@ -91,6 +92,7 @@ export const updateSlackNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
organizationId: input.organizationId,
|
||||
@@ -151,6 +153,7 @@ export const createTelegramNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "telegram",
|
||||
@@ -182,6 +185,7 @@ export const updateTelegramNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
organizationId: input.organizationId,
|
||||
@@ -242,6 +246,7 @@ export const createDiscordNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "discord",
|
||||
@@ -273,6 +278,7 @@ export const updateDiscordNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
organizationId: input.organizationId,
|
||||
@@ -336,6 +342,7 @@ export const createEmailNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "email",
|
||||
@@ -367,6 +374,7 @@ export const updateEmailNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
organizationId: input.organizationId,
|
||||
@@ -432,6 +440,7 @@ export const createGotifyNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "gotify",
|
||||
@@ -462,6 +471,7 @@ export const updateGotifyNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
organizationId: input.organizationId,
|
||||
@@ -522,6 +532,7 @@ export const createNtfyNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
notificationType: "ntfy",
|
||||
@@ -552,6 +563,7 @@ export const updateNtfyNotification = async (
|
||||
appDeploy: input.appDeploy,
|
||||
appBuildError: input.appBuildError,
|
||||
databaseBackup: input.databaseBackup,
|
||||
volumeBackup: input.volumeBackup,
|
||||
dokployRestart: input.dokployRestart,
|
||||
dockerCleanup: input.dockerCleanup,
|
||||
organizationId: input.organizationId,
|
||||
|
||||
@@ -19,7 +19,8 @@ export function getMountPath(dockerImage: string): string {
|
||||
if (versionMatch?.[1]) {
|
||||
const version = Number.parseInt(versionMatch[1], 10);
|
||||
if (version >= 18) {
|
||||
return `/var/lib/postgresql/${version}/data`;
|
||||
// PostgreSQL 18+ uses /var/lib/postgresql/{version}/docker as the default PGDATA
|
||||
return `/var/lib/postgresql/${version}/docker`;
|
||||
}
|
||||
}
|
||||
return "/var/lib/postgresql/data";
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
deployments as deploymentsSchema,
|
||||
rollbacks,
|
||||
} from "../db/schema";
|
||||
import { type ApplicationNested, getAuthConfig } from "../utils/builders";
|
||||
import type { ApplicationNested } from "../utils/builders";
|
||||
import { getRegistryTag } from "../utils/cluster/upload";
|
||||
import {
|
||||
calculateResources,
|
||||
generateBindMounts,
|
||||
@@ -22,11 +23,12 @@ import { findDeploymentById } from "./deployment";
|
||||
import type { Mount } from "./mount";
|
||||
import type { Port } from "./port";
|
||||
import type { Project } from "./project";
|
||||
import type { Registry } from "./registry";
|
||||
|
||||
export const createRollback = async (
|
||||
input: z.infer<typeof createRollbackSchema>,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
return await db.transaction(async (tx) => {
|
||||
const { fullContext, ...other } = input;
|
||||
const rollback = await tx
|
||||
.insert(rollbacks)
|
||||
@@ -70,9 +72,11 @@ export const createRollback = async (
|
||||
})
|
||||
.where(eq(deploymentsSchema.deploymentId, rollback.deploymentId));
|
||||
|
||||
await createRollbackImage(rest, tagImage);
|
||||
const updatedRollback = await tx.query.rollbacks.findFirst({
|
||||
where: eq(rollbacks.rollbackId, rollback.rollbackId),
|
||||
});
|
||||
|
||||
return rollback;
|
||||
return updatedRollback;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -103,27 +107,6 @@ export const findRollbackById = async (rollbackId: string) => {
|
||||
return result;
|
||||
};
|
||||
|
||||
const createRollbackImage = async (
|
||||
application: ApplicationNested,
|
||||
tagImage: string,
|
||||
) => {
|
||||
const docker = await getRemoteDocker(application.serverId);
|
||||
|
||||
const appTagName =
|
||||
application.sourceType === "docker"
|
||||
? application.dockerImage
|
||||
: `${application.appName}:latest`;
|
||||
|
||||
const result = docker.getImage(appTagName || "");
|
||||
|
||||
const [repo, version] = tagImage.split(":");
|
||||
|
||||
await result.tag({
|
||||
repo,
|
||||
tag: version,
|
||||
});
|
||||
};
|
||||
|
||||
const deleteRollbackImage = async (image: string, serverId?: string | null) => {
|
||||
const command = `docker image rm ${image} --force`;
|
||||
|
||||
@@ -179,7 +162,6 @@ export const rollback = async (rollbackId: string) => {
|
||||
if (!result.fullContext) {
|
||||
throw new Error("Rollback context not found");
|
||||
}
|
||||
|
||||
// Use the full context for rollback
|
||||
await rollbackApplication(
|
||||
application.appName,
|
||||
@@ -199,6 +181,7 @@ const rollbackApplication = async (
|
||||
};
|
||||
mounts: Mount[];
|
||||
ports: Port[];
|
||||
rollbackRegistry?: Registry;
|
||||
},
|
||||
) => {
|
||||
if (!fullContext) {
|
||||
@@ -245,16 +228,24 @@ const rollbackApplication = async (
|
||||
fullContext.environment.project.env,
|
||||
);
|
||||
|
||||
// For rollback, we use the provided image instead of calculating it
|
||||
const authConfig = getAuthConfig(fullContext as ApplicationNested);
|
||||
// Build the full registry image path if rollbackRegistry is available
|
||||
// e.g., "appName:v5" -> "siumauricio/appName:v5" or "registry.com/prefix/appName:v5"
|
||||
let rollbackImage = image;
|
||||
if (fullContext.rollbackRegistry) {
|
||||
rollbackImage = getRegistryTag(fullContext.rollbackRegistry, image);
|
||||
}
|
||||
|
||||
const settings: CreateServiceOptions = {
|
||||
authconfig: authConfig,
|
||||
authconfig: {
|
||||
password: fullContext.rollbackRegistry?.password || "",
|
||||
username: fullContext.rollbackRegistry?.username || "",
|
||||
serveraddress: fullContext.rollbackRegistry?.registryUrl || "",
|
||||
},
|
||||
Name: appName,
|
||||
TaskTemplate: {
|
||||
ContainerSpec: {
|
||||
HealthCheck,
|
||||
Image: image,
|
||||
Image: rollbackImage,
|
||||
Env: envVariables,
|
||||
Mounts: [...volumesMount, ...bindsMount],
|
||||
...(command
|
||||
@@ -297,7 +288,8 @@ const rollbackApplication = async (
|
||||
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
await docker.createService(settings);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -215,38 +215,6 @@ echo "$json_output"
|
||||
return result;
|
||||
};
|
||||
|
||||
export const cleanupFullDocker = async (serverId?: string | null) => {
|
||||
const cleanupImages = "docker image prune --force";
|
||||
const cleanupVolumes = "docker volume prune --force";
|
||||
const cleanupContainers = "docker container prune --force";
|
||||
const cleanupSystem = "docker system prune --force --volumes";
|
||||
const cleanupBuilder = "docker builder prune --force";
|
||||
|
||||
try {
|
||||
if (serverId) {
|
||||
await execAsyncRemote(
|
||||
serverId,
|
||||
`
|
||||
${cleanupImages}
|
||||
${cleanupVolumes}
|
||||
${cleanupContainers}
|
||||
${cleanupSystem}
|
||||
${cleanupBuilder}
|
||||
`,
|
||||
);
|
||||
}
|
||||
await execAsync(`
|
||||
${cleanupImages}
|
||||
${cleanupVolumes}
|
||||
${cleanupContainers}
|
||||
${cleanupSystem}
|
||||
${cleanupBuilder}
|
||||
`);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const getDockerResourceType = async (
|
||||
resourceName: string,
|
||||
serverId?: string,
|
||||
@@ -372,19 +340,27 @@ export const readPorts = async (
|
||||
publishedPort: number;
|
||||
protocol?: string;
|
||||
}[] = [];
|
||||
const seenPorts = new Set<string>();
|
||||
for (const key in parsedResult) {
|
||||
if (Object.hasOwn(parsedResult, key)) {
|
||||
const containerPortMapppings = parsedResult[key];
|
||||
const protocol = key.split("/")[1];
|
||||
const targetPort = Number.parseInt(key.split("/")[0] ?? "0", 10);
|
||||
|
||||
containerPortMapppings.forEach((mapping: any) => {
|
||||
ports.push({
|
||||
targetPort: targetPort,
|
||||
publishedPort: Number.parseInt(mapping.HostPort, 10),
|
||||
protocol: protocol,
|
||||
});
|
||||
});
|
||||
// Take only the first mapping to avoid duplicates (IPv4 and IPv6)
|
||||
const firstMapping = containerPortMapppings[0];
|
||||
if (firstMapping) {
|
||||
const publishedPort = Number.parseInt(firstMapping.HostPort, 10);
|
||||
const portKey = `${targetPort}-${publishedPort}-${protocol}`;
|
||||
if (!seenPorts.has(portKey)) {
|
||||
seenPorts.add(portKey);
|
||||
ports.push({
|
||||
targetPort: targetPort,
|
||||
publishedPort: publishedPort,
|
||||
protocol: protocol,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ports.filter(
|
||||
@@ -392,6 +368,28 @@ export const readPorts = async (
|
||||
);
|
||||
};
|
||||
|
||||
export const checkPortInUse = async (
|
||||
port: number,
|
||||
serverId?: string,
|
||||
): Promise<{ isInUse: boolean; conflictingContainer?: string }> => {
|
||||
try {
|
||||
const command = `docker ps -a --format '{{.Names}}' | grep -v '^dokploy-traefik$' | while read name; do docker port "$name" 2>/dev/null | grep -q ':${port}' && echo "$name" && break; done || true`;
|
||||
const { stdout } = serverId
|
||||
? await execAsyncRemote(serverId, command)
|
||||
: await execAsync(command);
|
||||
|
||||
const container = stdout.trim();
|
||||
|
||||
return {
|
||||
isInUse: !!container,
|
||||
conflictingContainer: container || undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error checking port availability:", error);
|
||||
return { isInUse: false };
|
||||
}
|
||||
};
|
||||
|
||||
export const writeTraefikSetup = async (input: TraefikOptions) => {
|
||||
const resourceType = await getDockerResourceType(
|
||||
"dokploy-traefik",
|
||||
|
||||
@@ -12,13 +12,69 @@ export const findVolumeBackupById = async (volumeBackupId: string) => {
|
||||
const volumeBackup = await db.query.volumeBackups.findFirst({
|
||||
where: eq(volumeBackups.volumeBackupId, volumeBackupId),
|
||||
with: {
|
||||
application: true,
|
||||
postgres: true,
|
||||
mysql: true,
|
||||
mariadb: true,
|
||||
mongo: true,
|
||||
redis: true,
|
||||
compose: true,
|
||||
application: {
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
postgres: {
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mysql: {
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mariadb: {
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
mongo: {
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
redis: {
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
compose: {
|
||||
with: {
|
||||
environment: {
|
||||
with: {
|
||||
project: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
destination: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { paths } from "@dokploy/server/constants";
|
||||
import { findAdmin } from "@dokploy/server/services/admin";
|
||||
import { findOwner } from "@dokploy/server/services/admin";
|
||||
import { updateUser } from "@dokploy/server/services/user";
|
||||
import { scheduledJobs, scheduleJob } from "node-schedule";
|
||||
import { execAsync } from "../process/execAsync";
|
||||
@@ -29,9 +29,9 @@ export const startLogCleanup = async (
|
||||
}
|
||||
});
|
||||
|
||||
const admin = await findAdmin();
|
||||
if (admin) {
|
||||
await updateUser(admin.user.id, {
|
||||
const owner = await findOwner();
|
||||
if (owner) {
|
||||
await updateUser(owner.user.id, {
|
||||
logCleanupCron: cronExpression,
|
||||
});
|
||||
}
|
||||
@@ -51,9 +51,9 @@ export const stopLogCleanup = async (): Promise<boolean> => {
|
||||
}
|
||||
|
||||
// Update database
|
||||
const admin = await findAdmin();
|
||||
if (admin) {
|
||||
await updateUser(admin.user.id, {
|
||||
const owner = await findOwner();
|
||||
if (owner) {
|
||||
await updateUser(owner.user.id, {
|
||||
logCleanupCron: null,
|
||||
});
|
||||
}
|
||||
@@ -69,8 +69,8 @@ export const getLogCleanupStatus = async (): Promise<{
|
||||
enabled: boolean;
|
||||
cronExpression: string | null;
|
||||
}> => {
|
||||
const admin = await findAdmin();
|
||||
const cronExpression = admin?.user.logCleanupCron ?? null;
|
||||
const owner = await findOwner();
|
||||
const cronExpression = owner?.user.logCleanupCron ?? null;
|
||||
return {
|
||||
enabled: cronExpression !== null,
|
||||
cronExpression,
|
||||
|
||||
@@ -6,11 +6,7 @@ import { eq } from "drizzle-orm";
|
||||
import { scheduleJob } from "node-schedule";
|
||||
import { db } from "../../db/index";
|
||||
import { startLogCleanup } from "../access-log/handler";
|
||||
import {
|
||||
cleanUpDockerBuilder,
|
||||
cleanUpSystemPrune,
|
||||
cleanUpUnusedImages,
|
||||
} from "../docker/utils";
|
||||
import { cleanupAll } from "../docker/utils";
|
||||
import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup";
|
||||
import { execAsync, execAsyncRemote } from "../process/execAsync";
|
||||
import { getS3Credentials, scheduleBackup } from "./utils";
|
||||
@@ -34,9 +30,9 @@ export const initCronJobs = async () => {
|
||||
console.log(
|
||||
`Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`,
|
||||
);
|
||||
await cleanUpUnusedImages();
|
||||
await cleanUpDockerBuilder();
|
||||
await cleanUpSystemPrune();
|
||||
|
||||
await cleanupAll();
|
||||
|
||||
await sendDockerCleanupNotifications(admin.user.id);
|
||||
});
|
||||
}
|
||||
@@ -50,9 +46,9 @@ export const initCronJobs = async () => {
|
||||
console.log(
|
||||
`SERVER-BACKUP[${new Date().toLocaleString()}] Running Cleanup ${name}`,
|
||||
);
|
||||
await cleanUpUnusedImages(serverId);
|
||||
await cleanUpDockerBuilder(serverId);
|
||||
await cleanUpSystemPrune(serverId);
|
||||
|
||||
await cleanupAll(serverId);
|
||||
|
||||
await sendDockerCleanupNotifications(
|
||||
admin.user.id,
|
||||
`Docker cleanup for Server ${name} (${serverId})`,
|
||||
|
||||
@@ -62,16 +62,16 @@ export const getS3Credentials = (destination: Destination) => {
|
||||
const { accessKey, secretAccessKey, region, endpoint, provider } =
|
||||
destination;
|
||||
const rcloneFlags = [
|
||||
`--s3-access-key-id=${accessKey}`,
|
||||
`--s3-secret-access-key=${secretAccessKey}`,
|
||||
`--s3-region=${region}`,
|
||||
`--s3-endpoint=${endpoint}`,
|
||||
`--s3-access-key-id="${accessKey}"`,
|
||||
`--s3-secret-access-key="${secretAccessKey}"`,
|
||||
`--s3-region="${region}"`,
|
||||
`--s3-endpoint="${endpoint}"`,
|
||||
"--s3-no-check-bucket",
|
||||
"--s3-force-path-style",
|
||||
];
|
||||
|
||||
if (provider) {
|
||||
rcloneFlags.unshift(`--s3-provider=${provider}`);
|
||||
rcloneFlags.unshift(`--s3-provider="${provider}"`);
|
||||
}
|
||||
|
||||
return rcloneFlags;
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
getDockerContextPath,
|
||||
} from "../filesystem/directory";
|
||||
import type { ApplicationNested } from ".";
|
||||
import { createEnvFileCommand } from "./utils";
|
||||
|
||||
export const getDockerCommand = (application: ApplicationNested) => {
|
||||
const {
|
||||
@@ -68,21 +67,7 @@ export const getDockerCommand = (application: ApplicationNested) => {
|
||||
commandArgs.push("--secret", `type=env,id=${key}`);
|
||||
}
|
||||
|
||||
/*
|
||||
Do not generate an environment file when publishDirectory is specified,
|
||||
as it could be publicly exposed.
|
||||
*/
|
||||
let command = "";
|
||||
if (!publishDirectory) {
|
||||
command += createEnvFileCommand(
|
||||
dockerFilePath,
|
||||
env,
|
||||
application.environment.project.env,
|
||||
application.environment.env,
|
||||
);
|
||||
}
|
||||
|
||||
command += `
|
||||
const command = `
|
||||
echo "Building ${appName}" ;
|
||||
cd ${dockerContextPath} || {
|
||||
echo "❌ The path ${dockerContextPath} does not exist" ;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { InferResultType } from "@dokploy/server/types/with";
|
||||
import type { CreateServiceOptions } from "dockerode";
|
||||
import { uploadImageRemoteCommand } from "../cluster/upload";
|
||||
import { getRegistryTag, uploadImageRemoteCommand } from "../cluster/upload";
|
||||
import {
|
||||
calculateResources,
|
||||
generateBindMounts,
|
||||
@@ -30,39 +30,45 @@ export type ApplicationNested = InferResultType<
|
||||
ports: true;
|
||||
registry: true;
|
||||
buildRegistry: true;
|
||||
rollbackRegistry: true;
|
||||
deployments: true;
|
||||
environment: { with: { project: true } };
|
||||
}
|
||||
>;
|
||||
|
||||
export const getBuildCommand = (application: ApplicationNested) => {
|
||||
export const getBuildCommand = async (application: ApplicationNested) => {
|
||||
let command = "";
|
||||
const { buildType } = application;
|
||||
|
||||
if (application.sourceType === "docker") {
|
||||
return "";
|
||||
if (application.sourceType !== "docker") {
|
||||
const { buildType } = application;
|
||||
switch (buildType) {
|
||||
case "nixpacks":
|
||||
command = getNixpacksCommand(application);
|
||||
break;
|
||||
case "heroku_buildpacks":
|
||||
command = getHerokuCommand(application);
|
||||
break;
|
||||
case "paketo_buildpacks":
|
||||
command = getPaketoCommand(application);
|
||||
break;
|
||||
case "static":
|
||||
command = getStaticCommand(application);
|
||||
break;
|
||||
case "dockerfile":
|
||||
command = getDockerCommand(application);
|
||||
break;
|
||||
case "railpack":
|
||||
command = getRailpackCommand(application);
|
||||
break;
|
||||
}
|
||||
}
|
||||
switch (buildType) {
|
||||
case "nixpacks":
|
||||
command = getNixpacksCommand(application);
|
||||
break;
|
||||
case "heroku_buildpacks":
|
||||
command = getHerokuCommand(application);
|
||||
break;
|
||||
case "paketo_buildpacks":
|
||||
command = getPaketoCommand(application);
|
||||
break;
|
||||
case "static":
|
||||
command = getStaticCommand(application);
|
||||
break;
|
||||
case "dockerfile":
|
||||
command = getDockerCommand(application);
|
||||
break;
|
||||
case "railpack":
|
||||
command = getRailpackCommand(application);
|
||||
break;
|
||||
}
|
||||
if (application.registry || application.buildRegistry) {
|
||||
command += uploadImageRemoteCommand(application);
|
||||
|
||||
if (
|
||||
application.registry ||
|
||||
application.buildRegistry ||
|
||||
application.rollbackRegistry
|
||||
) {
|
||||
command += await uploadImageRemoteCommand(application);
|
||||
}
|
||||
|
||||
return command;
|
||||
@@ -188,17 +194,11 @@ const getImageName = (application: ApplicationNested) => {
|
||||
}
|
||||
|
||||
if (registry) {
|
||||
const { registryUrl, imagePrefix, username } = registry;
|
||||
const registryTag = imagePrefix
|
||||
? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}`
|
||||
: `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`;
|
||||
const registryTag = getRegistryTag(registry, imageName);
|
||||
return registryTag;
|
||||
}
|
||||
if (buildRegistry) {
|
||||
const { registryUrl, imagePrefix, username } = buildRegistry;
|
||||
const registryTag = imagePrefix
|
||||
? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}`
|
||||
: `${registryUrl ? `${registryUrl}/` : ""}${username}/${imageName}`;
|
||||
const registryTag = getRegistryTag(buildRegistry, imageName);
|
||||
return registryTag;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
import { findAllDeploymentsByApplicationId } from "@dokploy/server/services/deployment";
|
||||
import type { Registry } from "@dokploy/server/services/registry";
|
||||
import { createRollback } from "@dokploy/server/services/rollbacks";
|
||||
import type { ApplicationNested } from "../builders";
|
||||
|
||||
export const uploadImageRemoteCommand = (application: ApplicationNested) => {
|
||||
export const uploadImageRemoteCommand = async (
|
||||
application: ApplicationNested,
|
||||
) => {
|
||||
const registry = application.registry;
|
||||
const buildRegistry = application.buildRegistry;
|
||||
const rollbackRegistry = application.rollbackRegistry;
|
||||
|
||||
if (!registry && !buildRegistry) {
|
||||
if (!registry && !buildRegistry && !rollbackRegistry) {
|
||||
throw new Error("No registry found");
|
||||
}
|
||||
|
||||
const { appName } = application;
|
||||
const imageName = `${appName}:latest`;
|
||||
const imageName =
|
||||
application.sourceType === "docker"
|
||||
? application.dockerImage || ""
|
||||
: `${appName}:latest`;
|
||||
|
||||
const commands: string[] = [];
|
||||
if (registry) {
|
||||
@@ -35,16 +43,38 @@ export const uploadImageRemoteCommand = (application: ApplicationNested) => {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (rollbackRegistry && application.rollbackActive) {
|
||||
const deployment = await findAllDeploymentsByApplicationId(
|
||||
application.applicationId,
|
||||
);
|
||||
if (!deployment || !deployment[0]) {
|
||||
throw new Error("Deployment not found");
|
||||
}
|
||||
const deploymentId = deployment[0].deploymentId;
|
||||
const rollback = await createRollback({
|
||||
appName: appName,
|
||||
deploymentId: deploymentId,
|
||||
});
|
||||
|
||||
const rollbackRegistryTag = getRegistryTag(
|
||||
rollbackRegistry,
|
||||
rollback?.image || "",
|
||||
);
|
||||
if (rollbackRegistryTag) {
|
||||
commands.push(`echo "🔄 [Enabled Rollback Registry]"`);
|
||||
commands.push(
|
||||
getRegistryCommands(rollbackRegistry, imageName, rollbackRegistryTag),
|
||||
);
|
||||
}
|
||||
}
|
||||
try {
|
||||
return commands.join("\n");
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
const getRegistryTag = (registry: Registry | null, imageName: string) => {
|
||||
if (!registry) {
|
||||
return null;
|
||||
}
|
||||
export const getRegistryTag = (registry: Registry, imageName: string) => {
|
||||
const { registryUrl, imagePrefix, username } = registry;
|
||||
return imagePrefix
|
||||
? `${registryUrl ? `${registryUrl}/` : ""}${imagePrefix}/${imageName}`
|
||||
|
||||
@@ -144,81 +144,116 @@ export const getContainerByName = (name: string): Promise<ContainerInfo> => {
|
||||
});
|
||||
});
|
||||
};
|
||||
export const cleanUpUnusedImages = async (serverId?: string) => {
|
||||
try {
|
||||
const command = "docker image prune --force";
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const cleanStoppedContainers = async (serverId?: string) => {
|
||||
/**
|
||||
* Docker commands passed through this method are held during Docker's build or pull process.
|
||||
*
|
||||
* https://github.com/Dokploy/dokploy/pull/3064
|
||||
* https://github.com/fir4tozden
|
||||
*/
|
||||
export const dockerSafeExec = (exec: string) => `CHECK_INTERVAL=10
|
||||
|
||||
echo "Preparing for execution..."
|
||||
|
||||
while true; do
|
||||
PROCESSES=$(ps aux | grep -E "docker build|docker pull" | grep -v grep)
|
||||
|
||||
if [ -z "$PROCESSES" ]; then
|
||||
echo "Docker is idle. Starting execution..."
|
||||
break
|
||||
else
|
||||
echo "Docker is busy. Will check again in $CHECK_INTERVAL seconds..."
|
||||
sleep $CHECK_INTERVAL
|
||||
fi
|
||||
done
|
||||
|
||||
${exec}
|
||||
|
||||
echo "Execution completed."`;
|
||||
|
||||
export const cleanupContainers = async (serverId?: string) => {
|
||||
try {
|
||||
const command = "docker container prune --force";
|
||||
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
await execAsyncRemote(serverId, dockerSafeExec(command));
|
||||
} else {
|
||||
await execAsync(command);
|
||||
await execAsync(dockerSafeExec(command));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const cleanUpUnusedVolumes = async (serverId?: string) => {
|
||||
export const cleanupImages = async (serverId?: string) => {
|
||||
try {
|
||||
const command = "docker volume prune --force";
|
||||
const command = "docker image prune --all --force";
|
||||
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
await execAsyncRemote(serverId, dockerSafeExec(command));
|
||||
} else await execAsync(dockerSafeExec(command));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const cleanupVolumes = async (serverId?: string) => {
|
||||
try {
|
||||
const command = "docker volume prune --all --force";
|
||||
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, dockerSafeExec(command));
|
||||
} else {
|
||||
await execAsync(command);
|
||||
await execAsync(dockerSafeExec(command));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const cleanUpInactiveContainers = async () => {
|
||||
export const cleanupBuilders = async (serverId?: string) => {
|
||||
try {
|
||||
const containers = await docker.listContainers({ all: true });
|
||||
const inactiveContainers = containers.filter(
|
||||
(container) => container.State !== "running",
|
||||
);
|
||||
const command = "docker builder prune --all --force";
|
||||
|
||||
for (const container of inactiveContainers) {
|
||||
await docker.getContainer(container.Id).remove({ force: true });
|
||||
console.log(`Cleaning up inactive container: ${container.Id}`);
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, dockerSafeExec(command));
|
||||
} else {
|
||||
await execAsync(dockerSafeExec(command));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error cleaning up inactive containers:", error);
|
||||
console.error(error);
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const cleanUpDockerBuilder = async (serverId?: string) => {
|
||||
const command = "docker builder prune --all --force";
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
export const cleanupSystem = async (serverId?: string) => {
|
||||
try {
|
||||
const command = "docker system prune --all --force";
|
||||
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, dockerSafeExec(command));
|
||||
} else {
|
||||
await execAsync(dockerSafeExec(command));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const cleanUpSystemPrune = async (serverId?: string) => {
|
||||
const command = "docker system prune --force --volumes";
|
||||
if (serverId) {
|
||||
await execAsyncRemote(serverId, command);
|
||||
} else {
|
||||
await execAsync(command);
|
||||
}
|
||||
export const cleanupAll = async (serverId?: string) => {
|
||||
await cleanupContainers(serverId);
|
||||
await cleanupImages(serverId);
|
||||
await cleanupBuilders(serverId);
|
||||
await cleanupSystem(serverId);
|
||||
};
|
||||
|
||||
export const startService = async (appName: string) => {
|
||||
|
||||
274
packages/server/src/utils/notifications/volume-backup.ts
Normal file
274
packages/server/src/utils/notifications/volume-backup.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { notifications } from "@dokploy/server/db/schema";
|
||||
import { VolumeBackupEmail } from "@dokploy/server/emails/emails/volume-backup";
|
||||
import { renderAsync } from "@react-email/components";
|
||||
import { format } from "date-fns";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import {
|
||||
sendDiscordNotification,
|
||||
sendEmailNotification,
|
||||
sendGotifyNotification,
|
||||
sendNtfyNotification,
|
||||
sendSlackNotification,
|
||||
sendTelegramNotification,
|
||||
} from "./utils";
|
||||
|
||||
export const sendVolumeBackupNotifications = async ({
|
||||
projectName,
|
||||
applicationName,
|
||||
volumeName,
|
||||
serviceType,
|
||||
type,
|
||||
errorMessage,
|
||||
organizationId,
|
||||
backupSize,
|
||||
}: {
|
||||
projectName: string;
|
||||
applicationName: string;
|
||||
volumeName: string;
|
||||
serviceType:
|
||||
| "application"
|
||||
| "postgres"
|
||||
| "mysql"
|
||||
| "mongodb"
|
||||
| "mariadb"
|
||||
| "redis"
|
||||
| "compose";
|
||||
type: "error" | "success";
|
||||
organizationId: string;
|
||||
errorMessage?: string;
|
||||
backupSize?: string;
|
||||
}) => {
|
||||
const date = new Date();
|
||||
const unixDate = ~~(Number(date) / 1000);
|
||||
const notificationList = await db.query.notifications.findMany({
|
||||
where: and(
|
||||
eq(notifications.volumeBackup, true),
|
||||
eq(notifications.organizationId, organizationId),
|
||||
),
|
||||
with: {
|
||||
email: true,
|
||||
discord: true,
|
||||
telegram: true,
|
||||
slack: true,
|
||||
gotify: true,
|
||||
ntfy: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const notification of notificationList) {
|
||||
const { email, discord, telegram, slack, gotify, ntfy } = notification;
|
||||
|
||||
if (email) {
|
||||
const subject = `Volume Backup ${type === "success" ? "Successful" : "Failed"} - ${applicationName}`;
|
||||
const htmlContent = await renderAsync(
|
||||
VolumeBackupEmail({
|
||||
projectName,
|
||||
applicationName,
|
||||
volumeName,
|
||||
serviceType,
|
||||
type,
|
||||
errorMessage,
|
||||
backupSize,
|
||||
date: date.toISOString(),
|
||||
}),
|
||||
);
|
||||
await sendEmailNotification(email, subject, htmlContent);
|
||||
}
|
||||
|
||||
if (discord) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${discord.decoration ? decoration : ""} ${text}`.trim();
|
||||
|
||||
await sendDiscordNotification(discord, {
|
||||
title:
|
||||
type === "success"
|
||||
? decorate(">", "`✅` Volume Backup Successful")
|
||||
: decorate(">", "`❌` Volume Backup Failed"),
|
||||
color: type === "success" ? 0x57f287 : 0xed4245,
|
||||
fields: [
|
||||
{
|
||||
name: decorate("`🛠️`", "Project"),
|
||||
value: projectName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⚙️`", "Application"),
|
||||
value: applicationName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`💾`", "Volume Name"),
|
||||
value: volumeName,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`🔧`", "Service Type"),
|
||||
value: serviceType,
|
||||
inline: true,
|
||||
},
|
||||
...(backupSize
|
||||
? [
|
||||
{
|
||||
name: decorate("`📊`", "Backup Size"),
|
||||
value: backupSize,
|
||||
inline: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: decorate("`📅`", "Date"),
|
||||
value: `<t:${unixDate}:D>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`⌚`", "Time"),
|
||||
value: `<t:${unixDate}:t>`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: decorate("`❓`", "Type"),
|
||||
value: type
|
||||
.replace("error", "Failed")
|
||||
.replace("success", "Successful"),
|
||||
inline: true,
|
||||
},
|
||||
...(type === "error" && errorMessage
|
||||
? [
|
||||
{
|
||||
name: decorate("`⚠️`", "Error Message"),
|
||||
value: `\`\`\`${errorMessage}\`\`\``,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
timestamp: date.toISOString(),
|
||||
footer: {
|
||||
text: "Dokploy Volume Backup Notification",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (gotify) {
|
||||
const decorate = (decoration: string, text: string) =>
|
||||
`${gotify.decoration ? decoration : ""} ${text}\n`;
|
||||
|
||||
await sendGotifyNotification(
|
||||
gotify,
|
||||
decorate(
|
||||
type === "success" ? "✅" : "❌",
|
||||
`Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
),
|
||||
`${decorate("🛠️", `Project: ${projectName}`)}` +
|
||||
`${decorate("⚙️", `Application: ${applicationName}`)}` +
|
||||
`${decorate("💾", `Volume Name: ${volumeName}`)}` +
|
||||
`${decorate("🔧", `Service Type: ${serviceType}`)}` +
|
||||
`${backupSize ? decorate("📊", `Backup Size: ${backupSize}`) : ""}` +
|
||||
`${decorate("🕒", `Date: ${date.toLocaleString()}`)}` +
|
||||
`${type === "error" && errorMessage ? decorate("❌", `Error:\n${errorMessage}`) : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (ntfy) {
|
||||
await sendNtfyNotification(
|
||||
ntfy,
|
||||
`Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
|
||||
`${type === "success" ? "white_check_mark" : "x"}`,
|
||||
"",
|
||||
`🛠️Project: ${projectName}\n` +
|
||||
`⚙️Application: ${applicationName}\n` +
|
||||
`💾Volume Name: ${volumeName}\n` +
|
||||
`🔧Service Type: ${serviceType}\n` +
|
||||
`${backupSize ? `📊Backup Size: ${backupSize}\n` : ""}` +
|
||||
`🕒Date: ${date.toLocaleString()}\n` +
|
||||
`${type === "error" && errorMessage ? `❌Error:\n${errorMessage}` : ""}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (telegram) {
|
||||
const isError = type === "error" && errorMessage;
|
||||
|
||||
const statusEmoji = type === "success" ? "✅" : "❌";
|
||||
const typeStatus = type === "success" ? "Successful" : "Failed";
|
||||
const errorMsg = isError
|
||||
? `\n\n<b>Error:</b>\n<pre>${errorMessage}</pre>`
|
||||
: "";
|
||||
const sizeInfo = backupSize ? `\n<b>Backup Size:</b> ${backupSize}` : "";
|
||||
|
||||
const messageText = `<b>${statusEmoji} Volume Backup ${typeStatus}</b>\n\n<b>Project:</b> ${projectName}\n<b>Application:</b> ${applicationName}\n<b>Volume Name:</b> ${volumeName}\n<b>Service Type:</b> ${serviceType}${sizeInfo}\n<b>Date:</b> ${format(date, "PP")}\n<b>Time:</b> ${format(date, "pp")}${isError ? errorMsg : ""}`;
|
||||
|
||||
await sendTelegramNotification(telegram, messageText);
|
||||
}
|
||||
|
||||
if (slack) {
|
||||
const { channel } = slack;
|
||||
await sendSlackNotification(slack, {
|
||||
channel: channel,
|
||||
attachments: [
|
||||
{
|
||||
color: type === "success" ? "#00FF00" : "#FF0000",
|
||||
pretext:
|
||||
type === "success"
|
||||
? ":white_check_mark: *Volume Backup Successful*"
|
||||
: ":x: *Volume Backup Failed*",
|
||||
fields: [
|
||||
...(type === "error" && errorMessage
|
||||
? [
|
||||
{
|
||||
title: "Error Message",
|
||||
value: errorMessage,
|
||||
short: false,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: "Project",
|
||||
value: projectName,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Application",
|
||||
value: applicationName,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Volume Name",
|
||||
value: volumeName,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Service Type",
|
||||
value: serviceType,
|
||||
short: true,
|
||||
},
|
||||
...(backupSize
|
||||
? [
|
||||
{
|
||||
title: "Backup Size",
|
||||
value: backupSize,
|
||||
short: true,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: "Time",
|
||||
value: date.toLocaleString(),
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Type",
|
||||
value: type,
|
||||
short: true,
|
||||
},
|
||||
{
|
||||
title: "Status",
|
||||
value: type === "success" ? "Successful" : "Failed",
|
||||
short: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -11,8 +11,54 @@ import {
|
||||
} from "@dokploy/server/utils/process/execAsync";
|
||||
import { scheduledJobs, scheduleJob } from "node-schedule";
|
||||
import { getS3Credentials, normalizeS3Path } from "../backups/utils";
|
||||
import { sendVolumeBackupNotifications } from "../notifications/volume-backup";
|
||||
import { backupVolume } from "./backup";
|
||||
|
||||
// Helper functions to extract project info from volume backup
|
||||
const getProjectName = (
|
||||
volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>,
|
||||
): string => {
|
||||
const services = [
|
||||
volumeBackup.application,
|
||||
volumeBackup.compose,
|
||||
volumeBackup.postgres,
|
||||
volumeBackup.mysql,
|
||||
volumeBackup.mariadb,
|
||||
volumeBackup.mongo,
|
||||
volumeBackup.redis,
|
||||
];
|
||||
|
||||
for (const service of services) {
|
||||
if (service?.environment?.project?.name) {
|
||||
return service.environment.project.name;
|
||||
}
|
||||
}
|
||||
|
||||
return "Unknown Project";
|
||||
};
|
||||
|
||||
const getOrganizationId = (
|
||||
volumeBackup: Awaited<ReturnType<typeof findVolumeBackupById>>,
|
||||
): string => {
|
||||
const services = [
|
||||
volumeBackup.application,
|
||||
volumeBackup.compose,
|
||||
volumeBackup.postgres,
|
||||
volumeBackup.mysql,
|
||||
volumeBackup.mariadb,
|
||||
volumeBackup.mongo,
|
||||
volumeBackup.redis,
|
||||
];
|
||||
|
||||
for (const service of services) {
|
||||
if (service?.environment?.project?.organizationId) {
|
||||
return service.environment.project.organizationId;
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
export const scheduleVolumeBackup = async (volumeBackupId: string) => {
|
||||
const volumeBackup = await findVolumeBackupById(volumeBackupId);
|
||||
scheduleJob(volumeBackupId, volumeBackup.cronExpression, async () => {
|
||||
@@ -61,7 +107,8 @@ export const runVolumeBackup = async (volumeBackupId: string) => {
|
||||
title: "Volume Backup",
|
||||
description: "Volume Backup",
|
||||
});
|
||||
|
||||
const projectName = getProjectName(volumeBackup);
|
||||
const organizationId = getOrganizationId(volumeBackup);
|
||||
try {
|
||||
const command = await backupVolume(volumeBackup);
|
||||
|
||||
@@ -77,6 +124,21 @@ export const runVolumeBackup = async (volumeBackupId: string) => {
|
||||
}
|
||||
|
||||
await updateDeploymentStatus(deployment.deploymentId, "done");
|
||||
|
||||
// Map service type to match notification function expectations
|
||||
const mappedServiceType =
|
||||
volumeBackup.serviceType === "mongo"
|
||||
? "mongodb"
|
||||
: volumeBackup.serviceType;
|
||||
|
||||
await sendVolumeBackupNotifications({
|
||||
projectName,
|
||||
applicationName: volumeBackup.name,
|
||||
volumeName: volumeBackup.volumeName,
|
||||
serviceType: mappedServiceType,
|
||||
type: "success",
|
||||
organizationId,
|
||||
});
|
||||
} catch (error) {
|
||||
const { VOLUME_BACKUPS_PATH } = paths(!!serverId);
|
||||
const volumeBackupPath = path.join(
|
||||
@@ -92,6 +154,20 @@ export const runVolumeBackup = async (volumeBackupId: string) => {
|
||||
}
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
|
||||
console.error(error);
|
||||
// Send error notification
|
||||
const mappedServiceType =
|
||||
volumeBackup.serviceType === "mongo"
|
||||
? "mongodb"
|
||||
: volumeBackup.serviceType;
|
||||
|
||||
await sendVolumeBackupNotifications({
|
||||
projectName,
|
||||
applicationName: volumeBackup.name,
|
||||
volumeName: volumeBackup.volumeName,
|
||||
serviceType: mappedServiceType,
|
||||
type: "error",
|
||||
organizationId,
|
||||
errorMessage: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
433
pnpm-lock.yaml
generated
433
pnpm-lock.yaml
generated
@@ -62,7 +62,7 @@ importers:
|
||||
version: 4.7.10
|
||||
inngest:
|
||||
specifier: 3.40.1
|
||||
version: 3.40.1(h3@1.15.3)(hono@4.7.10)(next@15.3.2(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(typescript@5.8.3)
|
||||
version: 3.40.1(h3@1.15.3)(hono@4.7.10)(next@16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(typescript@5.8.3)
|
||||
pino:
|
||||
specifier: 9.4.0
|
||||
version: 9.4.0
|
||||
@@ -237,7 +237,7 @@ importers:
|
||||
version: 10.45.2(@trpc/server@10.45.2)
|
||||
'@trpc/next':
|
||||
specifier: ^10.45.2
|
||||
version: 10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/react-query@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.45.2)(next@15.3.2(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
version: 10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/react-query@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.45.2)(next@16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@trpc/react-query':
|
||||
specifier: ^10.45.2
|
||||
version: 10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
@@ -338,17 +338,17 @@ importers:
|
||||
specifier: 3.3.11
|
||||
version: 3.3.11
|
||||
next:
|
||||
specifier: ^15.3.2
|
||||
version: 15.3.2(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
specifier: ^16.0.7
|
||||
version: 16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
next-i18next:
|
||||
specifier: ^15.4.2
|
||||
version: 15.4.2(i18next@23.16.8)(next@15.3.2(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-i18next@15.5.2(i18next@23.16.8)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3))(react@18.2.0)
|
||||
version: 15.4.2(i18next@23.16.8)(next@16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-i18next@15.5.2(i18next@23.16.8)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3))(react@18.2.0)
|
||||
next-themes:
|
||||
specifier: ^0.2.1
|
||||
version: 0.2.1(next@15.3.2(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
version: 0.2.1(next@16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
nextjs-toploader:
|
||||
specifier: ^3.9.17
|
||||
version: 3.9.17(next@15.3.2(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
version: 3.9.17(next@16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
node-os-utils:
|
||||
specifier: 2.0.1
|
||||
version: 2.0.1
|
||||
@@ -1089,8 +1089,8 @@ packages:
|
||||
'@drizzle-team/brocli@0.10.2':
|
||||
resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==}
|
||||
|
||||
'@emnapi/runtime@1.4.3':
|
||||
resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==}
|
||||
'@emnapi/runtime@1.7.1':
|
||||
resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==}
|
||||
|
||||
'@emotion/is-prop-valid@0.7.3':
|
||||
resolution: {integrity: sha512-uxJqm/sqwXw3YPA5GXX365OBcJGFtxUVkB6WyezqFHlNe9jqUWH5ur2O2M8dGBz61kn1g3ZBlzUunFQXQIClhA==}
|
||||
@@ -1703,118 +1703,139 @@ packages:
|
||||
peerDependencies:
|
||||
react-hook-form: ^7.0.0
|
||||
|
||||
'@img/sharp-darwin-arm64@0.34.2':
|
||||
resolution: {integrity: sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg==}
|
||||
'@img/colour@1.0.0':
|
||||
resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@img/sharp-darwin-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-darwin-x64@0.34.2':
|
||||
resolution: {integrity: sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g==}
|
||||
'@img/sharp-darwin-x64@0.34.5':
|
||||
resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.1.0':
|
||||
resolution: {integrity: sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==}
|
||||
'@img/sharp-libvips-darwin-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.1.0':
|
||||
resolution: {integrity: sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==}
|
||||
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||
resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.1.0':
|
||||
resolution: {integrity: sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==}
|
||||
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.1.0':
|
||||
resolution: {integrity: sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==}
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.1.0':
|
||||
resolution: {integrity: sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==}
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.1.0':
|
||||
resolution: {integrity: sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==}
|
||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.1.0':
|
||||
resolution: {integrity: sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==}
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.1.0':
|
||||
resolution: {integrity: sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==}
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.1.0':
|
||||
resolution: {integrity: sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==}
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.2':
|
||||
resolution: {integrity: sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==}
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-arm@0.34.2':
|
||||
resolution: {integrity: sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==}
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.2':
|
||||
resolution: {integrity: sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==}
|
||||
'@img/sharp-linux-ppc64@0.34.5':
|
||||
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-riscv64@0.34.5':
|
||||
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.5':
|
||||
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-x64@0.34.2':
|
||||
resolution: {integrity: sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==}
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.2':
|
||||
resolution: {integrity: sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==}
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.2':
|
||||
resolution: {integrity: sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==}
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-wasm32@0.34.2':
|
||||
resolution: {integrity: sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==}
|
||||
'@img/sharp-wasm32@0.34.5':
|
||||
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [wasm32]
|
||||
|
||||
'@img/sharp-win32-arm64@0.34.2':
|
||||
resolution: {integrity: sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ==}
|
||||
'@img/sharp-win32-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-ia32@0.34.2':
|
||||
resolution: {integrity: sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw==}
|
||||
'@img/sharp-win32-ia32@0.34.5':
|
||||
resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-x64@0.34.2':
|
||||
resolution: {integrity: sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw==}
|
||||
'@img/sharp-win32-x64@0.34.5':
|
||||
resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -1941,53 +1962,53 @@ packages:
|
||||
peerDependencies:
|
||||
redis: ^4.7.0
|
||||
|
||||
'@next/env@15.3.2':
|
||||
resolution: {integrity: sha512-xURk++7P7qR9JG1jJtLzPzf0qEvqCN0A/T3DXf8IPMKo9/6FfjxtEffRJIIew/bIL4T3C2jLLqBor8B/zVlx6g==}
|
||||
'@next/env@16.0.7':
|
||||
resolution: {integrity: sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw==}
|
||||
|
||||
'@next/swc-darwin-arm64@15.3.2':
|
||||
resolution: {integrity: sha512-2DR6kY/OGcokbnCsjHpNeQblqCZ85/1j6njYSkzRdpLn5At7OkSdmk7WyAmB9G0k25+VgqVZ/u356OSoQZ3z0g==}
|
||||
'@next/swc-darwin-arm64@16.0.7':
|
||||
resolution: {integrity: sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-darwin-x64@15.3.2':
|
||||
resolution: {integrity: sha512-ro/fdqaZWL6k1S/5CLv1I0DaZfDVJkWNaUU3un8Lg6m0YENWlDulmIWzV96Iou2wEYyEsZq51mwV8+XQXqMp3w==}
|
||||
'@next/swc-darwin-x64@16.0.7':
|
||||
resolution: {integrity: sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-linux-arm64-gnu@15.3.2':
|
||||
resolution: {integrity: sha512-covwwtZYhlbRWK2HlYX9835qXum4xYZ3E2Mra1mdQ+0ICGoMiw1+nVAn4d9Bo7R3JqSmK1grMq/va+0cdh7bJA==}
|
||||
'@next/swc-linux-arm64-gnu@16.0.7':
|
||||
resolution: {integrity: sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-arm64-musl@15.3.2':
|
||||
resolution: {integrity: sha512-KQkMEillvlW5Qk5mtGA/3Yz0/tzpNlSw6/3/ttsV1lNtMuOHcGii3zVeXZyi4EJmmLDKYcTcByV2wVsOhDt/zg==}
|
||||
'@next/swc-linux-arm64-musl@16.0.7':
|
||||
resolution: {integrity: sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-x64-gnu@15.3.2':
|
||||
resolution: {integrity: sha512-uRBo6THWei0chz+Y5j37qzx+BtoDRFIkDzZjlpCItBRXyMPIg079eIkOCl3aqr2tkxL4HFyJ4GHDes7W8HuAUg==}
|
||||
'@next/swc-linux-x64-gnu@16.0.7':
|
||||
resolution: {integrity: sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-x64-musl@15.3.2':
|
||||
resolution: {integrity: sha512-+uxFlPuCNx/T9PdMClOqeE8USKzj8tVz37KflT3Kdbx/LOlZBRI2yxuIcmx1mPNK8DwSOMNCr4ureSet7eyC0w==}
|
||||
'@next/swc-linux-x64-musl@16.0.7':
|
||||
resolution: {integrity: sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@15.3.2':
|
||||
resolution: {integrity: sha512-LLTKmaI5cfD8dVzh5Vt7+OMo+AIOClEdIU/TSKbXXT2iScUTSxOGoBhfuv+FU8R9MLmrkIL1e2fBMkEEjYAtPQ==}
|
||||
'@next/swc-win32-arm64-msvc@16.0.7':
|
||||
resolution: {integrity: sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@next/swc-win32-x64-msvc@15.3.2':
|
||||
resolution: {integrity: sha512-aW5B8wOPioJ4mBdMDXkt5f3j8pUr9W8AnlX0Df35uRWNT1Y6RIybxjnSUe+PhM+M1bwgyY8PHLmXZC6zT1o5tA==}
|
||||
'@next/swc-win32-x64-msvc@16.0.7':
|
||||
resolution: {integrity: sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -3827,9 +3848,6 @@ packages:
|
||||
resolution: {integrity: sha512-qMx1nOrzoB+PF+pzb26Q4Tc2sOlrx9Ba2UBNX9hB31Omrq+QoZ2Gly0KLrQWw4Of1AQ4J9lnD+XOdwOdcdXqqw==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
|
||||
'@swc/counter@0.1.3':
|
||||
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
||||
|
||||
@@ -4395,10 +4413,6 @@ packages:
|
||||
bullmq@5.4.2:
|
||||
resolution: {integrity: sha512-dkR/KGUw18miLe3QWtvSlmGvEe08aZF+w1jZyqEHMWFW3RP4162qp6OGud0/QCAOjusiRI8UOxUhbnortPY+rA==}
|
||||
|
||||
busboy@1.6.0:
|
||||
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
|
||||
engines: {node: '>=10.16.0'}
|
||||
|
||||
bytes@3.1.2:
|
||||
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -4562,17 +4576,10 @@ packages:
|
||||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
color-string@1.9.1:
|
||||
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
|
||||
|
||||
color-support@1.1.3:
|
||||
resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==}
|
||||
hasBin: true
|
||||
|
||||
color@4.2.3:
|
||||
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
|
||||
engines: {node: '>=12.5.0'}
|
||||
|
||||
colorette@2.0.20:
|
||||
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
|
||||
|
||||
@@ -4834,6 +4841,10 @@ packages:
|
||||
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
detect-libc@2.1.2:
|
||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
detect-node-es@1.1.0:
|
||||
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
|
||||
|
||||
@@ -5578,9 +5589,6 @@ packages:
|
||||
is-arrayish@0.2.1:
|
||||
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
|
||||
|
||||
is-arrayish@0.3.2:
|
||||
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
|
||||
|
||||
is-binary-path@2.1.0:
|
||||
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -6249,13 +6257,13 @@ packages:
|
||||
react: '*'
|
||||
react-dom: '*'
|
||||
|
||||
next@15.3.2:
|
||||
resolution: {integrity: sha512-CA3BatMyHkxZ48sgOCLdVHjFU36N7TF1HhqAHLFOkV6buwZnvMI84Cug8xD56B9mCuKrqXnLn94417GrZ/jjCQ==}
|
||||
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
|
||||
next@16.0.7:
|
||||
resolution: {integrity: sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': ^1.1.0
|
||||
'@playwright/test': ^1.41.2
|
||||
'@playwright/test': ^1.51.1
|
||||
babel-plugin-react-compiler: '*'
|
||||
react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
|
||||
react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
|
||||
@@ -7094,6 +7102,11 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
semver@7.7.3:
|
||||
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
serialize-error-cjs@0.1.4:
|
||||
resolution: {integrity: sha512-6a6dNqipzbCPlTFgztfNP2oG+IGcflMe/01zSzGrQcxGMKbIjOemBBD85pH92klWaJavAUWxAh9Z0aU28zxW6A==}
|
||||
deprecated: Rolling release, please update to 0.2.0
|
||||
@@ -7118,8 +7131,8 @@ packages:
|
||||
shallow-equal@1.2.1:
|
||||
resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==}
|
||||
|
||||
sharp@0.34.2:
|
||||
resolution: {integrity: sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg==}
|
||||
sharp@0.34.5:
|
||||
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
@@ -7167,9 +7180,6 @@ packages:
|
||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
simple-swizzle@0.2.2:
|
||||
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
|
||||
|
||||
slash@3.0.0:
|
||||
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -7242,10 +7252,6 @@ packages:
|
||||
std-env@3.9.0:
|
||||
resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==}
|
||||
|
||||
streamsearch@1.1.0:
|
||||
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
string-argv@0.3.2:
|
||||
resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
|
||||
engines: {node: '>=0.6.19'}
|
||||
@@ -8207,7 +8213,7 @@ snapshots:
|
||||
|
||||
'@drizzle-team/brocli@0.10.2': {}
|
||||
|
||||
'@emnapi/runtime@1.4.3':
|
||||
'@emnapi/runtime@1.7.1':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
@@ -8549,85 +8555,101 @@ snapshots:
|
||||
dependencies:
|
||||
react-hook-form: 7.56.4(react@18.2.0)
|
||||
|
||||
'@img/sharp-darwin-arm64@0.34.2':
|
||||
'@img/colour@1.0.0':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-darwin-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-arm64': 1.1.0
|
||||
'@img/sharp-libvips-darwin-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-darwin-x64@0.34.2':
|
||||
'@img/sharp-darwin-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-x64': 1.1.0
|
||||
'@img/sharp-libvips-darwin-x64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.1.0':
|
||||
'@img/sharp-libvips-darwin-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.1.0':
|
||||
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.1.0':
|
||||
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.1.0':
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.1.0':
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.1.0':
|
||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.1.0':
|
||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.1.0':
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.1.0':
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.2':
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm64': 1.1.0
|
||||
'@img/sharp-libvips-linux-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm@0.34.2':
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm': 1.1.0
|
||||
'@img/sharp-libvips-linux-arm': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.2':
|
||||
'@img/sharp-linux-ppc64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-s390x': 1.1.0
|
||||
'@img/sharp-libvips-linux-ppc64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-x64@0.34.2':
|
||||
'@img/sharp-linux-riscv64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-x64': 1.1.0
|
||||
'@img/sharp-libvips-linux-riscv64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.2':
|
||||
'@img/sharp-linux-s390x@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.1.0
|
||||
'@img/sharp-libvips-linux-s390x': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.2':
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.1.0
|
||||
'@img/sharp-libvips-linux-x64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-wasm32@0.34.2':
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-wasm32@0.34.5':
|
||||
dependencies:
|
||||
'@emnapi/runtime': 1.4.3
|
||||
'@emnapi/runtime': 1.7.1
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-arm64@0.34.2':
|
||||
'@img/sharp-win32-arm64@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-ia32@0.34.2':
|
||||
'@img/sharp-win32-ia32@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-x64@0.34.2':
|
||||
'@img/sharp-win32-x64@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@inngest/ai@0.1.5':
|
||||
@@ -8755,30 +8777,30 @@ snapshots:
|
||||
async-await-queue: 2.1.4
|
||||
redis: 4.7.0
|
||||
|
||||
'@next/env@15.3.2': {}
|
||||
'@next/env@16.0.7': {}
|
||||
|
||||
'@next/swc-darwin-arm64@15.3.2':
|
||||
'@next/swc-darwin-arm64@16.0.7':
|
||||
optional: true
|
||||
|
||||
'@next/swc-darwin-x64@15.3.2':
|
||||
'@next/swc-darwin-x64@16.0.7':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-gnu@15.3.2':
|
||||
'@next/swc-linux-arm64-gnu@16.0.7':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-musl@15.3.2':
|
||||
'@next/swc-linux-arm64-musl@16.0.7':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-gnu@15.3.2':
|
||||
'@next/swc-linux-x64-gnu@16.0.7':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-musl@15.3.2':
|
||||
'@next/swc-linux-x64-musl@16.0.7':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-arm64-msvc@15.3.2':
|
||||
'@next/swc-win32-arm64-msvc@16.0.7':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-x64-msvc@15.3.2':
|
||||
'@next/swc-win32-x64-msvc@16.0.7':
|
||||
optional: true
|
||||
|
||||
'@noble/ciphers@0.6.0': {}
|
||||
@@ -11176,8 +11198,6 @@ snapshots:
|
||||
dependencies:
|
||||
apg-lite: 1.0.4
|
||||
|
||||
'@swc/counter@0.1.3': {}
|
||||
|
||||
'@swc/helpers@0.5.15':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
@@ -11224,13 +11244,13 @@ snapshots:
|
||||
dependencies:
|
||||
'@trpc/server': 10.45.2
|
||||
|
||||
'@trpc/next@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/react-query@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.45.2)(next@15.3.2(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
'@trpc/next@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/react-query@10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.45.2)(next@16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||
dependencies:
|
||||
'@tanstack/react-query': 4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@trpc/client': 10.45.2(@trpc/server@10.45.2)
|
||||
'@trpc/react-query': 10.45.2(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.45.2(@trpc/server@10.45.2))(@trpc/server@10.45.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
'@trpc/server': 10.45.2
|
||||
next: 15.3.2(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
next: 16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
|
||||
@@ -11816,10 +11836,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
busboy@1.6.0:
|
||||
dependencies:
|
||||
streamsearch: 1.1.0
|
||||
|
||||
bytes@3.1.2: {}
|
||||
|
||||
cac@6.7.14: {}
|
||||
@@ -11986,20 +12002,8 @@ snapshots:
|
||||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
color-string@1.9.1:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
simple-swizzle: 0.2.2
|
||||
optional: true
|
||||
|
||||
color-support@1.1.3: {}
|
||||
|
||||
color@4.2.3:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
color-string: 1.9.1
|
||||
optional: true
|
||||
|
||||
colorette@2.0.20: {}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
@@ -12218,6 +12222,9 @@ snapshots:
|
||||
|
||||
detect-libc@2.0.4: {}
|
||||
|
||||
detect-libc@2.1.2:
|
||||
optional: true
|
||||
|
||||
detect-node-es@1.1.0: {}
|
||||
|
||||
devlop@1.1.0:
|
||||
@@ -12944,7 +12951,7 @@ snapshots:
|
||||
|
||||
inline-style-parser@0.2.4: {}
|
||||
|
||||
inngest@3.40.1(h3@1.15.3)(hono@4.7.10)(next@15.3.2(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(typescript@5.8.3):
|
||||
inngest@3.40.1(h3@1.15.3)(hono@4.7.10)(next@16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(typescript@5.8.3):
|
||||
dependencies:
|
||||
'@bufbuild/protobuf': 2.6.3
|
||||
'@inngest/ai': 0.1.5
|
||||
@@ -12971,7 +12978,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
h3: 1.15.3
|
||||
hono: 4.7.10
|
||||
next: 15.3.2(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
next: 16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
typescript: 5.8.3
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
@@ -13022,9 +13029,6 @@ snapshots:
|
||||
|
||||
is-arrayish@0.2.1: {}
|
||||
|
||||
is-arrayish@0.3.2:
|
||||
optional: true
|
||||
|
||||
is-binary-path@2.1.0:
|
||||
dependencies:
|
||||
binary-extensions: 2.3.0
|
||||
@@ -13785,7 +13789,7 @@ snapshots:
|
||||
|
||||
neotraverse@0.6.18: {}
|
||||
|
||||
next-i18next@15.4.2(i18next@23.16.8)(next@15.3.2(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-i18next@15.5.2(i18next@23.16.8)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3))(react@18.2.0):
|
||||
next-i18next@15.4.2(i18next@23.16.8)(next@16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-i18next@15.5.2(i18next@23.16.8)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3))(react@18.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.3
|
||||
'@types/hoist-non-react-statics': 3.3.6
|
||||
@@ -13793,45 +13797,43 @@ snapshots:
|
||||
hoist-non-react-statics: 3.3.2
|
||||
i18next: 23.16.8
|
||||
i18next-fs-backend: 2.6.0
|
||||
next: 15.3.2(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
next: 16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-i18next: 15.5.2(i18next@23.16.8)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.8.3)
|
||||
|
||||
next-themes@0.2.1(next@15.3.2(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
next-themes@0.2.1(next@16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
next: 15.3.2(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
next: 16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
|
||||
next@15.3.2(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
next@16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
'@next/env': 15.3.2
|
||||
'@swc/counter': 0.1.3
|
||||
'@next/env': 16.0.7
|
||||
'@swc/helpers': 0.5.15
|
||||
busboy: 1.6.0
|
||||
caniuse-lite: 1.0.30001718
|
||||
postcss: 8.4.31
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
styled-jsx: 5.1.6(react@18.2.0)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 15.3.2
|
||||
'@next/swc-darwin-x64': 15.3.2
|
||||
'@next/swc-linux-arm64-gnu': 15.3.2
|
||||
'@next/swc-linux-arm64-musl': 15.3.2
|
||||
'@next/swc-linux-x64-gnu': 15.3.2
|
||||
'@next/swc-linux-x64-musl': 15.3.2
|
||||
'@next/swc-win32-arm64-msvc': 15.3.2
|
||||
'@next/swc-win32-x64-msvc': 15.3.2
|
||||
'@next/swc-darwin-arm64': 16.0.7
|
||||
'@next/swc-darwin-x64': 16.0.7
|
||||
'@next/swc-linux-arm64-gnu': 16.0.7
|
||||
'@next/swc-linux-arm64-musl': 16.0.7
|
||||
'@next/swc-linux-x64-gnu': 16.0.7
|
||||
'@next/swc-linux-x64-musl': 16.0.7
|
||||
'@next/swc-win32-arm64-msvc': 16.0.7
|
||||
'@next/swc-win32-x64-msvc': 16.0.7
|
||||
'@opentelemetry/api': 1.9.0
|
||||
sharp: 0.34.2
|
||||
sharp: 0.34.5
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
|
||||
nextjs-toploader@3.9.17(next@15.3.2(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
nextjs-toploader@3.9.17(next@16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||
dependencies:
|
||||
next: 15.3.2(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
next: 16.0.7(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||
nprogress: 0.2.0
|
||||
prop-types: 15.8.1
|
||||
react: 18.2.0
|
||||
@@ -13857,7 +13859,7 @@ snapshots:
|
||||
|
||||
node-gyp-build-optional-packages@5.2.2:
|
||||
dependencies:
|
||||
detect-libc: 2.0.4
|
||||
detect-libc: 2.1.2
|
||||
optional: true
|
||||
|
||||
node-gyp-build@4.8.4:
|
||||
@@ -14686,6 +14688,9 @@ snapshots:
|
||||
|
||||
semver@7.7.2: {}
|
||||
|
||||
semver@7.7.3:
|
||||
optional: true
|
||||
|
||||
serialize-error-cjs@0.1.4: {}
|
||||
|
||||
serialize-error@8.1.0:
|
||||
@@ -14705,33 +14710,36 @@ snapshots:
|
||||
|
||||
shallow-equal@1.2.1: {}
|
||||
|
||||
sharp@0.34.2:
|
||||
sharp@0.34.5:
|
||||
dependencies:
|
||||
color: 4.2.3
|
||||
detect-libc: 2.0.4
|
||||
semver: 7.7.2
|
||||
'@img/colour': 1.0.0
|
||||
detect-libc: 2.1.2
|
||||
semver: 7.7.3
|
||||
optionalDependencies:
|
||||
'@img/sharp-darwin-arm64': 0.34.2
|
||||
'@img/sharp-darwin-x64': 0.34.2
|
||||
'@img/sharp-libvips-darwin-arm64': 1.1.0
|
||||
'@img/sharp-libvips-darwin-x64': 1.1.0
|
||||
'@img/sharp-libvips-linux-arm': 1.1.0
|
||||
'@img/sharp-libvips-linux-arm64': 1.1.0
|
||||
'@img/sharp-libvips-linux-ppc64': 1.1.0
|
||||
'@img/sharp-libvips-linux-s390x': 1.1.0
|
||||
'@img/sharp-libvips-linux-x64': 1.1.0
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.1.0
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.1.0
|
||||
'@img/sharp-linux-arm': 0.34.2
|
||||
'@img/sharp-linux-arm64': 0.34.2
|
||||
'@img/sharp-linux-s390x': 0.34.2
|
||||
'@img/sharp-linux-x64': 0.34.2
|
||||
'@img/sharp-linuxmusl-arm64': 0.34.2
|
||||
'@img/sharp-linuxmusl-x64': 0.34.2
|
||||
'@img/sharp-wasm32': 0.34.2
|
||||
'@img/sharp-win32-arm64': 0.34.2
|
||||
'@img/sharp-win32-ia32': 0.34.2
|
||||
'@img/sharp-win32-x64': 0.34.2
|
||||
'@img/sharp-darwin-arm64': 0.34.5
|
||||
'@img/sharp-darwin-x64': 0.34.5
|
||||
'@img/sharp-libvips-darwin-arm64': 1.2.4
|
||||
'@img/sharp-libvips-darwin-x64': 1.2.4
|
||||
'@img/sharp-libvips-linux-arm': 1.2.4
|
||||
'@img/sharp-libvips-linux-arm64': 1.2.4
|
||||
'@img/sharp-libvips-linux-ppc64': 1.2.4
|
||||
'@img/sharp-libvips-linux-riscv64': 1.2.4
|
||||
'@img/sharp-libvips-linux-s390x': 1.2.4
|
||||
'@img/sharp-libvips-linux-x64': 1.2.4
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
|
||||
'@img/sharp-linux-arm': 0.34.5
|
||||
'@img/sharp-linux-arm64': 0.34.5
|
||||
'@img/sharp-linux-ppc64': 0.34.5
|
||||
'@img/sharp-linux-riscv64': 0.34.5
|
||||
'@img/sharp-linux-s390x': 0.34.5
|
||||
'@img/sharp-linux-x64': 0.34.5
|
||||
'@img/sharp-linuxmusl-arm64': 0.34.5
|
||||
'@img/sharp-linuxmusl-x64': 0.34.5
|
||||
'@img/sharp-wasm32': 0.34.5
|
||||
'@img/sharp-win32-arm64': 0.34.5
|
||||
'@img/sharp-win32-ia32': 0.34.5
|
||||
'@img/sharp-win32-x64': 0.34.5
|
||||
optional: true
|
||||
|
||||
shebang-command@2.0.0:
|
||||
@@ -14780,11 +14788,6 @@ snapshots:
|
||||
|
||||
signal-exit@4.1.0: {}
|
||||
|
||||
simple-swizzle@0.2.2:
|
||||
dependencies:
|
||||
is-arrayish: 0.3.2
|
||||
optional: true
|
||||
|
||||
slash@3.0.0: {}
|
||||
|
||||
slice-ansi@5.0.0:
|
||||
@@ -14845,8 +14848,6 @@ snapshots:
|
||||
|
||||
std-env@3.9.0: {}
|
||||
|
||||
streamsearch@1.1.0: {}
|
||||
|
||||
string-argv@0.3.2: {}
|
||||
|
||||
string-width@4.2.3:
|
||||
|
||||
Reference in New Issue
Block a user