diff --git a/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx b/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx index ac125abb1..9c6648b07 100644 --- a/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx +++ b/apps/dokploy/components/dashboard/settings/destination/handle-destinations.tsx @@ -1,15 +1,11 @@ import { ADDITIONAL_FLAG_ERROR, ADDITIONAL_FLAG_REGEX, - FTP_TLS_CONFLICT_ERROR, - FTP_TLS_REQUIRED_ERROR, + getDestinationValidationIssues, getFtpTlsState, - hasSftpHostKeyVerification, isNamedRcloneDestinationProvider, + isRcloneDestinationProvider, RCLONE_DESTINATION_PROVIDERS, - RCLONE_REMOTE_NAME_ERROR, - RCLONE_REMOTE_NAME_REGEX, - SFTP_HOST_KEY_REQUIRED_ERROR, } from "@dokploy/server/db/validations/destination"; import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema"; import { PenBoxIcon, PlusIcon, Trash2 } from "lucide-react"; @@ -72,73 +68,22 @@ const addDestination = z .optional(), }) .superRefine((data, ctx) => { - if (isNamedRcloneDestinationProvider(data.provider)) { - if (!RCLONE_REMOTE_NAME_REGEX.test(data.endpoint.trim())) { - ctx.addIssue({ - code: "custom", - path: ["endpoint"], - message: RCLONE_REMOTE_NAME_ERROR, - }); - } - return; + const additionalFlags = data.additionalFlags?.map((flag) => flag.value) ?? []; + for (const issue of getDestinationValidationIssues({ + provider: data.provider, + accessKey: data.accessKeyId, + region: data.region, + endpoint: data.endpoint, + additionalFlags, + })) { + ctx.addIssue({ + code: "custom", + path: [issue.field === "accessKey" ? "accessKeyId" : issue.field], + message: issue.message, + }); } - if ( - data.provider === RCLONE_DESTINATION_PROVIDERS.FTP || - data.provider === RCLONE_DESTINATION_PROVIDERS.SFTP - ) { - if (!data.endpoint.trim()) { - ctx.addIssue({ - code: "custom", - path: ["endpoint"], - message: "Host is required", - }); - } - if (!data.accessKeyId.trim()) { - ctx.addIssue({ - code: "custom", - path: ["accessKeyId"], - message: "Username is required", - }); - } - if (data.region.trim()) { - const port = Number(data.region); - if (!Number.isInteger(port) || port < 1 || port > 65535) { - ctx.addIssue({ - code: "custom", - path: ["region"], - message: "Port must be an integer between 1 and 65535", - }); - } - } - - const flags = data.additionalFlags?.map((flag) => flag.value) ?? []; - if (data.provider === RCLONE_DESTINATION_PROVIDERS.FTP) { - const { implicitTlsEnabled, explicitTlsEnabled } = - getFtpTlsState(flags); - if (!implicitTlsEnabled && !explicitTlsEnabled) { - ctx.addIssue({ - code: "custom", - path: ["additionalFlags"], - message: FTP_TLS_REQUIRED_ERROR, - }); - } - if (implicitTlsEnabled && explicitTlsEnabled) { - ctx.addIssue({ - code: "custom", - path: ["additionalFlags"], - message: FTP_TLS_CONFLICT_ERROR, - }); - } - } else if (!hasSftpHostKeyVerification(flags)) { - ctx.addIssue({ - code: "custom", - path: ["additionalFlags"], - message: SFTP_HOST_KEY_REQUIRED_ERROR, - }); - } - return; - } + if (isRcloneDestinationProvider(data.provider)) return; for (const [field, label] of [ ["accessKeyId", "Access Key Id"], @@ -679,4 +624,4 @@ export const HandleDestinations = ({ destinationId }: Props) => { ); -}; +}; \ No newline at end of file diff --git a/packages/server/src/db/schema/destination.ts b/packages/server/src/db/schema/destination.ts index 1e961b9d6..117cca536 100644 --- a/packages/server/src/db/schema/destination.ts +++ b/packages/server/src/db/schema/destination.ts @@ -6,17 +6,7 @@ import { z } from "zod"; import { ADDITIONAL_FLAG_ERROR, ADDITIONAL_FLAG_REGEX, - FTP_CERTIFICATE_VERIFICATION_REQUIRED_ERROR, - FTP_TLS_CONFLICT_ERROR, - FTP_TLS_REQUIRED_ERROR, - getFtpTlsState, - hasDisabledFtpCertificateVerification, - hasSftpHostKeyVerification, - isNamedRcloneDestinationProvider, - RCLONE_DESTINATION_PROVIDERS, - RCLONE_REMOTE_NAME_ERROR, - RCLONE_REMOTE_NAME_REGEX, - SFTP_HOST_KEY_REQUIRED_ERROR, + getDestinationValidationIssues, } from "../validations/destination"; import { organization } from "./account"; import { backups } from "./backups"; @@ -75,84 +65,11 @@ const validateDestination = ( }, ctx: z.RefinementCtx, ) => { - if (isNamedRcloneDestinationProvider(data.provider)) { - const remoteName = data.endpoint?.trim() || ""; - if (!RCLONE_REMOTE_NAME_REGEX.test(remoteName)) { - ctx.addIssue({ - code: "custom", - path: ["endpoint"], - message: RCLONE_REMOTE_NAME_ERROR, - }); - } - return; - } - - if ( - data.provider === RCLONE_DESTINATION_PROVIDERS.FTP || - data.provider === RCLONE_DESTINATION_PROVIDERS.SFTP - ) { - if (!data.endpoint?.trim()) { - ctx.addIssue({ - code: "custom", - path: ["endpoint"], - message: "Host is required", - }); - } - if (!data.accessKey?.trim()) { - ctx.addIssue({ - code: "custom", - path: ["accessKey"], - message: "Username is required", - }); - } - if (data.region?.trim()) { - const port = Number(data.region); - if (!Number.isInteger(port) || port < 1 || port > 65535) { - ctx.addIssue({ - code: "custom", - path: ["region"], - message: "Port must be an integer between 1 and 65535", - }); - } - } - } - - if (data.provider === RCLONE_DESTINATION_PROVIDERS.FTP) { - const { implicitTlsEnabled, explicitTlsEnabled } = getFtpTlsState( - data.additionalFlags, - ); - - if (!implicitTlsEnabled && !explicitTlsEnabled) { - ctx.addIssue({ - code: "custom", - path: ["additionalFlags"], - message: FTP_TLS_REQUIRED_ERROR, - }); - } - if (implicitTlsEnabled && explicitTlsEnabled) { - ctx.addIssue({ - code: "custom", - path: ["additionalFlags"], - message: FTP_TLS_CONFLICT_ERROR, - }); - } - if (hasDisabledFtpCertificateVerification(data.additionalFlags)) { - ctx.addIssue({ - code: "custom", - path: ["additionalFlags"], - message: FTP_CERTIFICATE_VERIFICATION_REQUIRED_ERROR, - }); - } - } - - if ( - data.provider === RCLONE_DESTINATION_PROVIDERS.SFTP && - !hasSftpHostKeyVerification(data.additionalFlags) - ) { + for (const issue of getDestinationValidationIssues(data)) { ctx.addIssue({ code: "custom", - path: ["additionalFlags"], - message: SFTP_HOST_KEY_REQUIRED_ERROR, + path: [issue.field], + message: issue.message, }); } }; diff --git a/packages/server/src/db/validations/destination.ts b/packages/server/src/db/validations/destination.ts index 39b2c32de..763a9f0b4 100644 --- a/packages/server/src/db/validations/destination.ts +++ b/packages/server/src/db/validations/destination.ts @@ -104,3 +104,80 @@ export const hasSftpHostKeyVerification = ( const value = knownHostsFlags[0]?.slice(prefix.length).trim() ?? ""; return value.length > 0 && value !== "none"; }; + +type DestinationValidationField = + | "endpoint" + | "accessKey" + | "region" + | "additionalFlags"; + +export interface DestinationValidationIssue { + field: DestinationValidationField; + message: string; +} + +export interface DestinationValidationInput { + provider?: string | null; + accessKey?: string; + region?: string; + endpoint?: string; + additionalFlags?: readonly string[] | null; +} + +export const getDestinationValidationIssues = ( + data: DestinationValidationInput, +): DestinationValidationIssue[] => { + const issues: DestinationValidationIssue[] = []; + const provider = data.provider; + const flags = data.additionalFlags ?? []; + + if (isNamedRcloneDestinationProvider(provider)) { + if (!RCLONE_REMOTE_NAME_REGEX.test(data.endpoint?.trim() || "")) { + issues.push({ field: "endpoint", message: RCLONE_REMOTE_NAME_ERROR }); + } + return issues; + } + + if ( + provider !== RCLONE_DESTINATION_PROVIDERS.FTP && + provider !== RCLONE_DESTINATION_PROVIDERS.SFTP + ) { + return issues; + } + + if (!data.endpoint?.trim()) { + issues.push({ field: "endpoint", message: "Host is required" }); + } + if (!data.accessKey?.trim()) { + issues.push({ field: "accessKey", message: "Username is required" }); + } + if (data.region?.trim()) { + const port = Number(data.region); + if (!Number.isInteger(port) || port < 1 || port > 65535) { + issues.push({ + field: "region", + message: "Port must be an integer between 1 and 65535", + }); + } + } + + if (provider === RCLONE_DESTINATION_PROVIDERS.FTP) { + const { implicitTlsEnabled, explicitTlsEnabled } = getFtpTlsState(flags); + if (!implicitTlsEnabled && !explicitTlsEnabled) { + issues.push({ field: "additionalFlags", message: FTP_TLS_REQUIRED_ERROR }); + } + if (implicitTlsEnabled && explicitTlsEnabled) { + issues.push({ field: "additionalFlags", message: FTP_TLS_CONFLICT_ERROR }); + } + if (hasDisabledFtpCertificateVerification(flags)) { + issues.push({ + field: "additionalFlags", + message: FTP_CERTIFICATE_VERIFICATION_REQUIRED_ERROR, + }); + } + } else if (!hasSftpHostKeyVerification(flags)) { + issues.push({ field: "additionalFlags", message: SFTP_HOST_KEY_REQUIRED_ERROR }); + } + + return issues; +}; diff --git a/packages/server/src/utils/backups/utils.ts b/packages/server/src/utils/backups/utils.ts index 71d434a8f..4e6c7e059 100644 --- a/packages/server/src/utils/backups/utils.ts +++ b/packages/server/src/utils/backups/utils.ts @@ -1,16 +1,11 @@ import { ADDITIONAL_FLAG_ERROR, ADDITIONAL_FLAG_REGEX, - FTP_CERTIFICATE_VERIFICATION_REQUIRED_ERROR, - FTP_TLS_CONFLICT_ERROR, - FTP_TLS_REQUIRED_ERROR, + getDestinationValidationIssues, getFtpTlsState, - hasDisabledFtpCertificateVerification, - hasSftpHostKeyVerification, isNamedRcloneDestinationProvider, + isRcloneDestinationProvider, RCLONE_DESTINATION_PROVIDERS, - RCLONE_REMOTE_NAME_REGEX, - SFTP_HOST_KEY_REQUIRED_ERROR, } from "@dokploy/server/db/validations/destination"; import { logger } from "@dokploy/server/lib/logger"; import type { BackupSchedule } from "@dokploy/server/services/backup"; @@ -147,15 +142,16 @@ export const getRclonePathAndFlags = async ( const provider = destination.provider; const additionalFlags = getValidatedAdditionalFlags(destination); + if (isRcloneDestinationProvider(provider)) { + const [issue] = getDestinationValidationIssues(destination); + if (issue) throw new Error(issue.message); + } + if (isNamedRcloneDestinationProvider(provider)) { - const remoteName = destination.endpoint.trim(); - if (!RCLONE_REMOTE_NAME_REGEX.test(remoteName)) { - throw new Error("Invalid rclone remote name"); - } const remotePath = joinRclonePath(destination.bucket, path); return { flags: additionalFlags, - path: `${remoteName}:${remotePath}`, + path: `${destination.endpoint.trim()}:${remotePath}`, }; } @@ -165,25 +161,12 @@ export const getRclonePathAndFlags = async ( ) { const backend = provider === RCLONE_DESTINATION_PROVIDERS.FTP ? "ftp" : "sftp"; - - let defaultPort = "22"; - if (provider === RCLONE_DESTINATION_PROVIDERS.FTP) { - const { implicitTlsEnabled, explicitTlsEnabled } = - getFtpTlsState(additionalFlags); - if (!implicitTlsEnabled && !explicitTlsEnabled) { - throw new Error(FTP_TLS_REQUIRED_ERROR); - } - if (implicitTlsEnabled && explicitTlsEnabled) { - throw new Error(FTP_TLS_CONFLICT_ERROR); - } - if (hasDisabledFtpCertificateVerification(additionalFlags)) { - throw new Error(FTP_CERTIFICATE_VERIFICATION_REQUIRED_ERROR); - } - defaultPort = implicitTlsEnabled ? "990" : "21"; - } else if (!hasSftpHostKeyVerification(additionalFlags)) { - throw new Error(SFTP_HOST_KEY_REQUIRED_ERROR); - } - + const defaultPort = + provider === RCLONE_DESTINATION_PROVIDERS.FTP + ? getFtpTlsState(additionalFlags).implicitTlsEnabled + ? "990" + : "21" + : "22"; const port = destination.region.trim() || defaultPort; const flags = [ `--${backend}-host=${quote([destination.endpoint.trim()])}`, @@ -429,4 +412,4 @@ export const getBackupCommand = ( echo "[$(date)] ✅ Backup uploaded successfully" >> ${logPath}; echo "Backup done ✅" >> ${logPath}; `; -}; +}; \ No newline at end of file