mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-27 10:05:32 +02:00
Compare commits
31 Commits
feat/add-a
...
v0.26.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9498fbeff3 | ||
|
|
d2aa60ddf7 | ||
|
|
58b75205af | ||
|
|
9e03625586 | ||
|
|
260efdc2bb | ||
|
|
1b5bfe051d | ||
|
|
e4384075f2 | ||
|
|
b355d44605 | ||
|
|
f39aa23803 | ||
|
|
3abc4cdc3b | ||
|
|
ec56062f17 | ||
|
|
10c4f882a5 | ||
|
|
f1dfa9c6a2 | ||
|
|
6010643d9e | ||
|
|
1ccb205495 | ||
|
|
b2be5bc09f | ||
|
|
babd30a110 | ||
|
|
e77f276785 | ||
|
|
78c9a047b0 | ||
|
|
84e0f5856b | ||
|
|
2bfa4643fc | ||
|
|
8c7bc82712 | ||
|
|
44645a6fbe | ||
|
|
771d0dd8ab | ||
|
|
67725759e6 | ||
|
|
2065372d4f | ||
|
|
69d5c6f0cb | ||
|
|
53f67c6eb2 | ||
|
|
7c53a3ef75 | ||
|
|
d465fb4da1 | ||
|
|
698104e7b7 |
@@ -206,4 +206,38 @@ describe("getRegistryTag", () => {
|
|||||||
expect(result).toBe("docker.io/myuser/repo");
|
expect(result).toBe("docker.io/myuser/repo");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("special characters in username", () => {
|
||||||
|
it("should handle Harbor robot account username with $ (e.g. robot$library+dokploy)", () => {
|
||||||
|
const registry = createMockRegistry({
|
||||||
|
username: "robot$library+dokploy",
|
||||||
|
});
|
||||||
|
const result = getRegistryTag(registry, "nginx");
|
||||||
|
expect(result).toBe("docker.io/robot$library+dokploy/nginx");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle username with $ and other special characters", () => {
|
||||||
|
const registry = createMockRegistry({
|
||||||
|
username: "robot$test+app",
|
||||||
|
});
|
||||||
|
const result = getRegistryTag(registry, "myapp:latest");
|
||||||
|
expect(result).toBe("docker.io/robot$test+app/myapp:latest");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle username with multiple $ symbols", () => {
|
||||||
|
const registry = createMockRegistry({
|
||||||
|
username: "user$name$test",
|
||||||
|
});
|
||||||
|
const result = getRegistryTag(registry, "app");
|
||||||
|
expect(result).toBe("docker.io/user$name$test/app");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle username with + and - symbols", () => {
|
||||||
|
const registry = createMockRegistry({
|
||||||
|
username: "robot+test-user",
|
||||||
|
});
|
||||||
|
const result = getRegistryTag(registry, "nginx:latest");
|
||||||
|
expect(result).toBe("docker.io/robot+test-user/nginx:latest");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Domain } from "@dokploy/server";
|
import type { Domain } from "@dokploy/server";
|
||||||
import { createDomainLabels } from "@dokploy/server";
|
import { createDomainLabels } from "@dokploy/server";
|
||||||
import { parse, stringify } from "yaml";
|
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { parse, stringify } from "yaml";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Regression tests for Traefik Host rule label format.
|
* Regression tests for Traefik Host rule label format.
|
||||||
|
|||||||
@@ -5,21 +5,27 @@ vi.mock("node:fs", () => ({
|
|||||||
default: fs,
|
default: fs,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import type { FileConfig, User } from "@dokploy/server";
|
import type { FileConfig } from "@dokploy/server";
|
||||||
import {
|
import {
|
||||||
createDefaultServerTraefikConfig,
|
createDefaultServerTraefikConfig,
|
||||||
loadOrCreateConfig,
|
loadOrCreateConfig,
|
||||||
updateServerTraefik,
|
updateServerTraefik,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
|
import type { webServerSettings } from "@dokploy/server/db/schema";
|
||||||
import { beforeEach, expect, test, vi } from "vitest";
|
import { beforeEach, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
const baseAdmin: User = {
|
type WebServerSettings = typeof webServerSettings.$inferSelect;
|
||||||
|
|
||||||
|
const baseSettings: WebServerSettings = {
|
||||||
|
id: "",
|
||||||
https: false,
|
https: false,
|
||||||
enablePaidFeatures: false,
|
certificateType: "none",
|
||||||
allowImpersonation: false,
|
host: null,
|
||||||
role: "user",
|
serverIp: null,
|
||||||
firstName: "",
|
letsEncryptEmail: null,
|
||||||
lastName: "",
|
sshPrivateKey: null,
|
||||||
|
enableDockerCleanup: false,
|
||||||
|
logCleanupCron: null,
|
||||||
metricsConfig: {
|
metricsConfig: {
|
||||||
containers: {
|
containers: {
|
||||||
refreshRate: 20,
|
refreshRate: 20,
|
||||||
@@ -45,29 +51,8 @@ const baseAdmin: User = {
|
|||||||
cleanupCacheApplications: false,
|
cleanupCacheApplications: false,
|
||||||
cleanupCacheOnCompose: false,
|
cleanupCacheOnCompose: false,
|
||||||
cleanupCacheOnPreviews: false,
|
cleanupCacheOnPreviews: false,
|
||||||
createdAt: new Date(),
|
createdAt: null,
|
||||||
serverIp: null,
|
|
||||||
certificateType: "none",
|
|
||||||
host: null,
|
|
||||||
letsEncryptEmail: null,
|
|
||||||
sshPrivateKey: null,
|
|
||||||
enableDockerCleanup: false,
|
|
||||||
logCleanupCron: null,
|
|
||||||
serversQuantity: 0,
|
|
||||||
stripeCustomerId: "",
|
|
||||||
stripeSubscriptionId: "",
|
|
||||||
banExpires: new Date(),
|
|
||||||
banned: true,
|
|
||||||
banReason: "",
|
|
||||||
email: "",
|
|
||||||
expirationDate: "",
|
|
||||||
id: "",
|
|
||||||
isRegistered: false,
|
|
||||||
createdAt2: new Date().toISOString(),
|
|
||||||
emailVerified: false,
|
|
||||||
image: "",
|
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
twoFactorEnabled: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -85,7 +70,7 @@ test("Should read the configuration file", () => {
|
|||||||
test("Should apply redirect-to-https", () => {
|
test("Should apply redirect-to-https", () => {
|
||||||
updateServerTraefik(
|
updateServerTraefik(
|
||||||
{
|
{
|
||||||
...baseAdmin,
|
...baseSettings,
|
||||||
https: true,
|
https: true,
|
||||||
certificateType: "letsencrypt",
|
certificateType: "letsencrypt",
|
||||||
},
|
},
|
||||||
@@ -100,7 +85,7 @@ test("Should apply redirect-to-https", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("Should change only host when no certificate", () => {
|
test("Should change only host when no certificate", () => {
|
||||||
updateServerTraefik(baseAdmin, "example.com");
|
updateServerTraefik(baseSettings, "example.com");
|
||||||
|
|
||||||
const config: FileConfig = loadOrCreateConfig("dokploy");
|
const config: FileConfig = loadOrCreateConfig("dokploy");
|
||||||
|
|
||||||
@@ -110,7 +95,7 @@ test("Should change only host when no certificate", () => {
|
|||||||
test("Should not touch config without host", () => {
|
test("Should not touch config without host", () => {
|
||||||
const originalConfig: FileConfig = loadOrCreateConfig("dokploy");
|
const originalConfig: FileConfig = loadOrCreateConfig("dokploy");
|
||||||
|
|
||||||
updateServerTraefik(baseAdmin, null);
|
updateServerTraefik(baseSettings, null);
|
||||||
|
|
||||||
const config: FileConfig = loadOrCreateConfig("dokploy");
|
const config: FileConfig = loadOrCreateConfig("dokploy");
|
||||||
|
|
||||||
@@ -119,11 +104,14 @@ test("Should not touch config without host", () => {
|
|||||||
|
|
||||||
test("Should remove websecure if https rollback to http", () => {
|
test("Should remove websecure if https rollback to http", () => {
|
||||||
updateServerTraefik(
|
updateServerTraefik(
|
||||||
{ ...baseAdmin, certificateType: "letsencrypt" },
|
{ ...baseSettings, certificateType: "letsencrypt" },
|
||||||
"example.com",
|
"example.com",
|
||||||
);
|
);
|
||||||
|
|
||||||
updateServerTraefik({ ...baseAdmin, certificateType: "none" }, "example.com");
|
updateServerTraefik(
|
||||||
|
{ ...baseSettings, certificateType: "none" },
|
||||||
|
"example.com",
|
||||||
|
);
|
||||||
|
|
||||||
const config: FileConfig = loadOrCreateConfig("dokploy");
|
const config: FileConfig = loadOrCreateConfig("dokploy");
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import {
|
||||||
|
createConverter,
|
||||||
|
NumberInputWithSteps,
|
||||||
|
} from "@/components/ui/number-input";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@@ -30,6 +33,23 @@ import {
|
|||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
|
const CPU_STEP = 0.25;
|
||||||
|
const MEMORY_STEP_MB = 256;
|
||||||
|
|
||||||
|
const formatNumber = (value: number, decimals = 2): string =>
|
||||||
|
Number.isInteger(value) ? String(value) : value.toFixed(decimals);
|
||||||
|
|
||||||
|
const cpuConverter = createConverter(1_000_000_000, (cpu) =>
|
||||||
|
cpu <= 0 ? "" : `${formatNumber(cpu)} CPU`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const memoryConverter = createConverter(1024 * 1024, (mb) => {
|
||||||
|
if (mb <= 0) return "";
|
||||||
|
return mb >= 1024
|
||||||
|
? `${formatNumber(mb / 1024)} GB`
|
||||||
|
: `${formatNumber(mb)} MB`;
|
||||||
|
});
|
||||||
|
|
||||||
const addResourcesSchema = z.object({
|
const addResourcesSchema = z.object({
|
||||||
memoryReservation: z.string().optional(),
|
memoryReservation: z.string().optional(),
|
||||||
cpuLimit: z.string().optional(),
|
cpuLimit: z.string().optional(),
|
||||||
@@ -51,6 +71,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AddResources = z.infer<typeof addResourcesSchema>;
|
type AddResources = z.infer<typeof addResourcesSchema>;
|
||||||
|
|
||||||
export const ShowResources = ({ id, type }: Props) => {
|
export const ShowResources = ({ id, type }: Props) => {
|
||||||
const queryMap = {
|
const queryMap = {
|
||||||
postgres: () =>
|
postgres: () =>
|
||||||
@@ -163,16 +184,20 @@ export const ShowResources = ({ id, type }: Props) => {
|
|||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>
|
<p>
|
||||||
Memory hard limit in bytes. Example: 1GB =
|
Memory hard limit in bytes. Example: 1GB =
|
||||||
1073741824 bytes
|
1073741824 bytes. Use +/- buttons to adjust by
|
||||||
|
256 MB.
|
||||||
</p>
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<NumberInputWithSteps
|
||||||
|
value={field.value}
|
||||||
|
onChange={field.onChange}
|
||||||
placeholder="1073741824 (1GB in bytes)"
|
placeholder="1073741824 (1GB in bytes)"
|
||||||
{...field}
|
step={MEMORY_STEP_MB}
|
||||||
|
converter={memoryConverter}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -198,16 +223,20 @@ export const ShowResources = ({ id, type }: Props) => {
|
|||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>
|
<p>
|
||||||
Memory soft limit in bytes. Example: 256MB =
|
Memory soft limit in bytes. Example: 256MB =
|
||||||
268435456 bytes
|
268435456 bytes. Use +/- buttons to adjust by 256
|
||||||
|
MB.
|
||||||
</p>
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<NumberInputWithSteps
|
||||||
|
value={field.value}
|
||||||
|
onChange={field.onChange}
|
||||||
placeholder="268435456 (256MB in bytes)"
|
placeholder="268435456 (256MB in bytes)"
|
||||||
{...field}
|
step={MEMORY_STEP_MB}
|
||||||
|
converter={memoryConverter}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -234,17 +263,20 @@ export const ShowResources = ({ id, type }: Props) => {
|
|||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>
|
<p>
|
||||||
CPU quota in units of 10^-9 CPUs. Example: 2
|
CPU quota in units of 10^-9 CPUs. Example: 2
|
||||||
CPUs = 2000000000
|
CPUs = 2000000000. Use +/- buttons to adjust by
|
||||||
|
0.25 CPU.
|
||||||
</p>
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<NumberInputWithSteps
|
||||||
|
value={field.value}
|
||||||
|
onChange={field.onChange}
|
||||||
placeholder="2000000000 (2 CPUs)"
|
placeholder="2000000000 (2 CPUs)"
|
||||||
{...field}
|
step={CPU_STEP}
|
||||||
value={field.value?.toString() || ""}
|
converter={cpuConverter}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -271,14 +303,21 @@ export const ShowResources = ({ id, type }: Props) => {
|
|||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>
|
<p>
|
||||||
CPU shares (relative weight). Example: 1 CPU =
|
CPU shares (relative weight). Example: 1 CPU =
|
||||||
1000000000
|
1000000000. Use +/- buttons to adjust by 0.25
|
||||||
|
CPU.
|
||||||
</p>
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="1000000000 (1 CPU)" {...field} />
|
<NumberInputWithSteps
|
||||||
|
value={field.value}
|
||||||
|
onChange={field.onChange}
|
||||||
|
placeholder="1000000000 (1 CPU)"
|
||||||
|
step={CPU_STEP}
|
||||||
|
converter={cpuConverter}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -108,7 +108,8 @@ export const getLogType = (message: string): LogStyle => {
|
|||||||
/(?:might|may|could)\s+(?:not|cause|lead\s+to)/i.test(lowerMessage) ||
|
/(?:might|may|could)\s+(?:not|cause|lead\s+to)/i.test(lowerMessage) ||
|
||||||
/(?:!+\s*(?:warning|caution|attention)\s*!+)/i.test(lowerMessage) ||
|
/(?:!+\s*(?:warning|caution|attention)\s*!+)/i.test(lowerMessage) ||
|
||||||
/\b(?:deprecated|obsolete)\b/i.test(lowerMessage) ||
|
/\b(?:deprecated|obsolete)\b/i.test(lowerMessage) ||
|
||||||
/\b(?:unstable|experimental)\b/i.test(lowerMessage)
|
/\b(?:unstable|experimental)\b/i.test(lowerMessage) ||
|
||||||
|
/⚠|⚠️/i.test(lowerMessage)
|
||||||
) {
|
) {
|
||||||
return LOG_STYLES.warning;
|
return LOG_STYLES.warning;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Activity } from "lucide-react";
|
import { Activity } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -7,7 +8,6 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
import { ShowStorageActions } from "./show-storage-actions";
|
import { ShowStorageActions } from "./show-storage-actions";
|
||||||
import { ShowTraefikActions } from "./show-traefik-actions";
|
import { ShowTraefikActions } from "./show-traefik-actions";
|
||||||
|
|||||||
@@ -7,9 +7,12 @@ interface Props {
|
|||||||
serverId?: string;
|
serverId?: string;
|
||||||
}
|
}
|
||||||
export const ToggleDockerCleanup = ({ serverId }: Props) => {
|
export const ToggleDockerCleanup = ({ serverId }: Props) => {
|
||||||
const { data, refetch } = api.user.get.useQuery(undefined, {
|
const { data, refetch } = api.settings.getWebServerSettings.useQuery(
|
||||||
enabled: !serverId,
|
undefined,
|
||||||
});
|
{
|
||||||
|
enabled: !serverId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const { data: server, refetch: refetchServer } = api.server.one.useQuery(
|
const { data: server, refetch: refetchServer } = api.server.one.useQuery(
|
||||||
{
|
{
|
||||||
@@ -22,7 +25,7 @@ export const ToggleDockerCleanup = ({ serverId }: Props) => {
|
|||||||
|
|
||||||
const enabled = serverId
|
const enabled = serverId
|
||||||
? server?.enableDockerCleanup
|
? server?.enableDockerCleanup
|
||||||
: data?.user.enableDockerCleanup;
|
: data?.enableDockerCleanup;
|
||||||
|
|
||||||
const { mutateAsync } = api.settings.updateDockerCleanup.useMutation();
|
const { mutateAsync } = api.settings.updateDockerCleanup.useMutation();
|
||||||
|
|
||||||
@@ -30,7 +33,10 @@ export const ToggleDockerCleanup = ({ serverId }: Props) => {
|
|||||||
try {
|
try {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
enableDockerCleanup: checked,
|
enableDockerCleanup: checked,
|
||||||
serverId: serverId,
|
...(serverId && { serverId }),
|
||||||
|
} as {
|
||||||
|
enableDockerCleanup: boolean;
|
||||||
|
serverId?: string;
|
||||||
});
|
});
|
||||||
if (serverId) {
|
if (serverId) {
|
||||||
await refetchServer();
|
await refetchServer();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PlusIcon, Pencil } from "lucide-react";
|
import { Pencil, PlusIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ const Schema = z.object({
|
|||||||
type Schema = z.infer<typeof Schema>;
|
type Schema = z.infer<typeof Schema>;
|
||||||
|
|
||||||
export const SetupMonitoring = ({ serverId }: Props) => {
|
export const SetupMonitoring = ({ serverId }: Props) => {
|
||||||
const { data } = serverId
|
const { data: serverData } = serverId
|
||||||
? api.server.one.useQuery(
|
? api.server.one.useQuery(
|
||||||
{
|
{
|
||||||
serverId: serverId || "",
|
serverId: serverId || "",
|
||||||
@@ -89,7 +89,14 @@ export const SetupMonitoring = ({ serverId }: Props) => {
|
|||||||
enabled: !!serverId,
|
enabled: !!serverId,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
: api.user.getServerMetrics.useQuery();
|
: { data: null };
|
||||||
|
|
||||||
|
const { data: webServerSettings } =
|
||||||
|
api.settings.getWebServerSettings.useQuery(undefined, {
|
||||||
|
enabled: !serverId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = serverId ? serverData : webServerSettings;
|
||||||
|
|
||||||
const url = useUrl();
|
const url = useUrl();
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import {
|
import {
|
||||||
|
Clock,
|
||||||
|
Key,
|
||||||
KeyIcon,
|
KeyIcon,
|
||||||
Loader2,
|
Loader2,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
ServerIcon,
|
|
||||||
Clock,
|
|
||||||
User,
|
|
||||||
Key,
|
|
||||||
Network,
|
Network,
|
||||||
Terminal,
|
|
||||||
Settings,
|
|
||||||
Pencil,
|
Pencil,
|
||||||
|
ServerIcon,
|
||||||
|
Settings,
|
||||||
|
Terminal,
|
||||||
Trash2,
|
Trash2,
|
||||||
|
User,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ type AddServerDomain = z.infer<typeof addServerDomain>;
|
|||||||
|
|
||||||
export const WebDomain = () => {
|
export const WebDomain = () => {
|
||||||
const { t } = useTranslation("settings");
|
const { t } = useTranslation("settings");
|
||||||
const { data, refetch } = api.user.get.useQuery();
|
const { data, refetch } = api.settings.getWebServerSettings.useQuery();
|
||||||
const { mutateAsync, isLoading } =
|
const { mutateAsync, isLoading } =
|
||||||
api.settings.assignDomainServer.useMutation();
|
api.settings.assignDomainServer.useMutation();
|
||||||
|
|
||||||
@@ -82,15 +82,15 @@ export const WebDomain = () => {
|
|||||||
});
|
});
|
||||||
const https = form.watch("https");
|
const https = form.watch("https");
|
||||||
const domain = form.watch("domain") || "";
|
const domain = form.watch("domain") || "";
|
||||||
const host = data?.user?.host || "";
|
const host = data?.host || "";
|
||||||
const hasChanged = domain !== host;
|
const hasChanged = domain !== host;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
form.reset({
|
form.reset({
|
||||||
domain: data?.user?.host || "",
|
domain: data?.host || "",
|
||||||
certificateType: data?.user?.certificateType,
|
certificateType: data?.certificateType || "none",
|
||||||
letsEncryptEmail: data?.user?.letsEncryptEmail || "",
|
letsEncryptEmail: data?.letsEncryptEmail || "",
|
||||||
https: data?.user?.https || false,
|
https: data?.https || false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form, form.reset, data]);
|
}, [form, form.reset, data]);
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ import { UpdateServer } from "./web-server/update-server";
|
|||||||
|
|
||||||
export const WebServer = () => {
|
export const WebServer = () => {
|
||||||
const { t } = useTranslation("settings");
|
const { t } = useTranslation("settings");
|
||||||
const { data } = api.user.get.useQuery();
|
const { data: webServerSettings } =
|
||||||
|
api.settings.getWebServerSettings.useQuery();
|
||||||
|
|
||||||
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
||||||
|
|
||||||
@@ -53,7 +54,7 @@ export const WebServer = () => {
|
|||||||
|
|
||||||
<div className="flex items-center flex-wrap justify-between gap-4">
|
<div className="flex items-center flex-wrap justify-between gap-4">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
Server IP: {data?.user.serverIp}
|
Server IP: {webServerSettings?.serverIp}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
Version: {dokployVersion}
|
Version: {dokployVersion}
|
||||||
|
|||||||
@@ -46,15 +46,15 @@ interface Props {
|
|||||||
export const UpdateServerIp = ({ children }: Props) => {
|
export const UpdateServerIp = ({ children }: Props) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const { data } = api.user.get.useQuery();
|
const { data, refetch } = api.settings.getWebServerSettings.useQuery();
|
||||||
const { data: ip } = api.server.publicIp.useQuery();
|
const { data: ip } = api.server.publicIp.useQuery();
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
api.user.update.useMutation();
|
api.settings.updateServerIp.useMutation();
|
||||||
|
|
||||||
const form = useForm<Schema>({
|
const form = useForm<Schema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
serverIp: data?.user.serverIp || "",
|
serverIp: data?.serverIp || "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
});
|
});
|
||||||
@@ -62,13 +62,11 @@ export const UpdateServerIp = ({ children }: Props) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
form.reset({
|
form.reset({
|
||||||
serverIp: data.user.serverIp || "",
|
serverIp: data.serverIp || "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form, form.reset, data]);
|
}, [form, form.reset, data]);
|
||||||
|
|
||||||
const utils = api.useUtils();
|
|
||||||
|
|
||||||
const setCurrentIp = () => {
|
const setCurrentIp = () => {
|
||||||
if (!ip) return;
|
if (!ip) return;
|
||||||
form.setValue("serverIp", ip);
|
form.setValue("serverIp", ip);
|
||||||
@@ -80,7 +78,7 @@ export const UpdateServerIp = ({ children }: Props) => {
|
|||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Server IP Updated");
|
toast.success("Server IP Updated");
|
||||||
await utils.user.get.invalidate();
|
await refetch();
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
|
import { ChevronDown } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { ChevronDown } from "lucide-react";
|
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
BreadcrumbList,
|
BreadcrumbList,
|
||||||
BreadcrumbSeparator,
|
|
||||||
BreadcrumbPage,
|
BreadcrumbPage,
|
||||||
|
BreadcrumbSeparator,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
|
|||||||
84
apps/dokploy/components/ui/number-input.tsx
Normal file
84
apps/dokploy/components/ui/number-input.tsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { MinusIcon, PlusIcon } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
||||||
|
export interface UnitConverter {
|
||||||
|
toValue: (raw: string | undefined) => number;
|
||||||
|
fromValue: (value: number) => string;
|
||||||
|
formatDisplay: (value: number) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createConverter = (
|
||||||
|
multiplier: number,
|
||||||
|
formatDisplay: (value: number) => string,
|
||||||
|
): UnitConverter => ({
|
||||||
|
toValue: (raw) => {
|
||||||
|
if (!raw) return 0;
|
||||||
|
const value = Number.parseInt(raw, 10);
|
||||||
|
return Number.isNaN(value) ? 0 : value / multiplier;
|
||||||
|
},
|
||||||
|
fromValue: (value) =>
|
||||||
|
value <= 0 ? "" : String(Math.round(value * multiplier)),
|
||||||
|
formatDisplay,
|
||||||
|
});
|
||||||
|
|
||||||
|
interface NumberInputWithStepsProps {
|
||||||
|
value: string | undefined;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
placeholder: string;
|
||||||
|
step: number;
|
||||||
|
converter: UnitConverter;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NumberInputWithSteps = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder,
|
||||||
|
step,
|
||||||
|
converter,
|
||||||
|
}: NumberInputWithStepsProps) => {
|
||||||
|
const numericValue = converter.toValue(value);
|
||||||
|
const displayValue = converter.formatDisplay(numericValue);
|
||||||
|
|
||||||
|
const handleIncrement = () =>
|
||||||
|
onChange(converter.fromValue(numericValue + step));
|
||||||
|
const handleDecrement = () =>
|
||||||
|
onChange(converter.fromValue(Math.max(0, numericValue - step)));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="h-9 w-9 shrink-0"
|
||||||
|
onClick={handleDecrement}
|
||||||
|
disabled={numericValue <= 0}
|
||||||
|
>
|
||||||
|
<MinusIcon className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Input
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={value || ""}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
className="text-center"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="h-9 w-9 shrink-0"
|
||||||
|
onClick={handleIncrement}
|
||||||
|
>
|
||||||
|
<PlusIcon className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{displayValue && (
|
||||||
|
<span className="text-xs text-muted-foreground text-center">
|
||||||
|
{displayValue}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
114
apps/dokploy/drizzle/0133_striped_the_order.sql
Normal file
114
apps/dokploy/drizzle/0133_striped_the_order.sql
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
CREATE TABLE "webServerSettings" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"serverIp" text,
|
||||||
|
"certificateType" "certificateType" DEFAULT 'none' NOT NULL,
|
||||||
|
"https" boolean DEFAULT false NOT NULL,
|
||||||
|
"host" text,
|
||||||
|
"letsEncryptEmail" text,
|
||||||
|
"sshPrivateKey" text,
|
||||||
|
"enableDockerCleanup" boolean DEFAULT true NOT NULL,
|
||||||
|
"logCleanupCron" text DEFAULT '0 0 * * *',
|
||||||
|
"metricsConfig" jsonb DEFAULT '{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}'::jsonb NOT NULL,
|
||||||
|
"cleanupCacheApplications" boolean DEFAULT false NOT NULL,
|
||||||
|
"cleanupCacheOnPreviews" boolean DEFAULT false NOT NULL,
|
||||||
|
"cleanupCacheOnCompose" boolean DEFAULT false NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT now(),
|
||||||
|
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Migrate data from user table to webServerSettings
|
||||||
|
-- Get the owner user's data and insert into webServerSettings
|
||||||
|
INSERT INTO "webServerSettings" (
|
||||||
|
"id",
|
||||||
|
"serverIp",
|
||||||
|
"certificateType",
|
||||||
|
"https",
|
||||||
|
"host",
|
||||||
|
"letsEncryptEmail",
|
||||||
|
"sshPrivateKey",
|
||||||
|
"enableDockerCleanup",
|
||||||
|
"logCleanupCron",
|
||||||
|
"metricsConfig",
|
||||||
|
"cleanupCacheApplications",
|
||||||
|
"cleanupCacheOnPreviews",
|
||||||
|
"cleanupCacheOnCompose",
|
||||||
|
"created_at",
|
||||||
|
"updated_at"
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
gen_random_uuid()::text as "id",
|
||||||
|
u."serverIp",
|
||||||
|
COALESCE(u."certificateType", 'none') as "certificateType",
|
||||||
|
COALESCE(u."https", false) as "https",
|
||||||
|
u."host",
|
||||||
|
u."letsEncryptEmail",
|
||||||
|
u."sshPrivateKey",
|
||||||
|
COALESCE(u."enableDockerCleanup", true) as "enableDockerCleanup",
|
||||||
|
COALESCE(u."logCleanupCron", '0 0 * * *') as "logCleanupCron",
|
||||||
|
COALESCE(
|
||||||
|
u."metricsConfig",
|
||||||
|
'{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}'::jsonb
|
||||||
|
) as "metricsConfig",
|
||||||
|
COALESCE(u."cleanupCacheApplications", false) as "cleanupCacheApplications",
|
||||||
|
COALESCE(u."cleanupCacheOnPreviews", false) as "cleanupCacheOnPreviews",
|
||||||
|
COALESCE(u."cleanupCacheOnCompose", false) as "cleanupCacheOnCompose",
|
||||||
|
NOW() as "created_at",
|
||||||
|
NOW() as "updated_at"
|
||||||
|
FROM "user" u
|
||||||
|
INNER JOIN "member" m ON u."id" = m."user_id"
|
||||||
|
WHERE m."role" = 'owner'
|
||||||
|
ORDER BY m."created_at" ASC
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- If no owner found, create a default entry
|
||||||
|
INSERT INTO "webServerSettings" (
|
||||||
|
"id",
|
||||||
|
"serverIp",
|
||||||
|
"certificateType",
|
||||||
|
"https",
|
||||||
|
"host",
|
||||||
|
"letsEncryptEmail",
|
||||||
|
"sshPrivateKey",
|
||||||
|
"enableDockerCleanup",
|
||||||
|
"logCleanupCron",
|
||||||
|
"metricsConfig",
|
||||||
|
"cleanupCacheApplications",
|
||||||
|
"cleanupCacheOnPreviews",
|
||||||
|
"cleanupCacheOnCompose",
|
||||||
|
"created_at",
|
||||||
|
"updated_at"
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
gen_random_uuid()::text as "id",
|
||||||
|
NULL as "serverIp",
|
||||||
|
'none' as "certificateType",
|
||||||
|
false as "https",
|
||||||
|
NULL as "host",
|
||||||
|
NULL as "letsEncryptEmail",
|
||||||
|
NULL as "sshPrivateKey",
|
||||||
|
true as "enableDockerCleanup",
|
||||||
|
'0 0 * * *' as "logCleanupCron",
|
||||||
|
'{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}'::jsonb as "metricsConfig",
|
||||||
|
false as "cleanupCacheApplications",
|
||||||
|
false as "cleanupCacheOnPreviews",
|
||||||
|
false as "cleanupCacheOnCompose",
|
||||||
|
NOW() as "created_at",
|
||||||
|
NOW() as "updated_at"
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM "webServerSettings"
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "user" DROP COLUMN "serverIp";--> statement-breakpoint
|
||||||
|
ALTER TABLE "user" DROP COLUMN "certificateType";--> statement-breakpoint
|
||||||
|
ALTER TABLE "user" DROP COLUMN "https";--> statement-breakpoint
|
||||||
|
ALTER TABLE "user" DROP COLUMN "host";--> statement-breakpoint
|
||||||
|
ALTER TABLE "user" DROP COLUMN "letsEncryptEmail";--> statement-breakpoint
|
||||||
|
ALTER TABLE "user" DROP COLUMN "sshPrivateKey";--> statement-breakpoint
|
||||||
|
ALTER TABLE "user" DROP COLUMN "enableDockerCleanup";--> statement-breakpoint
|
||||||
|
ALTER TABLE "user" DROP COLUMN "logCleanupCron";--> statement-breakpoint
|
||||||
|
ALTER TABLE "user" DROP COLUMN "metricsConfig";--> statement-breakpoint
|
||||||
|
ALTER TABLE "user" DROP COLUMN "cleanupCacheApplications";--> statement-breakpoint
|
||||||
|
ALTER TABLE "user" DROP COLUMN "cleanupCacheOnPreviews";--> statement-breakpoint
|
||||||
|
ALTER TABLE "user" DROP COLUMN "cleanupCacheOnCompose";
|
||||||
6968
apps/dokploy/drizzle/meta/0133_snapshot.json
Normal file
6968
apps/dokploy/drizzle/meta/0133_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -932,6 +932,13 @@
|
|||||||
"when": 1765346573500,
|
"when": 1765346573500,
|
||||||
"tag": "0132_clean_layla_miller",
|
"tag": "0132_clean_layla_miller",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 133,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1766301478005,
|
||||||
|
"tag": "0133_striped_the_order",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"docker:build:canary": "./docker/build.sh canary",
|
"docker:build:canary": "./docker/build.sh canary",
|
||||||
"docker:push:canary": "./docker/push.sh canary",
|
"docker:push:canary": "./docker/push.sh canary",
|
||||||
"version": "echo $(node -p \"require('./package.json').version\")",
|
"version": "echo $(node -p \"require('./package.json').version\")",
|
||||||
"test": "vitest --config __test__/vitest.config.ts volume-backups",
|
"test": "vitest --config __test__/vitest.config.ts",
|
||||||
"generate:openapi": "tsx -r dotenv/config scripts/generate-openapi.ts"
|
"generate:openapi": "tsx -r dotenv/config scripts/generate-openapi.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -140,7 +140,6 @@
|
|||||||
"react-i18next": "^15.5.2",
|
"react-i18next": "^15.5.2",
|
||||||
"react-markdown": "^9.1.0",
|
"react-markdown": "^9.1.0",
|
||||||
"recharts": "^2.15.3",
|
"recharts": "^2.15.3",
|
||||||
"rotating-file-stream": "3.2.3",
|
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
"sonner": "^1.7.4",
|
"sonner": "^1.7.4",
|
||||||
"ssh2": "1.15.0",
|
"ssh2": "1.15.0",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
findUserById,
|
getWebServerSettings,
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
setupWebMonitoring,
|
setupWebMonitoring,
|
||||||
updateUser,
|
updateWebServerSettings,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { apiUpdateWebServerMonitoring } from "@/server/db/schema";
|
import { apiUpdateWebServerMonitoring } from "@/server/db/schema";
|
||||||
@@ -11,7 +11,7 @@ import { adminProcedure, createTRPCRouter } from "../trpc";
|
|||||||
export const adminRouter = createTRPCRouter({
|
export const adminRouter = createTRPCRouter({
|
||||||
setupMonitoring: adminProcedure
|
setupMonitoring: adminProcedure
|
||||||
.input(apiUpdateWebServerMonitoring)
|
.input(apiUpdateWebServerMonitoring)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -19,15 +19,8 @@ export const adminRouter = createTRPCRouter({
|
|||||||
message: "Feature disabled on cloud",
|
message: "Feature disabled on cloud",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const user = await findUserById(ctx.user.ownerId);
|
|
||||||
if (user.id !== ctx.user.ownerId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not authorized to setup the monitoring",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateUser(user.id, {
|
await updateWebServerSettings({
|
||||||
metricsConfig: {
|
metricsConfig: {
|
||||||
server: {
|
server: {
|
||||||
type: "Dokploy",
|
type: "Dokploy",
|
||||||
@@ -52,8 +45,9 @@ export const adminRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentServer = await setupWebMonitoring(user.id);
|
await setupWebMonitoring();
|
||||||
return currentServer;
|
const settings = await getWebServerSettings();
|
||||||
|
return settings;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,40 @@ export const aiRouter = createTRPCRouter({
|
|||||||
{ headers: {} },
|
{ headers: {} },
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case "perplexity":
|
||||||
|
// Perplexity doesn't have a /models endpoint, return hardcoded list
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "sonar-deep-research",
|
||||||
|
object: "model",
|
||||||
|
created: Date.now(),
|
||||||
|
owned_by: "perplexity",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sonar-reasoning-pro",
|
||||||
|
object: "model",
|
||||||
|
created: Date.now(),
|
||||||
|
owned_by: "perplexity",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sonar-reasoning",
|
||||||
|
object: "model",
|
||||||
|
created: Date.now(),
|
||||||
|
owned_by: "perplexity",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sonar-pro",
|
||||||
|
object: "model",
|
||||||
|
created: Date.now(),
|
||||||
|
owned_by: "perplexity",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sonar",
|
||||||
|
object: "model",
|
||||||
|
created: Date.now(),
|
||||||
|
owned_by: "perplexity",
|
||||||
|
},
|
||||||
|
] as Model[];
|
||||||
default:
|
default:
|
||||||
if (!input.apiKey)
|
if (!input.apiKey)
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ import {
|
|||||||
findGitProviderById,
|
findGitProviderById,
|
||||||
findProjectById,
|
findProjectById,
|
||||||
findServerById,
|
findServerById,
|
||||||
findUserById,
|
|
||||||
getComposeContainer,
|
getComposeContainer,
|
||||||
|
getWebServerSettings,
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
loadServices,
|
loadServices,
|
||||||
randomizeComposeFile,
|
randomizeComposeFile,
|
||||||
@@ -569,8 +569,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const template = await fetchTemplateFiles(input.id, input.baseUrl);
|
const template = await fetchTemplateFiles(input.id, input.baseUrl);
|
||||||
|
|
||||||
const admin = await findUserById(ctx.user.ownerId);
|
let serverIp = "127.0.0.1";
|
||||||
let serverIp = admin.serverIp || "127.0.0.1";
|
|
||||||
|
|
||||||
const project = await findProjectById(environment.projectId);
|
const project = await findProjectById(environment.projectId);
|
||||||
|
|
||||||
@@ -579,6 +578,9 @@ export const composeRouter = createTRPCRouter({
|
|||||||
serverIp = server.ipAddress;
|
serverIp = server.ipAddress;
|
||||||
} else if (process.env.NODE_ENV === "development") {
|
} else if (process.env.NODE_ENV === "development") {
|
||||||
serverIp = "127.0.0.1";
|
serverIp = "127.0.0.1";
|
||||||
|
} else {
|
||||||
|
const settings = await getWebServerSettings();
|
||||||
|
serverIp = settings?.serverIp || "127.0.0.1";
|
||||||
}
|
}
|
||||||
|
|
||||||
const projectName = slugify(`${project.name} ${input.id}`);
|
const projectName = slugify(`${project.name} ${input.id}`);
|
||||||
@@ -803,14 +805,16 @@ export const composeRouter = createTRPCRouter({
|
|||||||
const decodedData = Buffer.from(input.base64, "base64").toString(
|
const decodedData = Buffer.from(input.base64, "base64").toString(
|
||||||
"utf-8",
|
"utf-8",
|
||||||
);
|
);
|
||||||
const admin = await findUserById(ctx.user.ownerId);
|
let serverIp = "127.0.0.1";
|
||||||
let serverIp = admin.serverIp || "127.0.0.1";
|
|
||||||
|
|
||||||
if (compose.serverId) {
|
if (compose.serverId) {
|
||||||
const server = await findServerById(compose.serverId);
|
const server = await findServerById(compose.serverId);
|
||||||
serverIp = server.ipAddress;
|
serverIp = server.ipAddress;
|
||||||
} else if (process.env.NODE_ENV === "development") {
|
} else if (process.env.NODE_ENV === "development") {
|
||||||
serverIp = "127.0.0.1";
|
serverIp = "127.0.0.1";
|
||||||
|
} else {
|
||||||
|
const settings = await getWebServerSettings();
|
||||||
|
serverIp = settings?.serverIp || "127.0.0.1";
|
||||||
}
|
}
|
||||||
const templateData = JSON.parse(decodedData);
|
const templateData = JSON.parse(decodedData);
|
||||||
const config = parse(templateData.config) as CompleteTemplate;
|
const config = parse(templateData.config) as CompleteTemplate;
|
||||||
@@ -880,14 +884,16 @@ export const composeRouter = createTRPCRouter({
|
|||||||
await removeDomainById(domain.domainId);
|
await removeDomainById(domain.domainId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const admin = await findUserById(ctx.user.ownerId);
|
let serverIp = "127.0.0.1";
|
||||||
let serverIp = admin.serverIp || "127.0.0.1";
|
|
||||||
|
|
||||||
if (compose.serverId) {
|
if (compose.serverId) {
|
||||||
const server = await findServerById(compose.serverId);
|
const server = await findServerById(compose.serverId);
|
||||||
serverIp = server.ipAddress;
|
serverIp = server.ipAddress;
|
||||||
} else if (process.env.NODE_ENV === "development") {
|
} else if (process.env.NODE_ENV === "development") {
|
||||||
serverIp = "127.0.0.1";
|
serverIp = "127.0.0.1";
|
||||||
|
} else {
|
||||||
|
const settings = await getWebServerSettings();
|
||||||
|
serverIp = settings?.serverIp || "127.0.0.1";
|
||||||
}
|
}
|
||||||
|
|
||||||
const templateData = JSON.parse(decodedData);
|
const templateData = JSON.parse(decodedData);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
findPreviewDeploymentById,
|
findPreviewDeploymentById,
|
||||||
findServerById,
|
findServerById,
|
||||||
generateTraefikMeDomain,
|
generateTraefikMeDomain,
|
||||||
|
getWebServerSettings,
|
||||||
manageDomain,
|
manageDomain,
|
||||||
removeDomain,
|
removeDomain,
|
||||||
removeDomainById,
|
removeDomainById,
|
||||||
@@ -107,16 +108,13 @@ export const domainRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
canGenerateTraefikMeDomains: protectedProcedure
|
canGenerateTraefikMeDomains: protectedProcedure
|
||||||
.input(z.object({ serverId: z.string() }))
|
.input(z.object({ serverId: z.string() }))
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input }) => {
|
||||||
const organization = await findOrganizationById(
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (input.serverId) {
|
if (input.serverId) {
|
||||||
const server = await findServerById(input.serverId);
|
const server = await findServerById(input.serverId);
|
||||||
return server.ipAddress;
|
return server.ipAddress;
|
||||||
}
|
}
|
||||||
return organization?.owner.serverIp;
|
const settings = await getWebServerSettings();
|
||||||
|
return settings?.serverIp || "";
|
||||||
}),
|
}),
|
||||||
|
|
||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
createSlackNotification,
|
createSlackNotification,
|
||||||
createTelegramNotification,
|
createTelegramNotification,
|
||||||
findNotificationById,
|
findNotificationById,
|
||||||
|
getWebServerSettings,
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
removeNotificationById,
|
removeNotificationById,
|
||||||
sendCustomNotification,
|
sendCustomNotification,
|
||||||
@@ -66,7 +67,6 @@ import {
|
|||||||
apiUpdateTelegram,
|
apiUpdateTelegram,
|
||||||
notifications,
|
notifications,
|
||||||
server,
|
server,
|
||||||
user,
|
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
|
|
||||||
export const notificationRouter = createTRPCRouter({
|
export const notificationRouter = createTRPCRouter({
|
||||||
@@ -364,21 +364,20 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
let organizationId = "";
|
let organizationId = "";
|
||||||
let ServerName = "";
|
let ServerName = "";
|
||||||
if (input.ServerType === "Dokploy") {
|
if (input.ServerType === "Dokploy") {
|
||||||
const result = await db
|
const settings = await getWebServerSettings();
|
||||||
.select()
|
if (
|
||||||
.from(user)
|
!settings?.metricsConfig?.server?.token ||
|
||||||
.where(
|
settings.metricsConfig.server.token !== input.Token
|
||||||
sql`${user.metricsConfig}::jsonb -> 'server' ->> 'token' = ${input.Token}`,
|
) {
|
||||||
);
|
|
||||||
|
|
||||||
if (!result?.[0]?.id) {
|
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Token not found",
|
message: "Token not found",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
organizationId = result?.[0]?.id;
|
// For Dokploy server type, we don't have a specific organizationId
|
||||||
|
// This might need to be adjusted based on your business logic
|
||||||
|
organizationId = "";
|
||||||
ServerName = "Dokploy";
|
ServerName = "Dokploy";
|
||||||
} else {
|
} else {
|
||||||
const result = await db
|
const result = await db
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ import {
|
|||||||
DEFAULT_UPDATE_DATA,
|
DEFAULT_UPDATE_DATA,
|
||||||
execAsync,
|
execAsync,
|
||||||
findServerById,
|
findServerById,
|
||||||
findUserById,
|
|
||||||
getDokployImage,
|
getDokployImage,
|
||||||
getDokployImageTag,
|
getDokployImageTag,
|
||||||
getLogCleanupStatus,
|
getLogCleanupStatus,
|
||||||
getUpdateData,
|
getUpdateData,
|
||||||
|
getWebServerSettings,
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
parseRawConfig,
|
parseRawConfig,
|
||||||
paths,
|
paths,
|
||||||
@@ -40,7 +40,7 @@ import {
|
|||||||
updateLetsEncryptEmail,
|
updateLetsEncryptEmail,
|
||||||
updateServerById,
|
updateServerById,
|
||||||
updateServerTraefik,
|
updateServerTraefik,
|
||||||
updateUser,
|
updateWebServerSettings,
|
||||||
writeConfig,
|
writeConfig,
|
||||||
writeMainConfig,
|
writeMainConfig,
|
||||||
writeTraefikConfigInPath,
|
writeTraefikConfigInPath,
|
||||||
@@ -77,6 +77,13 @@ import {
|
|||||||
} from "../trpc";
|
} from "../trpc";
|
||||||
|
|
||||||
export const settingsRouter = createTRPCRouter({
|
export const settingsRouter = createTRPCRouter({
|
||||||
|
getWebServerSettings: protectedProcedure.query(async () => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const settings = await getWebServerSettings();
|
||||||
|
return settings;
|
||||||
|
}),
|
||||||
reloadServer: adminProcedure.mutation(async () => {
|
reloadServer: adminProcedure.mutation(async () => {
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return true;
|
return true;
|
||||||
@@ -209,11 +216,11 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
saveSSHPrivateKey: adminProcedure
|
saveSSHPrivateKey: adminProcedure
|
||||||
.input(apiSaveSSHKey)
|
.input(apiSaveSSHKey)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input }) => {
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
await updateUser(ctx.user.ownerId, {
|
await updateWebServerSettings({
|
||||||
sshPrivateKey: input.sshPrivateKey,
|
sshPrivateKey: input.sshPrivateKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -221,36 +228,36 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
assignDomainServer: adminProcedure
|
assignDomainServer: adminProcedure
|
||||||
.input(apiAssignDomain)
|
.input(apiAssignDomain)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ input }) => {
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const user = await updateUser(ctx.user.ownerId, {
|
const settings = await updateWebServerSettings({
|
||||||
host: input.host,
|
host: input.host,
|
||||||
letsEncryptEmail: input.letsEncryptEmail,
|
letsEncryptEmail: input.letsEncryptEmail,
|
||||||
certificateType: input.certificateType,
|
certificateType: input.certificateType,
|
||||||
https: input.https,
|
https: input.https,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!settings) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "NOT_FOUND",
|
code: "NOT_FOUND",
|
||||||
message: "User not found",
|
message: "Web server settings not found",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateServerTraefik(user, input.host);
|
updateServerTraefik(settings, input.host);
|
||||||
if (input.letsEncryptEmail) {
|
if (input.letsEncryptEmail) {
|
||||||
updateLetsEncryptEmail(input.letsEncryptEmail);
|
updateLetsEncryptEmail(input.letsEncryptEmail);
|
||||||
}
|
}
|
||||||
|
|
||||||
return user;
|
return settings;
|
||||||
}),
|
}),
|
||||||
cleanSSHPrivateKey: adminProcedure.mutation(async ({ ctx }) => {
|
cleanSSHPrivateKey: adminProcedure.mutation(async () => {
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
await updateUser(ctx.user.ownerId, {
|
await updateWebServerSettings({
|
||||||
sshPrivateKey: null,
|
sshPrivateKey: null,
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
@@ -310,11 +317,11 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (!IS_CLOUD) {
|
} else if (!IS_CLOUD) {
|
||||||
const userUpdated = await updateUser(ctx.user.ownerId, {
|
const settingsUpdated = await updateWebServerSettings({
|
||||||
enableDockerCleanup: input.enableDockerCleanup,
|
enableDockerCleanup: input.enableDockerCleanup,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (userUpdated?.enableDockerCleanup) {
|
if (settingsUpdated?.enableDockerCleanup) {
|
||||||
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
|
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
|
||||||
console.log(
|
console.log(
|
||||||
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
|
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
|
||||||
@@ -488,13 +495,28 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return readConfigInPath(input.path, input.serverId);
|
return readConfigInPath(input.path, input.serverId);
|
||||||
}),
|
}),
|
||||||
getIp: protectedProcedure.query(async ({ ctx }) => {
|
getIp: protectedProcedure.query(async () => {
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return true;
|
return "";
|
||||||
}
|
}
|
||||||
const user = await findUserById(ctx.user.ownerId);
|
const settings = await getWebServerSettings();
|
||||||
return user.serverIp;
|
return settings?.serverIp || "";
|
||||||
}),
|
}),
|
||||||
|
updateServerIp: adminProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
serverIp: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const settings = await updateWebServerSettings({
|
||||||
|
serverIp: input.serverIp,
|
||||||
|
});
|
||||||
|
return settings;
|
||||||
|
}),
|
||||||
|
|
||||||
getOpenApiDocument: protectedProcedure.query(
|
getOpenApiDocument: protectedProcedure.query(
|
||||||
async ({ ctx }): Promise<unknown> => {
|
async ({ ctx }): Promise<unknown> => {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
findUserById,
|
findUserById,
|
||||||
getDokployUrl,
|
getDokployUrl,
|
||||||
getUserByToken,
|
getUserByToken,
|
||||||
|
getWebServerSettings,
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
removeUserById,
|
removeUserById,
|
||||||
sendEmailNotification,
|
sendEmailNotification,
|
||||||
@@ -214,10 +215,11 @@ export const userRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
getMetricsToken: protectedProcedure.query(async ({ ctx }) => {
|
getMetricsToken: protectedProcedure.query(async ({ ctx }) => {
|
||||||
const user = await findUserById(ctx.user.ownerId);
|
const user = await findUserById(ctx.user.ownerId);
|
||||||
|
const settings = await getWebServerSettings();
|
||||||
return {
|
return {
|
||||||
serverIp: user.serverIp,
|
serverIp: settings?.serverIp,
|
||||||
enabledFeatures: user.enablePaidFeatures,
|
enabledFeatures: user.enablePaidFeatures,
|
||||||
metricsConfig: user?.metricsConfig,
|
metricsConfig: settings?.metricsConfig,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
remove: protectedProcedure
|
remove: protectedProcedure
|
||||||
|
|||||||
@@ -75,7 +75,6 @@
|
|||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"rotating-file-stream": "3.2.3",
|
|
||||||
"shell-quote": "^1.8.1",
|
"shell-quote": "^1.8.1",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
"ssh2": "1.15.0",
|
"ssh2": "1.15.0",
|
||||||
|
|||||||
@@ -35,3 +35,4 @@ export * from "./ssh-key";
|
|||||||
export * from "./user";
|
export * from "./user";
|
||||||
export * from "./utils";
|
export * from "./utils";
|
||||||
export * from "./volume-backups";
|
export * from "./volume-backups";
|
||||||
|
export * from "./web-server-settings";
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { relations } from "drizzle-orm";
|
|||||||
import {
|
import {
|
||||||
boolean,
|
boolean,
|
||||||
integer,
|
integer,
|
||||||
jsonb,
|
|
||||||
pgTable,
|
pgTable,
|
||||||
text,
|
text,
|
||||||
timestamp,
|
timestamp,
|
||||||
@@ -15,7 +14,6 @@ import { account, apikey, organization } from "./account";
|
|||||||
import { backups } from "./backups";
|
import { backups } from "./backups";
|
||||||
import { projects } from "./project";
|
import { projects } from "./project";
|
||||||
import { schedules } from "./schedule";
|
import { schedules } from "./schedule";
|
||||||
import { certificateType } from "./shared";
|
|
||||||
/**
|
/**
|
||||||
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
|
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
|
||||||
* database instance for multiple projects.
|
* database instance for multiple projects.
|
||||||
@@ -51,73 +49,10 @@ export const user = pgTable("user", {
|
|||||||
banExpires: timestamp("ban_expires"),
|
banExpires: timestamp("ban_expires"),
|
||||||
updatedAt: timestamp("updated_at").notNull(),
|
updatedAt: timestamp("updated_at").notNull(),
|
||||||
// Admin
|
// Admin
|
||||||
serverIp: text("serverIp"),
|
|
||||||
certificateType: certificateType("certificateType").notNull().default("none"),
|
|
||||||
https: boolean("https").notNull().default(false),
|
|
||||||
host: text("host"),
|
|
||||||
letsEncryptEmail: text("letsEncryptEmail"),
|
|
||||||
sshPrivateKey: text("sshPrivateKey"),
|
|
||||||
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(true),
|
|
||||||
logCleanupCron: text("logCleanupCron").default("0 0 * * *"),
|
|
||||||
role: text("role").notNull().default("user"),
|
role: text("role").notNull().default("user"),
|
||||||
// Metrics
|
// Metrics
|
||||||
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
|
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
|
||||||
allowImpersonation: boolean("allowImpersonation").notNull().default(false),
|
allowImpersonation: boolean("allowImpersonation").notNull().default(false),
|
||||||
metricsConfig: jsonb("metricsConfig")
|
|
||||||
.$type<{
|
|
||||||
server: {
|
|
||||||
type: "Dokploy" | "Remote";
|
|
||||||
refreshRate: number;
|
|
||||||
port: number;
|
|
||||||
token: string;
|
|
||||||
urlCallback: string;
|
|
||||||
retentionDays: number;
|
|
||||||
cronJob: string;
|
|
||||||
thresholds: {
|
|
||||||
cpu: number;
|
|
||||||
memory: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
containers: {
|
|
||||||
refreshRate: number;
|
|
||||||
services: {
|
|
||||||
include: string[];
|
|
||||||
exclude: string[];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}>()
|
|
||||||
.notNull()
|
|
||||||
.default({
|
|
||||||
server: {
|
|
||||||
type: "Dokploy",
|
|
||||||
refreshRate: 60,
|
|
||||||
port: 4500,
|
|
||||||
token: "",
|
|
||||||
retentionDays: 2,
|
|
||||||
cronJob: "",
|
|
||||||
urlCallback: "",
|
|
||||||
thresholds: {
|
|
||||||
cpu: 0,
|
|
||||||
memory: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
containers: {
|
|
||||||
refreshRate: 60,
|
|
||||||
services: {
|
|
||||||
include: [],
|
|
||||||
exclude: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
cleanupCacheApplications: boolean("cleanupCacheApplications")
|
|
||||||
.notNull()
|
|
||||||
.default(false),
|
|
||||||
cleanupCacheOnPreviews: boolean("cleanupCacheOnPreviews")
|
|
||||||
.notNull()
|
|
||||||
.default(false),
|
|
||||||
cleanupCacheOnCompose: boolean("cleanupCacheOnCompose")
|
|
||||||
.notNull()
|
|
||||||
.default(false),
|
|
||||||
stripeCustomerId: text("stripeCustomerId"),
|
stripeCustomerId: text("stripeCustomerId"),
|
||||||
stripeSubscriptionId: text("stripeSubscriptionId"),
|
stripeSubscriptionId: text("stripeSubscriptionId"),
|
||||||
serversQuantity: integer("serversQuantity").notNull().default(0),
|
serversQuantity: integer("serversQuantity").notNull().default(0),
|
||||||
@@ -203,33 +138,6 @@ export const apiFindOneUserByAuth = createSchema
|
|||||||
// authId: true,
|
// authId: true,
|
||||||
})
|
})
|
||||||
.required();
|
.required();
|
||||||
export const apiSaveSSHKey = createSchema
|
|
||||||
.pick({
|
|
||||||
sshPrivateKey: true,
|
|
||||||
})
|
|
||||||
.required();
|
|
||||||
|
|
||||||
export const apiAssignDomain = createSchema
|
|
||||||
.pick({
|
|
||||||
host: true,
|
|
||||||
certificateType: true,
|
|
||||||
letsEncryptEmail: true,
|
|
||||||
https: true,
|
|
||||||
})
|
|
||||||
.required()
|
|
||||||
.partial({
|
|
||||||
letsEncryptEmail: true,
|
|
||||||
https: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const apiUpdateDockerCleanup = createSchema
|
|
||||||
.pick({
|
|
||||||
enableDockerCleanup: true,
|
|
||||||
})
|
|
||||||
.required()
|
|
||||||
.extend({
|
|
||||||
serverId: z.string().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const apiTraefikConfig = z.object({
|
export const apiTraefikConfig = z.object({
|
||||||
traefikConfig: z.string().min(1),
|
traefikConfig: z.string().min(1),
|
||||||
@@ -298,32 +206,6 @@ export const apiReadStatsLogs = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiUpdateWebServerMonitoring = z.object({
|
|
||||||
metricsConfig: z
|
|
||||||
.object({
|
|
||||||
server: z.object({
|
|
||||||
refreshRate: z.number().min(2),
|
|
||||||
port: z.number().min(1),
|
|
||||||
token: z.string(),
|
|
||||||
urlCallback: z.string().url(),
|
|
||||||
retentionDays: z.number().min(1),
|
|
||||||
cronJob: z.string().min(1),
|
|
||||||
thresholds: z.object({
|
|
||||||
cpu: z.number().min(0),
|
|
||||||
memory: z.number().min(0),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
containers: z.object({
|
|
||||||
refreshRate: z.number().min(2),
|
|
||||||
services: z.object({
|
|
||||||
include: z.array(z.string()).optional(),
|
|
||||||
exclude: z.array(z.string()).optional(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.required(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const apiUpdateUser = createSchema.partial().extend({
|
export const apiUpdateUser = createSchema.partial().extend({
|
||||||
email: z
|
email: z
|
||||||
.string()
|
.string()
|
||||||
@@ -334,29 +216,4 @@ export const apiUpdateUser = createSchema.partial().extend({
|
|||||||
currentPassword: z.string().optional(),
|
currentPassword: z.string().optional(),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
lastName: z.string().optional(),
|
lastName: z.string().optional(),
|
||||||
metricsConfig: z
|
|
||||||
.object({
|
|
||||||
server: z.object({
|
|
||||||
type: z.enum(["Dokploy", "Remote"]),
|
|
||||||
refreshRate: z.number(),
|
|
||||||
port: z.number(),
|
|
||||||
token: z.string(),
|
|
||||||
urlCallback: z.string(),
|
|
||||||
retentionDays: z.number(),
|
|
||||||
cronJob: z.string(),
|
|
||||||
thresholds: z.object({
|
|
||||||
cpu: z.number(),
|
|
||||||
memory: z.number(),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
containers: z.object({
|
|
||||||
refreshRate: z.number(),
|
|
||||||
services: z.object({
|
|
||||||
include: z.array(z.string()),
|
|
||||||
exclude: z.array(z.string()),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
logCleanupCron: z.string().optional().nullable(),
|
|
||||||
});
|
});
|
||||||
|
|||||||
178
packages/server/src/db/schema/web-server-settings.ts
Normal file
178
packages/server/src/db/schema/web-server-settings.ts
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import { relations } from "drizzle-orm";
|
||||||
|
import { boolean, jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
||||||
|
import { createInsertSchema } from "drizzle-zod";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { certificateType } from "./shared";
|
||||||
|
|
||||||
|
export const webServerSettings = pgTable("webServerSettings", {
|
||||||
|
id: text("id")
|
||||||
|
.notNull()
|
||||||
|
.primaryKey()
|
||||||
|
.$defaultFn(() => nanoid()),
|
||||||
|
// Web Server Configuration
|
||||||
|
serverIp: text("serverIp"),
|
||||||
|
certificateType: certificateType("certificateType").notNull().default("none"),
|
||||||
|
https: boolean("https").notNull().default(false),
|
||||||
|
host: text("host"),
|
||||||
|
letsEncryptEmail: text("letsEncryptEmail"),
|
||||||
|
sshPrivateKey: text("sshPrivateKey"),
|
||||||
|
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(true),
|
||||||
|
logCleanupCron: text("logCleanupCron").default("0 0 * * *"),
|
||||||
|
// Metrics Configuration
|
||||||
|
metricsConfig: jsonb("metricsConfig")
|
||||||
|
.$type<{
|
||||||
|
server: {
|
||||||
|
type: "Dokploy" | "Remote";
|
||||||
|
refreshRate: number;
|
||||||
|
port: number;
|
||||||
|
token: string;
|
||||||
|
urlCallback: string;
|
||||||
|
retentionDays: number;
|
||||||
|
cronJob: string;
|
||||||
|
thresholds: {
|
||||||
|
cpu: number;
|
||||||
|
memory: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
containers: {
|
||||||
|
refreshRate: number;
|
||||||
|
services: {
|
||||||
|
include: string[];
|
||||||
|
exclude: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}>()
|
||||||
|
.notNull()
|
||||||
|
.default({
|
||||||
|
server: {
|
||||||
|
type: "Dokploy",
|
||||||
|
refreshRate: 60,
|
||||||
|
port: 4500,
|
||||||
|
token: "",
|
||||||
|
retentionDays: 2,
|
||||||
|
cronJob: "",
|
||||||
|
urlCallback: "",
|
||||||
|
thresholds: {
|
||||||
|
cpu: 0,
|
||||||
|
memory: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
containers: {
|
||||||
|
refreshRate: 60,
|
||||||
|
services: {
|
||||||
|
include: [],
|
||||||
|
exclude: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
// Cache Cleanup Configuration
|
||||||
|
cleanupCacheApplications: boolean("cleanupCacheApplications")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
cleanupCacheOnPreviews: boolean("cleanupCacheOnPreviews")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
cleanupCacheOnCompose: boolean("cleanupCacheOnCompose")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
createdAt: timestamp("created_at").defaultNow(),
|
||||||
|
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const webServerSettingsRelations = relations(
|
||||||
|
webServerSettings,
|
||||||
|
() => ({}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const createSchema = createInsertSchema(webServerSettings, {
|
||||||
|
id: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const apiUpdateWebServerSettings = createSchema.partial().extend({
|
||||||
|
serverIp: z.string().optional(),
|
||||||
|
certificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
|
||||||
|
https: z.boolean().optional(),
|
||||||
|
host: z.string().optional(),
|
||||||
|
letsEncryptEmail: z.string().email().optional().nullable(),
|
||||||
|
sshPrivateKey: z.string().optional(),
|
||||||
|
enableDockerCleanup: z.boolean().optional(),
|
||||||
|
logCleanupCron: z.string().optional().nullable(),
|
||||||
|
metricsConfig: z
|
||||||
|
.object({
|
||||||
|
server: z.object({
|
||||||
|
type: z.enum(["Dokploy", "Remote"]),
|
||||||
|
refreshRate: z.number(),
|
||||||
|
port: z.number(),
|
||||||
|
token: z.string(),
|
||||||
|
urlCallback: z.string(),
|
||||||
|
retentionDays: z.number(),
|
||||||
|
cronJob: z.string(),
|
||||||
|
thresholds: z.object({
|
||||||
|
cpu: z.number(),
|
||||||
|
memory: z.number(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
containers: z.object({
|
||||||
|
refreshRate: z.number(),
|
||||||
|
services: z.object({
|
||||||
|
include: z.array(z.string()),
|
||||||
|
exclude: z.array(z.string()),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
cleanupCacheApplications: z.boolean().optional(),
|
||||||
|
cleanupCacheOnPreviews: z.boolean().optional(),
|
||||||
|
cleanupCacheOnCompose: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const apiAssignDomain = z
|
||||||
|
.object({
|
||||||
|
host: z.string(),
|
||||||
|
certificateType: z.enum(["letsencrypt", "none", "custom"]),
|
||||||
|
letsEncryptEmail: z.string().email().optional().nullable(),
|
||||||
|
https: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
.required()
|
||||||
|
.partial({
|
||||||
|
letsEncryptEmail: true,
|
||||||
|
https: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const apiSaveSSHKey = z
|
||||||
|
.object({
|
||||||
|
sshPrivateKey: z.string(),
|
||||||
|
})
|
||||||
|
.required();
|
||||||
|
|
||||||
|
export const apiUpdateDockerCleanup = z.object({
|
||||||
|
enableDockerCleanup: z.boolean(),
|
||||||
|
serverId: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const apiUpdateWebServerMonitoring = z.object({
|
||||||
|
metricsConfig: z
|
||||||
|
.object({
|
||||||
|
server: z.object({
|
||||||
|
refreshRate: z.number().min(2),
|
||||||
|
port: z.number().min(1),
|
||||||
|
token: z.string(),
|
||||||
|
urlCallback: z.string().url(),
|
||||||
|
retentionDays: z.number().min(1),
|
||||||
|
cronJob: z.string().min(1),
|
||||||
|
thresholds: z.object({
|
||||||
|
cpu: z.number().min(0),
|
||||||
|
memory: z.number().min(0),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
containers: z.object({
|
||||||
|
refreshRate: z.number().min(2),
|
||||||
|
services: z.object({
|
||||||
|
include: z.array(z.string()).optional(),
|
||||||
|
exclude: z.array(z.string()).optional(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.required(),
|
||||||
|
});
|
||||||
@@ -41,6 +41,7 @@ export * from "./services/settings";
|
|||||||
export * from "./services/ssh-key";
|
export * from "./services/ssh-key";
|
||||||
export * from "./services/user";
|
export * from "./services/user";
|
||||||
export * from "./services/volume-backups";
|
export * from "./services/volume-backups";
|
||||||
|
export * from "./services/web-server-settings";
|
||||||
export * from "./setup/config-paths";
|
export * from "./setup/config-paths";
|
||||||
export * from "./setup/monitoring-setup";
|
export * from "./setup/monitoring-setup";
|
||||||
export * from "./setup/postgres-setup";
|
export * from "./setup/postgres-setup";
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import { IS_CLOUD } from "../constants";
|
|||||||
import { db } from "../db";
|
import { db } from "../db";
|
||||||
import * as schema from "../db/schema";
|
import * as schema from "../db/schema";
|
||||||
import { getUserByToken } from "../services/admin";
|
import { getUserByToken } from "../services/admin";
|
||||||
import { updateUser } from "../services/user";
|
import {
|
||||||
|
getWebServerSettings,
|
||||||
|
updateWebServerSettings,
|
||||||
|
} from "../services/web-server-settings";
|
||||||
import { getHubSpotUTK, submitToHubSpot } from "../utils/tracking/hubspot";
|
import { getHubSpotUTK, submitToHubSpot } from "../utils/tracking/hubspot";
|
||||||
import { sendEmail } from "../verification/send-verification-email";
|
import { sendEmail } from "../verification/send-verification-email";
|
||||||
import { getPublicIpWithFallback } from "../wss/utils";
|
import { getPublicIpWithFallback } from "../wss/utils";
|
||||||
@@ -35,22 +38,14 @@ const { handler, api } = betterAuth({
|
|||||||
},
|
},
|
||||||
...(!IS_CLOUD && {
|
...(!IS_CLOUD && {
|
||||||
async trustedOrigins() {
|
async trustedOrigins() {
|
||||||
const admin = await db.query.member.findFirst({
|
const settings = await getWebServerSettings();
|
||||||
where: eq(schema.member.role, "owner"),
|
if (!settings) {
|
||||||
with: {
|
return [];
|
||||||
user: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (admin?.user) {
|
|
||||||
return [
|
|
||||||
...(admin.user.serverIp
|
|
||||||
? [`http://${admin.user.serverIp}:3000`]
|
|
||||||
: []),
|
|
||||||
...(admin.user.host ? [`https://${admin.user.host}`] : []),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
return [];
|
return [
|
||||||
|
...(settings?.serverIp ? [`http://${settings?.serverIp}:3000`] : []),
|
||||||
|
...(settings?.host ? [`https://${settings?.host}`] : []),
|
||||||
|
];
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
emailVerification: {
|
emailVerification: {
|
||||||
@@ -122,7 +117,7 @@ const { handler, api } = betterAuth({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!IS_CLOUD) {
|
if (!IS_CLOUD) {
|
||||||
await updateUser(user.id, {
|
await updateWebServerSettings({
|
||||||
serverIp: await getPublicIpWithFallback(),
|
serverIp: await getPublicIpWithFallback(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { IS_CLOUD } from "../constants";
|
import { IS_CLOUD } from "../constants";
|
||||||
|
import { getWebServerSettings } from "./web-server-settings";
|
||||||
|
|
||||||
export const findUserById = async (userId: string) => {
|
export const findUserById = async (userId: string) => {
|
||||||
const userResult = await db.query.user.findFirst({
|
const userResult = await db.query.user.findFirst({
|
||||||
@@ -107,11 +108,11 @@ export const getDokployUrl = async () => {
|
|||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return "https://app.dokploy.com";
|
return "https://app.dokploy.com";
|
||||||
}
|
}
|
||||||
const owner = await findOwner();
|
const settings = await getWebServerSettings();
|
||||||
|
|
||||||
if (owner.user.host) {
|
if (settings?.host) {
|
||||||
const protocol = owner.user.https ? "https" : "http";
|
const protocol = settings?.https ? "https" : "http";
|
||||||
return `${protocol}://${owner.user.host}`;
|
return `${protocol}://${settings?.host}`;
|
||||||
}
|
}
|
||||||
return `http://${owner.user.serverIp}:${process.env.PORT}`;
|
return `http://${settings?.serverIp}:${process.env.PORT}`;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { generateObject } from "ai";
|
|||||||
import { desc, eq } from "drizzle-orm";
|
import { desc, eq } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { IS_CLOUD } from "../constants";
|
import { IS_CLOUD } from "../constants";
|
||||||
import { findOrganizationById } from "./admin";
|
|
||||||
import { findServerById } from "./server";
|
import { findServerById } from "./server";
|
||||||
|
import { getWebServerSettings } from "./web-server-settings";
|
||||||
|
|
||||||
export const getAiSettingsByOrganizationId = async (organizationId: string) => {
|
export const getAiSettingsByOrganizationId = async (organizationId: string) => {
|
||||||
const aiSettings = await db.query.ai.findMany({
|
const aiSettings = await db.query.ai.findMany({
|
||||||
@@ -79,8 +79,8 @@ export const suggestVariants = async ({
|
|||||||
|
|
||||||
let ip = "";
|
let ip = "";
|
||||||
if (!IS_CLOUD) {
|
if (!IS_CLOUD) {
|
||||||
const organization = await findOrganizationById(organizationId);
|
const settings = await getWebServerSettings();
|
||||||
ip = organization?.owner.serverIp || "";
|
ip = settings?.serverIp || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (serverId) {
|
if (serverId) {
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import { promisify } from "node:util";
|
|||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { generateRandomDomain } from "@dokploy/server/templates";
|
import { generateRandomDomain } from "@dokploy/server/templates";
|
||||||
import { manageDomain } from "@dokploy/server/utils/traefik/domain";
|
import { manageDomain } from "@dokploy/server/utils/traefik/domain";
|
||||||
|
import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { type apiCreateDomain, domains } from "../db/schema";
|
import { type apiCreateDomain, domains } from "../db/schema";
|
||||||
import { findUserById } from "./admin";
|
|
||||||
import { findApplicationById } from "./application";
|
import { findApplicationById } from "./application";
|
||||||
import { detectCDNProvider } from "./cdn";
|
import { detectCDNProvider } from "./cdn";
|
||||||
import { findServerById } from "./server";
|
import { findServerById } from "./server";
|
||||||
@@ -61,9 +61,9 @@ export const generateTraefikMeDomain = async (
|
|||||||
projectName: appName,
|
projectName: appName,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const admin = await findUserById(userId);
|
const settings = await getWebServerSettings();
|
||||||
return generateRandomDomain({
|
return generateRandomDomain({
|
||||||
serverIp: admin?.serverIp || "",
|
serverIp: settings?.serverIp || "",
|
||||||
projectName: appName,
|
projectName: appName,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ import { removeDirectoryCode } from "../utils/filesystem/directory";
|
|||||||
import { authGithub } from "../utils/providers/github";
|
import { authGithub } from "../utils/providers/github";
|
||||||
import { removeTraefikConfig } from "../utils/traefik/application";
|
import { removeTraefikConfig } from "../utils/traefik/application";
|
||||||
import { manageDomain } from "../utils/traefik/domain";
|
import { manageDomain } from "../utils/traefik/domain";
|
||||||
import { findUserById } from "./admin";
|
|
||||||
import { findApplicationById } from "./application";
|
import { findApplicationById } from "./application";
|
||||||
import { removeDeploymentsByPreviewDeploymentId } from "./deployment";
|
import { removeDeploymentsByPreviewDeploymentId } from "./deployment";
|
||||||
import { createDomain } from "./domain";
|
import { createDomain } from "./domain";
|
||||||
import { type Github, getIssueComment } from "./github";
|
import { type Github, getIssueComment } from "./github";
|
||||||
|
import { getWebServerSettings } from "./web-server-settings";
|
||||||
|
|
||||||
export type PreviewDeployment = typeof previewDeployments.$inferSelect;
|
export type PreviewDeployment = typeof previewDeployments.$inferSelect;
|
||||||
|
|
||||||
@@ -253,8 +253,8 @@ const generateWildcardDomain = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!ip) {
|
if (!ip) {
|
||||||
const admin = await findUserById(userId);
|
const settings = await getWebServerSettings();
|
||||||
ip = admin?.serverIp || "";
|
ip = settings?.serverIp || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const slugIp = ip.replaceAll(".", "-");
|
const slugIp = ip.replaceAll(".", "-");
|
||||||
|
|||||||
44
packages/server/src/services/web-server-settings.ts
Normal file
44
packages/server/src/services/web-server-settings.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { db } from "@dokploy/server/db";
|
||||||
|
import { webServerSettings } from "@dokploy/server/db/schema";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the web server settings (singleton - only one row should exist)
|
||||||
|
*/
|
||||||
|
export const getWebServerSettings = async () => {
|
||||||
|
const settings = await db.query.webServerSettings.findFirst({
|
||||||
|
orderBy: (settings, { asc }) => [asc(settings.createdAt)],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!settings) {
|
||||||
|
// Create default settings if none exist
|
||||||
|
const [newSettings] = await db
|
||||||
|
.insert(webServerSettings)
|
||||||
|
.values({})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return newSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update web server settings
|
||||||
|
*/
|
||||||
|
export const updateWebServerSettings = async (
|
||||||
|
updates: Partial<typeof webServerSettings.$inferInsert>,
|
||||||
|
) => {
|
||||||
|
const current = await getWebServerSettings();
|
||||||
|
|
||||||
|
const [updated] = await db
|
||||||
|
.update(webServerSettings)
|
||||||
|
.set({
|
||||||
|
...updates,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(webServerSettings.id, current?.id ?? ""))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { findServerById } from "@dokploy/server/services/server";
|
import { findServerById } from "@dokploy/server/services/server";
|
||||||
|
import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
|
||||||
import type { ContainerCreateOptions } from "dockerode";
|
import type { ContainerCreateOptions } from "dockerode";
|
||||||
import { IS_CLOUD } from "../constants";
|
import { IS_CLOUD } from "../constants";
|
||||||
import { findUserById } from "../services/admin";
|
|
||||||
import { getDokployImageTag } from "../services/settings";
|
import { getDokployImageTag } from "../services/settings";
|
||||||
import { pullImage, pullRemoteImage } from "../utils/docker/utils";
|
import { pullImage, pullRemoteImage } from "../utils/docker/utils";
|
||||||
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
|
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
|
||||||
@@ -83,8 +83,8 @@ export const setupMonitoring = async (serverId: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setupWebMonitoring = async (userId: string) => {
|
export const setupWebMonitoring = async () => {
|
||||||
const user = await findUserById(userId);
|
const webServerSettings = await getWebServerSettings();
|
||||||
|
|
||||||
const containerName = "dokploy-monitoring";
|
const containerName = "dokploy-monitoring";
|
||||||
let imageName = "dokploy/monitoring:latest";
|
let imageName = "dokploy/monitoring:latest";
|
||||||
@@ -99,7 +99,7 @@ export const setupWebMonitoring = async (userId: string) => {
|
|||||||
|
|
||||||
const settings: ContainerCreateOptions = {
|
const settings: ContainerCreateOptions = {
|
||||||
name: containerName,
|
name: containerName,
|
||||||
Env: [`METRICS_CONFIG=${JSON.stringify(user?.metricsConfig)}`],
|
Env: [`METRICS_CONFIG=${JSON.stringify(webServerSettings?.metricsConfig)}`],
|
||||||
Image: imageName,
|
Image: imageName,
|
||||||
HostConfig: {
|
HostConfig: {
|
||||||
// Memory: 100 * 1024 * 1024, // 100MB en bytes
|
// Memory: 100 * 1024 * 1024, // 100MB en bytes
|
||||||
@@ -110,9 +110,9 @@ export const setupWebMonitoring = async (userId: string) => {
|
|||||||
Name: "always",
|
Name: "always",
|
||||||
},
|
},
|
||||||
PortBindings: {
|
PortBindings: {
|
||||||
[`${user?.metricsConfig?.server?.port}/tcp`]: [
|
[`${webServerSettings?.metricsConfig?.server?.port}/tcp`]: [
|
||||||
{
|
{
|
||||||
HostPort: user?.metricsConfig?.server?.port.toString(),
|
HostPort: webServerSettings?.metricsConfig?.server?.port.toString(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -126,7 +126,7 @@ export const setupWebMonitoring = async (userId: string) => {
|
|||||||
// NetworkMode: "host",
|
// NetworkMode: "host",
|
||||||
},
|
},
|
||||||
ExposedPorts: {
|
ExposedPorts: {
|
||||||
[`${user?.metricsConfig?.server?.port}/tcp`]: {},
|
[`${webServerSettings?.metricsConfig?.server?.port}/tcp`]: {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const docker = await getRemoteDocker();
|
const docker = await getRemoteDocker();
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { paths } from "@dokploy/server/constants";
|
import { paths } from "@dokploy/server/constants";
|
||||||
import { findOwner } from "@dokploy/server/services/admin";
|
import {
|
||||||
import { updateUser } from "@dokploy/server/services/user";
|
getWebServerSettings,
|
||||||
|
updateWebServerSettings,
|
||||||
|
} from "@dokploy/server/services/web-server-settings";
|
||||||
import { scheduledJobs, scheduleJob } from "node-schedule";
|
import { scheduledJobs, scheduleJob } from "node-schedule";
|
||||||
import { execAsync } from "../process/execAsync";
|
import { execAsync } from "../process/execAsync";
|
||||||
|
|
||||||
@@ -29,12 +31,9 @@ export const startLogCleanup = async (
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const owner = await findOwner();
|
await updateWebServerSettings({
|
||||||
if (owner) {
|
logCleanupCron: cronExpression,
|
||||||
await updateUser(owner.user.id, {
|
});
|
||||||
logCleanupCron: cronExpression,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -51,12 +50,9 @@ export const stopLogCleanup = async (): Promise<boolean> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update database
|
// Update database
|
||||||
const owner = await findOwner();
|
await updateWebServerSettings({
|
||||||
if (owner) {
|
logCleanupCron: null,
|
||||||
await updateUser(owner.user.id, {
|
});
|
||||||
logCleanupCron: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -69,8 +65,8 @@ export const getLogCleanupStatus = async (): Promise<{
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
cronExpression: string | null;
|
cronExpression: string | null;
|
||||||
}> => {
|
}> => {
|
||||||
const owner = await findOwner();
|
const settings = await getWebServerSettings();
|
||||||
const cronExpression = owner?.user.logCleanupCron ?? null;
|
const cronExpression = settings?.logCleanupCron ?? null;
|
||||||
return {
|
return {
|
||||||
enabled: cronExpression !== null,
|
enabled: cronExpression !== null,
|
||||||
cronExpression,
|
cronExpression,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import path from "node:path";
|
|||||||
import { member } from "@dokploy/server/db/schema";
|
import { member } from "@dokploy/server/db/schema";
|
||||||
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
import type { BackupSchedule } from "@dokploy/server/services/backup";
|
||||||
import { getAllServers } from "@dokploy/server/services/server";
|
import { getAllServers } from "@dokploy/server/services/server";
|
||||||
|
import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { scheduleJob } from "node-schedule";
|
import { scheduleJob } from "node-schedule";
|
||||||
import { db } from "../../db/index";
|
import { db } from "../../db/index";
|
||||||
@@ -25,7 +26,9 @@ export const initCronJobs = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (admin?.user?.enableDockerCleanup) {
|
const webServerSettings = await getWebServerSettings();
|
||||||
|
|
||||||
|
if (webServerSettings?.enableDockerCleanup) {
|
||||||
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
|
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
|
||||||
console.log(
|
console.log(
|
||||||
`Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`,
|
`Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`,
|
||||||
@@ -82,9 +85,12 @@ export const initCronJobs = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (admin?.user?.logCleanupCron) {
|
if (webServerSettings?.logCleanupCron) {
|
||||||
console.log("Starting log requests cleanup", admin.user.logCleanupCron);
|
console.log(
|
||||||
await startLogCleanup(admin.user.logCleanupCron);
|
"Starting log requests cleanup",
|
||||||
|
webServerSettings.logCleanupCron,
|
||||||
|
);
|
||||||
|
await startLogCleanup(webServerSettings.logCleanupCron);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export const createCommand = (compose: ComposeNested) => {
|
|||||||
if (composeType === "docker-compose") {
|
if (composeType === "docker-compose") {
|
||||||
command = `compose -p ${appName} -f ${path} up -d --build --remove-orphans`;
|
command = `compose -p ${appName} -f ${path} up -d --build --remove-orphans`;
|
||||||
} else if (composeType === "stack") {
|
} else if (composeType === "stack") {
|
||||||
command = `stack deploy -c ${path} ${appName} --prune`;
|
command = `stack deploy -c ${path} ${appName} --prune --with-registry-auth`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return command;
|
return command;
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ const getRegistryCommands = (
|
|||||||
): string => {
|
): string => {
|
||||||
return `
|
return `
|
||||||
echo "📦 [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" ;
|
echo "📦 [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" ;
|
||||||
echo "${registry.password}" | docker login ${registry.registryUrl} -u ${registry.username} --password-stdin || {
|
echo "${registry.password}" | docker login ${registry.registryUrl} -u '${registry.username}' --password-stdin || {
|
||||||
echo "❌ DockerHub Failed" ;
|
echo "❌ DockerHub Failed" ;
|
||||||
exit 1;
|
exit 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { paths } from "@dokploy/server/constants";
|
import { paths } from "@dokploy/server/constants";
|
||||||
import type { User } from "@dokploy/server/services/user";
|
import type { webServerSettings } from "@dokploy/server/db/schema/web-server-settings";
|
||||||
import { parse, stringify } from "yaml";
|
import { parse, stringify } from "yaml";
|
||||||
import {
|
import {
|
||||||
loadOrCreateConfig,
|
loadOrCreateConfig,
|
||||||
@@ -12,10 +12,10 @@ import type { FileConfig } from "./file-types";
|
|||||||
import type { MainTraefikConfig } from "./types";
|
import type { MainTraefikConfig } from "./types";
|
||||||
|
|
||||||
export const updateServerTraefik = (
|
export const updateServerTraefik = (
|
||||||
user: User | null,
|
settings: typeof webServerSettings.$inferSelect | null,
|
||||||
newHost: string | null,
|
newHost: string | null,
|
||||||
) => {
|
) => {
|
||||||
const { https, certificateType } = user || {};
|
const { https, certificateType } = settings || {};
|
||||||
const appName = "dokploy";
|
const appName = "dokploy";
|
||||||
const config: FileConfig = loadOrCreateConfig(appName);
|
const config: FileConfig = loadOrCreateConfig(appName);
|
||||||
|
|
||||||
|
|||||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@@ -406,9 +406,6 @@ importers:
|
|||||||
recharts:
|
recharts:
|
||||||
specifier: ^2.15.3
|
specifier: ^2.15.3
|
||||||
version: 2.15.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
version: 2.15.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
rotating-file-stream:
|
|
||||||
specifier: 3.2.3
|
|
||||||
version: 3.2.3
|
|
||||||
shell-quote:
|
shell-quote:
|
||||||
specifier: ^1.8.1
|
specifier: ^1.8.1
|
||||||
version: 1.8.2
|
version: 1.8.2
|
||||||
@@ -732,9 +729,6 @@ importers:
|
|||||||
react-dom:
|
react-dom:
|
||||||
specifier: 18.2.0
|
specifier: 18.2.0
|
||||||
version: 18.2.0(react@18.2.0)
|
version: 18.2.0(react@18.2.0)
|
||||||
rotating-file-stream:
|
|
||||||
specifier: 3.2.3
|
|
||||||
version: 3.2.3
|
|
||||||
shell-quote:
|
shell-quote:
|
||||||
specifier: ^1.8.1
|
specifier: ^1.8.1
|
||||||
version: 1.8.2
|
version: 1.8.2
|
||||||
@@ -7064,10 +7058,6 @@ packages:
|
|||||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
rotating-file-stream@3.2.3:
|
|
||||||
resolution: {integrity: sha512-cfmm3tqdnbuYw2FBmRTPBDaohYEbMJ3211T35o6eZdr4d7v69+ZeK1Av84Br7FLj2dlzyeZSbN6qTuXXE6dawQ==}
|
|
||||||
engines: {node: '>=14.0'}
|
|
||||||
|
|
||||||
rou3@0.5.1:
|
rou3@0.5.1:
|
||||||
resolution: {integrity: sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==}
|
resolution: {integrity: sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==}
|
||||||
|
|
||||||
@@ -14660,8 +14650,6 @@ snapshots:
|
|||||||
'@rollup/rollup-win32-x64-msvc': 4.41.1
|
'@rollup/rollup-win32-x64-msvc': 4.41.1
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
rotating-file-stream@3.2.3: {}
|
|
||||||
|
|
||||||
rou3@0.5.1: {}
|
rou3@0.5.1: {}
|
||||||
|
|
||||||
run-parallel@1.2.0:
|
run-parallel@1.2.0:
|
||||||
|
|||||||
Reference in New Issue
Block a user