Merge branch 'canary' into feature/custom-entrypoint

This commit is contained in:
Mauricio Siu
2026-04-03 15:45:59 -06:00
108 changed files with 18110 additions and 320 deletions
@@ -292,7 +292,7 @@ networks:
dokploy-network:
`;
test("It shoudn't add suffix to dokploy-network", () => {
test("It shouldn't add suffix to dokploy-network", () => {
const composeData = parse(composeFile7) as ComposeSpecification;
const suffix = generateRandomHash();
@@ -195,7 +195,7 @@ services:
- dokploy-network
`;
test("It shoudn't add suffix to dokploy-network in services", () => {
test("It shouldn'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 shoudn't add suffix to dokploy-network in services multiples cases", () => {
test("It shouldn't add suffix to dokploy-network in services multiples cases", () => {
const composeData = parse(composeFile8) as ComposeSpecification;
const suffix = generateRandomHash();
+10 -10
View File
@@ -1,4 +1,4 @@
import { getEnviromentVariablesObject } from "@dokploy/server/index";
import { getEnvironmentVariablesObject } 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("getEnviromentVariablesObject with environment variables (Stack compose)", () => {
describe("getEnvironmentVariablesObject 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 = getEnviromentVariablesObject(
const result = getEnvironmentVariablesObject(
serviceEnv,
projectEnv,
environmentEnv,
@@ -45,7 +45,7 @@ DATABASE_URL=\${{project.DATABASE_URL}}
SERVICE_PORT=4000
`;
const result = getEnviromentVariablesObject(
const result = getEnvironmentVariablesObject(
serviceEnv,
projectEnv,
environmentEnv,
@@ -72,7 +72,7 @@ PASSWORD=secret123
DATABASE_URL=postgresql://\${{environment.USERNAME}}:\${{environment.PASSWORD}}@\${{environment.HOST}}:\${{environment.PORT}}/mydb
`;
const result = getEnviromentVariablesObject(serviceEnv, "", multiRefEnv);
const result = getEnvironmentVariablesObject(serviceEnv, "", multiRefEnv);
expect(result).toEqual({
DATABASE_URL: "postgresql://postgres:secret123@localhost:5432/mydb",
@@ -85,7 +85,7 @@ UNDEFINED_VAR=\${{environment.UNDEFINED_VAR}}
`;
expect(() =>
getEnviromentVariablesObject(serviceWithUndefined, "", environmentEnv),
getEnvironmentVariablesObject(serviceWithUndefined, "", environmentEnv),
).toThrow("Invalid environment variable: environment.UNDEFINED_VAR");
});
@@ -95,7 +95,7 @@ NODE_ENV=production
API_URL=\${{environment.API_URL}}
`;
const result = getEnviromentVariablesObject(
const result = getEnvironmentVariablesObject(
serviceOverrideEnv,
"",
environmentEnv,
@@ -115,7 +115,7 @@ SERVICE_NAME=my-service
COMPLEX_VAR=\${{SERVICE_NAME}}-\${{environment.NODE_ENV}}-\${{project.ENVIRONMENT}}
`;
const result = getEnviromentVariablesObject(
const result = getEnvironmentVariablesObject(
complexServiceEnv,
projectEnv,
environmentEnv,
@@ -150,7 +150,7 @@ ENV_VAR=\${{environment.API_URL}}
DB_NAME=\${{environment.DATABASE_NAME}}
`;
const result = getEnviromentVariablesObject(
const result = getEnvironmentVariablesObject(
serviceWithConflicts,
conflictingProjectEnv,
conflictingEnvironmentEnv,
@@ -170,7 +170,7 @@ SERVICE_VAR=test
PROJECT_VAR=\${{project.ENVIRONMENT}}
`;
const result = getEnviromentVariablesObject(
const result = getEnvironmentVariablesObject(
serviceWithEmpty,
projectEnv,
"",
@@ -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",
@@ -40,12 +40,12 @@ interface Props {
type: "application" | "mariadb" | "mongo" | "mysql" | "postgres" | "redis";
}
const AddRedirectchema = z.object({
const AddRedirectSchema = z.object({
replicas: z.number().min(1, "Replicas must be at least 1"),
registryId: z.string().optional(),
});
type AddCommand = z.infer<typeof AddRedirectchema>;
type AddCommand = z.infer<typeof AddRedirectSchema>;
export const ShowClusterSettings = ({ id, type }: Props) => {
const queryMap = {
@@ -87,7 +87,7 @@ export const ShowClusterSettings = ({ id, type }: Props) => {
: {}),
replicas: data?.replicas || 1,
},
resolver: zodResolver(AddRedirectchema),
resolver: zodResolver(AddRedirectSchema),
});
useEffect(() => {
@@ -37,13 +37,13 @@ import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
const AddRedirectchema = z.object({
const AddRedirectSchema = 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 AddRedirectchema>;
type AddRedirect = z.infer<typeof AddRedirectSchema>;
// Default presets
const redirectPresets = [
@@ -110,7 +110,7 @@ export const HandleRedirect = ({
regex: "",
replacement: "",
},
resolver: zodResolver(AddRedirectchema),
resolver: zodResolver(AddRedirectSchema),
});
useEffect(() => {
@@ -149,7 +149,7 @@ export const HandleRedirect = ({
const onDialogToggle = (open: boolean) => {
setIsOpen(open);
// commented for the moment because not reseting the form if accidentally closed the dialog can be considered as a feature instead of a bug
// commented for the moment because not resetting the form if accidentally closed the dialog can be considered as a feature instead of a bug
// setPresetSelected("");
// form.reset();
};
@@ -1,3 +1,4 @@
import copy from "copy-to-clipboard";
import {
ChevronDown,
ChevronUp,
@@ -11,7 +12,6 @@ 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";
@@ -91,7 +91,7 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
}, [option, services, containers]);
const isLoading = option === "native" ? containersLoading : servicesLoading;
const containersLenght =
const containersLength =
option === "native" ? containers?.length : services?.length;
return (
@@ -167,7 +167,7 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
</>
)}
<SelectLabel>Containers ({containersLenght})</SelectLabel>
<SelectLabel>Containers ({containersLength})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
@@ -1,2 +1,2 @@
export * from "./show-patches";
export * from "./patch-editor";
export * from "./show-patches";
@@ -483,7 +483,7 @@ export const HandleVolumeBackups = ({
</SelectContent>
</Select>
<FormDescription>
Choose the volume to backup, if you dont see the
Choose the volume to backup. If you do not 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 dont see the volume
Choose the volume to backup. If you do not see the volume
here, you can type the volume name manually
</FormDescription>
<FormMessage />
@@ -77,7 +77,7 @@ export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
}, [option, services, containers]);
const isLoading = option === "native" ? containersLoading : servicesLoading;
const containersLenght =
const containersLength =
option === "native" ? containers?.length : services?.length;
return (
@@ -152,7 +152,7 @@ export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
</>
)}
<SelectLabel>Containers ({containersLenght})</SelectLabel>
<SelectLabel>Containers ({containersLength})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
@@ -225,7 +225,7 @@ export const RestoreBackup = ({
resolver: zodResolver(RestoreBackupSchema),
});
const destionationId = form.watch("destinationId");
const destinationId = 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: destionationId,
destinationId: destinationId,
search: debouncedSearchTerm,
serverId: serverId ?? "",
},
{
enabled: isOpen && !!destionationId,
enabled: isOpen && !!destinationId,
},
);
@@ -1,8 +1,8 @@
"use client";
import type { inferRouterOutputs } from "@trpc/server";
import Link from "next/link";
import { ArrowRight, ListTodo, Loader2, XCircle } from "lucide-react";
import Link from "next/link";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
@@ -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 colapsible later */}
{/* Icon to expand the log item maybe implement a collapsible later */}
{/* <Square className="size-4 text-muted-foreground opacity-0 group-hover/logitem:opacity-100 transition-opacity" /> */}
{tooltip(color, rawTimestamp)}
{!noTimestamp && (
@@ -35,7 +35,7 @@ import {
TableRow,
} from "@/components/ui/table";
import { api, type RouterOutputs } from "@/utils/api";
import { columns } from "./colums";
import { columns } from "./columns";
export type Container = NonNullable<
RouterOutputs["docker"]["getContainers"]
>[0];
@@ -11,11 +11,11 @@ import {
import type { DockerStatsJSON } from "./show-free-container-monitoring";
interface Props {
acummulativeData: DockerStatsJSON["block"];
accumulativeData: DockerStatsJSON["block"];
}
export const DockerBlockChart = ({ acummulativeData }: Props) => {
const transformedData = acummulativeData.map((item, index) => {
export const DockerBlockChart = ({ accumulativeData }: Props) => {
const transformedData = accumulativeData.map((item, index) => {
return {
time: item.time,
name: `Point ${index + 1}`,
@@ -11,11 +11,11 @@ import {
import type { DockerStatsJSON } from "./show-free-container-monitoring";
interface Props {
acummulativeData: DockerStatsJSON["cpu"];
accumulativeData: DockerStatsJSON["cpu"];
}
export const DockerCpuChart = ({ acummulativeData }: Props) => {
const transformedData = acummulativeData.map((item, index) => {
export const DockerCpuChart = ({ accumulativeData }: Props) => {
const transformedData = accumulativeData.map((item, index) => {
return {
name: `Point ${index + 1}`,
time: item.time,
@@ -11,12 +11,12 @@ import {
import type { DockerStatsJSON } from "./show-free-container-monitoring";
interface Props {
acummulativeData: DockerStatsJSON["disk"];
accumulativeData: DockerStatsJSON["disk"];
diskTotal: number;
}
export const DockerDiskChart = ({ acummulativeData, diskTotal }: Props) => {
const transformedData = acummulativeData.map((item, index) => {
export const DockerDiskChart = ({ accumulativeData, diskTotal }: Props) => {
const transformedData = accumulativeData.map((item, index) => {
return {
time: item.time,
name: `Point ${index + 1}`,
@@ -12,15 +12,15 @@ import type { DockerStatsJSON } from "./show-free-container-monitoring";
import { convertMemoryToBytes } from "./show-free-container-monitoring";
interface Props {
acummulativeData: DockerStatsJSON["memory"];
accumulativeData: DockerStatsJSON["memory"];
memoryLimitGB: number;
}
export const DockerMemoryChart = ({
acummulativeData,
accumulativeData,
memoryLimitGB,
}: Props) => {
const transformedData = acummulativeData.map((item, index) => {
const transformedData = accumulativeData.map((item, index) => {
return {
time: item.time,
name: `Point ${index + 1}`,
@@ -11,11 +11,11 @@ import {
import type { DockerStatsJSON } from "./show-free-container-monitoring";
interface Props {
acummulativeData: DockerStatsJSON["network"];
accumulativeData: DockerStatsJSON["network"];
}
export const DockerNetworkChart = ({ acummulativeData }: Props) => {
const transformedData = acummulativeData.map((item, index) => {
export const DockerNetworkChart = ({ accumulativeData }: Props) => {
const transformedData = accumulativeData.map((item, index) => {
return {
time: item.time,
name: `Point ${index + 1}`,
@@ -124,7 +124,7 @@ export const ContainerFreeMonitoring = ({
refetchOnWindowFocus: false,
},
);
const [acummulativeData, setAcummulativeData] = useState<DockerStatsJSON>({
const [accumulativeData, setAccumulativeData] = useState<DockerStatsJSON>({
cpu: [],
memory: [],
block: [],
@@ -136,7 +136,7 @@ export const ContainerFreeMonitoring = ({
useEffect(() => {
setCurrentData(defaultData);
setAcummulativeData({
setAccumulativeData({
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,
});
setAcummulativeData({
setAccumulativeData({
block: data?.block || [],
cpu: data?.cpu || [],
disk: data?.disk || [],
@@ -184,7 +184,7 @@ export const ContainerFreeMonitoring = ({
setCurrentData(data);
const MAX_DATA_POINTS = 300;
setAcummulativeData((prevData) => ({
setAccumulativeData((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 acummulativeData={acummulativeData.cpu} />
<DockerCpuChart accumulativeData={accumulativeData.cpu} />
</div>
</CardContent>
</Card>
@@ -252,7 +252,7 @@ export const ContainerFreeMonitoring = ({
className="w-[100%]"
/>
<DockerMemoryChart
acummulativeData={acummulativeData.memory}
accumulativeData={accumulativeData.memory}
memoryLimitGB={
// @ts-ignore
convertMemoryToBytes(currentData.memory.value.total) /
@@ -277,7 +277,7 @@ export const ContainerFreeMonitoring = ({
className="w-[100%]"
/>
<DockerDiskChart
acummulativeData={acummulativeData.disk}
accumulativeData={accumulativeData.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 acummulativeData={acummulativeData.block} />
<DockerBlockChart accumulativeData={accumulativeData.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 acummulativeData={acummulativeData.network} />
<DockerNetworkChart accumulativeData={accumulativeData.network} />
</div>
</CardContent>
</Card>
@@ -1,4 +1,13 @@
import { AlertCircle, Link, Loader2, ShieldCheck, Trash2 } from "lucide-react";
import {
AlertCircle,
ChevronDown,
ChevronRight,
Link,
Loader2,
ShieldCheck,
Trash2,
} from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { AlertBlock } from "@/components/shared/alert-block";
import { DialogAction } from "@/components/shared/dialog-action";
@@ -12,13 +21,19 @@ import {
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { AddCertificate } from "./add-certificate";
import { getCertificateChainInfo, getExpirationStatus } from "./utils";
import {
extractLeafCommonName,
getCertificateChainExpirationDetails,
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">
@@ -66,6 +81,30 @@ 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}
@@ -77,12 +116,52 @@ 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 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 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>
)}
<div
@@ -1,5 +1,13 @@
// @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
@@ -94,8 +102,156 @@ 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 expirationDate = extractExpirationDate(certData);
const chainInfo = getCertificateChainInfo(certData);
const expirationDate = chainInfo.isChain
? getEarliestExpirationDate(certData)
: extractExpirationDate(certData);
if (!expirationDate)
return {
@@ -153,3 +309,67 @@ 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,
};
});
};
@@ -1,3 +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 { useEffect, useState } from "react";
@@ -35,10 +39,6 @@ import {
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import {
ADDITIONAL_FLAG_ERROR,
ADDITIONAL_FLAG_REGEX,
} from "@dokploy/server/db/validations/destination";
import { S3_PROVIDERS } from "./constants";
const addDestination = z.object({
@@ -283,7 +283,7 @@ export const AddGitlabProvider = () => {
</FormLabel>
<FormControl>
<Input
placeholder="For organization/group access use the slugish name of the group eg: my-org"
placeholder="For organization/group access use the slug name of the group eg: my-org"
{...field}
/>
</FormControl>
@@ -192,7 +192,7 @@ export const EditGitlabProvider = ({ gitlabId }: Props) => {
</FormLabel>
<FormControl>
<Input
placeholder="For organization/group access use the slugish name of the group eg: my-org"
placeholder="For organization/group access use the slug name of the group eg: my-org"
{...field}
/>
</FormControl>
@@ -5,6 +5,7 @@ import {
ImportIcon,
Loader2,
Trash2,
Users,
} from "lucide-react";
import Link from "next/link";
import { toast } from "sonner";
@@ -24,6 +25,13 @@ 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";
@@ -39,6 +47,8 @@ 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 = (
@@ -154,10 +164,62 @@ 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 ? (
@@ -222,62 +284,71 @@ export const ShowGitProviders = () => {
</div>
)}
{isGithub && haveGithubRequirements && (
<EditGithubProvider
githubId={gitProvider.github?.githubId}
/>
)}
{gitProvider.isOwner && (
<>
{isGithub && haveGithubRequirements && (
<EditGithubProvider
githubId={gitProvider.github?.githubId}
/>
)}
{isGitlab && (
<EditGitlabProvider
gitlabId={gitProvider.gitlab?.gitlabId}
/>
)}
{isGitlab && (
<EditGitlabProvider
gitlabId={gitProvider.gitlab?.gitlabId}
/>
)}
{isBitbucket && (
<EditBitbucketProvider
bitbucketId={
gitProvider.bitbucket?.bitbucketId
}
/>
)}
{isBitbucket && (
<EditBitbucketProvider
bitbucketId={
gitProvider.bitbucket?.bitbucketId
}
/>
)}
{isGitea && (
<EditGiteaProvider
giteaId={gitProvider.gitea?.giteaId}
/>
)}
{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>
<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>
</>
)}
</div>
</div>
</div>
@@ -12,9 +12,9 @@ import { toast } from "sonner";
import { z } from "zod";
import {
DiscordIcon,
MattermostIcon,
GotifyIcon,
LarkIcon,
MattermostIcon,
NtfyIcon,
PushoverIcon,
ResendIcon,
@@ -54,6 +54,7 @@ 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),
@@ -355,6 +356,7 @@ 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,
@@ -369,6 +371,7 @@ 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 || "",
@@ -384,6 +387,7 @@ 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,
@@ -398,6 +402,7 @@ 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,
@@ -416,6 +421,7 @@ 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,
@@ -431,6 +437,7 @@ 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,
@@ -446,6 +453,7 @@ 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 || "",
@@ -462,6 +470,7 @@ 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,
@@ -477,6 +486,7 @@ 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,
@@ -490,6 +500,7 @@ 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,
@@ -503,6 +514,7 @@ 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
@@ -524,6 +536,7 @@ 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,
@@ -562,6 +575,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy,
dokployRestart,
databaseBackup,
dokployBackup,
volumeBackup,
dockerCleanup,
serverThreshold,
@@ -573,6 +587,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: appDeploy,
dokployRestart: dokployRestart,
databaseBackup: databaseBackup,
dokployBackup: dokployBackup,
volumeBackup: volumeBackup,
webhookUrl: data.webhookUrl,
channel: data.channel,
@@ -588,6 +603,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: appDeploy,
dokployRestart: dokployRestart,
databaseBackup: databaseBackup,
dokployBackup: dokployBackup,
volumeBackup: volumeBackup,
botToken: data.botToken,
messageThreadId: data.messageThreadId || "",
@@ -604,6 +620,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: appDeploy,
dokployRestart: dokployRestart,
databaseBackup: databaseBackup,
dokployBackup: dokployBackup,
volumeBackup: volumeBackup,
webhookUrl: data.webhookUrl,
decoration: data.decoration,
@@ -619,6 +636,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: appDeploy,
dokployRestart: dokployRestart,
databaseBackup: databaseBackup,
dokployBackup: dokployBackup,
volumeBackup: volumeBackup,
smtpServer: data.smtpServer,
smtpPort: data.smtpPort,
@@ -638,6 +656,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: appDeploy,
dokployRestart: dokployRestart,
databaseBackup: databaseBackup,
dokployBackup: dokployBackup,
volumeBackup: volumeBackup,
apiKey: data.apiKey,
fromAddress: data.fromAddress,
@@ -654,6 +673,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: appDeploy,
dokployRestart: dokployRestart,
databaseBackup: databaseBackup,
dokployBackup: dokployBackup,
volumeBackup: volumeBackup,
serverUrl: data.serverUrl,
appToken: data.appToken,
@@ -670,6 +690,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: appDeploy,
dokployRestart: dokployRestart,
databaseBackup: databaseBackup,
dokployBackup: dokployBackup,
volumeBackup: volumeBackup,
serverUrl: data.serverUrl,
accessToken: data.accessToken || "",
@@ -686,6 +707,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: appDeploy,
dokployRestart: dokployRestart,
databaseBackup: databaseBackup,
dokployBackup: dokployBackup,
volumeBackup: volumeBackup,
webhookUrl: data.webhookUrl,
channel: data.channel || undefined,
@@ -702,6 +724,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: appDeploy,
dokployRestart: dokployRestart,
databaseBackup: databaseBackup,
dokployBackup: dokployBackup,
volumeBackup: volumeBackup,
webhookUrl: data.webhookUrl,
name: data.name,
@@ -716,6 +739,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: appDeploy,
dokployRestart: dokployRestart,
databaseBackup: databaseBackup,
dokployBackup: dokployBackup,
volumeBackup: volumeBackup,
webhookUrl: data.webhookUrl,
name: data.name,
@@ -742,6 +766,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: appDeploy,
dokployRestart: dokployRestart,
databaseBackup: databaseBackup,
dokployBackup: dokployBackup,
volumeBackup: volumeBackup,
endpoint: data.endpoint,
headers: headersRecord,
@@ -761,6 +786,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
appDeploy: appDeploy,
dokployRestart: dokployRestart,
databaseBackup: databaseBackup,
dokployBackup: dokployBackup,
volumeBackup: volumeBackup,
userKey: data.userKey,
apiToken: data.apiToken,
@@ -1856,6 +1882,27 @@ 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"
@@ -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
preffered provider (Hostinger, Digital Ocean, Hetzner,
preferred provider (Hostinger, Digital Ocean, Hetzner,
etc){" "}
</li>
<li>2. Add The SSH Key to Server Manually</li>
@@ -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 { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
import { WelcomeSubscription } from "./welcome-stripe/welcome-subscription";
export const ShowServers = () => {
const router = useRouter();
@@ -63,7 +63,7 @@ export const ShowServers = () => {
return (
<div className="w-full">
{query?.success && isCloud && <WelcomeSuscription />}
{query?.success && isCloud && <WelcomeSubscription />}
<Card className="h-full p-2.5 rounded-xl max-w-5xl mx-auto">
<div className="rounded-xl bg-background shadow-md ">
<CardHeader className="">
@@ -51,7 +51,7 @@ export const { useStepper, steps, Scoped } = defineStepper(
{ id: "complete", title: "Complete", description: "Checkout complete" },
);
export const WelcomeSuscription = () => {
export const WelcomeSubscription = () => {
const [showConfetti, setShowConfetti] = useState(false);
const stepper = useStepper();
const [isOpen, setIsOpen] = useState(true);
@@ -26,6 +26,7 @@ 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. */
@@ -170,6 +171,7 @@ 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),
@@ -196,6 +198,15 @@ 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(
{
@@ -214,6 +225,7 @@ export const AddUserPermissions = ({ userId, role }: Props) => {
accessedProjects: [],
accessedEnvironments: [],
accessedServices: [],
accessedGitProviders: [],
canDeleteEnvironments: false,
canCreateProjects: false,
canCreateServices: false,
@@ -235,6 +247,7 @@ 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,
@@ -262,6 +275,7 @@ 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,
@@ -870,6 +884,78 @@ 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}
@@ -153,7 +153,7 @@ export const ChangeRole = ({ memberId, currentRole, userEmail }: Props) => {
)}
<br />
<em className="text-muted-foreground text-xs">
Note: Owner role is intransferible.
Note: Owner role is nontransferable.
</em>
</FormDescription>
<FormMessage />
@@ -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 intransferible
// - Owner role is nontransferable
const canChangeRole =
member.role !== "owner" &&
member.user.id !== session?.user?.id &&
+1 -1
View File
@@ -101,7 +101,7 @@ const BreadcrumbEllipsis = ({
<span className="sr-only">More</span>
</span>
);
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
BreadcrumbEllipsis.displayName = "BreadcrumbEllipsis";
export {
Breadcrumb,
+6 -6
View File
@@ -19,7 +19,7 @@ interface TreeDataItem {
type TreeProps = React.HTMLAttributes<HTMLDivElement> & {
data: TreeDataItem[] | TreeDataItem;
initialSlelectedItemId?: string;
initialSelectedItemId?: string;
onSelectChange?: (item: TreeDataItem | undefined) => void;
expandAll?: boolean;
folderIcon?: LucideIcon;
@@ -30,7 +30,7 @@ const Tree = React.forwardRef<HTMLDivElement, TreeProps>(
(
{
data,
initialSlelectedItemId,
initialSelectedItemId,
onSelectChange,
expandAll,
folderIcon,
@@ -42,7 +42,7 @@ const Tree = React.forwardRef<HTMLDivElement, TreeProps>(
) => {
const [selectedItemId, setSelectedItemId] = React.useState<
string | undefined
>(initialSlelectedItemId);
>(initialSelectedItemId);
const handleSelectChange = React.useCallback(
(item: TreeDataItem | undefined) => {
@@ -55,7 +55,7 @@ const Tree = React.forwardRef<HTMLDivElement, TreeProps>(
);
const expandedItemIds = React.useMemo(() => {
if (!initialSlelectedItemId) {
if (!initialSelectedItemId) {
return [] as string[];
}
@@ -81,9 +81,9 @@ const Tree = React.forwardRef<HTMLDivElement, TreeProps>(
}
}
walkTreeItems(data, initialSlelectedItemId);
walkTreeItems(data, initialSelectedItemId);
return ids;
}, [data, initialSlelectedItemId]);
}, [data, initialSelectedItemId]);
const { ref: refRoot } = useResizeObserver();
@@ -0,0 +1 @@
ALTER TABLE "notification" ADD COLUMN "dokployBackup" boolean DEFAULT false NOT NULL;
@@ -0,0 +1,2 @@
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
+14
View File
@@ -1093,6 +1093,20 @@
"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
}
]
}
+1 -1
View File
@@ -1,5 +1,5 @@
import { ssoClient } from "@better-auth/sso/client";
import { apiKeyClient } from "@better-auth/api-key/client";
import { ssoClient } from "@better-auth/sso/client";
import {
adminClient,
inferAdditionalFields,
@@ -196,7 +196,7 @@ export default async function handler(
return;
}
const commitedPaths = await extractCommitedPaths(
const committedPaths = await extractCommittedPaths(
req.body,
application.bitbucket,
application.bitbucketRepositorySlug ||
@@ -206,7 +206,7 @@ export default async function handler(
const shouldDeployPaths = shouldDeploy(
application.watchPaths,
commitedPaths,
committedPaths,
);
if (!shouldDeployPaths) {
@@ -538,7 +538,7 @@ export const getProviderByHeader = (headers: any) => {
return null;
};
export const extractCommitedPaths = async (
export const extractCommittedPaths = async (
body: any,
bitbucket: Bitbucket | null,
repository: string,
@@ -548,7 +548,7 @@ export const extractCommitedPaths = async (
const commitHashes = changes
.map((change: any) => change.new?.target?.hash)
.filter(Boolean);
const commitedPaths: string[] = [];
const committedPaths: string[] = [];
const username =
bitbucket?.bitbucketWorkspaceName || bitbucket?.bitbucketUsername || "";
for (const commit of commitHashes) {
@@ -559,7 +559,7 @@ export const extractCommitedPaths = async (
});
const data = await response.json();
for (const value of data.values) {
if (value?.new?.path) commitedPaths.push(value.new.path);
if (value?.new?.path) committedPaths.push(value.new.path);
}
} catch (error) {
console.error(
@@ -571,5 +571,5 @@ export const extractCommitedPaths = async (
}
}
return commitedPaths;
return committedPaths;
};
@@ -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 commitedPaths = await extractCommitedPaths(
const committedPaths = await extractCommittedPaths(
req.body,
composeResult.bitbucket,
composeResult.bitbucketRepositorySlug ||
@@ -107,7 +107,7 @@ export default async function handler(
const shouldDeployPaths = shouldDeploy(
composeResult.watchPaths,
commitedPaths,
committedPaths,
);
if (!shouldDeployPaths) {
+6 -6
View File
@@ -174,27 +174,27 @@ export default async function handler(
case "invoice.payment_succeeded": {
const newInvoice = event.data.object as Stripe.Invoice;
const suscription = await stripe.subscriptions.retrieve(
const subscription = await stripe.subscriptions.retrieve(
newInvoice.subscription as string,
);
if (suscription.status !== "active") {
if (subscription.status !== "active") {
console.log(
`Skipping invoice.payment_succeeded for subscription ${suscription.id} with status ${suscription.status}`,
`Skipping invoice.payment_succeeded for subscription ${subscription.id} with status ${subscription.status}`,
);
break;
}
const serversQuantity = getSubscriptionServersQuantity(
suscription?.items?.data ?? [],
subscription?.items?.data ?? [],
);
await db
.update(user)
.set({ serversQuantity })
.where(eq(user.stripeCustomerId, suscription.customer as string));
.where(eq(user.stripeCustomerId, subscription.customer as string));
const admin = await findUserByStripeCustomerId(
suscription.customer as string,
subscription.customer as string,
);
if (!admin) {
@@ -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-enviroment";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
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";
@@ -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-enviroment";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
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";
@@ -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-enviroment";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
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";
@@ -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-enviroment";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
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";
@@ -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-enviroment";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
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";
@@ -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-enviroment";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
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";
@@ -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-enviroment";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
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";
@@ -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 { 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 { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { ManageCustomRoles } from "@/components/proprietary/roles/manage-custom-roles";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
+1 -1
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,
+35 -45
View File
@@ -28,6 +28,7 @@ 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,
@@ -53,7 +54,6 @@ import {
protectedProcedure,
withPermission,
} from "@/server/api/trpc";
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
import { audit } from "@/server/api/utils/audit";
import {
apiCreateBackup,
@@ -545,52 +545,42 @@ export const backupRouter = createTRPCRouter({
}
const destination = await findDestinationById(input.destinationId);
const queue: string[] = [];
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);
});
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);
}
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);
};
runRestore()
.catch((error) => {
onLog(
`Error: ${error instanceof Error ? error.message : String(error)}`,
);
})
.finally(() => {
done = true;
});
}
while (!done || queue.length > 0) {
if (queue.length > 0) {
yield queue.shift()!;
+4 -1
View File
@@ -1,6 +1,7 @@
import {
createBitbucket,
findBitbucketById,
getAccessibleGitProviderIds,
getBitbucketBranches,
getBitbucketRepositories,
testBitbucketConnection,
@@ -54,6 +55,8 @@ 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,
@@ -67,7 +70,7 @@ export const bitbucketRouter = createTRPCRouter({
return (
provider.gitProvider.organizationId ===
ctx.session.activeOrganizationId &&
provider.gitProvider.userId === ctx.session.userId
accessibleIds.has(provider.gitProvider.gitProviderId)
);
});
return result;
@@ -7,8 +7,8 @@ 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 { createTRPCRouter, withPermission } from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import {
apiCreateCertificate,
apiFindCertificate,
+2 -2
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 { audit } from "../utils/audit";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
import { audit } from "../utils/audit";
export const composeRouter = createTRPCRouter({
create: protectedProcedure
@@ -1,5 +1,5 @@
import {
createDestintation,
createDestination,
execAsync,
execAsyncRemote,
findDestinationById,
@@ -25,7 +25,7 @@ export const destinationRouter = createTRPCRouter({
.input(apiCreateDestination)
.mutation(async ({ input, ctx }) => {
try {
const result = await createDestintation(
const result = await createDestination(
input,
ctx.session.activeOrganizationId,
);
@@ -1,18 +1,34 @@
import { findGitProviderById, removeGitProvider } from "@dokploy/server";
import {
findGitProviderById,
getAccessibleGitProviderIds,
removeGitProvider,
updateGitProvider,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
import { TRPCError } from "@trpc/server";
import { and, desc, eq } from "drizzle-orm";
import { audit } from "@/server/api/utils/audit";
import { desc, eq, inArray } from "drizzle-orm";
import {
createTRPCRouter,
protectedProcedure,
withPermission,
} from "@/server/api/trpc";
import { apiRemoveGitProvider, gitProvider } from "@/server/db/schema";
import { audit } from "@/server/api/utils/audit";
import {
apiRemoveGitProvider,
apiToggleShareGitProvider,
gitProvider,
} from "@/server/db/schema";
export const gitProviderRouter = createTRPCRouter({
getAll: protectedProcedure.query(async ({ ctx }) => {
return await db.query.gitProvider.findMany({
const accessibleIds = await getAccessibleGitProviderIds(ctx.session);
if (accessibleIds.size === 0) {
return [];
}
const results = await db.query.gitProvider.findMany({
with: {
gitlab: true,
bitbucket: true,
@@ -20,12 +36,65 @@ export const gitProviderRouter = createTRPCRouter({
gitea: true,
},
orderBy: desc(gitProvider.createdAt),
where: and(
eq(gitProvider.userId, ctx.session.userId),
eq(gitProvider.organizationId, ctx.session.activeOrganizationId),
),
where: inArray(gitProvider.gitProviderId, [...accessibleIds]),
});
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 }) => {
+4 -1
View File
@@ -1,6 +1,7 @@
import {
createGitea,
findGiteaById,
getAccessibleGitProviderIds,
getGiteaBranches,
getGiteaRepositories,
haveGiteaRequirements,
@@ -57,6 +58,8 @@ 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,
@@ -67,7 +70,7 @@ export const giteaRouter = createTRPCRouter({
(provider) =>
provider.gitProvider.organizationId ===
ctx.session.activeOrganizationId &&
provider.gitProvider.userId === ctx.session.userId,
accessibleIds.has(provider.gitProvider.gitProviderId),
);
const filtered = result
+4 -1
View File
@@ -1,5 +1,6 @@
import {
findGithubById,
getAccessibleGitProviderIds,
getGithubBranches,
getGithubRepositories,
haveGithubRequirements,
@@ -35,6 +36,8 @@ 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,
@@ -45,7 +48,7 @@ export const githubRouter = createTRPCRouter({
(provider) =>
provider.gitProvider.organizationId ===
ctx.session.activeOrganizationId &&
provider.gitProvider.userId === ctx.session.userId,
accessibleIds.has(provider.gitProvider.gitProviderId),
);
const filtered = result
+4 -1
View File
@@ -1,6 +1,7 @@
import {
createGitlab,
findGitlabById,
getAccessibleGitProviderIds,
getGitlabBranches,
getGitlabRepositories,
haveGitlabRequirements,
@@ -54,6 +55,8 @@ 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,
@@ -64,7 +67,7 @@ export const gitlabRouter = createTRPCRouter({
return (
provider.gitProvider.organizationId ===
ctx.session.activeOrganizationId &&
provider.gitProvider.userId === ctx.session.userId
accessibleIds.has(provider.gitProvider.gitProviderId)
);
});
const filtered = result
+6 -2
View File
@@ -246,11 +246,15 @@ export const libsqlRouter = createTRPCRouter({
deployment: ["create"],
});
const queue: string[] = [];
const done = false;
let done = false;
deployLibsql(input.libsqlId, (log) => {
queue.push(log);
});
})
.catch(() => {})
.finally(() => {
done = true;
});
while (!done || queue.length > 0) {
if (queue.length > 0) {
+10 -5
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 { audit } from "@/server/api/utils/audit";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import {
apiChangeMongoStatus,
apiCreateMongo,
@@ -39,9 +39,10 @@ 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
@@ -228,11 +229,15 @@ export const mongoRouter = createTRPCRouter({
deployment: ["create"],
});
const queue: string[] = [];
const done = false;
let done = false;
deployMongo(input.mongoId, (log) => {
queue.push(log);
});
})
.catch(() => {})
.finally(() => {
done = true;
});
while (!done || queue.length > 0) {
if (queue.length > 0) {
+8 -4
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 { audit } from "@/server/api/utils/audit";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import {
apiChangeMySqlStatus,
apiCreateMySql,
@@ -230,11 +230,15 @@ export const mysqlRouter = createTRPCRouter({
});
const queue: string[] = [];
const done = false;
let done = false;
deployMySql(input.mysqlId, (log) => {
queue.push(log);
});
})
.catch(() => {})
.finally(() => {
done = true;
});
while (!done || queue.length > 0) {
if (queue.length > 0) {
@@ -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 intransferible - cannot change to or from owner
// Owner role is nontransferable - cannot change to or from owner
if (target.role === "owner" || input.role === "owner") {
throw new TRPCError({
code: "FORBIDDEN",
message: "The owner role is intransferible",
message: "The owner role is nontransferable",
});
}
+10 -5
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 { audit } from "@/server/api/utils/audit";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import {
apiChangePostgresStatus,
apiCreatePostgres,
@@ -40,9 +40,10 @@ 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({
@@ -233,11 +234,15 @@ export const postgresRouter = createTRPCRouter({
});
const queue: string[] = [];
const done = false;
let done = false;
deployPostgres(input.postgresId, (log) => {
queue.push(log);
});
})
.catch(() => {})
.finally(() => {
done = true;
});
while (!done || queue.length > 0) {
if (queue.length > 0) {
@@ -143,7 +143,12 @@ export const licenseKeyRouter = createTRPCRouter({
});
}
await deactivateLicenseKey(currentUser.licenseKey);
try {
await deactivateLicenseKey(currentUser.licenseKey);
} catch (err) {
console.error("Failed to deactivate license key remotely:", err);
}
await db
.update(user)
.set({
+10 -5
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 { audit } from "@/server/api/utils/audit";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import {
apiChangeRedisStatus,
apiCreateRedis,
@@ -38,9 +38,10 @@ 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)
@@ -251,11 +252,15 @@ export const redisRouter = createTRPCRouter({
deployment: ["create"],
});
const queue: string[] = [];
const done = false;
let done = false;
deployRedis(input.redisId, (log) => {
queue.push(log);
});
})
.catch(() => {})
.finally(() => {
done = true;
});
while (!done || queue.length > 0) {
if (queue.length > 0) {
+1 -1
View File
@@ -10,6 +10,7 @@ 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,
@@ -19,7 +20,6 @@ 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")
+3 -2
View File
@@ -7,6 +7,7 @@ import {
updateScheduleSchema,
} from "@dokploy/server/db/schema/schedule";
import { runCommand } from "@dokploy/server/index";
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
import {
createSchedule,
deleteSchedule,
@@ -14,11 +15,10 @@ import {
updateSchedule,
} from "@dokploy/server/services/schedule";
import { TRPCError } from "@trpc/server";
import { desc, eq } from "drizzle-orm";
import { asc, 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,6 +157,7 @@ export const scheduleRouter = createTRPCRouter({
};
return db.query.schedules.findMany({
where: where[input.scheduleType],
orderBy: [asc(schedules.createdAt)],
with: {
application: true,
server: true,
+1 -1
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,
+1 -1
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 { audit } from "@/server/api/utils/audit";
import { createTRPCRouter, withPermission } from "@/server/api/trpc";
import { audit } from "@/server/api/utils/audit";
import {
apiCreateSshKey,
apiFindOneSshKey,
+9 -1
View File
@@ -13,6 +13,7 @@ import {
updateUser,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
import {
account,
apiAssignPermissions,
@@ -344,12 +345,19 @@ export const userRouter = createTRPCRouter({
});
}
const { id, ...rest } = input;
const { id, accessedGitProviders, ...rest } = input;
const licensed = await hasValidLicense(
ctx.session?.activeOrganizationId || "",
);
await db
.update(member)
.set({
...rest,
...(licensed && accessedGitProviders !== undefined
? { accessedGitProviders }
: {}),
})
.where(
and(
+1 -1
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 typesafety on the frontend if your procedure fails due to validation
* ZodErrors so that you get type safety on the frontend if your procedure fails due to validation
* errors on the backend.
*/
+1 -1
View File
@@ -1,5 +1,5 @@
import { createAuditLog } from "@dokploy/server/services/proprietary/audit-log";
import type { AuditAction, AuditResourceType } from "@dokploy/server/db/schema";
import { createAuditLog } from "@dokploy/server/services/proprietary/audit-log";
interface AuditCtx {
user: { id: string; email: string; role: string };
+3 -3
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";
+4
View File
@@ -163,6 +163,10 @@ 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 }) => ({
@@ -1,5 +1,5 @@
import { relations } from "drizzle-orm";
import { pgEnum, pgTable, text } from "drizzle-orm/pg-core";
import { boolean, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
import { nanoid } from "nanoid";
import { z } from "zod";
import { organization } from "./account";
@@ -32,6 +32,9 @@ 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 }) => ({
@@ -64,3 +67,8 @@ 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(),
});
+1 -1
View File
@@ -1,7 +1,7 @@
export * from "./account";
export * from "./ai";
export * from "./audit-log";
export * from "./application";
export * from "./audit-log";
export * from "./backups";
export * from "./bitbucket";
export * from "./certificate";
+3 -5
View File
@@ -179,11 +179,9 @@ export const apiCreateLibsql = createSchema
}
});
export const apiFindOneLibsql = createSchema
.pick({
libsqlId: true,
})
.required();
export const apiFindOneLibsql = z.object({
libsqlId: z.string().min(1),
});
export const apiChangeLibsqlStatus = createSchema
.pick({
+7 -8
View File
@@ -149,14 +149,13 @@ export const apiRemoveMount = createSchema
// })
.required();
export const apiFindMountByApplicationId = createSchema
.pick({
serviceType: true,
})
.required()
.extend({
serviceId: z.string().min(1),
});
export const apiFindMountByApplicationId = z.object({
serviceType: z
.string()
.min(1)
.transform((val) => val as ServiceType),
serviceId: z.string().min(1),
});
export const apiUpdateMount = createSchema.partial().extend({
mountId: z.string().min(1),
@@ -38,6 +38,7 @@ 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(),
@@ -266,6 +267,7 @@ export const apiCreateSlack = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -294,6 +296,7 @@ export const apiCreateTelegram = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -324,6 +327,7 @@ export const apiCreateDiscord = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -355,6 +359,7 @@ export const apiCreateEmail = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -391,6 +396,7 @@ export const apiCreateResend = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -421,6 +427,7 @@ export const apiCreateGotify = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -455,6 +462,7 @@ export const apiCreateNtfy = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -486,6 +494,7 @@ export const apiCreateMattermost = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -503,6 +512,7 @@ export const apiCreateMattermost = notificationsSchema
webhookUrl: true,
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
appDeploy: true,
@@ -535,6 +545,7 @@ export const apiCreateCustom = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -562,6 +573,7 @@ export const apiCreateLark = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -588,6 +600,7 @@ export const apiCreateTeams = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -614,6 +627,7 @@ export const apiCreatePushover = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
dokployBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
@@ -648,6 +662,7 @@ 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(),
+1
View File
@@ -126,6 +126,7 @@ 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(),
@@ -1,6 +1,5 @@
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";
@@ -11,6 +10,7 @@ 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";
@@ -0,0 +1,106 @@
import {
Body,
Container,
Head,
Heading,
Html,
Img,
Preview,
Section,
Tailwind,
Text,
} from "@react-email/components";
export type TemplateProps = {
type: "error" | "success";
errorMessage?: string;
date: string;
backupSize?: string;
};
export const DokployBackupEmail = ({
type = "success",
errorMessage,
date = "2023-05-01T00:00:00.000Z",
backupSize,
}: TemplateProps) => {
const previewText = `Dokploy instance backup was ${type === "success" ? "successful ✅" : "failed ❌"}`;
return (
<Html>
<Preview>{previewText}</Preview>
<Tailwind
config={{
theme: {
extend: {
colors: {
brand: "#007291",
},
},
},
}}
>
<Head />
<Body className="bg-white my-auto mx-auto font-sans px-2">
<Container className="border border-solid border-[#eaeaea] rounded-lg my-[40px] mx-auto p-[20px] max-w-[465px]">
<Section className="mt-[32px]">
<Img
src={
"https://raw.githubusercontent.com/Dokploy/dokploy/refs/heads/canary/apps/dokploy/logo.png"
}
width="100"
height="50"
alt="Dokploy"
className="my-0 mx-auto"
/>
</Section>
<Heading className="text-black text-[24px] font-normal text-center p-0 my-[30px] mx-0">
Dokploy Instance Backup
</Heading>
<Text className="text-black text-[14px] leading-[24px]">
Hello,
</Text>
<Text className="text-black text-[14px] leading-[24px]">
Your Dokploy instance backup was{" "}
{type === "success"
? "successful ✅"
: "failed. Please check the error message below. ❌"}
.
</Text>
<Section className="flex text-black text-[14px] leading-[24px] bg-[#F4F4F5] rounded-lg p-2">
<Text className="!leading-3 font-bold">Details: </Text>
<Text className="!leading-3">
Backup Type: <strong>Complete Dokploy Instance</strong>
</Text>
<Text className="!leading-3">
Content: <strong>/etc/dokploy + PostgreSQL Database</strong>
</Text>
{backupSize && (
<Text className="!leading-3">
Backup Size: <strong>{backupSize}</strong>
</Text>
)}
<Text className="!leading-3">
Date: <strong>{date}</strong>
</Text>
<Text className="!leading-3">
Status:{" "}
<strong>{type === "success" ? "Successful" : "Failed"}</strong>
</Text>
</Section>
{type === "error" && errorMessage ? (
<Section className="flex text-black text-[14px] mt-4 leading-[24px] bg-[#F4F4F5] rounded-lg p-2">
<Text className="!leading-3 font-bold">Reason: </Text>
<Text className="text-[12px] leading-[24px]">
{errorMessage || "Error message not provided"}
</Text>
</Section>
) : null}
</Container>
</Body>
</Tailwind>
</Html>
);
};
export default DokployBackupEmail;
+1 -1
View File
@@ -121,7 +121,7 @@ export * from "./utils/providers/raw";
export * from "./utils/schedules/index";
export * from "./utils/schedules/utils";
export * from "./utils/servers/remote-docker";
export * from "./utils/startup/cancell-deployments";
export * from "./utils/startup/cancel-deployments";
export * from "./utils/tracking/hubspot";
export * from "./utils/traefik/application";
export * from "./utils/traefik/domain";
+7 -7
View File
@@ -23,7 +23,7 @@ import {
} from "@dokploy/server/utils/process/execAsync";
import { TRPCError } from "@trpc/server";
import { format } from "date-fns";
import { desc, eq, and, inArray, or, sql } from "drizzle-orm";
import { and, desc, eq, inArray, or, sql } from "drizzle-orm";
import type { z } from "zod";
import {
type Application,
@@ -177,7 +177,7 @@ export const createDeployment = async (
status: "error",
logPath: "",
description: deployment.description || "",
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
errorMessage: `An error have occurred: ${error instanceof Error ? error.message : error}`,
startedAt: new Date().toISOString(),
finishedAt: new Date().toISOString(),
})
@@ -257,7 +257,7 @@ export const createDeploymentPreview = async (
status: "error",
logPath: "",
description: deployment.description || "",
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
errorMessage: `An error have occurred: ${error instanceof Error ? error.message : error}`,
startedAt: new Date().toISOString(),
finishedAt: new Date().toISOString(),
})
@@ -334,7 +334,7 @@ echo "Initializing deployment\n" >> ${logFilePath};
status: "error",
logPath: "",
description: deployment.description || "",
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
errorMessage: `An error have occurred: ${error instanceof Error ? error.message : error}`,
startedAt: new Date().toISOString(),
finishedAt: new Date().toISOString(),
})
@@ -418,7 +418,7 @@ echo "Initializing backup\n" >> ${logFilePath};
status: "error",
logPath: "",
description: deployment.description || "",
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
errorMessage: `An error have occurred: ${error instanceof Error ? error.message : error}`,
startedAt: new Date().toISOString(),
finishedAt: new Date().toISOString(),
})
@@ -493,7 +493,7 @@ export const createDeploymentSchedule = async (
status: "error",
logPath: "",
description: deployment.description || "",
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
errorMessage: `An error have occurred: ${error instanceof Error ? error.message : error}`,
startedAt: new Date().toISOString(),
finishedAt: new Date().toISOString(),
})
@@ -578,7 +578,7 @@ export const createDeploymentVolumeBackup = async (
status: "error",
logPath: "",
description: deployment.description || "",
errorMessage: `An error have occured: ${error instanceof Error ? error.message : error}`,
errorMessage: `An error have occurred: ${error instanceof Error ? error.message : error}`,
startedAt: new Date().toISOString(),
finishedAt: new Date().toISOString(),
})
+1 -1
View File
@@ -9,7 +9,7 @@ import type { z } from "zod";
export type Destination = typeof destinations.$inferSelect;
export const createDestintation = async (
export const createDestination = async (
input: z.infer<typeof apiCreateDestination>,
organizationId: string,
) => {
+48 -2
View File
@@ -1,7 +1,8 @@
import { db } from "@dokploy/server/db";
import { gitProvider } from "@dokploy/server/db/schema";
import { gitProvider, member } from "@dokploy/server/db/schema";
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { and, eq } from "drizzle-orm";
export type GitProvider = typeof gitProvider.$inferSelect;
@@ -41,3 +42,48 @@ export const updateGitProvider = async (
.returning()
.then((response) => response[0]);
};
export const getAccessibleGitProviderIds = async (session: {
userId: string;
activeOrganizationId: string;
}): Promise<Set<string>> => {
const { userId, activeOrganizationId } = session;
const allOrgProviders = await db.query.gitProvider.findMany({
where: eq(gitProvider.organizationId, activeOrganizationId),
columns: {
gitProviderId: true,
userId: true,
sharedWithOrganization: true,
},
});
const memberRecord = await db.query.member.findFirst({
where: and(
eq(member.userId, userId),
eq(member.organizationId, activeOrganizationId),
),
columns: { accessedGitProviders: true, role: true },
});
if (memberRecord?.role === "owner" || memberRecord?.role === "admin") {
return new Set(allOrgProviders.map((p) => p.gitProviderId));
}
const licensed = await hasValidLicense(activeOrganizationId);
const assignedSet = licensed
? new Set(memberRecord?.accessedGitProviders ?? [])
: new Set<string>();
const result = new Set<string>();
for (const p of allOrgProviders) {
if (
p.userId === userId ||
p.sharedWithOrganization ||
assignedSet.has(p.gitProviderId)
) {
result.add(p.gitProviderId);
}
}
return result;
};
@@ -73,6 +73,7 @@ export const createSlackNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployBackup: input.dokployBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
@@ -105,6 +106,7 @@ export const updateSlackNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployBackup: input.dokployBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
@@ -166,6 +168,7 @@ export const createTelegramNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployBackup: input.dokployBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
@@ -198,6 +201,7 @@ export const updateTelegramNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployBackup: input.dokployBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
@@ -259,6 +263,7 @@ export const createDiscordNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployBackup: input.dokployBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
@@ -291,6 +296,7 @@ export const updateDiscordNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployBackup: input.dokployBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
@@ -355,6 +361,7 @@ export const createEmailNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployBackup: input.dokployBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
@@ -387,6 +394,7 @@ export const updateEmailNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployBackup: input.dokployBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
@@ -452,6 +460,7 @@ export const createResendNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployBackup: input.dokployBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
@@ -484,6 +493,7 @@ export const updateResendNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployBackup: input.dokployBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
@@ -547,6 +557,7 @@ export const createGotifyNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployBackup: input.dokployBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
@@ -578,6 +589,7 @@ export const updateGotifyNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployBackup: input.dokployBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
@@ -639,6 +651,7 @@ export const createNtfyNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployBackup: input.dokployBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
@@ -670,6 +683,7 @@ export const updateNtfyNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployBackup: input.dokployBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
@@ -729,6 +743,7 @@ export const createCustomNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployBackup: input.dokployBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
@@ -761,6 +776,7 @@ export const updateCustomNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployBackup: input.dokployBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
@@ -854,6 +870,7 @@ export const createLarkNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployBackup: input.dokployBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
@@ -886,6 +903,7 @@ export const updateLarkNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployBackup: input.dokployBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
@@ -944,6 +962,7 @@ export const createTeamsNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployBackup: input.dokployBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
@@ -976,6 +995,7 @@ export const updateTeamsNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployBackup: input.dokployBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
@@ -1051,6 +1071,7 @@ export const createMattermostNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployBackup: input.dokployBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
@@ -1083,6 +1104,7 @@ export const updateMattermostNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployBackup: input.dokployBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
@@ -1147,6 +1169,7 @@ export const createPushoverNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployBackup: input.dokployBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
@@ -1179,6 +1202,7 @@ export const updatePushoverNotification = async (
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
dokployBackup: input.dokployBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
@@ -1,6 +1,6 @@
import { db } from "@dokploy/server/db";
import { auditLog } from "@dokploy/server/db/schema";
import type { AuditAction, AuditResourceType } from "@dokploy/server/db/schema";
import { auditLog } from "@dokploy/server/db/schema";
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
import { and, desc, eq, gte, ilike, lte } from "drizzle-orm";
+2 -2
View File
@@ -383,12 +383,12 @@ export const readPorts = async (
const seenPorts = new Set<string>();
for (const key in parsedResult) {
if (Object.hasOwn(parsedResult, key)) {
const containerPortMapppings = parsedResult[key];
const containerPortMappings = parsedResult[key];
const protocol = key.split("/")[1];
const targetPort = Number.parseInt(key.split("/")[0] ?? "0", 10);
// Take only the first mapping to avoid duplicates (IPv4 and IPv6)
const firstMapping = containerPortMapppings[0];
const firstMapping = containerPortMappings[0];
if (firstMapping) {
const publishedPort = Number.parseInt(firstMapping.HostPort, 10);
const portKey = `${targetPort}-${publishedPort}-${protocol}`;
+2 -2
View File
@@ -89,7 +89,7 @@ export const canPerformAccessService = async (
return false;
};
export const canPeformDeleteService = async (
export const canPerformDeleteService = async (
userId: string,
serviceId: string,
organizationId: string,
@@ -215,7 +215,7 @@ export const checkServiceAccess = async (
);
break;
case "delete":
hasPermission = await canPeformDeleteService(
hasPermission = await canPerformDeleteService(
userId,
serviceId,
organizationId,
+4 -4
View File
@@ -3,13 +3,13 @@ import { docker } from "../constants";
export const initializeSwarm = async () => {
const swarmInitialized = await dockerSwarmInitialized();
if (swarmInitialized) {
console.log("Swarm is already initilized");
console.log("Swarm is already initialized");
} else {
await docker.swarmInit({
AdvertiseAddr: "127.0.0.1",
ListenAddr: "0.0.0.0",
});
console.log("Swarm was initilized");
console.log("Swarm was initialized");
}
};
@@ -26,14 +26,14 @@ export const dockerSwarmInitialized = async () => {
export const initializeNetwork = async () => {
const networkInitialized = await dockerNetworkInitialized();
if (networkInitialized) {
console.log("Network is already initilized");
console.log("Network is already initialized");
} else {
docker.createNetwork({
Attachable: true,
Name: "dokploy-network",
Driver: "overlay",
});
console.log("Network was initilized");
console.log("Network was initialized");
}
};
@@ -1,5 +1,5 @@
import { createWriteStream } from "node:fs";
import { mkdtemp, rm } from "node:fs/promises";
import { mkdtemp, rm, stat } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { IS_CLOUD, paths } from "@dokploy/server/constants";
@@ -9,9 +9,19 @@ import {
updateDeploymentStatus,
} from "@dokploy/server/services/deployment";
import { findDestinationById } from "@dokploy/server/services/destination";
import { sendDokployBackupNotifications } from "../notifications/dokploy-backup";
import { execAsync } from "../process/execAsync";
import { getS3Credentials, normalizeS3Path } from "./utils";
function formatBytes(bytes?: number) {
if (bytes === undefined) return "Unknown size";
if (bytes === 0) return "0 B";
const sizes = ["B", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
const value = bytes / Math.pow(1024, i);
return `${value.toFixed(2)} ${sizes[i]} (${bytes} bytes)`;
}
export const runWebServerBackup = async (backup: BackupSchedule) => {
if (IS_CLOUD) {
return;
@@ -23,7 +33,7 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
description: "Web Server Backup",
});
const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
let computedBackupSize: number | undefined;
try {
const destination = await findDestinationById(backup.destinationId);
const rcloneFlags = getS3Credentials(destination);
@@ -79,11 +89,24 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
writeStream.write("Zipped database and filesystem\n");
const uploadCommand = `rclone copyto ${rcloneFlags.join(" ")} "${tempDir}/${backupFileName}" "${s3Path}"`;
const zipPath = join(tempDir, backupFileName);
try {
const { size } = await stat(zipPath);
computedBackupSize = size;
writeStream.write(`Backup size: ${size} bytes\n`);
} catch {
// If stat fails, keep undefined
}
const uploadCommand = `rclone copyto ${rcloneFlags.join(" ")} "${zipPath}" "${s3Path}"`;
writeStream.write("Running command to upload backup to S3\n");
await execAsync(uploadCommand);
writeStream.write("Uploaded backup to S3 ✅\n");
writeStream.end();
await sendDokployBackupNotifications({
type: "success",
backupSize: formatBytes(computedBackupSize),
});
await updateDeploymentStatus(deployment.deploymentId, "done");
return true;
} finally {
@@ -100,6 +123,12 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
error instanceof Error ? error.message : "Unknown error\n",
);
writeStream.end();
await sendDokployBackupNotifications({
type: "error",
// @ts-ignore
errorMessage: error?.message || "Error message not provided",
backupSize: formatBytes(computedBackupSize),
});
await updateDeploymentStatus(deployment.deploymentId, "error");
throw error;
}

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