diff --git a/apps/dokploy/__test__/compose/network/network-root.test.ts b/apps/dokploy/__test__/compose/network/network-root.test.ts index 0d3c841d4..1a6817913 100644 --- a/apps/dokploy/__test__/compose/network/network-root.test.ts +++ b/apps/dokploy/__test__/compose/network/network-root.test.ts @@ -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(); diff --git a/apps/dokploy/__test__/compose/network/network-service.test.ts b/apps/dokploy/__test__/compose/network/network-service.test.ts index e07fa1546..073e61615 100644 --- a/apps/dokploy/__test__/compose/network/network-service.test.ts +++ b/apps/dokploy/__test__/compose/network/network-service.test.ts @@ -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(); diff --git a/apps/dokploy/__test__/compose/service/sevice-volumes-from.test.ts b/apps/dokploy/__test__/compose/service/service-volumes-from.test.ts similarity index 100% rename from apps/dokploy/__test__/compose/service/sevice-volumes-from.test.ts rename to apps/dokploy/__test__/compose/service/service-volumes-from.test.ts diff --git a/apps/dokploy/__test__/env/stack-environment.test.ts b/apps/dokploy/__test__/env/stack-environment.test.ts index 13f5adb53..773adf3ed 100644 --- a/apps/dokploy/__test__/env/stack-environment.test.ts +++ b/apps/dokploy/__test__/env/stack-environment.test.ts @@ -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, "", diff --git a/apps/dokploy/__test__/permissions/enterprise-only-resources.test.ts b/apps/dokploy/__test__/permissions/enterprise-only-resources.test.ts index 3138085e3..bb6f5f18b 100644 --- a/apps/dokploy/__test__/permissions/enterprise-only-resources.test.ts +++ b/apps/dokploy/__test__/permissions/enterprise-only-resources.test.ts @@ -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", diff --git a/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx b/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx index 02db06345..95f849480 100644 --- a/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/cluster/show-cluster-settings.tsx @@ -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; +type AddCommand = z.infer; 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(() => { diff --git a/apps/dokploy/components/dashboard/application/advanced/redirects/handle-redirect.tsx b/apps/dokploy/components/dashboard/application/advanced/redirects/handle-redirect.tsx index 172c042f1..683e0ebba 100644 --- a/apps/dokploy/components/dashboard/application/advanced/redirects/handle-redirect.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/redirects/handle-redirect.tsx @@ -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; +type AddRedirect = z.infer; // 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(); }; diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx index 3cecef1ec..ccf2564b0 100644 --- a/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/show-deployments.tsx @@ -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"; diff --git a/apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx b/apps/dokploy/components/dashboard/application/environment/show-environment.tsx similarity index 100% rename from apps/dokploy/components/dashboard/application/environment/show-enviroment.tsx rename to apps/dokploy/components/dashboard/application/environment/show-environment.tsx diff --git a/apps/dokploy/components/dashboard/application/logs/show.tsx b/apps/dokploy/components/dashboard/application/logs/show.tsx index cbb6bce09..06b257766 100644 --- a/apps/dokploy/components/dashboard/application/logs/show.tsx +++ b/apps/dokploy/components/dashboard/application/logs/show.tsx @@ -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) => { )} - Containers ({containersLenght}) + Containers ({containersLength}) diff --git a/apps/dokploy/components/dashboard/application/patches/index.ts b/apps/dokploy/components/dashboard/application/patches/index.ts index 1854bd3e5..053e644b7 100644 --- a/apps/dokploy/components/dashboard/application/patches/index.ts +++ b/apps/dokploy/components/dashboard/application/patches/index.ts @@ -1,2 +1,2 @@ -export * from "./show-patches"; export * from "./patch-editor"; +export * from "./show-patches"; diff --git a/apps/dokploy/components/dashboard/application/volume-backups/handle-volume-backups.tsx b/apps/dokploy/components/dashboard/application/volume-backups/handle-volume-backups.tsx index 0d87080d7..f3d60f27f 100644 --- a/apps/dokploy/components/dashboard/application/volume-backups/handle-volume-backups.tsx +++ b/apps/dokploy/components/dashboard/application/volume-backups/handle-volume-backups.tsx @@ -483,7 +483,7 @@ export const HandleVolumeBackups = ({ - 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 @@ -518,7 +518,7 @@ export const HandleVolumeBackups = ({ - 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 diff --git a/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx b/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx index 159ab3485..4c3067b15 100644 --- a/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx +++ b/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx @@ -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) => { )} - Containers ({containersLenght}) + Containers ({containersLength}) diff --git a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx index 00647aea7..7b212acb9 100644 --- a/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx +++ b/apps/dokploy/components/dashboard/database/backups/restore-backup.tsx @@ -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, }, ); diff --git a/apps/dokploy/components/dashboard/deployments/show-queue-table.tsx b/apps/dokploy/components/dashboard/deployments/show-queue-table.tsx index e46b33a6a..22b132f16 100644 --- a/apps/dokploy/components/dashboard/deployments/show-queue-table.tsx +++ b/apps/dokploy/components/dashboard/deployments/show-queue-table.tsx @@ -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 { diff --git a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx index 9d4f47c4a..bed5c6f5d 100644 --- a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx @@ -103,7 +103,7 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) { > {" "}
- {/* Icon to expand the log item maybe implement a colapsible later */} + {/* Icon to expand the log item maybe implement a collapsible later */} {/* */} {tooltip(color, rawTimestamp)} {!noTimestamp && ( diff --git a/apps/dokploy/components/dashboard/docker/show/colums.tsx b/apps/dokploy/components/dashboard/docker/show/columns.tsx similarity index 100% rename from apps/dokploy/components/dashboard/docker/show/colums.tsx rename to apps/dokploy/components/dashboard/docker/show/columns.tsx diff --git a/apps/dokploy/components/dashboard/docker/show/show-containers.tsx b/apps/dokploy/components/dashboard/docker/show/show-containers.tsx index 69b0a0da2..8a19566e8 100644 --- a/apps/dokploy/components/dashboard/docker/show/show-containers.tsx +++ b/apps/dokploy/components/dashboard/docker/show/show-containers.tsx @@ -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]; diff --git a/apps/dokploy/components/dashboard/monitoring/free/container/docker-block-chart.tsx b/apps/dokploy/components/dashboard/monitoring/free/container/docker-block-chart.tsx index 718ddafa4..6dc5cd90c 100644 --- a/apps/dokploy/components/dashboard/monitoring/free/container/docker-block-chart.tsx +++ b/apps/dokploy/components/dashboard/monitoring/free/container/docker-block-chart.tsx @@ -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}`, diff --git a/apps/dokploy/components/dashboard/monitoring/free/container/docker-cpu-chart.tsx b/apps/dokploy/components/dashboard/monitoring/free/container/docker-cpu-chart.tsx index c24a63638..67404268b 100644 --- a/apps/dokploy/components/dashboard/monitoring/free/container/docker-cpu-chart.tsx +++ b/apps/dokploy/components/dashboard/monitoring/free/container/docker-cpu-chart.tsx @@ -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, diff --git a/apps/dokploy/components/dashboard/monitoring/free/container/docker-disk-chart.tsx b/apps/dokploy/components/dashboard/monitoring/free/container/docker-disk-chart.tsx index 5fe62154c..58cefe6b3 100644 --- a/apps/dokploy/components/dashboard/monitoring/free/container/docker-disk-chart.tsx +++ b/apps/dokploy/components/dashboard/monitoring/free/container/docker-disk-chart.tsx @@ -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}`, diff --git a/apps/dokploy/components/dashboard/monitoring/free/container/docker-memory-chart.tsx b/apps/dokploy/components/dashboard/monitoring/free/container/docker-memory-chart.tsx index 34a3913a7..226623fa2 100644 --- a/apps/dokploy/components/dashboard/monitoring/free/container/docker-memory-chart.tsx +++ b/apps/dokploy/components/dashboard/monitoring/free/container/docker-memory-chart.tsx @@ -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}`, diff --git a/apps/dokploy/components/dashboard/monitoring/free/container/docker-network-chart.tsx b/apps/dokploy/components/dashboard/monitoring/free/container/docker-network-chart.tsx index 5e2414cea..8dafcb465 100644 --- a/apps/dokploy/components/dashboard/monitoring/free/container/docker-network-chart.tsx +++ b/apps/dokploy/components/dashboard/monitoring/free/container/docker-network-chart.tsx @@ -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}`, diff --git a/apps/dokploy/components/dashboard/monitoring/free/container/show-free-container-monitoring.tsx b/apps/dokploy/components/dashboard/monitoring/free/container/show-free-container-monitoring.tsx index 42bb361bb..6e572c224 100644 --- a/apps/dokploy/components/dashboard/monitoring/free/container/show-free-container-monitoring.tsx +++ b/apps/dokploy/components/dashboard/monitoring/free/container/show-free-container-monitoring.tsx @@ -124,7 +124,7 @@ export const ContainerFreeMonitoring = ({ refetchOnWindowFocus: false, }, ); - const [acummulativeData, setAcummulativeData] = useState({ + const [accumulativeData, setAccumulativeData] = useState({ 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%]" /> - +
@@ -252,7 +252,7 @@ export const ContainerFreeMonitoring = ({ className="w-[100%]" /> @@ -294,7 +294,7 @@ export const ContainerFreeMonitoring = ({ {`Read: ${currentData.block.value.readMb} / Write: ${currentData.block.value.writeMb} `} - + @@ -307,7 +307,7 @@ export const ContainerFreeMonitoring = ({ {`In MB: ${currentData.network.value.inputMb} / Out MB: ${currentData.network.value.outputMb} `} - + diff --git a/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx b/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx index 76c54cdfa..be29ae4ca 100644 --- a/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx +++ b/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx @@ -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>(new Set()); return (
@@ -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 (
{ {index + 1}. {certificate.name} + {commonName && ( + + CN: {commonName} + + )} {chainInfo.isChain && ( -
- - - Chain ({chainInfo.count}) - +
+ + {isExpanded && ( +
+ {chainDetails?.map((cert) => ( +
+ + {cert.label} + + {cert.commonName && ( + + CN: {cert.commonName} + + )} + + {cert.message} + +
+ ))} +
+ )}
)}
{ + 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: + 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: + len = (len << 8) + der[pos++]; + } + } + return { length: len, offset: pos }; + } + + // Helper: skip a field + function skipField(pos: number): number { + // biome-ignore lint/style/noParameterAssign: + 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, + }; + }); +}; diff --git a/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx b/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx index 25a3a1048..e7ecf92b2 100644 --- a/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx +++ b/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx @@ -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({ diff --git a/apps/dokploy/components/dashboard/settings/git/gitlab/add-gitlab-provider.tsx b/apps/dokploy/components/dashboard/settings/git/gitlab/add-gitlab-provider.tsx index 7c637f5ef..b48f8253b 100644 --- a/apps/dokploy/components/dashboard/settings/git/gitlab/add-gitlab-provider.tsx +++ b/apps/dokploy/components/dashboard/settings/git/gitlab/add-gitlab-provider.tsx @@ -283,7 +283,7 @@ export const AddGitlabProvider = () => { diff --git a/apps/dokploy/components/dashboard/settings/git/gitlab/edit-gitlab-provider.tsx b/apps/dokploy/components/dashboard/settings/git/gitlab/edit-gitlab-provider.tsx index e48df084b..43c174055 100644 --- a/apps/dokploy/components/dashboard/settings/git/gitlab/edit-gitlab-provider.tsx +++ b/apps/dokploy/components/dashboard/settings/git/gitlab/edit-gitlab-provider.tsx @@ -192,7 +192,7 @@ export const EditGitlabProvider = ({ gitlabId }: Props) => { diff --git a/apps/dokploy/components/dashboard/settings/git/show-git-providers.tsx b/apps/dokploy/components/dashboard/settings/git/show-git-providers.tsx index a96bcf26c..2827a9611 100644 --- a/apps/dokploy/components/dashboard/settings/git/show-git-providers.tsx +++ b/apps/dokploy/components/dashboard/settings/git/show-git-providers.tsx @@ -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 = () => { )}
+ {!gitProvider.isOwner && ( + + + Shared + + )}
+ {gitProvider.isOwner && ( + + + +
+ + { + await toggleShare({ + gitProviderId: + gitProvider.gitProviderId, + sharedWithOrganization: checked, + }) + .then(() => { + toast.success( + checked + ? "Provider shared with organization" + : "Provider unshared", + ); + refetch(); + }) + .catch(() => { + toast.error( + "Error updating sharing", + ); + }); + }} + /> +
+
+ + Share with entire organization + +
+
+ )} + {isBitbucket && gitProvider.bitbucket?.appPassword && !gitProvider.bitbucket?.apiToken ? ( @@ -222,62 +284,71 @@ export const ShowGitProviders = () => {
)} - {isGithub && haveGithubRequirements && ( - - )} + {gitProvider.isOwner && ( + <> + {isGithub && haveGithubRequirements && ( + + )} - {isGitlab && ( - - )} + {isGitlab && ( + + )} - {isBitbucket && ( - - )} + {isBitbucket && ( + + )} - {isGitea && ( - - )} + {isGitea && ( + + )} - { - await mutateAsync({ - gitProviderId: gitProvider.gitProviderId, - }) - .then(() => { - toast.success( - "Git Provider deleted successfully", - ); - refetch(); - }) - .catch(() => { - toast.error( - "Error deleting Git Provider", - ); - }); - }} - > - - + { + await mutateAsync({ + gitProviderId: + gitProvider.gitProviderId, + }) + .then(() => { + toast.success( + "Git Provider deleted successfully", + ); + refetch(); + }) + .catch(() => { + toast.error( + "Error deleting Git Provider", + ); + }); + }} + > + + + + )}
diff --git a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx index 28d762c1c..37463fd62 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/handle-notifications.tsx @@ -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) => { )} /> + ( + +
+ Dokploy Backup + + Trigger the action when a dokploy backup is created. + +
+ + + +
+ )} + /> + {
  • 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){" "}
  • 2. Add The SSH Key to Server Manually
  • diff --git a/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx b/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx index 859098394..1326c554a 100644 --- a/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx @@ -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 (
    - {query?.success && isCloud && } + {query?.success && isCloud && }
    diff --git a/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/welcome-suscription.tsx b/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/welcome-subscription.tsx similarity index 99% rename from apps/dokploy/components/dashboard/settings/servers/welcome-stripe/welcome-suscription.tsx rename to apps/dokploy/components/dashboard/settings/servers/welcome-stripe/welcome-subscription.tsx index 004f79f74..91dce72b9 100644 --- a/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/welcome-suscription.tsx +++ b/apps/dokploy/components/dashboard/settings/servers/welcome-stripe/welcome-subscription.tsx @@ -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); diff --git a/apps/dokploy/components/dashboard/settings/users/add-permissions.tsx b/apps/dokploy/components/dashboard/settings/users/add-permissions.tsx index fdcf51ad7..ee2d1d44c 100644 --- a/apps/dokploy/components/dashboard/settings/users/add-permissions.tsx +++ b/apps/dokploy/components/dashboard/settings/users/add-permissions.tsx @@ -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) => { )} /> + {haveValidLicense ? ( + ( + +
    + Git Providers + + Select the Git Providers that the user can access + +
    + {gitProviders?.length === 0 && ( +

    + No git providers found +

    + )} +
    + {gitProviders?.map((provider) => ( + ( + + + { + if (checked) { + field.onChange([ + ...(field.value || []), + provider.gitProviderId, + ]); + } else { + field.onChange( + field.value?.filter( + (v) => v !== provider.gitProviderId, + ), + ); + } + }} + /> + +
    + + {provider.name} + + + ({provider.providerType}) + +
    +
    + )} + /> + ))} +
    + +
    + )} + /> + ) : ( +
    + +
    + )}