refactor: centralize destination validation

This commit is contained in:
Furox 2026-09-07 01:50:28 +03:00 committed by Furox88
parent 79050dd970
commit 4b753b2180
4 changed files with 113 additions and 191 deletions

View File

@ -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) => {
</DialogContent>
</Dialog>
);
};
};

View File

@ -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,
});
}
};

View File

@ -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;
};

View File

@ -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};
`;
};
};