From 126c3be60626e0b363b72c447ea4582799acadf5 Mon Sep 17 00:00:00 2001 From: Fang-Pen Lin Date: Wed, 19 Nov 2025 17:37:26 -0800 Subject: [PATCH] Add scaffolding for DNS made easy --- .../dns-made-easy-connection-router.ts | 52 +++++++++ .../routes/v1/app-connection-routers/index.ts | 2 + .../app-connection/app-connection-enums.ts | 1 + .../app-connection/app-connection-fns.ts | 8 ++ .../app-connection/app-connection-maps.ts | 2 + .../app-connection/app-connection-service.ts | 4 + .../dns-made-easy-connection-enum.ts | 3 + .../dns-made-easy-connection-fns.ts | 101 ++++++++++++++++++ .../dns-made-easy-connection-schema.ts | 64 +++++++++++ .../dns-made-easy-connection-service.ts | 32 ++++++ .../dns-made-easy-connection-types.ts | 24 +++++ 11 files changed, 293 insertions(+) create mode 100644 backend/src/server/routes/v1/app-connection-routers/dns-made-easy-connection-router.ts create mode 100644 backend/src/services/app-connection/dns-made-easy/dns-made-easy-connection-enum.ts create mode 100644 backend/src/services/app-connection/dns-made-easy/dns-made-easy-connection-fns.ts create mode 100644 backend/src/services/app-connection/dns-made-easy/dns-made-easy-connection-schema.ts create mode 100644 backend/src/services/app-connection/dns-made-easy/dns-made-easy-connection-service.ts create mode 100644 backend/src/services/app-connection/dns-made-easy/dns-made-easy-connection-types.ts diff --git a/backend/src/server/routes/v1/app-connection-routers/dns-made-easy-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/dns-made-easy-connection-router.ts new file mode 100644 index 000000000..398bd860e --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/dns-made-easy-connection-router.ts @@ -0,0 +1,52 @@ +import z from "zod"; + +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreateDNSMadeEasyConnectionSchema, + SanitizedDNSMadeEasyConnectionSchema, + UpdateDNSMadeEasyConnectionSchema +} from "@app/services/app-connection/dns-made-easy/dns-made-easy-connection-schema"; +import { AuthMode } from "@app/services/auth/auth-type"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerDNSMadeEasyConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.DNSMadeEasy, + server, + sanitizedResponseSchema: SanitizedDNSMadeEasyConnectionSchema, + createSchema: CreateDNSMadeEasyConnectionSchema, + updateSchema: UpdateDNSMadeEasyConnectionSchema + }); + + // The below endpoints are not exposed and for Infisical App use + server.route({ + method: "GET", + url: `/:connectionId/dns-made-easy-zones`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z + .object({ + id: z.string(), + name: z.string() + }) + .array() + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const zones = await server.services.appConnection.dnsMadeEasy.listZones(connectionId, req.permission); + return zones; + } + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index aa1d671b6..d7a4065fd 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -16,6 +16,7 @@ import { registerCamundaConnectionRouter } from "./camunda-connection-router"; import { registerChecklyConnectionRouter } from "./checkly-connection-router"; import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router"; import { registerDatabricksConnectionRouter } from "./databricks-connection-router"; +import { registerDNSMadeEasyConnectionRouter } from "./dns-made-easy-connection-router"; import { registerDigitalOceanConnectionRouter } from "./digital-ocean-connection-router"; import { registerFlyioConnectionRouter } from "./flyio-connection-router"; import { registerGcpConnectionRouter } from "./gcp-connection-router"; @@ -78,6 +79,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record { getFlyioConnectionListItem(), getGitLabConnectionListItem(), getCloudflareConnectionListItem(), + getDNSMadeEasyConnectionListItem(), getZabbixConnectionListItem(), getRailwayConnectionListItem(), getBitbucketConnectionListItem(), @@ -339,6 +345,7 @@ export const validateAppConnectionCredentials = async ( [AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.DNSMadeEasy]: validateDNSMadeEasyConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Zabbix]: validateZabbixConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Railway]: validateRailwayConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator, @@ -483,6 +490,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.Flyio]: platformManagedCredentialsNotSupported, [AppConnection.GitLab]: platformManagedCredentialsNotSupported, [AppConnection.Cloudflare]: platformManagedCredentialsNotSupported, + [AppConnection.DNSMadeEasy]: platformManagedCredentialsNotSupported, [AppConnection.Zabbix]: platformManagedCredentialsNotSupported, [AppConnection.Railway]: platformManagedCredentialsNotSupported, [AppConnection.Bitbucket]: platformManagedCredentialsNotSupported, diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index 5b8cc3fc1..27d6a27a8 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -32,6 +32,7 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.Flyio]: "Fly.io", [AppConnection.GitLab]: "GitLab", [AppConnection.Cloudflare]: "Cloudflare", + [AppConnection.DNSMadeEasy]: "DNS Made Easy", [AppConnection.Zabbix]: "Zabbix", [AppConnection.Railway]: "Railway", [AppConnection.Bitbucket]: "Bitbucket", @@ -77,6 +78,7 @@ export const APP_CONNECTION_PLAN_MAP: Record { + return { + name: "DNS Made Easy" as const, + app: AppConnection.DNSMadeEasy as const, + methods: Object.values(DNSMadeEasyConnectionMethod) as [DNSMadeEasyConnectionMethod.APIKey] + }; +}; + +export const listDNSMadeEasyZones = async (appConnection: TDNSMadeEasyConnection): Promise => { + // TODO: Implement DNS Made Easy zones listing + // This should call the DNS Made Easy API to list all zones/domains + // Example API endpoint: GET https://api.dnsmadeeasy.com/V2.0/dns/managed + // Authentication: Use API key and secret from appConnection.credentials + // Return format: Array of { id: string, name: string } + + if (appConnection.method !== DNSMadeEasyConnectionMethod.APIKey) { + throw new Error("Unsupported DNS Made Easy connection method"); + } + + const { + credentials: { apiKey, apiSecret } + } = appConnection; + + // TODO: Make API request to DNS Made Easy + // const { data } = await request.get<{ data: { id: number; name: string }[] }>( + // `${IntegrationUrls.DNS_MADE_EASY_API_URL}/V2.0/dns/managed`, + // { + // headers: { + // "x-dnsme-apiKey": apiKey, + // "x-dnsme-hmac": generateHMAC(apiSecret, ...), + // "x-dnsme-requestDate": requestDate, + // Accept: "application/json" + // } + // } + // ); + + // TODO: Transform response data to match TDNSMadeEasyZone format + // return data.data.map((zone) => ({ + // name: zone.name, + // id: zone.id.toString() + // })); + + throw new Error("Not implemented: listDNSMadeEasyZones"); +}; + +export const validateDNSMadeEasyConnectionCredentials = async (config: TDNSMadeEasyConnectionConfig) => { + // TODO: Implement DNS Made Easy credentials validation + // This should call the DNS Made Easy API to validate the API key and secret + // Example API endpoint: GET https://api.dnsmadeeasy.com/V2.0/account + // Authentication: Use API key and secret from config.credentials + + if (config.method !== DNSMadeEasyConnectionMethod.APIKey) { + throw new Error("Unsupported DNS Made Easy connection method"); + } + + const { apiKey, apiSecret } = config.credentials; + + try { + // TODO: Make API request to validate credentials + // const resp = await request.get(`${IntegrationUrls.DNS_MADE_EASY_API_URL}/V2.0/account`, { + // headers: { + // "x-dnsme-apiKey": apiKey, + // "x-dnsme-hmac": generateHMAC(apiSecret, ...), + // "x-dnsme-requestDate": requestDate, + // Accept: "application/json" + // } + // }); + // TODO: Validate response + // if (resp.data === null || !resp.data.id) { + // throw new BadRequestError({ + // message: "Unable to validate connection: Invalid API credentials provided." + // }); + // } + } catch (error: unknown) { + if (error instanceof AxiosError) { + throw new BadRequestError({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + message: `Failed to validate credentials: ${error.response?.data?.error?.[0] || error.message || "Unknown error"}` + }); + } + throw new BadRequestError({ + message: "Unable to validate connection: verify credentials" + }); + } + + return config.credentials; +}; diff --git a/backend/src/services/app-connection/dns-made-easy/dns-made-easy-connection-schema.ts b/backend/src/services/app-connection/dns-made-easy/dns-made-easy-connection-schema.ts new file mode 100644 index 000000000..eb39b6235 --- /dev/null +++ b/backend/src/services/app-connection/dns-made-easy/dns-made-easy-connection-schema.ts @@ -0,0 +1,64 @@ +import z from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps"; +import { DNSMadeEasyConnectionMethod } from "./dns-made-easy-connection-enum"; + +export const DNSMadeEasyConnectionApiKeyCredentialsSchema = z.object({ + apiKey: z.string().trim().min(1, "API key required").max(256, "API key cannot exceed 256 characters"), + apiSecret: z.string().trim().min(1, "API secret required").max(256, "API secret cannot exceed 256 characters") +}); + +const BaseDNSMadeEasyConnectionSchema = BaseAppConnectionSchema.extend({ + app: z.literal(AppConnection.DNSMadeEasy) +}); + +export const DNSMadeEasyConnectionSchema = BaseDNSMadeEasyConnectionSchema.extend({ + method: z.literal(DNSMadeEasyConnectionMethod.APIKey), + credentials: DNSMadeEasyConnectionApiKeyCredentialsSchema +}); + +export const SanitizedDNSMadeEasyConnectionSchema = z.discriminatedUnion("method", [ + BaseDNSMadeEasyConnectionSchema.extend({ + method: z.literal(DNSMadeEasyConnectionMethod.APIKey), + credentials: DNSMadeEasyConnectionApiKeyCredentialsSchema.pick({ apiKey: true }) + }).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.DNSMadeEasy]} (API Key)` })) +]); + +export const ValidateDNSMadeEasyConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z + .literal(DNSMadeEasyConnectionMethod.APIKey) + .describe(AppConnections.CREATE(AppConnection.DNSMadeEasy).method), + credentials: DNSMadeEasyConnectionApiKeyCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.DNSMadeEasy).credentials + ) + }) +]); + +export const CreateDNSMadeEasyConnectionSchema = ValidateDNSMadeEasyConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.DNSMadeEasy) +); + +export const UpdateDNSMadeEasyConnectionSchema = z + .object({ + credentials: DNSMadeEasyConnectionApiKeyCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.DNSMadeEasy).credentials + ) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.DNSMadeEasy)); + +export const DNSMadeEasyConnectionListItemSchema = z + .object({ + name: z.literal("DNS Made Easy"), + app: z.literal(AppConnection.DNSMadeEasy), + methods: z.nativeEnum(DNSMadeEasyConnectionMethod).array() + }) + .describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.DNSMadeEasy] })); diff --git a/backend/src/services/app-connection/dns-made-easy/dns-made-easy-connection-service.ts b/backend/src/services/app-connection/dns-made-easy/dns-made-easy-connection-service.ts new file mode 100644 index 000000000..d5da33c93 --- /dev/null +++ b/backend/src/services/app-connection/dns-made-easy/dns-made-easy-connection-service.ts @@ -0,0 +1,32 @@ +import { logger } from "@app/lib/logger"; +import { OrgServiceActor } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { listDNSMadeEasyZones } from "./dns-made-easy-connection-fns"; +import { TDNSMadeEasyConnection } from "./dns-made-easy-connection-types"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +export const dnsMadeEasyConnectionService = (getAppConnection: TGetAppConnectionFunc) => { + const listZones = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.DNSMadeEasy, connectionId, actor); + try { + const zones = await listDNSMadeEasyZones(appConnection); + return zones; + } catch (error) { + logger.error( + error, + `Failed to list DNS Made Easy zones for DNS Made Easy connection [connectionId=${connectionId}]` + ); + return []; + } + }; + + return { + listZones + }; +}; diff --git a/backend/src/services/app-connection/dns-made-easy/dns-made-easy-connection-types.ts b/backend/src/services/app-connection/dns-made-easy/dns-made-easy-connection-types.ts new file mode 100644 index 000000000..dabb72771 --- /dev/null +++ b/backend/src/services/app-connection/dns-made-easy/dns-made-easy-connection-types.ts @@ -0,0 +1,24 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { DNSMadeEasyConnectionSchema, CreateDNSMadeEasyConnectionSchema } from "./dns-made-easy-connection-schema"; + +export type TDNSMadeEasyConnection = z.infer; + +export type TDNSMadeEasyConnectionInput = z.infer & { + app: AppConnection.DNSMadeEasy; +}; + +export type TDNSMadeEasyConnectionConfig = DiscriminativePick< + TDNSMadeEasyConnectionInput, + "method" | "app" | "credentials" +> & { + orgId: string; +}; + +export type TDNSMadeEasyZone = { + id: string; + name: string; +};