Compare commits

..

1 Commits

Author SHA1 Message Date
dosubot[bot]
a4406e0542 docs: add pull request template with contribution checklist 2026-03-27 15:30:21 +00:00
123 changed files with 350 additions and 26819 deletions

View File

@@ -7,7 +7,7 @@ Please describe in a short paragraph what this PR is about.
Before submitting this PR, please make sure that:
- [ ] You created a dedicated branch based on the `canary` branch.
- [ ] You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request
- [ ] You have read the suggestions in the CONTRIBUTING.md file https://github.com/MkinG2k0/dokploy/blob/canary/CONTRIBUTING.md#pull-request
- [ ] You have tested this PR in your local instance. If you have not tested it yet, please do so before submitting. This helps avoid wasting maintainers' time reviewing code that has not been verified by you.
## Issues related (if applicable)

View File

@@ -32,7 +32,6 @@ describe("Host rule format regression tests", () => {
previewDeploymentId: "",
internalPath: "/",
stripPath: false,
customEntrypoint: null,
};
describe("Host rule format validation", () => {

View File

@@ -7,7 +7,6 @@ describe("createDomainLabels", () => {
const baseDomain: Domain = {
host: "example.com",
port: 8080,
customEntrypoint: null,
https: false,
uniqueConfigKey: 1,
customCertResolver: null,
@@ -241,134 +240,4 @@ describe("createDomainLabels", () => {
"traefik.http.routers.test-app-1-websecure.middlewares=stripprefix-test-app-1,addprefix-test-app-1",
);
});
it("should create basic labels for custom entrypoint", async () => {
const labels = await createDomainLabels(
appName,
{ ...baseDomain, customEntrypoint: "custom" },
"custom",
);
expect(labels).toEqual([
"traefik.http.routers.test-app-1-custom.rule=Host(`example.com`)",
"traefik.http.routers.test-app-1-custom.entrypoints=custom",
"traefik.http.services.test-app-1-custom.loadbalancer.server.port=8080",
"traefik.http.routers.test-app-1-custom.service=test-app-1-custom",
]);
});
it("should create https labels for custom entrypoint", async () => {
const labels = await createDomainLabels(
appName,
{
...baseDomain,
https: true,
customEntrypoint: "custom",
certificateType: "letsencrypt",
},
"custom",
);
expect(labels).toEqual([
"traefik.http.routers.test-app-1-custom.rule=Host(`example.com`)",
"traefik.http.routers.test-app-1-custom.entrypoints=custom",
"traefik.http.services.test-app-1-custom.loadbalancer.server.port=8080",
"traefik.http.routers.test-app-1-custom.service=test-app-1-custom",
"traefik.http.routers.test-app-1-custom.tls.certresolver=letsencrypt",
]);
});
it("should add stripPath middleware for custom entrypoint", async () => {
const labels = await createDomainLabels(
appName,
{
...baseDomain,
customEntrypoint: "custom",
path: "/api",
stripPath: true,
},
"custom",
);
expect(labels).toContain(
"traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api",
);
expect(labels).toContain(
"traefik.http.routers.test-app-1-custom.middlewares=stripprefix-test-app-1",
);
});
it("should add internalPath middleware for custom entrypoint", async () => {
const labels = await createDomainLabels(
appName,
{
...baseDomain,
customEntrypoint: "custom",
internalPath: "/hello",
},
"custom",
);
expect(labels).toContain(
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
);
expect(labels).toContain(
"traefik.http.routers.test-app-1-custom.middlewares=addprefix-test-app-1",
);
});
it("should add path prefix in rule for custom entrypoint", async () => {
const labels = await createDomainLabels(
appName,
{
...baseDomain,
customEntrypoint: "custom",
path: "/api",
},
"custom",
);
expect(labels).toContain(
"traefik.http.routers.test-app-1-custom.rule=Host(`example.com`) && PathPrefix(`/api`)",
);
});
it("should combine all middlewares for custom entrypoint", async () => {
const labels = await createDomainLabels(
appName,
{
...baseDomain,
customEntrypoint: "custom",
path: "/api",
stripPath: true,
internalPath: "/hello",
},
"custom",
);
expect(labels).toContain(
"traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api",
);
expect(labels).toContain(
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
);
expect(labels).toContain(
"traefik.http.routers.test-app-1-custom.middlewares=stripprefix-test-app-1,addprefix-test-app-1",
);
});
it("should not add redirect-to-https for custom entrypoint even with https", async () => {
const labels = await createDomainLabels(
appName,
{
...baseDomain,
customEntrypoint: "custom",
https: true,
certificateType: "letsencrypt",
},
"custom",
);
const middlewareLabel = labels.find((l) => l.includes(".middlewares="));
// Should not contain redirect-to-https since there's only one router
expect(middlewareLabel).toBeUndefined();
});
});

View File

@@ -292,7 +292,7 @@ networks:
dokploy-network:
`;
test("It shouldn't add suffix to dokploy-network", () => {
test("It shoudn't add suffix to dokploy-network", () => {
const composeData = parse(composeFile7) as ComposeSpecification;
const suffix = generateRandomHash();

View File

@@ -195,7 +195,7 @@ services:
- dokploy-network
`;
test("It shouldn't add suffix to dokploy-network in services", () => {
test("It shoudn't add suffix to dokploy-network in services", () => {
const composeData = parse(composeFile7) as ComposeSpecification;
const suffix = generateRandomHash();
@@ -241,10 +241,10 @@ services:
dokploy-network:
aliases:
- apid
`;
test("It shouldn't add suffix to dokploy-network in services multiples cases", () => {
test("It shoudn't add suffix to dokploy-network in services multiples cases", () => {
const composeData = parse(composeFile8) as ComposeSpecification;
const suffix = generateRandomHash();

View File

@@ -1,4 +1,4 @@
import { getEnvironmentVariablesObject } from "@dokploy/server/index";
import { getEnviromentVariablesObject } from "@dokploy/server/index";
import { describe, expect, it } from "vitest";
const projectEnv = `
@@ -15,7 +15,7 @@ DATABASE_NAME=dev_database
SECRET_KEY=env-secret-123
`;
describe("getEnvironmentVariablesObject with environment variables (Stack compose)", () => {
describe("getEnviromentVariablesObject with environment variables (Stack compose)", () => {
it("resolves environment variables correctly for Stack compose", () => {
const serviceEnv = `
FOO=\${{environment.NODE_ENV}}
@@ -23,7 +23,7 @@ BAR=\${{environment.API_URL}}
BAZ=test
`;
const result = getEnvironmentVariablesObject(
const result = getEnviromentVariablesObject(
serviceEnv,
projectEnv,
environmentEnv,
@@ -45,7 +45,7 @@ DATABASE_URL=\${{project.DATABASE_URL}}
SERVICE_PORT=4000
`;
const result = getEnvironmentVariablesObject(
const result = getEnviromentVariablesObject(
serviceEnv,
projectEnv,
environmentEnv,
@@ -72,7 +72,7 @@ PASSWORD=secret123
DATABASE_URL=postgresql://\${{environment.USERNAME}}:\${{environment.PASSWORD}}@\${{environment.HOST}}:\${{environment.PORT}}/mydb
`;
const result = getEnvironmentVariablesObject(serviceEnv, "", multiRefEnv);
const result = getEnviromentVariablesObject(serviceEnv, "", multiRefEnv);
expect(result).toEqual({
DATABASE_URL: "postgresql://postgres:secret123@localhost:5432/mydb",
@@ -85,7 +85,7 @@ UNDEFINED_VAR=\${{environment.UNDEFINED_VAR}}
`;
expect(() =>
getEnvironmentVariablesObject(serviceWithUndefined, "", environmentEnv),
getEnviromentVariablesObject(serviceWithUndefined, "", environmentEnv),
).toThrow("Invalid environment variable: environment.UNDEFINED_VAR");
});
@@ -95,7 +95,7 @@ NODE_ENV=production
API_URL=\${{environment.API_URL}}
`;
const result = getEnvironmentVariablesObject(
const result = getEnviromentVariablesObject(
serviceOverrideEnv,
"",
environmentEnv,
@@ -115,7 +115,7 @@ SERVICE_NAME=my-service
COMPLEX_VAR=\${{SERVICE_NAME}}-\${{environment.NODE_ENV}}-\${{project.ENVIRONMENT}}
`;
const result = getEnvironmentVariablesObject(
const result = getEnviromentVariablesObject(
complexServiceEnv,
projectEnv,
environmentEnv,
@@ -150,7 +150,7 @@ ENV_VAR=\${{environment.API_URL}}
DB_NAME=\${{environment.DATABASE_NAME}}
`;
const result = getEnvironmentVariablesObject(
const result = getEnviromentVariablesObject(
serviceWithConflicts,
conflictingProjectEnv,
conflictingEnvironmentEnv,
@@ -170,7 +170,7 @@ SERVICE_VAR=test
PROJECT_VAR=\${{project.ENVIRONMENT}}
`;
const result = getEnvironmentVariablesObject(
const result = getEnviromentVariablesObject(
serviceWithEmpty,
projectEnv,
"",

View File

@@ -1,8 +1,8 @@
import { describe, it, expect } from "vitest";
import {
enterpriseOnlyResources,
statements,
} from "@dokploy/server/lib/access-control";
import { describe, expect, it } from "vitest";
const FREE_TIER_RESOURCES = [
"organization",

View File

@@ -137,7 +137,6 @@ const baseDomain: Domain = {
https: false,
path: null,
port: null,
customEntrypoint: null,
serviceName: "",
composeId: "",
customCertResolver: null,
@@ -277,110 +276,6 @@ test("CertificateType on websecure entrypoint", async () => {
expect(router.tls?.certResolver).toBe("letsencrypt");
});
test("Custom entrypoint on http domain", async () => {
const router = await createRouterConfig(
baseApp,
{ ...baseDomain, https: false, customEntrypoint: "custom" },
"custom",
);
expect(router.entryPoints).toEqual(["custom"]);
expect(router.middlewares).not.toContain("redirect-to-https");
expect(router.tls).toBeUndefined();
});
test("Custom entrypoint on https domain", async () => {
const router = await createRouterConfig(
baseApp,
{
...baseDomain,
https: true,
customEntrypoint: "custom",
certificateType: "letsencrypt",
},
"custom",
);
expect(router.entryPoints).toEqual(["custom"]);
expect(router.middlewares).not.toContain("redirect-to-https");
expect(router.tls?.certResolver).toBe("letsencrypt");
});
test("Custom entrypoint with path includes PathPrefix in rule", async () => {
const router = await createRouterConfig(
baseApp,
{ ...baseDomain, customEntrypoint: "custom", path: "/api" },
"custom",
);
expect(router.rule).toContain("PathPrefix(`/api`)");
expect(router.entryPoints).toEqual(["custom"]);
});
test("Custom entrypoint with stripPath adds stripprefix middleware", async () => {
const router = await createRouterConfig(
baseApp,
{
...baseDomain,
customEntrypoint: "custom",
path: "/api",
stripPath: true,
},
"custom",
);
expect(router.middlewares).toContain("stripprefix--1");
expect(router.entryPoints).toEqual(["custom"]);
});
test("Custom entrypoint with internalPath adds addprefix middleware", async () => {
const router = await createRouterConfig(
baseApp,
{
...baseDomain,
customEntrypoint: "custom",
internalPath: "/hello",
},
"custom",
);
expect(router.middlewares).toContain("addprefix--1");
expect(router.entryPoints).toEqual(["custom"]);
});
test("Custom entrypoint with https and custom cert resolver", async () => {
const router = await createRouterConfig(
baseApp,
{
...baseDomain,
https: true,
customEntrypoint: "custom",
certificateType: "custom",
customCertResolver: "myresolver",
},
"custom",
);
expect(router.entryPoints).toEqual(["custom"]);
expect(router.tls?.certResolver).toBe("myresolver");
});
test("Custom entrypoint without https should not have tls", async () => {
const router = await createRouterConfig(
baseApp,
{
...baseDomain,
https: false,
customEntrypoint: "custom",
certificateType: "letsencrypt",
},
"custom",
);
expect(router.entryPoints).toEqual(["custom"]);
expect(router.tls).toBeUndefined();
});
/** IDN/Punycode */
test("Internationalized domain name is converted to punycode", async () => {

View File

@@ -40,12 +40,12 @@ interface Props {
type: "application" | "mariadb" | "mongo" | "mysql" | "postgres" | "redis";
}
const AddRedirectSchema = z.object({
const AddRedirectchema = z.object({
replicas: z.number().min(1, "Replicas must be at least 1"),
registryId: z.string().optional(),
});
type AddCommand = z.infer<typeof AddRedirectSchema>;
type AddCommand = z.infer<typeof AddRedirectchema>;
export const ShowClusterSettings = ({ id, type }: Props) => {
const queryMap = {
@@ -87,7 +87,7 @@ export const ShowClusterSettings = ({ id, type }: Props) => {
: {}),
replicas: data?.replicas || 1,
},
resolver: zodResolver(AddRedirectSchema),
resolver: zodResolver(AddRedirectchema),
});
useEffect(() => {

View File

@@ -37,13 +37,13 @@ import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
const AddRedirectSchema = z.object({
const AddRedirectchema = z.object({
regex: z.string().min(1, "Regex required"),
permanent: z.boolean().default(false),
replacement: z.string().min(1, "Replacement required"),
});
type AddRedirect = z.infer<typeof AddRedirectSchema>;
type AddRedirect = z.infer<typeof AddRedirectchema>;
// Default presets
const redirectPresets = [
@@ -110,7 +110,7 @@ export const HandleRedirect = ({
regex: "",
replacement: "",
},
resolver: zodResolver(AddRedirectSchema),
resolver: zodResolver(AddRedirectchema),
});
useEffect(() => {
@@ -149,7 +149,7 @@ export const HandleRedirect = ({
const onDialogToggle = (open: boolean) => {
setIsOpen(open);
// commented for the moment because not resetting the form if accidentally closed the dialog can be considered as a feature instead of a bug
// commented for the moment because not reseting the form if accidentally closed the dialog can be considered as a feature instead of a bug
// setPresetSelected("");
// form.reset();
};

View File

@@ -1,4 +1,3 @@
import copy from "copy-to-clipboard";
import {
ChevronDown,
ChevronUp,
@@ -12,6 +11,7 @@ import {
} from "lucide-react";
import React, { useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import copy from "copy-to-clipboard";
import { AlertBlock } from "@/components/shared/alert-block";
import { DateTooltip } from "@/components/shared/date-tooltip";
import { DialogAction } from "@/components/shared/dialog-action";

View File

@@ -61,8 +61,6 @@ export const domain = z
.min(1, { message: "Port must be at least 1" })
.max(65535, { message: "Port must be 65535 or below" })
.optional(),
useCustomEntrypoint: z.boolean(),
customEntrypoint: z.string().optional(),
https: z.boolean().optional(),
certificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
customCertResolver: z.string().optional(),
@@ -116,14 +114,6 @@ export const domain = z
message: "Internal path must start with '/'",
});
}
if (input.useCustomEntrypoint && !input.customEntrypoint) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["customEntrypoint"],
message: "Custom entry point must be specified",
});
}
});
type Domain = z.infer<typeof domain>;
@@ -206,8 +196,6 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
internalPath: undefined,
stripPath: false,
port: undefined,
useCustomEntrypoint: false,
customEntrypoint: undefined,
https: false,
certificateType: undefined,
customCertResolver: undefined,
@@ -218,7 +206,6 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
});
const certificateType = form.watch("certificateType");
const useCustomEntrypoint = form.watch("useCustomEntrypoint");
const https = form.watch("https");
const domainType = form.watch("domainType");
const host = form.watch("host");
@@ -233,8 +220,6 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
internalPath: data?.internalPath || undefined,
stripPath: data?.stripPath || false,
port: data?.port || undefined,
useCustomEntrypoint: !!data.customEntrypoint,
customEntrypoint: data.customEntrypoint || undefined,
certificateType: data?.certificateType || undefined,
customCertResolver: data?.customCertResolver || undefined,
serviceName: data?.serviceName || undefined,
@@ -249,8 +234,6 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
internalPath: undefined,
stripPath: false,
port: undefined,
useCustomEntrypoint: false,
customEntrypoint: undefined,
https: false,
certificateType: undefined,
customCertResolver: undefined,
@@ -652,50 +635,6 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
}}
/>
<FormField
control={form.control}
name="useCustomEntrypoint"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
<div className="space-y-0.5">
<FormLabel>Custom Entrypoint</FormLabel>
<FormDescription>
Use custom entrypoint for domina
<br />
"web" and/or "websecure" is used by default.
</FormDescription>
<FormMessage />
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
{useCustomEntrypoint && (
<FormField
control={form.control}
name="customEntrypoint"
render={({ field }) => (
<FormItem className="w-full">
<FormLabel>Entrypoint Name</FormLabel>
<FormControl>
<Input
placeholder="Enter entrypoint name manually"
{...field}
className="w-full"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="https"

View File

@@ -91,7 +91,7 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
}, [option, services, containers]);
const isLoading = option === "native" ? containersLoading : servicesLoading;
const containersLength =
const containersLenght =
option === "native" ? containers?.length : services?.length;
return (
@@ -167,7 +167,7 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
</>
)}
<SelectLabel>Containers ({containersLength})</SelectLabel>
<SelectLabel>Containers ({containersLenght})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>

View File

@@ -1,2 +1,2 @@
export * from "./patch-editor";
export * from "./show-patches";
export * from "./patch-editor";

View File

@@ -483,7 +483,7 @@ export const HandleVolumeBackups = ({
</SelectContent>
</Select>
<FormDescription>
Choose the volume to backup. If you do not see the
Choose the volume to backup, if you dont see the
volume here, you can type the volume name manually
</FormDescription>
<FormMessage />
@@ -518,7 +518,7 @@ export const HandleVolumeBackups = ({
</SelectContent>
</Select>
<FormDescription>
Choose the volume to backup. If you do not see the volume
Choose the volume to backup, if you dont see the volume
here, you can type the volume name manually
</FormDescription>
<FormMessage />

View File

@@ -77,7 +77,7 @@ export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
}, [option, services, containers]);
const isLoading = option === "native" ? containersLoading : servicesLoading;
const containersLength =
const containersLenght =
option === "native" ? containers?.length : services?.length;
return (
@@ -152,7 +152,7 @@ export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
</>
)}
<SelectLabel>Containers ({containersLength})</SelectLabel>
<SelectLabel>Containers ({containersLenght})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>

View File

@@ -225,7 +225,7 @@ export const RestoreBackup = ({
resolver: zodResolver(RestoreBackupSchema),
});
const destinationId = form.watch("destinationId");
const destionationId = form.watch("destinationId");
const currentDatabaseType = form.watch("databaseType");
const metadata = form.watch("metadata");
@@ -240,12 +240,12 @@ export const RestoreBackup = ({
const { data: files = [], isPending } = api.backup.listBackupFiles.useQuery(
{
destinationId: destinationId,
destinationId: destionationId,
search: debouncedSearchTerm,
serverId: serverId ?? "",
},
{
enabled: isOpen && !!destinationId,
enabled: isOpen && !!destionationId,
},
);

View File

@@ -1,8 +1,8 @@
"use client";
import type { inferRouterOutputs } from "@trpc/server";
import { ArrowRight, ListTodo, Loader2, XCircle } from "lucide-react";
import Link from "next/link";
import { ArrowRight, ListTodo, Loader2, XCircle } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {

View File

@@ -103,7 +103,7 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
>
{" "}
<div className="flex items-start gap-x-2">
{/* Icon to expand the log item maybe implement a collapsible later */}
{/* Icon to expand the log item maybe implement a colapsible later */}
{/* <Square className="size-4 text-muted-foreground opacity-0 group-hover/logitem:opacity-100 transition-opacity" /> */}
{tooltip(color, rawTimestamp)}
{!noTimestamp && (

View File

@@ -130,7 +130,7 @@ export const columns: ColumnDef<Container>[] = [
</DockerTerminalModal>
<RemoveContainerDialog
containerId={container.containerId}
serverId={container.serverId ?? undefined}
serverId={container.serverId}
/>
</DropdownMenuContent>
</DropdownMenu>

View File

@@ -35,7 +35,7 @@ import {
TableRow,
} from "@/components/ui/table";
import { api, type RouterOutputs } from "@/utils/api";
import { columns } from "./columns";
import { columns } from "./colums";
export type Container = NonNullable<
RouterOutputs["docker"]["getContainers"]
>[0];

View File

@@ -11,11 +11,11 @@ import {
import type { DockerStatsJSON } from "./show-free-container-monitoring";
interface Props {
accumulativeData: DockerStatsJSON["block"];
acummulativeData: DockerStatsJSON["block"];
}
export const DockerBlockChart = ({ accumulativeData }: Props) => {
const transformedData = accumulativeData.map((item, index) => {
export const DockerBlockChart = ({ acummulativeData }: Props) => {
const transformedData = acummulativeData.map((item, index) => {
return {
time: item.time,
name: `Point ${index + 1}`,

View File

@@ -11,11 +11,11 @@ import {
import type { DockerStatsJSON } from "./show-free-container-monitoring";
interface Props {
accumulativeData: DockerStatsJSON["cpu"];
acummulativeData: DockerStatsJSON["cpu"];
}
export const DockerCpuChart = ({ accumulativeData }: Props) => {
const transformedData = accumulativeData.map((item, index) => {
export const DockerCpuChart = ({ acummulativeData }: Props) => {
const transformedData = acummulativeData.map((item, index) => {
return {
name: `Point ${index + 1}`,
time: item.time,

View File

@@ -11,12 +11,12 @@ import {
import type { DockerStatsJSON } from "./show-free-container-monitoring";
interface Props {
accumulativeData: DockerStatsJSON["disk"];
acummulativeData: DockerStatsJSON["disk"];
diskTotal: number;
}
export const DockerDiskChart = ({ accumulativeData, diskTotal }: Props) => {
const transformedData = accumulativeData.map((item, index) => {
export const DockerDiskChart = ({ acummulativeData, diskTotal }: Props) => {
const transformedData = acummulativeData.map((item, index) => {
return {
time: item.time,
name: `Point ${index + 1}`,

View File

@@ -12,15 +12,15 @@ import type { DockerStatsJSON } from "./show-free-container-monitoring";
import { convertMemoryToBytes } from "./show-free-container-monitoring";
interface Props {
accumulativeData: DockerStatsJSON["memory"];
acummulativeData: DockerStatsJSON["memory"];
memoryLimitGB: number;
}
export const DockerMemoryChart = ({
accumulativeData,
acummulativeData,
memoryLimitGB,
}: Props) => {
const transformedData = accumulativeData.map((item, index) => {
const transformedData = acummulativeData.map((item, index) => {
return {
time: item.time,
name: `Point ${index + 1}`,

View File

@@ -11,11 +11,11 @@ import {
import type { DockerStatsJSON } from "./show-free-container-monitoring";
interface Props {
accumulativeData: DockerStatsJSON["network"];
acummulativeData: DockerStatsJSON["network"];
}
export const DockerNetworkChart = ({ accumulativeData }: Props) => {
const transformedData = accumulativeData.map((item, index) => {
export const DockerNetworkChart = ({ acummulativeData }: Props) => {
const transformedData = acummulativeData.map((item, index) => {
return {
time: item.time,
name: `Point ${index + 1}`,

View File

@@ -124,7 +124,7 @@ export const ContainerFreeMonitoring = ({
refetchOnWindowFocus: false,
},
);
const [accumulativeData, setAccumulativeData] = useState<DockerStatsJSON>({
const [acummulativeData, setAcummulativeData] = useState<DockerStatsJSON>({
cpu: [],
memory: [],
block: [],
@@ -136,7 +136,7 @@ export const ContainerFreeMonitoring = ({
useEffect(() => {
setCurrentData(defaultData);
setAccumulativeData({
setAcummulativeData({
cpu: [],
memory: [],
block: [],
@@ -155,7 +155,7 @@ export const ContainerFreeMonitoring = ({
network: data.network[data.network.length - 1] ?? currentData.network,
disk: data.disk[data.disk.length - 1] ?? currentData.disk,
});
setAccumulativeData({
setAcummulativeData({
block: data?.block || [],
cpu: data?.cpu || [],
disk: data?.disk || [],
@@ -184,7 +184,7 @@ export const ContainerFreeMonitoring = ({
setCurrentData(data);
const MAX_DATA_POINTS = 300;
setAccumulativeData((prevData) => ({
setAcummulativeData((prevData) => ({
cpu: [...prevData.cpu, data.cpu].slice(-MAX_DATA_POINTS),
memory: [...prevData.memory, data.memory].slice(-MAX_DATA_POINTS),
block: [...prevData.block, data.block].slice(-MAX_DATA_POINTS),
@@ -228,7 +228,7 @@ export const ContainerFreeMonitoring = ({
)}
className="w-[100%]"
/>
<DockerCpuChart accumulativeData={accumulativeData.cpu} />
<DockerCpuChart acummulativeData={acummulativeData.cpu} />
</div>
</CardContent>
</Card>
@@ -252,7 +252,7 @@ export const ContainerFreeMonitoring = ({
className="w-[100%]"
/>
<DockerMemoryChart
accumulativeData={accumulativeData.memory}
acummulativeData={acummulativeData.memory}
memoryLimitGB={
// @ts-ignore
convertMemoryToBytes(currentData.memory.value.total) /
@@ -277,7 +277,7 @@ export const ContainerFreeMonitoring = ({
className="w-[100%]"
/>
<DockerDiskChart
accumulativeData={accumulativeData.disk}
acummulativeData={acummulativeData.disk}
diskTotal={currentData.disk.value.diskTotal}
/>
</div>
@@ -294,7 +294,7 @@ export const ContainerFreeMonitoring = ({
<span className="text-sm text-muted-foreground">
{`Read: ${currentData.block.value.readMb} / Write: ${currentData.block.value.writeMb} `}
</span>
<DockerBlockChart accumulativeData={accumulativeData.block} />
<DockerBlockChart acummulativeData={acummulativeData.block} />
</div>
</CardContent>
</Card>
@@ -307,7 +307,7 @@ export const ContainerFreeMonitoring = ({
<span className="text-sm text-muted-foreground">
{`In MB: ${currentData.network.value.inputMb} / Out MB: ${currentData.network.value.outputMb} `}
</span>
<DockerNetworkChart accumulativeData={accumulativeData.network} />
<DockerNetworkChart acummulativeData={acummulativeData.network} />
</div>
</CardContent>
</Card>

View File

@@ -1,13 +1,4 @@
import {
AlertCircle,
ChevronDown,
ChevronRight,
Link,
Loader2,
ShieldCheck,
Trash2,
} from "lucide-react";
import { useState } from "react";
import { AlertCircle, Link, Loader2, ShieldCheck, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { AlertBlock } from "@/components/shared/alert-block";
import { DialogAction } from "@/components/shared/dialog-action";
@@ -21,19 +12,13 @@ import {
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { AddCertificate } from "./add-certificate";
import {
extractLeafCommonName,
getCertificateChainExpirationDetails,
getCertificateChainInfo,
getExpirationStatus,
} from "./utils";
import { getCertificateChainInfo, getExpirationStatus } from "./utils";
export const ShowCertificates = () => {
const { mutateAsync, isPending: isRemoving } =
api.certificates.remove.useMutation();
const { data, isPending, refetch } = api.certificates.all.useQuery();
const { data: permissions } = api.user.getPermissions.useQuery();
const [expandedChains, setExpandedChains] = useState<Set<string>>(new Set());
return (
<div className="w-full">
@@ -81,30 +66,6 @@ export const ShowCertificates = () => {
const chainInfo = getCertificateChainInfo(
certificate.certificateData,
);
const commonName = extractLeafCommonName(
certificate.certificateData,
);
const chainDetails = chainInfo.isChain
? getCertificateChainExpirationDetails(
certificate.certificateData,
)
: null;
const isExpanded = expandedChains.has(
certificate.certificateId,
);
const toggleChain = () => {
setExpandedChains((prev) => {
const next = new Set(prev);
if (next.has(certificate.certificateId)) {
next.delete(certificate.certificateId);
} else {
next.add(certificate.certificateId);
}
return next;
});
};
return (
<div
key={certificate.certificateId}
@@ -116,52 +77,12 @@ export const ShowCertificates = () => {
<span className="text-sm font-medium">
{index + 1}. {certificate.name}
</span>
{commonName && (
<span className="text-xs text-muted-foreground">
CN: {commonName}
</span>
)}
{chainInfo.isChain && (
<div className="flex flex-col gap-1.5 mt-1">
<button
type="button"
onClick={toggleChain}
className="flex items-center gap-1 px-1.5 py-0.5 rounded bg-muted/50 w-fit hover:bg-muted transition-colors"
>
{isExpanded ? (
<ChevronDown className="size-3 text-muted-foreground" />
) : (
<ChevronRight className="size-3 text-muted-foreground" />
)}
<Link className="size-3 text-muted-foreground" />
<span className="text-xs text-muted-foreground">
Chain ({chainInfo.count} certificates)
</span>
</button>
{isExpanded && (
<div className="flex flex-col gap-3 pl-2 border-l-2 border-muted">
{chainDetails?.map((cert) => (
<div
key={cert.index}
className="flex flex-col gap-1 p-2 rounded-md bg-muted/30"
>
<span className="text-xs font-medium text-muted-foreground">
{cert.label}
</span>
{cert.commonName && (
<span className="text-xs text-muted-foreground/80">
CN: {cert.commonName}
</span>
)}
<span
className={`text-xs ${cert.className}`}
>
{cert.message}
</span>
</div>
))}
</div>
)}
<div className="flex items-center gap-1 px-1.5 py-0.5 rounded bg-muted/50">
<Link className="size-3 text-muted-foreground" />
<span className="text-xs text-muted-foreground">
Chain ({chainInfo.count})
</span>
</div>
)}
<div

View File

@@ -1,13 +1,5 @@
// @ts-nocheck
// Split certificate chain into individual certificates
export const splitCertificateChain = (certData: string): string[] => {
const certRegex =
/(-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----)/g;
const matches = certData.match(certRegex);
return matches || [];
};
export const extractExpirationDate = (certData: string): Date | null => {
try {
// Decode PEM base64 to DER binary
@@ -102,156 +94,8 @@ export const extractExpirationDate = (certData: string): Date | null => {
}
};
export const extractCommonName = (certData: string): string | null => {
try {
// Decode PEM base64 to DER binary
const b64 = certData.replace(/-----[^-]+-----/g, "").replace(/\s+/g, "");
const binStr = atob(b64);
const der = new Uint8Array(binStr.length);
for (let i = 0; i < binStr.length; i++) {
der[i] = binStr.charCodeAt(i);
}
let offset = 0;
// Helper: read ASN.1 length field
function readLength(pos: number): { length: number; offset: number } {
// biome-ignore lint/style/noParameterAssign: <explanation>
let len = der[pos++];
if (len & 0x80) {
const bytes = len & 0x7f;
len = 0;
for (let i = 0; i < bytes; i++) {
// biome-ignore lint/style/noParameterAssign: <explanation>
len = (len << 8) + der[pos++];
}
}
return { length: len, offset: pos };
}
// Helper: skip a field
function skipField(pos: number): number {
// biome-ignore lint/style/noParameterAssign: <explanation>
pos++;
const fieldLen = readLength(pos);
return fieldLen.offset + fieldLen.length;
}
// Skip the outer certificate sequence
if (der[offset++] !== 0x30) throw new Error("Expected sequence");
({ offset } = readLength(offset));
// Skip tbsCertificate sequence
if (der[offset++] !== 0x30) throw new Error("Expected tbsCertificate");
({ offset } = readLength(offset));
// Check for optional version field (context-specific tag [0])
if (der[offset] === 0xa0) {
offset++;
const versionLen = readLength(offset);
offset = versionLen.offset + versionLen.length;
}
// Skip serialNumber
offset = skipField(offset);
// Skip signature
offset = skipField(offset);
// Skip issuer
offset = skipField(offset);
// Skip validity
offset = skipField(offset);
// Subject sequence - where we find the CN
if (der[offset++] !== 0x30) throw new Error("Expected subject sequence");
const subjectLen = readLength(offset);
const subjectEnd = subjectLen.offset + subjectLen.length;
offset = subjectLen.offset;
// Parse subject RDNs looking for CN (OID 2.5.4.3)
while (offset < subjectEnd) {
if (der[offset++] !== 0x31) continue; // SET
const setLen = readLength(offset);
offset = setLen.offset;
if (der[offset++] !== 0x30) continue; // SEQUENCE
const seqLen = readLength(offset);
offset = seqLen.offset;
if (der[offset++] !== 0x06) continue; // OID
const oidLen = readLength(offset);
offset = oidLen.offset;
// Check if OID is 2.5.4.3 (commonName)
const oid = Array.from(der.slice(offset, offset + oidLen.length));
offset += oidLen.length;
// OID 2.5.4.3 in DER: [0x55, 0x04, 0x03]
if (
oid.length === 3 &&
oid[0] === 0x55 &&
oid[1] === 0x04 &&
oid[2] === 0x03
) {
// Next should be the string value
const strType = der[offset++];
const strLen = readLength(offset);
const cnBytes = der.slice(strLen.offset, strLen.offset + strLen.length);
return new TextDecoder().decode(cnBytes);
}
}
return null;
} catch (error) {
console.error("Error parsing certificate CN:", error);
return null;
}
};
// Extract the Common Name from the first (leaf) certificate in a chain
export const extractLeafCommonName = (certData: string): string | null => {
const certs = splitCertificateChain(certData);
if (certs.length === 0) return null;
return extractCommonName(certs[0]);
};
// Extract expiration dates from all certificates in a chain
export const extractAllExpirationDates = (
certData: string,
): Array<{
cert: string;
index: number;
expirationDate: Date | null;
commonName: string | null;
}> => {
const certs = splitCertificateChain(certData);
return certs.map((cert, index) => ({
cert,
index,
expirationDate: extractExpirationDate(cert),
commonName: extractCommonName(cert),
}));
};
// Get the earliest expiration date from a certificate chain
export const getEarliestExpirationDate = (certData: string): Date | null => {
const expirationDates = extractAllExpirationDates(certData);
const validDates = expirationDates
.filter((item) => item.expirationDate !== null)
.map((item) => item.expirationDate as Date);
if (validDates.length === 0) return null;
return new Date(Math.min(...validDates.map((date) => date.getTime())));
};
export const getExpirationStatus = (certData: string) => {
const chainInfo = getCertificateChainInfo(certData);
const expirationDate = chainInfo.isChain
? getEarliestExpirationDate(certData)
: extractExpirationDate(certData);
const expirationDate = extractExpirationDate(certData);
if (!expirationDate)
return {
@@ -309,67 +153,3 @@ export const getCertificateChainInfo = (certData: string) => {
count: 1,
};
};
// Get detailed expiration information for all certificates in a chain
export const getCertificateChainExpirationDetails = (certData: string) => {
const allExpirations = extractAllExpirationDates(certData);
const now = new Date();
return allExpirations.map(({ index, expirationDate, commonName }) => {
if (!expirationDate) {
return {
index,
label: `Certificate ${index + 1}`,
commonName,
status: "unknown" as const,
className: "text-muted-foreground",
message: "Could not determine expiration",
expirationDate: null,
};
}
const daysUntilExpiration = Math.ceil(
(expirationDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24),
);
let status: "expired" | "warning" | "valid";
let className: string;
let message: string;
if (daysUntilExpiration < 0) {
status = "expired";
className = "text-red-500";
message = `Expired on ${expirationDate.toLocaleDateString([], {
year: "numeric",
month: "long",
day: "numeric",
})}`;
} else if (daysUntilExpiration <= 30) {
status = "warning";
className = "text-yellow-500";
message = `Expires in ${daysUntilExpiration} days`;
} else {
status = "valid";
className = "text-muted-foreground";
message = `Expires ${expirationDate.toLocaleDateString([], {
year: "numeric",
month: "long",
day: "numeric",
})}`;
}
return {
index,
label:
index === 0
? `Certificate ${index + 1} (Leaf)`
: `Certificate ${index + 1}`,
commonName,
status,
className,
message,
expirationDate,
daysUntilExpiration,
};
});
};

View File

@@ -1,11 +1,7 @@
import {
ADDITIONAL_FLAG_ERROR,
ADDITIONAL_FLAG_REGEX,
} from "@dokploy/server/db/validations/destination";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { PenBoxIcon, PlusIcon, Trash2 } from "lucide-react";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useFieldArray, useForm } from "react-hook-form";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
@@ -50,16 +46,6 @@ const addDestination = z.object({
region: z.string(),
endpoint: z.string().min(1, "Endpoint is required"),
serverId: z.string().optional(),
additionalFlags: z
.array(
z.object({
value: z
.string()
.min(1, "Flag cannot be empty")
.regex(ADDITIONAL_FLAG_REGEX, ADDITIONAL_FLAG_ERROR),
}),
)
.optional(),
});
type AddDestination = z.infer<typeof addDestination>;
@@ -103,16 +89,9 @@ export const HandleDestinations = ({ destinationId }: Props) => {
region: "",
secretAccessKey: "",
endpoint: "",
additionalFlags: [],
},
resolver: zodResolver(addDestination),
});
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "additionalFlags",
});
useEffect(() => {
if (destination) {
form.reset({
@@ -123,8 +102,6 @@ export const HandleDestinations = ({ destinationId }: Props) => {
bucket: destination.bucket,
region: destination.region,
endpoint: destination.endpoint,
additionalFlags:
destination.additionalFlags?.map((f) => ({ value: f })) ?? [],
});
} else {
form.reset();
@@ -141,7 +118,6 @@ export const HandleDestinations = ({ destinationId }: Props) => {
region: data.region,
secretAccessKey: data.secretAccessKey,
destinationId: destinationId || "",
additionalFlags: data.additionalFlags?.map((f) => f.value) ?? [],
})
.then(async () => {
toast.success(`Destination ${destinationId ? "Updated" : "Created"}`);
@@ -151,12 +127,9 @@ export const HandleDestinations = ({ destinationId }: Props) => {
}
setOpen(false);
})
.catch((e) => {
.catch(() => {
toast.error(
`Error ${destinationId ? "Updating" : "Creating"} the Destination`,
{
description: e.message,
},
);
});
};
@@ -168,7 +141,6 @@ export const HandleDestinations = ({ destinationId }: Props) => {
"secretAccessKey",
"bucket",
"endpoint",
"additionalFlags",
]);
if (!result) {
@@ -207,8 +179,6 @@ export const HandleDestinations = ({ destinationId }: Props) => {
region,
secretAccessKey: secretKey,
serverId,
additionalFlags:
form.getValues("additionalFlags")?.map((f) => f.value) ?? [],
})
.then(() => {
toast.success("Connection Success");
@@ -388,48 +358,6 @@ export const HandleDestinations = ({ destinationId }: Props) => {
</FormItem>
)}
/>
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<FormLabel>Additional Flags (Optional)</FormLabel>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => append({ value: "" })}
>
<PlusIcon className="size-4" />
Add Flag
</Button>
</div>
{fields.map((field, index) => (
<FormField
key={field.id}
control={form.control}
name={`additionalFlags.${index}.value`}
render={({ field }) => (
<FormItem>
<div className="flex items-center gap-2">
<FormControl>
<Input
placeholder="--s3-sign-accept-encoding=false"
{...field}
/>
</FormControl>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => remove(index)}
>
<Trash2 className="size-4 text-muted-foreground" />
</Button>
</div>
<FormMessage />
</FormItem>
)}
/>
))}
</div>
</form>
<DialogFooter

View File

@@ -283,7 +283,7 @@ export const AddGitlabProvider = () => {
</FormLabel>
<FormControl>
<Input
placeholder="For organization/group access use the slug name of the group eg: my-org"
placeholder="For organization/group access use the slugish name of the group eg: my-org"
{...field}
/>
</FormControl>

View File

@@ -192,7 +192,7 @@ export const EditGitlabProvider = ({ gitlabId }: Props) => {
</FormLabel>
<FormControl>
<Input
placeholder="For organization/group access use the slug name of the group eg: my-org"
placeholder="For organization/group access use the slugish name of the group eg: my-org"
{...field}
/>
</FormControl>

View File

@@ -5,7 +5,6 @@ import {
ImportIcon,
Loader2,
Trash2,
Users,
} from "lucide-react";
import Link from "next/link";
import { toast } from "sonner";
@@ -25,13 +24,6 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import { useUrl } from "@/utils/hooks/use-url";
import { AddBitbucketProvider } from "./bitbucket/add-bitbucket-provider";
@@ -47,8 +39,6 @@ export const ShowGitProviders = () => {
const { data, isPending, refetch } = api.gitProvider.getAll.useQuery();
const { mutateAsync, isPending: isRemoving } =
api.gitProvider.remove.useMutation();
const { mutateAsync: toggleShare, isPending: isToggling } =
api.gitProvider.toggleShare.useMutation();
const url = useUrl();
const getGitlabUrl = (
@@ -164,62 +154,10 @@ export const ShowGitProviders = () => {
)}
</span>
</div>
{!gitProvider.isOwner && (
<Badge
variant="secondary"
className="text-xs"
>
<Users className="size-3 mr-1" />
Shared
</Badge>
)}
</div>
</div>
<div className="flex flex-row gap-1 items-center">
{gitProvider.isOwner && (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-1.5 mr-2">
<Users className="size-4 text-muted-foreground" />
<Switch
disabled={isToggling}
checked={
gitProvider.sharedWithOrganization
}
onCheckedChange={async (
checked,
) => {
await toggleShare({
gitProviderId:
gitProvider.gitProviderId,
sharedWithOrganization: checked,
})
.then(() => {
toast.success(
checked
? "Provider shared with organization"
: "Provider unshared",
);
refetch();
})
.catch(() => {
toast.error(
"Error updating sharing",
);
});
}}
/>
</div>
</TooltipTrigger>
<TooltipContent>
Share with entire organization
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{isBitbucket &&
gitProvider.bitbucket?.appPassword &&
!gitProvider.bitbucket?.apiToken ? (
@@ -284,71 +222,62 @@ export const ShowGitProviders = () => {
</div>
)}
{gitProvider.isOwner && (
<>
{isGithub && haveGithubRequirements && (
<EditGithubProvider
githubId={gitProvider.github?.githubId}
/>
)}
{isGitlab && (
<EditGitlabProvider
gitlabId={gitProvider.gitlab?.gitlabId}
/>
)}
{isBitbucket && (
<EditBitbucketProvider
bitbucketId={
gitProvider.bitbucket?.bitbucketId
}
/>
)}
{isGitea && (
<EditGiteaProvider
giteaId={gitProvider.gitea?.giteaId}
/>
)}
<DialogAction
title="Delete Git Provider"
description={
gitProvider.sharedWithOrganization
? "This provider is shared with the organization. Deleting it will remove access for all members. Are you sure?"
: "Are you sure you want to delete this Git Provider?"
}
type="destructive"
onClick={async () => {
await mutateAsync({
gitProviderId:
gitProvider.gitProviderId,
})
.then(() => {
toast.success(
"Git Provider deleted successfully",
);
refetch();
})
.catch(() => {
toast.error(
"Error deleting Git Provider",
);
});
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10"
isLoading={isRemoving}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
</>
{isGithub && haveGithubRequirements && (
<EditGithubProvider
githubId={gitProvider.github?.githubId}
/>
)}
{isGitlab && (
<EditGitlabProvider
gitlabId={gitProvider.gitlab?.gitlabId}
/>
)}
{isBitbucket && (
<EditBitbucketProvider
bitbucketId={
gitProvider.bitbucket?.bitbucketId
}
/>
)}
{isGitea && (
<EditGiteaProvider
giteaId={gitProvider.gitea?.giteaId}
/>
)}
<DialogAction
title="Delete Git Provider"
description="Are you sure you want to delete this Git Provider?"
type="destructive"
onClick={async () => {
await mutateAsync({
gitProviderId: gitProvider.gitProviderId,
})
.then(() => {
toast.success(
"Git Provider deleted successfully",
);
refetch();
})
.catch(() => {
toast.error(
"Error deleting Git Provider",
);
});
}}
>
<Button
variant="ghost"
size="icon"
className="group hover:bg-red-500/10"
isLoading={isRemoving}
>
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
</Button>
</DialogAction>
</div>
</div>
</div>

View File

@@ -12,9 +12,9 @@ import { toast } from "sonner";
import { z } from "zod";
import {
DiscordIcon,
MattermostIcon,
GotifyIcon,
LarkIcon,
MattermostIcon,
NtfyIcon,
PushoverIcon,
ResendIcon,
@@ -54,7 +54,6 @@ const notificationBaseSchema = z.object({
appDeploy: z.boolean().default(false),
appBuildError: z.boolean().default(false),
databaseBackup: z.boolean().default(false),
dokployBackup: z.boolean().default(false),
volumeBackup: z.boolean().default(false),
dokployRestart: z.boolean().default(false),
dockerCleanup: z.boolean().default(false),
@@ -356,7 +355,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: notification.appDeploy,
dokployRestart: notification.dokployRestart,
databaseBackup: notification.databaseBackup,
dokployBackup: notification.dokployBackup,
volumeBackup: notification.volumeBackup,
dockerCleanup: notification.dockerCleanup,
webhookUrl: notification.slack?.webhookUrl,
@@ -371,7 +369,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: notification.appDeploy,
dokployRestart: notification.dokployRestart,
databaseBackup: notification.databaseBackup,
dokployBackup: notification.dokployBackup,
volumeBackup: notification.volumeBackup,
botToken: notification.telegram?.botToken,
messageThreadId: notification.telegram?.messageThreadId || "",
@@ -387,7 +384,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: notification.appDeploy,
dokployRestart: notification.dokployRestart,
databaseBackup: notification.databaseBackup,
dokployBackup: notification.dokployBackup,
volumeBackup: notification.volumeBackup,
type: notification.notificationType,
webhookUrl: notification.discord?.webhookUrl,
@@ -402,7 +398,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: notification.appDeploy,
dokployRestart: notification.dokployRestart,
databaseBackup: notification.databaseBackup,
dokployBackup: notification.dokployBackup,
volumeBackup: notification.volumeBackup,
type: notification.notificationType,
smtpServer: notification.email?.smtpServer,
@@ -421,7 +416,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: notification.appDeploy,
dokployRestart: notification.dokployRestart,
databaseBackup: notification.databaseBackup,
dokployBackup: notification.dokployBackup,
volumeBackup: notification.volumeBackup,
type: notification.notificationType,
apiKey: notification.resend?.apiKey,
@@ -437,7 +431,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: notification.appDeploy,
dokployRestart: notification.dokployRestart,
databaseBackup: notification.databaseBackup,
dokployBackup: notification.dokployBackup,
volumeBackup: notification.volumeBackup,
type: notification.notificationType,
appToken: notification.gotify?.appToken,
@@ -453,7 +446,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: notification.appDeploy,
dokployRestart: notification.dokployRestart,
databaseBackup: notification.databaseBackup,
dokployBackup: notification.dokployBackup,
volumeBackup: notification.volumeBackup,
type: notification.notificationType,
accessToken: notification.ntfy?.accessToken || "",
@@ -470,7 +462,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: notification.appDeploy,
dokployRestart: notification.dokployRestart,
databaseBackup: notification.databaseBackup,
dokployBackup: notification.dokployBackup,
volumeBackup: notification.volumeBackup,
type: notification.notificationType,
webhookUrl: notification.mattermost?.webhookUrl,
@@ -486,7 +477,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: notification.appDeploy,
dokployRestart: notification.dokployRestart,
databaseBackup: notification.databaseBackup,
dokployBackup: notification.dokployBackup,
type: notification.notificationType,
webhookUrl: notification.lark?.webhookUrl,
name: notification.name,
@@ -500,7 +490,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: notification.appDeploy,
dokployRestart: notification.dokployRestart,
databaseBackup: notification.databaseBackup,
dokployBackup: notification.dokployBackup,
volumeBackup: notification.volumeBackup,
type: notification.notificationType,
webhookUrl: notification.teams?.webhookUrl,
@@ -514,7 +503,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: notification.appDeploy,
dokployRestart: notification.dokployRestart,
databaseBackup: notification.databaseBackup,
dokployBackup: notification.dokployBackup,
type: notification.notificationType,
endpoint: notification.custom?.endpoint || "",
headers: notification.custom?.headers
@@ -536,7 +524,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: notification.appDeploy,
dokployRestart: notification.dokployRestart,
databaseBackup: notification.databaseBackup,
dokployBackup: notification.dokployBackup,
volumeBackup: notification.volumeBackup,
type: notification.notificationType,
userKey: notification.pushover?.userKey,
@@ -575,7 +562,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy,
dokployRestart,
databaseBackup,
dokployBackup,
volumeBackup,
dockerCleanup,
serverThreshold,
@@ -587,7 +573,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: appDeploy,
dokployRestart: dokployRestart,
databaseBackup: databaseBackup,
dokployBackup: dokployBackup,
volumeBackup: volumeBackup,
webhookUrl: data.webhookUrl,
channel: data.channel,
@@ -603,7 +588,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: appDeploy,
dokployRestart: dokployRestart,
databaseBackup: databaseBackup,
dokployBackup: dokployBackup,
volumeBackup: volumeBackup,
botToken: data.botToken,
messageThreadId: data.messageThreadId || "",
@@ -620,7 +604,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: appDeploy,
dokployRestart: dokployRestart,
databaseBackup: databaseBackup,
dokployBackup: dokployBackup,
volumeBackup: volumeBackup,
webhookUrl: data.webhookUrl,
decoration: data.decoration,
@@ -636,7 +619,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: appDeploy,
dokployRestart: dokployRestart,
databaseBackup: databaseBackup,
dokployBackup: dokployBackup,
volumeBackup: volumeBackup,
smtpServer: data.smtpServer,
smtpPort: data.smtpPort,
@@ -656,7 +638,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: appDeploy,
dokployRestart: dokployRestart,
databaseBackup: databaseBackup,
dokployBackup: dokployBackup,
volumeBackup: volumeBackup,
apiKey: data.apiKey,
fromAddress: data.fromAddress,
@@ -673,7 +654,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: appDeploy,
dokployRestart: dokployRestart,
databaseBackup: databaseBackup,
dokployBackup: dokployBackup,
volumeBackup: volumeBackup,
serverUrl: data.serverUrl,
appToken: data.appToken,
@@ -690,7 +670,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: appDeploy,
dokployRestart: dokployRestart,
databaseBackup: databaseBackup,
dokployBackup: dokployBackup,
volumeBackup: volumeBackup,
serverUrl: data.serverUrl,
accessToken: data.accessToken || "",
@@ -707,7 +686,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: appDeploy,
dokployRestart: dokployRestart,
databaseBackup: databaseBackup,
dokployBackup: dokployBackup,
volumeBackup: volumeBackup,
webhookUrl: data.webhookUrl,
channel: data.channel || undefined,
@@ -724,7 +702,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: appDeploy,
dokployRestart: dokployRestart,
databaseBackup: databaseBackup,
dokployBackup: dokployBackup,
volumeBackup: volumeBackup,
webhookUrl: data.webhookUrl,
name: data.name,
@@ -739,7 +716,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: appDeploy,
dokployRestart: dokployRestart,
databaseBackup: databaseBackup,
dokployBackup: dokployBackup,
volumeBackup: volumeBackup,
webhookUrl: data.webhookUrl,
name: data.name,
@@ -766,7 +742,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: appDeploy,
dokployRestart: dokployRestart,
databaseBackup: databaseBackup,
dokployBackup: dokployBackup,
volumeBackup: volumeBackup,
endpoint: data.endpoint,
headers: headersRecord,
@@ -786,7 +761,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: appDeploy,
dokployRestart: dokployRestart,
databaseBackup: databaseBackup,
dokployBackup: dokployBackup,
volumeBackup: volumeBackup,
userKey: data.userKey,
apiToken: data.apiToken,
@@ -1882,27 +1856,6 @@ export const HandleNotifications = ({ notificationId }: Props) => {
)}
/>
<FormField
control={form.control}
name="dokployBackup"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
<div className="space-y-0.5">
<FormLabel>Dokploy Backup</FormLabel>
<FormDescription>
Trigger the action when a dokploy backup is created.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="volumeBackup"

View File

@@ -161,7 +161,7 @@ export const SetupServer = ({ serverId, asButton = false }: Props) => {
<ul>
<li>
1. Add the public SSH Key when you create a server in your
preferred provider (Hostinger, Digital Ocean, Hetzner,
preffered provider (Hostinger, Digital Ocean, Hetzner,
etc){" "}
</li>
<li>2. Add The SSH Key to Server Manually</li>

View File

@@ -48,7 +48,7 @@ import { ShowMonitoringModal } from "./show-monitoring-modal";
import { ShowSchedulesModal } from "./show-schedules-modal";
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
import { WelcomeSubscription } from "./welcome-stripe/welcome-subscription";
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
export const ShowServers = () => {
const router = useRouter();
@@ -63,7 +63,7 @@ export const ShowServers = () => {
return (
<div className="w-full">
{query?.success && isCloud && <WelcomeSubscription />}
{query?.success && isCloud && <WelcomeSuscription />}
<Card className="h-full p-2.5 rounded-xl max-w-5xl mx-auto">
<div className="rounded-xl bg-background shadow-md ">
<CardHeader className="">

View File

@@ -51,7 +51,7 @@ export const { useStepper, steps, Scoped } = defineStepper(
{ id: "complete", title: "Complete", description: "Checkout complete" },
);
export const WelcomeSubscription = () => {
export const WelcomeSuscription = () => {
const [showConfetti, setShowConfetti] = useState(false);
const stepper = useStepper();
const [isOpen, setIsOpen] = useState(true);

View File

@@ -26,7 +26,6 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Switch } from "@/components/ui/switch";
import { EnterpriseFeatureLocked } from "@/components/proprietary/enterprise-feature-gate";
import { api, type RouterOutputs } from "@/utils/api";
/** Shape returned by project.allForPermissions (admin only). Used for the permissions UI. */
@@ -171,7 +170,6 @@ const addPermissions = z.object({
accessedProjects: z.array(z.string()).optional(),
accessedEnvironments: z.array(z.string()).optional(),
accessedServices: z.array(z.string()).optional(),
accessedGitProviders: z.array(z.string()).optional(),
canCreateProjects: z.boolean().optional().default(false),
canCreateServices: z.boolean().optional().default(false),
canDeleteProjects: z.boolean().optional().default(false),
@@ -198,15 +196,6 @@ export const AddUserPermissions = ({ userId, role }: Props) => {
const { data: projects } = api.project.allForPermissions.useQuery(undefined, {
enabled: isOpen,
});
const { data: haveValidLicense } =
api.licenseKey.haveValidLicenseKey.useQuery();
const { data: gitProviders } = api.gitProvider.allForPermissions.useQuery(
undefined,
{
enabled: isOpen && !!haveValidLicense,
},
);
const { data, refetch } = api.user.one.useQuery(
{
@@ -225,7 +214,6 @@ export const AddUserPermissions = ({ userId, role }: Props) => {
accessedProjects: [],
accessedEnvironments: [],
accessedServices: [],
accessedGitProviders: [],
canDeleteEnvironments: false,
canCreateProjects: false,
canCreateServices: false,
@@ -247,7 +235,6 @@ export const AddUserPermissions = ({ userId, role }: Props) => {
accessedProjects: data.accessedProjects || [],
accessedEnvironments: data.accessedEnvironments || [],
accessedServices: data.accessedServices || [],
accessedGitProviders: data.accessedGitProviders || [],
canCreateProjects: data.canCreateProjects,
canCreateServices: data.canCreateServices,
canDeleteProjects: data.canDeleteProjects,
@@ -275,7 +262,6 @@ export const AddUserPermissions = ({ userId, role }: Props) => {
accessedProjects: data.accessedProjects || [],
accessedEnvironments: data.accessedEnvironments || [],
accessedServices: data.accessedServices || [],
accessedGitProviders: data.accessedGitProviders || [],
canAccessToDocker: data.canAccessToDocker,
canAccessToAPI: data.canAccessToAPI,
canAccessToSSHKeys: data.canAccessToSSHKeys,
@@ -884,78 +870,6 @@ export const AddUserPermissions = ({ userId, role }: Props) => {
</FormItem>
)}
/>
{haveValidLicense ? (
<FormField
control={form.control}
name="accessedGitProviders"
render={() => (
<FormItem className="md:col-span-2">
<div className="mb-4">
<FormLabel className="text-base">Git Providers</FormLabel>
<FormDescription>
Select the Git Providers that the user can access
</FormDescription>
</div>
{gitProviders?.length === 0 && (
<p className="text-sm text-muted-foreground">
No git providers found
</p>
)}
<div className="grid md:grid-cols-1 gap-2">
{gitProviders?.map((provider) => (
<FormField
key={provider.gitProviderId}
control={form.control}
name="accessedGitProviders"
render={({ field }) => (
<FormItem className="flex flex-row items-center space-x-3 space-y-0 rounded-lg border p-3">
<FormControl>
<Checkbox
checked={field.value?.includes(
provider.gitProviderId,
)}
onCheckedChange={(checked) => {
if (checked) {
field.onChange([
...(field.value || []),
provider.gitProviderId,
]);
} else {
field.onChange(
field.value?.filter(
(v) => v !== provider.gitProviderId,
),
);
}
}}
/>
</FormControl>
<div className="flex items-center gap-2">
<FormLabel className="text-sm cursor-pointer">
{provider.name}
</FormLabel>
<span className="text-xs text-muted-foreground capitalize">
({provider.providerType})
</span>
</div>
</FormItem>
)}
/>
))}
</div>
<FormMessage />
</FormItem>
)}
/>
) : (
<div className="md:col-span-2">
<EnterpriseFeatureLocked
compact
title="Git Provider Assignment"
description="Assign specific Git Providers to users with an Enterprise license."
/>
</div>
)}
<DialogFooter className="flex w-full flex-row justify-end md:col-span-2">
<Button
isLoading={isPending}

View File

@@ -153,7 +153,7 @@ export const ChangeRole = ({ memberId, currentRole, userEmail }: Props) => {
)}
<br />
<em className="text-muted-foreground text-xs">
Note: Owner role is nontransferable.
Note: Owner role is intransferible.
</em>
</FormDescription>
<FormMessage />

View File

@@ -122,7 +122,7 @@ export const ShowUsers = () => {
// Can change role based on hierarchy:
// - Owner: Can change anyone's role (except themselves and other owners)
// - Admin: Can only change member/custom roles (not other admins or owners)
// - Owner role is nontransferable
// - Owner role is intransferible
const canChangeRole =
member.role !== "owner" &&
member.user.id !== session?.user?.id &&

View File

@@ -101,7 +101,7 @@ const BreadcrumbEllipsis = ({
<span className="sr-only">More</span>
</span>
);
BreadcrumbEllipsis.displayName = "BreadcrumbEllipsis";
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
export {
Breadcrumb,

View File

@@ -19,7 +19,7 @@ interface TreeDataItem {
type TreeProps = React.HTMLAttributes<HTMLDivElement> & {
data: TreeDataItem[] | TreeDataItem;
initialSelectedItemId?: string;
initialSlelectedItemId?: string;
onSelectChange?: (item: TreeDataItem | undefined) => void;
expandAll?: boolean;
folderIcon?: LucideIcon;
@@ -30,7 +30,7 @@ const Tree = React.forwardRef<HTMLDivElement, TreeProps>(
(
{
data,
initialSelectedItemId,
initialSlelectedItemId,
onSelectChange,
expandAll,
folderIcon,
@@ -42,7 +42,7 @@ const Tree = React.forwardRef<HTMLDivElement, TreeProps>(
) => {
const [selectedItemId, setSelectedItemId] = React.useState<
string | undefined
>(initialSelectedItemId);
>(initialSlelectedItemId);
const handleSelectChange = React.useCallback(
(item: TreeDataItem | undefined) => {
@@ -55,7 +55,7 @@ const Tree = React.forwardRef<HTMLDivElement, TreeProps>(
);
const expandedItemIds = React.useMemo(() => {
if (!initialSelectedItemId) {
if (!initialSlelectedItemId) {
return [] as string[];
}
@@ -81,9 +81,9 @@ const Tree = React.forwardRef<HTMLDivElement, TreeProps>(
}
}
walkTreeItems(data, initialSelectedItemId);
walkTreeItems(data, initialSlelectedItemId);
return ids;
}, [data, initialSelectedItemId]);
}, [data, initialSlelectedItemId]);
const { ref: refRoot } = useResizeObserver();

View File

@@ -1 +0,0 @@
ALTER TABLE "domain" ADD COLUMN "customEntrypoint" text;

View File

@@ -1 +0,0 @@
ALTER TABLE "destination" ADD COLUMN "additionalFlags" text[];

View File

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

View File

@@ -1,2 +0,0 @@
ALTER TABLE "member" ADD COLUMN "accessedGitProviders" text[] DEFAULT ARRAY[]::text[] NOT NULL;--> statement-breakpoint
ALTER TABLE "git_provider" ADD COLUMN "sharedWithOrganization" boolean DEFAULT false NOT NULL;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1086,27 +1086,6 @@
"when": 1774337356154,
"tag": "0154_careful_eternals",
"breakpoints": true
},
{
"idx": 155,
"version": "7",
"when": 1774794547865,
"tag": "0155_careless_clea",
"breakpoints": true
},
{
"idx": 156,
"version": "7",
"when": 1774910955774,
"tag": "0156_fair_vargas",
"breakpoints": true
},
{
"idx": 157,
"version": "7",
"when": 1775246622387,
"tag": "0157_stiff_misty_knight",
"breakpoints": true
}
]
}

View File

@@ -1,5 +1,5 @@
import { apiKeyClient } from "@better-auth/api-key/client";
import { ssoClient } from "@better-auth/sso/client";
import { apiKeyClient } from "@better-auth/api-key/client";
import {
adminClient,
inferAdditionalFields,

View File

@@ -196,7 +196,7 @@ export default async function handler(
return;
}
const committedPaths = await extractCommittedPaths(
const commitedPaths = await extractCommitedPaths(
req.body,
application.bitbucket,
application.bitbucketRepositorySlug ||
@@ -206,7 +206,7 @@ export default async function handler(
const shouldDeployPaths = shouldDeploy(
application.watchPaths,
committedPaths,
commitedPaths,
);
if (!shouldDeployPaths) {
@@ -538,7 +538,7 @@ export const getProviderByHeader = (headers: any) => {
return null;
};
export const extractCommittedPaths = async (
export const extractCommitedPaths = async (
body: any,
bitbucket: Bitbucket | null,
repository: string,
@@ -548,7 +548,7 @@ export const extractCommittedPaths = async (
const commitHashes = changes
.map((change: any) => change.new?.target?.hash)
.filter(Boolean);
const committedPaths: string[] = [];
const commitedPaths: string[] = [];
const username =
bitbucket?.bitbucketWorkspaceName || bitbucket?.bitbucketUsername || "";
for (const commit of commitHashes) {
@@ -559,7 +559,7 @@ export const extractCommittedPaths = async (
});
const data = await response.json();
for (const value of data.values) {
if (value?.new?.path) committedPaths.push(value.new.path);
if (value?.new?.path) commitedPaths.push(value.new.path);
}
} catch (error) {
console.error(
@@ -571,5 +571,5 @@ export const extractCommittedPaths = async (
}
}
return committedPaths;
return commitedPaths;
};

View File

@@ -8,8 +8,8 @@ import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import {
extractBranchName,
extractCommitedPaths,
extractCommitMessage,
extractCommittedPaths,
extractHash,
getProviderByHeader,
} from "../[refreshToken]";
@@ -97,7 +97,7 @@ export default async function handler(
return;
}
const committedPaths = await extractCommittedPaths(
const commitedPaths = await extractCommitedPaths(
req.body,
composeResult.bitbucket,
composeResult.bitbucketRepositorySlug ||
@@ -107,7 +107,7 @@ export default async function handler(
const shouldDeployPaths = shouldDeploy(
composeResult.watchPaths,
committedPaths,
commitedPaths,
);
if (!shouldDeployPaths) {

View File

@@ -174,27 +174,27 @@ export default async function handler(
case "invoice.payment_succeeded": {
const newInvoice = event.data.object as Stripe.Invoice;
const subscription = await stripe.subscriptions.retrieve(
const suscription = await stripe.subscriptions.retrieve(
newInvoice.subscription as string,
);
if (subscription.status !== "active") {
if (suscription.status !== "active") {
console.log(
`Skipping invoice.payment_succeeded for subscription ${subscription.id} with status ${subscription.status}`,
`Skipping invoice.payment_succeeded for subscription ${suscription.id} with status ${suscription.status}`,
);
break;
}
const serversQuantity = getSubscriptionServersQuantity(
subscription?.items?.data ?? [],
suscription?.items?.data ?? [],
);
await db
.update(user)
.set({ serversQuantity })
.where(eq(user.stripeCustomerId, subscription.customer as string));
.where(eq(user.stripeCustomerId, suscription.customer as string));
const admin = await findUserByStripeCustomerId(
subscription.customer as string,
suscription.customer as string,
);
if (!admin) {

View File

@@ -16,7 +16,7 @@ import { ShowImport } from "@/components/dashboard/application/advanced/import/s
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
import { ShowDeployments } from "@/components/dashboard/application/deployments/show-deployments";
import { ShowDomains } from "@/components/dashboard/application/domains/show-domains";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
import { ShowPatches } from "@/components/dashboard/application/patches/show-patches";
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
import { ShowVolumeBackups } from "@/components/dashboard/application/volume-backups/show-volume-backups";

View File

@@ -10,7 +10,7 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { type ReactElement, useState } from "react";
import superjson from "superjson";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";

View File

@@ -10,7 +10,7 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { type ReactElement, useState } from "react";
import superjson from "superjson";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";

View File

@@ -10,7 +10,7 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { type ReactElement, useState } from "react";
import superjson from "superjson";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";

View File

@@ -10,7 +10,7 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { type ReactElement, useState } from "react";
import superjson from "superjson";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";

View File

@@ -10,7 +10,7 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { type ReactElement, useState } from "react";
import superjson from "superjson";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";

View File

@@ -10,7 +10,7 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { type ReactElement, useState } from "react";
import superjson from "superjson";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";

View File

@@ -3,10 +3,10 @@ import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import type { ReactElement } from "react";
import superjson from "superjson";
import { ShowInvitations } from "@/components/dashboard/settings/users/show-invitations";
import { ShowUsers } from "@/components/dashboard/settings/users/show-users";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { ManageCustomRoles } from "@/components/proprietary/roles/manage-custom-roles";
import { ShowInvitations } from "@/components/dashboard/settings/users/show-invitations";
import { ShowUsers } from "@/components/dashboard/settings/users/show-users";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";

View File

@@ -17,11 +17,11 @@ import {
suggestVariants,
} from "@dokploy/server/services/ai";
import { createComposeByTemplate } from "@dokploy/server/services/compose";
import { findProjectById } from "@dokploy/server/services/project";
import {
addNewService,
checkServiceAccess,
} from "@dokploy/server/services/permission";
import { findProjectById } from "@dokploy/server/services/project";
import {
getProviderHeaders,
getProviderName,

View File

@@ -28,7 +28,6 @@ import {
updateBackupById,
} from "@dokploy/server";
import { findDestinationById } from "@dokploy/server/services/destination";
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
import { runComposeBackup } from "@dokploy/server/utils/backups/compose";
import {
getS3Credentials,
@@ -54,6 +53,7 @@ import {
protectedProcedure,
withPermission,
} from "@/server/api/trpc";
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
import { audit } from "@/server/api/utils/audit";
import {
apiCreateBackup,
@@ -545,42 +545,52 @@ export const backupRouter = createTRPCRouter({
}
const destination = await findDestinationById(input.destinationId);
const queue: string[] = [];
let done = false;
const onLog = (log: string) => queue.push(log);
const runRestore = async () => {
if (input.backupType === "database") {
if (input.databaseType === "postgres") {
const postgres = await findPostgresById(input.databaseId);
await restorePostgresBackup(postgres, destination, input, onLog);
} else if (input.databaseType === "mysql") {
const mysql = await findMySqlById(input.databaseId);
await restoreMySqlBackup(mysql, destination, input, onLog);
} else if (input.databaseType === "mariadb") {
const mariadb = await findMariadbById(input.databaseId);
await restoreMariadbBackup(mariadb, destination, input, onLog);
} else if (input.databaseType === "mongo") {
const mongo = await findMongoById(input.databaseId);
await restoreMongoBackup(mongo, destination, input, onLog);
} else if (input.databaseType === "libsql") {
const libsql = await findLibsqlById(input.databaseId);
await restoreLibsqlBackup(libsql, destination, input, onLog);
} else if (input.databaseType === "web-server") {
await restoreWebServerBackup(destination, input.backupFile, onLog);
}
} else if (input.backupType === "compose") {
const compose = await findComposeById(input.databaseId);
await restoreComposeBackup(compose, destination, input, onLog);
const done = false;
if (input.backupType === "database") {
if (input.databaseType === "postgres") {
const postgres = await findPostgresById(input.databaseId);
restorePostgresBackup(postgres, destination, input, (log) => {
queue.push(log);
});
}
};
runRestore()
.catch((error) => {
onLog(
`Error: ${error instanceof Error ? error.message : String(error)}`,
);
})
.finally(() => {
done = true;
if (input.databaseType === "mysql") {
const mysql = await findMySqlById(input.databaseId);
restoreMySqlBackup(mysql, destination, input, (log) => {
queue.push(log);
});
}
if (input.databaseType === "mariadb") {
const mariadb = await findMariadbById(input.databaseId);
restoreMariadbBackup(mariadb, destination, input, (log) => {
queue.push(log);
});
}
if (input.databaseType === "mongo") {
const mongo = await findMongoById(input.databaseId);
restoreMongoBackup(mongo, destination, input, (log) => {
queue.push(log);
});
}
if (input.databaseType === "libsql") {
const libsql = await findLibsqlById(input.databaseId);
restoreLibsqlBackup(libsql, destination, input, (log) => {
queue.push(log);
});
}
if (input.databaseType === "web-server") {
restoreWebServerBackup(destination, input.backupFile, (log) => {
queue.push(log);
});
}
}
if (input.backupType === "compose") {
const compose = await findComposeById(input.databaseId);
restoreComposeBackup(compose, destination, input, (log) => {
queue.push(log);
});
}
while (!done || queue.length > 0) {
if (queue.length > 0) {
yield queue.shift()!;

View File

@@ -1,7 +1,6 @@
import {
createBitbucket,
findBitbucketById,
getAccessibleGitProviderIds,
getBitbucketBranches,
getBitbucketRepositories,
testBitbucketConnection,
@@ -55,8 +54,6 @@ export const bitbucketRouter = createTRPCRouter({
return await findBitbucketById(input.bitbucketId);
}),
bitbucketProviders: protectedProcedure.query(async ({ ctx }) => {
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
let result = await db.query.bitbucket.findMany({
with: {
gitProvider: true,
@@ -70,7 +67,7 @@ export const bitbucketRouter = createTRPCRouter({
return (
provider.gitProvider.organizationId ===
ctx.session.activeOrganizationId &&
accessibleIds.has(provider.gitProvider.gitProviderId)
provider.gitProvider.userId === ctx.session.userId
);
});
return result;

View File

@@ -7,8 +7,8 @@ import {
import { db } from "@dokploy/server/db";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { createTRPCRouter, withPermission } from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import { createTRPCRouter, withPermission } from "@/server/api/trpc";
import {
apiCreateCertificate,
apiFindCertificate,

View File

@@ -31,13 +31,13 @@ import {
updateCompose,
updateDeploymentStatus,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import {
addNewService,
checkServiceAccess,
checkServicePermissionAndAccess,
findMemberByUserId,
} from "@dokploy/server/services/permission";
import { db } from "@dokploy/server/db";
import {
type CompleteTemplate,
fetchTemplateFiles,
@@ -75,8 +75,8 @@ import {
} from "@/server/queues/queueSetup";
import { cancelDeployment, deploy } from "@/server/utils/deploy";
import { generatePassword } from "@/templates/utils";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
import { audit } from "../utils/audit";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
export const composeRouter = createTRPCRouter({
create: protectedProcedure

View File

@@ -1,5 +1,5 @@
import {
createDestination,
createDestintation,
execAsync,
execAsyncRemote,
findDestinationById,
@@ -25,7 +25,7 @@ export const destinationRouter = createTRPCRouter({
.input(apiCreateDestination)
.mutation(async ({ input, ctx }) => {
try {
const result = await createDestination(
const result = await createDestintation(
input,
ctx.session.activeOrganizationId,
);
@@ -47,15 +47,8 @@ export const destinationRouter = createTRPCRouter({
testConnection: withPermission("destination", "create")
.input(apiCreateDestination)
.mutation(async ({ input }) => {
const {
secretAccessKey,
bucket,
region,
endpoint,
accessKey,
provider,
additionalFlags,
} = input;
const { secretAccessKey, bucket, region, endpoint, accessKey, provider } =
input;
try {
const rcloneFlags = [
`--s3-access-key-id="${accessKey}"`,
@@ -72,9 +65,6 @@ export const destinationRouter = createTRPCRouter({
if (provider) {
rcloneFlags.unshift(`--s3-provider="${provider}"`);
}
if (additionalFlags?.length) {
rcloneFlags.push(...additionalFlags);
}
const rcloneDestination = `:s3:${bucket}`;
const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
@@ -169,14 +159,7 @@ export const destinationRouter = createTRPCRouter({
});
return result;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message:
error instanceof Error
? error?.message
: "Error connecting to bucket",
cause: error,
});
throw error;
}
}),
});

View File

@@ -1,34 +1,18 @@
import {
findGitProviderById,
getAccessibleGitProviderIds,
removeGitProvider,
updateGitProvider,
} from "@dokploy/server";
import { findGitProviderById, removeGitProvider } from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
import { TRPCError } from "@trpc/server";
import { desc, eq, inArray } from "drizzle-orm";
import { and, desc, eq } from "drizzle-orm";
import { audit } from "@/server/api/utils/audit";
import {
createTRPCRouter,
protectedProcedure,
withPermission,
} from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import {
apiRemoveGitProvider,
apiToggleShareGitProvider,
gitProvider,
} from "@/server/db/schema";
import { apiRemoveGitProvider, gitProvider } from "@/server/db/schema";
export const gitProviderRouter = createTRPCRouter({
getAll: protectedProcedure.query(async ({ ctx }) => {
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
if (accessibleIds.size === 0) {
return [];
}
const results = await db.query.gitProvider.findMany({
return await db.query.gitProvider.findMany({
with: {
gitlab: true,
bitbucket: true,
@@ -36,65 +20,12 @@ export const gitProviderRouter = createTRPCRouter({
gitea: true,
},
orderBy: desc(gitProvider.createdAt),
where: inArray(gitProvider.gitProviderId, [...accessibleIds]),
where: and(
eq(gitProvider.userId, ctx.session.userId),
eq(gitProvider.organizationId, ctx.session.activeOrganizationId),
),
});
return results.map((r) => ({
...r,
isOwner: r.userId === ctx.session.userId,
}));
}),
toggleShare: protectedProcedure
.input(apiToggleShareGitProvider)
.mutation(async ({ input, ctx }) => {
const provider = await findGitProviderById(input.gitProviderId);
if (
provider.userId !== ctx.session.userId ||
provider.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Only the owner can share this provider",
});
}
await audit(ctx, {
action: "update",
resourceType: "gitProvider",
resourceId: provider.gitProviderId,
resourceName: provider.name ?? provider.gitProviderId,
});
return await updateGitProvider(input.gitProviderId, {
sharedWithOrganization: input.sharedWithOrganization,
});
}),
allForPermissions: withPermission("member", "update")
.use(async ({ ctx, next }) => {
const licensed = await hasValidLicense(ctx.session.activeOrganizationId);
if (!licensed) {
throw new TRPCError({
code: "FORBIDDEN",
message: "Valid enterprise license required",
});
}
return next();
})
.query(async ({ ctx }) => {
return await db.query.gitProvider.findMany({
columns: {
gitProviderId: true,
name: true,
providerType: true,
},
orderBy: desc(gitProvider.createdAt),
where: eq(gitProvider.organizationId, ctx.session.activeOrganizationId),
});
}),
remove: withPermission("gitProviders", "delete")
.input(apiRemoveGitProvider)
.mutation(async ({ input, ctx }) => {

View File

@@ -1,7 +1,6 @@
import {
createGitea,
findGiteaById,
getAccessibleGitProviderIds,
getGiteaBranches,
getGiteaRepositories,
haveGiteaRequirements,
@@ -58,8 +57,6 @@ export const giteaRouter = createTRPCRouter({
}),
giteaProviders: protectedProcedure.query(async ({ ctx }) => {
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
let result = await db.query.gitea.findMany({
with: {
gitProvider: true,
@@ -70,7 +67,7 @@ export const giteaRouter = createTRPCRouter({
(provider) =>
provider.gitProvider.organizationId ===
ctx.session.activeOrganizationId &&
accessibleIds.has(provider.gitProvider.gitProviderId),
provider.gitProvider.userId === ctx.session.userId,
);
const filtered = result

View File

@@ -1,6 +1,5 @@
import {
findGithubById,
getAccessibleGitProviderIds,
getGithubBranches,
getGithubRepositories,
haveGithubRequirements,
@@ -36,8 +35,6 @@ export const githubRouter = createTRPCRouter({
return await getGithubBranches(input);
}),
githubProviders: protectedProcedure.query(async ({ ctx }) => {
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
let result = await db.query.github.findMany({
with: {
gitProvider: true,
@@ -48,7 +45,7 @@ export const githubRouter = createTRPCRouter({
(provider) =>
provider.gitProvider.organizationId ===
ctx.session.activeOrganizationId &&
accessibleIds.has(provider.gitProvider.gitProviderId),
provider.gitProvider.userId === ctx.session.userId,
);
const filtered = result

View File

@@ -1,7 +1,6 @@
import {
createGitlab,
findGitlabById,
getAccessibleGitProviderIds,
getGitlabBranches,
getGitlabRepositories,
haveGitlabRequirements,
@@ -55,8 +54,6 @@ export const gitlabRouter = createTRPCRouter({
return await findGitlabById(input.gitlabId);
}),
gitlabProviders: protectedProcedure.query(async ({ ctx }) => {
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
let result = await db.query.gitlab.findMany({
with: {
gitProvider: true,
@@ -67,7 +64,7 @@ export const gitlabRouter = createTRPCRouter({
return (
provider.gitProvider.organizationId ===
ctx.session.activeOrganizationId &&
accessibleIds.has(provider.gitProvider.gitProviderId)
provider.gitProvider.userId === ctx.session.userId
);
});
const filtered = result

View File

@@ -246,15 +246,11 @@ export const libsqlRouter = createTRPCRouter({
deployment: ["create"],
});
const queue: string[] = [];
let done = false;
const done = false;
deployLibsql(input.libsqlId, (log) => {
queue.push(log);
})
.catch(() => {})
.finally(() => {
done = true;
});
});
while (!done || queue.length > 0) {
if (queue.length > 0) {

View File

@@ -17,18 +17,18 @@ import {
stopServiceRemote,
updateMongoById,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import {
addNewService,
checkServiceAccess,
checkServicePermissionAndAccess,
findMemberByUserId,
} from "@dokploy/server/services/permission";
import { db } from "@dokploy/server/db";
import { TRPCError } from "@trpc/server";
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
apiChangeMongoStatus,
apiCreateMongo,
@@ -39,10 +39,9 @@ import {
apiSaveEnvironmentVariablesMongo,
apiSaveExternalPortMongo,
apiUpdateMongo,
environments,
mongo as mongoTable,
projects,
} from "@/server/db/schema";
import { environments, projects } from "@/server/db/schema";
import { cancelJobs } from "@/server/utils/backup";
export const mongoRouter = createTRPCRouter({
create: protectedProcedure
@@ -229,15 +228,11 @@ export const mongoRouter = createTRPCRouter({
deployment: ["create"],
});
const queue: string[] = [];
let done = false;
const done = false;
deployMongo(input.mongoId, (log) => {
queue.push(log);
})
.catch(() => {})
.finally(() => {
done = true;
});
});
while (!done || queue.length > 0) {
if (queue.length > 0) {

View File

@@ -17,18 +17,18 @@ import {
stopServiceRemote,
updateMySqlById,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import {
addNewService,
checkServiceAccess,
checkServicePermissionAndAccess,
findMemberByUserId,
} from "@dokploy/server/services/permission";
import { db } from "@dokploy/server/db";
import { TRPCError } from "@trpc/server";
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
apiChangeMySqlStatus,
apiCreateMySql,
@@ -230,15 +230,11 @@ export const mysqlRouter = createTRPCRouter({
});
const queue: string[] = [];
let done = false;
const done = false;
deployMySql(input.mysqlId, (log) => {
queue.push(log);
})
.catch(() => {})
.finally(() => {
done = true;
});
});
while (!done || queue.length > 0) {
if (queue.length > 0) {

View File

@@ -1,10 +1,10 @@
import { db } from "@dokploy/server/db";
import { IS_CLOUD } from "@dokploy/server/index";
import { audit } from "@/server/api/utils/audit";
import { TRPCError } from "@trpc/server";
import { and, desc, eq, exists } from "drizzle-orm";
import { nanoid } from "nanoid";
import { z } from "zod";
import { audit } from "@/server/api/utils/audit";
import {
invitation,
member,
@@ -409,11 +409,11 @@ export const organizationRouter = createTRPCRouter({
});
}
// Owner role is nontransferable - cannot change to or from owner
// Owner role is intransferible - cannot change to or from owner
if (target.role === "owner" || input.role === "owner") {
throw new TRPCError({
code: "FORBIDDEN",
message: "The owner role is nontransferable",
message: "The owner role is intransferible",
});
}

View File

@@ -18,18 +18,18 @@ import {
stopServiceRemote,
updatePostgresById,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import {
addNewService,
checkServiceAccess,
checkServicePermissionAndAccess,
findMemberByUserId,
} from "@dokploy/server/services/permission";
import { db } from "@dokploy/server/db";
import { TRPCError } from "@trpc/server";
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
apiChangePostgresStatus,
apiCreatePostgres,
@@ -40,10 +40,9 @@ import {
apiSaveEnvironmentVariablesPostgres,
apiSaveExternalPortPostgres,
apiUpdatePostgres,
environments,
postgres as postgresTable,
projects,
} from "@/server/db/schema";
import { environments, projects } from "@/server/db/schema";
import { cancelJobs } from "@/server/utils/backup";
export const postgresRouter = createTRPCRouter({
@@ -234,15 +233,11 @@ export const postgresRouter = createTRPCRouter({
});
const queue: string[] = [];
let done = false;
const done = false;
deployPostgres(input.postgresId, (log) => {
queue.push(log);
})
.catch(() => {})
.finally(() => {
done = true;
});
});
while (!done || queue.length > 0) {
if (queue.length > 0) {

View File

@@ -143,12 +143,7 @@ export const licenseKeyRouter = createTRPCRouter({
});
}
try {
await deactivateLicenseKey(currentUser.licenseKey);
} catch (err) {
console.error("Failed to deactivate license key remotely:", err);
}
await deactivateLicenseKey(currentUser.licenseKey);
await db
.update(user)
.set({

View File

@@ -16,18 +16,18 @@ import {
stopServiceRemote,
updateRedisById,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import {
addNewService,
checkServiceAccess,
checkServicePermissionAndAccess,
findMemberByUserId,
} from "@dokploy/server/services/permission";
import { db } from "@dokploy/server/db";
import { TRPCError } from "@trpc/server";
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import {
apiChangeRedisStatus,
apiCreateRedis,
@@ -38,10 +38,9 @@ import {
apiSaveEnvironmentVariablesRedis,
apiSaveExternalPortRedis,
apiUpdateRedis,
environments,
projects,
redis as redisTable,
} from "@/server/db/schema";
import { environments, projects } from "@/server/db/schema";
export const redisRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateRedis)
@@ -252,15 +251,11 @@ export const redisRouter = createTRPCRouter({
deployment: ["create"],
});
const queue: string[] = [];
let done = false;
const done = false;
deployRedis(input.redisId, (log) => {
queue.push(log);
})
.catch(() => {})
.finally(() => {
done = true;
});
});
while (!done || queue.length > 0) {
if (queue.length > 0) {

View File

@@ -10,7 +10,6 @@ import {
import { db } from "@dokploy/server/db";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { audit } from "@/server/api/utils/audit";
import {
apiCreateRegistry,
apiFindOneRegistry,
@@ -20,6 +19,7 @@ import {
apiUpdateRegistry,
registry,
} from "@/server/db/schema";
import { audit } from "@/server/api/utils/audit";
import { createTRPCRouter, withPermission } from "../trpc";
export const registryRouter = createTRPCRouter({
create: withPermission("registry", "create")

View File

@@ -7,7 +7,6 @@ import {
updateScheduleSchema,
} from "@dokploy/server/db/schema/schedule";
import { runCommand } from "@dokploy/server/index";
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
import {
createSchedule,
deleteSchedule,
@@ -15,10 +14,11 @@ import {
updateSchedule,
} from "@dokploy/server/services/schedule";
import { TRPCError } from "@trpc/server";
import { asc, desc, eq } from "drizzle-orm";
import { desc, eq } from "drizzle-orm";
import { z } from "zod";
import { audit } from "@/server/api/utils/audit";
import { removeJob, schedule } from "@/server/utils/backup";
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const scheduleRouter = createTRPCRouter({
create: protectedProcedure
@@ -157,7 +157,6 @@ export const scheduleRouter = createTRPCRouter({
};
return db.query.schedules.findMany({
where: where[input.scheduleType],
orderBy: [asc(schedules.createdAt)],
with: {
application: true,
server: true,

View File

@@ -21,12 +21,12 @@ import { observable } from "@trpc/server/observable";
import { and, desc, eq, getTableColumns, isNotNull, sql } from "drizzle-orm";
import { z } from "zod";
import { updateServersBasedOnQuantity } from "@/pages/api/stripe/webhook";
import { audit } from "@/server/api/utils/audit";
import {
createTRPCRouter,
protectedProcedure,
withPermission,
} from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import {
apiCreateServer,
apiFindOneServer,

View File

@@ -8,8 +8,8 @@ import {
import { db } from "@dokploy/server/db";
import { TRPCError } from "@trpc/server";
import { desc, eq } from "drizzle-orm";
import { createTRPCRouter, withPermission } from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import { createTRPCRouter, withPermission } from "@/server/api/trpc";
import {
apiCreateSshKey,
apiFindOneSshKey,

View File

@@ -13,7 +13,6 @@ import {
updateUser,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
import {
account,
apiAssignPermissions,
@@ -345,19 +344,12 @@ export const userRouter = createTRPCRouter({
});
}
const { id, accessedGitProviders, ...rest } = input;
const licensed = await hasValidLicense(
ctx.session?.activeOrganizationId || "",
);
const { id, ...rest } = input;
await db
.update(member)
.set({
...rest,
...(licensed && accessedGitProviders !== undefined
? { accessedGitProviders }
: {}),
})
.where(
and(

View File

@@ -106,7 +106,7 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => {
* 2. INITIALIZATION
*
* This is where the tRPC API is initialized, connecting the context and transformer. We also parse
* ZodErrors so that you get type safety on the frontend if your procedure fails due to validation
* ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation
* errors on the backend.
*/

View File

@@ -1,5 +1,5 @@
import type { AuditAction, AuditResourceType } from "@dokploy/server/db/schema";
import { createAuditLog } from "@dokploy/server/services/proprietary/audit-log";
import type { AuditAction, AuditResourceType } from "@dokploy/server/db/schema";
interface AuditCtx {
user: { id: string; email: string; role: string };

View File

@@ -1,11 +1,11 @@
import { relations } from "drizzle-orm";
import {
boolean,
index,
integer,
pgTable,
text,
timestamp,
boolean,
integer,
index,
uniqueIndex,
} from "drizzle-orm/pg-core";

View File

@@ -163,10 +163,6 @@ export const member = pgTable("member", {
.array()
.notNull()
.default(sql`ARRAY[]::text[]`),
accessedGitProviders: text("accessedGitProviders")
.array()
.notNull()
.default(sql`ARRAY[]::text[]`),
});
export const memberRelations = relations(member, ({ one }) => ({

View File

@@ -3,10 +3,6 @@ import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import {
ADDITIONAL_FLAG_ERROR,
ADDITIONAL_FLAG_REGEX,
} from "../validations/destination";
import { organization } from "./account";
import { backups } from "./backups";
@@ -22,7 +18,6 @@ export const destinations = pgTable("destination", {
bucket: text("bucket").notNull(),
region: text("region").notNull(),
endpoint: text("endpoint").notNull(),
additionalFlags: text("additionalFlags").array(),
organizationId: text("organizationId")
.notNull()
.references(() => organization.id, { onDelete: "cascade" }),
@@ -49,9 +44,6 @@ const createSchema = createInsertSchema(destinations, {
endpoint: z.string(),
secretAccessKey: z.string(),
region: z.string(),
additionalFlags: z
.array(z.string().regex(ADDITIONAL_FLAG_REGEX, ADDITIONAL_FLAG_ERROR))
.default([]),
});
export const apiCreateDestination = createSchema
@@ -63,7 +55,6 @@ export const apiCreateDestination = createSchema
region: true,
endpoint: true,
secretAccessKey: true,
additionalFlags: true,
})
.required()
.extend({
@@ -90,7 +81,6 @@ export const apiUpdateDestination = createSchema
secretAccessKey: true,
destinationId: true,
provider: true,
additionalFlags: true,
})
.required()
.extend({

View File

@@ -31,7 +31,6 @@ export const domains = pgTable("domain", {
host: text("host").notNull(),
https: boolean("https").notNull().default(false),
port: integer("port").default(3000),
customEntrypoint: text("customEntrypoint"),
path: text("path").default("/"),
serviceName: text("serviceName"),
domainType: domainType("domainType").default("application"),
@@ -81,7 +80,6 @@ export const apiCreateDomain = createSchema.pick({
host: true,
path: true,
port: true,
customEntrypoint: true,
https: true,
applicationId: true,
certificateType: true,
@@ -115,7 +113,6 @@ export const apiUpdateDomain = createSchema
host: true,
path: true,
port: true,
customEntrypoint: true,
https: true,
certificateType: true,
customCertResolver: true,

View File

@@ -1,5 +1,5 @@
import { relations } from "drizzle-orm";
import { boolean, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
import { pgEnum, pgTable, text } from "drizzle-orm/pg-core";
import { nanoid } from "nanoid";
import { z } from "zod";
import { organization } from "./account";
@@ -32,9 +32,6 @@ export const gitProvider = pgTable("git_provider", {
userId: text("userId")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
sharedWithOrganization: boolean("sharedWithOrganization")
.notNull()
.default(false),
});
export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
@@ -67,8 +64,3 @@ export const gitProviderRelations = relations(gitProvider, ({ one }) => ({
export const apiRemoveGitProvider = z.object({
gitProviderId: z.string().min(1),
});
export const apiToggleShareGitProvider = z.object({
gitProviderId: z.string().min(1),
sharedWithOrganization: z.boolean(),
});

View File

@@ -1,7 +1,7 @@
export * from "./account";
export * from "./ai";
export * from "./application";
export * from "./audit-log";
export * from "./application";
export * from "./backups";
export * from "./bitbucket";
export * from "./certificate";

View File

@@ -179,9 +179,11 @@ export const apiCreateLibsql = createSchema
}
});
export const apiFindOneLibsql = z.object({
libsqlId: z.string().min(1),
});
export const apiFindOneLibsql = createSchema
.pick({
libsqlId: true,
})
.required();
export const apiChangeLibsqlStatus = createSchema
.pick({

View File

@@ -149,13 +149,14 @@ export const apiRemoveMount = createSchema
// })
.required();
export const apiFindMountByApplicationId = z.object({
serviceType: z
.string()
.min(1)
.transform((val) => val as ServiceType),
serviceId: z.string().min(1),
});
export const apiFindMountByApplicationId = createSchema
.pick({
serviceType: true,
})
.required()
.extend({
serviceId: z.string().min(1),
});
export const apiUpdateMount = createSchema.partial().extend({
mountId: z.string().min(1),

View File

@@ -38,7 +38,6 @@ export const notifications = pgTable("notification", {
databaseBackup: boolean("databaseBackup").notNull().default(false),
volumeBackup: boolean("volumeBackup").notNull().default(false),
dokployRestart: boolean("dokployRestart").notNull().default(false),
dokployBackup: boolean("dokployBackup").notNull().default(false),
dockerCleanup: boolean("dockerCleanup").notNull().default(false),
serverThreshold: boolean("serverThreshold").notNull().default(false),
notificationType: notificationType("notificationType").notNull(),
@@ -267,7 +266,6 @@ export const apiCreateSlack = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -296,7 +294,6 @@ export const apiCreateTelegram = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -327,7 +324,6 @@ export const apiCreateDiscord = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -359,7 +355,6 @@ export const apiCreateEmail = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -396,7 +391,6 @@ export const apiCreateResend = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -427,7 +421,6 @@ export const apiCreateGotify = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -462,7 +455,6 @@ export const apiCreateNtfy = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -494,7 +486,6 @@ export const apiCreateMattermost = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -512,7 +503,6 @@ export const apiCreateMattermost = notificationsSchema
webhookUrl: true,
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
appDeploy: true,
@@ -545,7 +535,6 @@ export const apiCreateCustom = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -573,7 +562,6 @@ export const apiCreateLark = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -600,7 +588,6 @@ export const apiCreateTeams = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -627,7 +614,6 @@ export const apiCreatePushover = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -662,7 +648,6 @@ export const apiUpdatePushover = z.object({
expire: z.number().min(1).max(10800).nullish(),
appBuildError: z.boolean().optional(),
databaseBackup: z.boolean().optional(),
dokployBackup: z.boolean().optional(),
volumeBackup: z.boolean().optional(),
dokployRestart: z.boolean().optional(),
name: z.string().optional(),

View File

@@ -126,7 +126,6 @@ export const apiAssignPermissions = createSchema
accessedProjects: z.array(z.string()).optional(),
accessedEnvironments: z.array(z.string()).optional(),
accessedServices: z.array(z.string()).optional(),
accessedGitProviders: z.array(z.string()).optional(),
canCreateProjects: z.boolean().optional(),
canCreateServices: z.boolean().optional(),
canDeleteProjects: z.boolean().optional(),

View File

@@ -1,5 +1,6 @@
import { relations } from "drizzle-orm";
import { boolean, integer, pgTable, text } from "drizzle-orm/pg-core";
import { serviceType } from "./mount";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
@@ -10,7 +11,6 @@ import { destinations } from "./destination";
import { libsql } from "./libsql";
import { mariadb } from "./mariadb";
import { mongo } from "./mongo";
import { serviceType } from "./mount";
import { mysql } from "./mysql";
import { postgres } from "./postgres";
import { redis } from "./redis";

View File

@@ -1,3 +0,0 @@
export const ADDITIONAL_FLAG_REGEX = /^--[a-zA-Z0-9-]+(=[a-zA-Z0-9._:/@-]+)?$/;
export const ADDITIONAL_FLAG_ERROR =
"Invalid flag format. Must start with -- (e.g. --s3-sign-accept-encoding=false)";

Some files were not shown because too many files have changed in this diff Show More