mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #4912 from Infisical/PKI-58-add-dns-made-easy-for-external-ca-acme
[PKI-58] Add dns made easy for external ca acme
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
Feature: External CA
|
||||
|
||||
Scenario: Issue a certificate from an external CA
|
||||
@cloudflare
|
||||
Scenario Outline: Issue a certificate from an external CA with Cloudflare
|
||||
Given I create a Cloudflare connection as cloudflare
|
||||
Then I memorize cloudflare with jq ".appConnection.id" as app_conn_id
|
||||
Given I create a external ACME CA with the following config as ext_ca
|
||||
@@ -92,9 +93,7 @@ Feature: External CA
|
||||
When I create certificate signing request as csr
|
||||
Then I add names to certificate signing request csr
|
||||
"""
|
||||
{
|
||||
"COMMON_NAME": "localhost"
|
||||
}
|
||||
<subject>
|
||||
"""
|
||||
# Pebble has a strict rule to only takes SANs
|
||||
Then I add subject alternative name to certificate signing request csr
|
||||
@@ -177,4 +176,196 @@ Feature: External CA
|
||||
[
|
||||
"localhost"
|
||||
]
|
||||
"""
|
||||
"""
|
||||
|
||||
Examples:
|
||||
| subject |
|
||||
| {"COMMON_NAME": "localhost"} |
|
||||
| {} |
|
||||
|
||||
@dnsme
|
||||
Scenario Outline: Issue a certificate from an external CA with DNS Made Easy
|
||||
Given I create a DNS Made Easy connection as dnsme
|
||||
Then I memorize dnsme with jq ".appConnection.id" as app_conn_id
|
||||
Given I create a external ACME CA with the following config as ext_ca
|
||||
"""
|
||||
{
|
||||
"dnsProviderConfig": {
|
||||
"provider": "dns-made-easy",
|
||||
"hostedZoneId": "MOCK_ZONE_ID"
|
||||
},
|
||||
"directoryUrl": "{PEBBLE_URL}",
|
||||
"accountEmail": "fangpen@infisical.com",
|
||||
"dnsAppConnectionId": "{app_conn_id}",
|
||||
"eabKid": "",
|
||||
"eabHmacKey": ""
|
||||
}
|
||||
"""
|
||||
Then I memorize ext_ca with jq ".id" as ext_ca_id
|
||||
Given I create a certificate template with the following config as cert_template
|
||||
"""
|
||||
{
|
||||
"subject": [
|
||||
{
|
||||
"type": "common_name",
|
||||
"allowed": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"sans": [
|
||||
{
|
||||
"type": "dns_name",
|
||||
"allowed": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"keyUsages": {
|
||||
"required": [],
|
||||
"allowed": [
|
||||
"digital_signature",
|
||||
"key_encipherment",
|
||||
"non_repudiation",
|
||||
"data_encipherment",
|
||||
"key_agreement",
|
||||
"key_cert_sign",
|
||||
"crl_sign",
|
||||
"encipher_only",
|
||||
"decipher_only"
|
||||
]
|
||||
},
|
||||
"extendedKeyUsages": {
|
||||
"required": [],
|
||||
"allowed": [
|
||||
"client_auth",
|
||||
"server_auth",
|
||||
"code_signing",
|
||||
"email_protection",
|
||||
"ocsp_signing",
|
||||
"time_stamping"
|
||||
]
|
||||
},
|
||||
"algorithms": {
|
||||
"signature": [
|
||||
"SHA256-RSA",
|
||||
"SHA512-RSA",
|
||||
"SHA384-ECDSA",
|
||||
"SHA384-RSA",
|
||||
"SHA256-ECDSA",
|
||||
"SHA512-ECDSA"
|
||||
],
|
||||
"keyAlgorithm": [
|
||||
"RSA-2048",
|
||||
"RSA-4096",
|
||||
"ECDSA-P384",
|
||||
"RSA-3072",
|
||||
"ECDSA-P256",
|
||||
"ECDSA-P521"
|
||||
]
|
||||
},
|
||||
"validity": {
|
||||
"max": "365d"
|
||||
}
|
||||
}
|
||||
"""
|
||||
Then I memorize cert_template with jq ".certificateTemplate.id" as cert_template_id
|
||||
Given I create an ACME profile with ca {ext_ca_id} and template {cert_template_id} as "acme_profile"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
|
||||
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
|
||||
When I create certificate signing request as csr
|
||||
Then I add names to certificate signing request csr
|
||||
"""
|
||||
<subject>
|
||||
"""
|
||||
# Pebble has a strict rule to only takes SANs
|
||||
Then I add subject alternative name to certificate signing request csr
|
||||
"""
|
||||
[
|
||||
"localhost"
|
||||
]
|
||||
"""
|
||||
And I create a RSA private key pair as cert_key
|
||||
And I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format
|
||||
And I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order
|
||||
And I select challenge with type http-01 for domain localhost from order in order as challenge
|
||||
And I serve challenge response for challenge at localhost
|
||||
And I tell ACME server that challenge is ready to be verified
|
||||
Given I intercept outgoing requests
|
||||
"""
|
||||
[
|
||||
{
|
||||
"scope": "https://api.dnsmadeeasy.com:443",
|
||||
"method": "POST",
|
||||
"path": "/V2.0/dns/managed/MOCK_ZONE_ID/records",
|
||||
"status": 201,
|
||||
"response": {
|
||||
"gtdLocation": "DEFAULT",
|
||||
"failed": false,
|
||||
"monitor": false,
|
||||
"failover": false,
|
||||
"sourceId": 895364,
|
||||
"dynamicDns": false,
|
||||
"hardLink": false,
|
||||
"ttl": 60,
|
||||
"source": 1,
|
||||
"name": "_acme-challenge",
|
||||
"value": "\"MOCK_HTTP_01_VALUE\"",
|
||||
"id": 12345678,
|
||||
"type": "TXT"
|
||||
},
|
||||
"responseIsBinary": false
|
||||
},
|
||||
{
|
||||
"scope": "https://api.dnsmadeeasy.com:443",
|
||||
"method": "GET",
|
||||
"path": "/V2.0/dns/managed/MOCK_ZONE_ID/records?type=TXT&recordName=_acme-challenge&page=0",
|
||||
"status": 200,
|
||||
"response": {
|
||||
"totalRecords": 1,
|
||||
"totalPages": 1,
|
||||
"data": [
|
||||
{
|
||||
"gtdLocation": "DEFAULT",
|
||||
"failed": false,
|
||||
"monitor": false,
|
||||
"failover": false,
|
||||
"sourceId": 895364,
|
||||
"dynamicDns": false,
|
||||
"hardLink": false,
|
||||
"ttl": 60,
|
||||
"source": 1,
|
||||
"name": "_acme-challenge",
|
||||
"value": "\"MOCK_CHALLENGE_VALUE\"",
|
||||
"id": 1111111,
|
||||
"type": "TXT"
|
||||
}
|
||||
],
|
||||
"page": 0
|
||||
},
|
||||
"responseIsBinary": false
|
||||
},
|
||||
{
|
||||
"scope": "https://api.dnsmadeeasy.com:443",
|
||||
"method": "DELETE",
|
||||
"path": "/V2.0/dns/managed/MOCK_ZONE_ID/records/1111111",
|
||||
"status": 200,
|
||||
"response": "",
|
||||
"responseIsBinary": false
|
||||
}
|
||||
]
|
||||
"""
|
||||
Then I poll and finalize the ACME order order as finalized_order
|
||||
And the value finalized_order.body with jq ".status" should be equal to "valid"
|
||||
And I parse the full-chain certificate from order finalized_order as cert
|
||||
And the value cert with jq "[.extensions.subjectAltName.general_names.[].value] | sort" should be equal to json
|
||||
"""
|
||||
[
|
||||
"localhost"
|
||||
]
|
||||
"""
|
||||
|
||||
Examples:
|
||||
| subject |
|
||||
| {"COMMON_NAME": "localhost"} |
|
||||
| {} |
|
||||
|
||||
@@ -147,6 +147,40 @@ def step_impl(context: Context, var_name: str):
|
||||
context.vars[var_name] = response
|
||||
|
||||
|
||||
@given("I create a DNS Made Easy connection as {var_name}")
|
||||
def step_impl(context: Context, var_name: str):
|
||||
jwt_token = context.vars["AUTH_TOKEN"]
|
||||
conn_slug = faker.slug()
|
||||
with with_nocks(
|
||||
context,
|
||||
definitions=[
|
||||
{
|
||||
"scope": "https://api.dnsmadeeasy.com:443",
|
||||
"method": "GET",
|
||||
"path": "/V2.0/dns/managed/",
|
||||
"status": 200,
|
||||
"response": {"totalRecords": 0, "totalPages": 1, "data": [], "page": 0},
|
||||
"responseIsBinary": False,
|
||||
}
|
||||
],
|
||||
):
|
||||
response = context.http_client.post(
|
||||
"/api/v1/app-connections/dns-made-easy",
|
||||
headers=dict(authorization="Bearer {}".format(jwt_token)),
|
||||
json={
|
||||
"name": conn_slug,
|
||||
"description": "",
|
||||
"method": "api-key-secret",
|
||||
"credentials": {
|
||||
"apiKey": "MOCK_API_KEY",
|
||||
"secretKey": "MOCK_SECRET_KEY",
|
||||
},
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
context.vars[var_name] = response
|
||||
|
||||
|
||||
@given("I create a external ACME CA with the following config as {var_name}")
|
||||
def step_impl(context: Context, var_name: str):
|
||||
jwt_token = context.vars["AUTH_TOKEN"]
|
||||
|
||||
@@ -776,7 +776,9 @@ export const pkiAcmeServiceFactory = ({
|
||||
const cert = await orderCertificate(
|
||||
{
|
||||
caId: certificateAuthority!.id,
|
||||
commonName: certificateRequest.commonName!,
|
||||
// It is possible that the CSR does not have a common name, in which case we use an empty string
|
||||
// (more likely than not for a CSR from a modern ACME client like certbot, cert-manager, etc.)
|
||||
commonName: certificateRequest.commonName ?? "",
|
||||
altNames: certificateRequest.subjectAlternativeNames?.map((san) => san.value),
|
||||
csr: Buffer.from(csrPem),
|
||||
// TODO: not 100% sure what are these columns for, but let's put the values for common website SSL certs for now
|
||||
|
||||
@@ -119,6 +119,7 @@ const envSchema = z
|
||||
})
|
||||
.default("{}")
|
||||
),
|
||||
DNS_MADE_EASY_SANDBOX_ENABLED: zodStrBool.default("false").optional(),
|
||||
// smtp options
|
||||
SMTP_HOST: zpStr(z.string().optional()),
|
||||
SMTP_IGNORE_TLS: zodStrBool.default("false"),
|
||||
|
||||
@@ -61,6 +61,10 @@ import {
|
||||
DigitalOceanConnectionListItemSchema,
|
||||
SanitizedDigitalOceanConnectionSchema
|
||||
} from "@app/services/app-connection/digital-ocean";
|
||||
import {
|
||||
DNSMadeEasyConnectionListItemSchema,
|
||||
SanitizedDNSMadeEasyConnectionSchema
|
||||
} from "@app/services/app-connection/dns-made-easy/dns-made-easy-connection-schema";
|
||||
import { FlyioConnectionListItemSchema, SanitizedFlyioConnectionSchema } from "@app/services/app-connection/flyio";
|
||||
import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp";
|
||||
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
|
||||
@@ -170,7 +174,8 @@ const SanitizedAppConnectionSchema = z.union([
|
||||
...SanitizedAzureADCSConnectionSchema.options,
|
||||
...SanitizedRedisConnectionSchema.options,
|
||||
...SanitizedLaravelForgeConnectionSchema.options,
|
||||
...SanitizedChefConnectionSchema.options
|
||||
...SanitizedChefConnectionSchema.options,
|
||||
...SanitizedDNSMadeEasyConnectionSchema.options
|
||||
]);
|
||||
|
||||
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
@@ -215,7 +220,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
AzureADCSConnectionListItemSchema,
|
||||
RedisConnectionListItemSchema,
|
||||
LaravelForgeConnectionListItemSchema,
|
||||
ChefConnectionListItemSchema
|
||||
ChefConnectionListItemSchema,
|
||||
DNSMadeEasyConnectionListItemSchema
|
||||
]);
|
||||
|
||||
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
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;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -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<AppConnection, (server:
|
||||
[AppConnection.Flyio]: registerFlyioConnectionRouter,
|
||||
[AppConnection.GitLab]: registerGitLabConnectionRouter,
|
||||
[AppConnection.Cloudflare]: registerCloudflareConnectionRouter,
|
||||
[AppConnection.DNSMadeEasy]: registerDNSMadeEasyConnectionRouter,
|
||||
[AppConnection.Bitbucket]: registerBitbucketConnectionRouter,
|
||||
[AppConnection.Zabbix]: registerZabbixConnectionRouter,
|
||||
[AppConnection.Railway]: registerRailwayConnectionRouter,
|
||||
|
||||
@@ -29,6 +29,7 @@ export enum AppConnection {
|
||||
Flyio = "flyio",
|
||||
GitLab = "gitlab",
|
||||
Cloudflare = "cloudflare",
|
||||
DNSMadeEasy = "dns-made-easy",
|
||||
Zabbix = "zabbix",
|
||||
Railway = "railway",
|
||||
Bitbucket = "bitbucket",
|
||||
|
||||
@@ -88,6 +88,11 @@ import {
|
||||
getDigitalOceanConnectionListItem,
|
||||
validateDigitalOceanConnectionCredentials
|
||||
} from "./digital-ocean";
|
||||
import { DNSMadeEasyConnectionMethod } from "./dns-made-easy/dns-made-easy-connection-enum";
|
||||
import {
|
||||
getDNSMadeEasyConnectionListItem,
|
||||
validateDNSMadeEasyConnectionCredentials
|
||||
} from "./dns-made-easy/dns-made-easy-connection-fns";
|
||||
import { FlyioConnectionMethod, getFlyioConnectionListItem, validateFlyioConnectionCredentials } from "./flyio";
|
||||
import { GcpConnectionMethod, getGcpConnectionListItem, validateGcpConnectionCredentials } from "./gcp";
|
||||
import { getGitHubConnectionListItem, GitHubConnectionMethod, validateGitHubConnectionCredentials } from "./github";
|
||||
@@ -171,7 +176,8 @@ const PKI_APP_CONNECTIONS = [
|
||||
AppConnection.Cloudflare,
|
||||
AppConnection.AzureADCS,
|
||||
AppConnection.AzureKeyVault,
|
||||
AppConnection.Chef
|
||||
AppConnection.Chef,
|
||||
AppConnection.DNSMadeEasy
|
||||
];
|
||||
|
||||
export const listAppConnectionOptions = (projectType?: ProjectType) => {
|
||||
@@ -207,6 +213,7 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => {
|
||||
getFlyioConnectionListItem(),
|
||||
getGitLabConnectionListItem(),
|
||||
getCloudflareConnectionListItem(),
|
||||
getDNSMadeEasyConnectionListItem(),
|
||||
getZabbixConnectionListItem(),
|
||||
getRailwayConnectionListItem(),
|
||||
getBitbucketConnectionListItem(),
|
||||
@@ -339,6 +346,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,
|
||||
@@ -395,6 +403,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
case OktaConnectionMethod.ApiToken:
|
||||
case LaravelForgeConnectionMethod.ApiToken:
|
||||
return "API Token";
|
||||
case DNSMadeEasyConnectionMethod.APIKeySecret:
|
||||
return "API Key & Secret";
|
||||
case PostgresConnectionMethod.UsernameAndPassword:
|
||||
case MsSqlConnectionMethod.UsernameAndPassword:
|
||||
case MySqlConnectionMethod.UsernameAndPassword:
|
||||
@@ -483,6 +493,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,
|
||||
|
||||
@@ -32,6 +32,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[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<AppConnection, AppConnectionPlanTyp
|
||||
[AppConnection.Flyio]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.GitLab]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Cloudflare]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.DNSMadeEasy]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Zabbix]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Railway]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Bitbucket]: AppConnectionPlanType.Regular,
|
||||
|
||||
@@ -72,6 +72,8 @@ import { checklyConnectionService } from "./checkly/checkly-connection-service";
|
||||
import { ValidateCloudflareConnectionCredentialsSchema } from "./cloudflare/cloudflare-connection-schema";
|
||||
import { cloudflareConnectionService } from "./cloudflare/cloudflare-connection-service";
|
||||
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
|
||||
import { ValidateDNSMadeEasyConnectionCredentialsSchema } from "./dns-made-easy/dns-made-easy-connection-schema";
|
||||
import { dnsMadeEasyConnectionService } from "./dns-made-easy/dns-made-easy-connection-service";
|
||||
import { databricksConnectionService } from "./databricks/databricks-connection-service";
|
||||
import { ValidateDigitalOceanConnectionCredentialsSchema } from "./digital-ocean";
|
||||
import { digitalOceanAppPlatformConnectionService } from "./digital-ocean/digital-ocean-connection-service";
|
||||
@@ -167,6 +169,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.Flyio]: ValidateFlyioConnectionCredentialsSchema,
|
||||
[AppConnection.GitLab]: ValidateGitLabConnectionCredentialsSchema,
|
||||
[AppConnection.Cloudflare]: ValidateCloudflareConnectionCredentialsSchema,
|
||||
[AppConnection.DNSMadeEasy]: ValidateDNSMadeEasyConnectionCredentialsSchema,
|
||||
[AppConnection.Zabbix]: ValidateZabbixConnectionCredentialsSchema,
|
||||
[AppConnection.Railway]: ValidateRailwayConnectionCredentialsSchema,
|
||||
[AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema,
|
||||
@@ -875,6 +878,7 @@ export const appConnectionServiceFactory = ({
|
||||
flyio: flyioConnectionService(connectAppConnectionById),
|
||||
gitlab: gitlabConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
cloudflare: cloudflareConnectionService(connectAppConnectionById),
|
||||
dnsMadeEasy: dnsMadeEasyConnectionService(connectAppConnectionById),
|
||||
zabbix: zabbixConnectionService(connectAppConnectionById),
|
||||
railway: railwayConnectionService(connectAppConnectionById),
|
||||
bitbucket: bitbucketConnectionService(connectAppConnectionById),
|
||||
|
||||
@@ -15,8 +15,8 @@ import {
|
||||
TOracleDBConnectionInput,
|
||||
TValidateOracleDBConnectionCredentialsSchema
|
||||
} from "@app/ee/services/app-connections/oracledb";
|
||||
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||
import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service";
|
||||
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
@@ -106,6 +106,12 @@ import {
|
||||
TDigitalOceanConnectionInput,
|
||||
TValidateDigitalOceanCredentialsSchema
|
||||
} from "./digital-ocean";
|
||||
import {
|
||||
TDNSMadeEasyConnection,
|
||||
TDNSMadeEasyConnectionConfig,
|
||||
TDNSMadeEasyConnectionInput,
|
||||
TValidateDNSMadeEasyConnectionCredentialsSchema
|
||||
} from "./dns-made-easy/dns-made-easy-connection-types";
|
||||
import {
|
||||
TFlyioConnection,
|
||||
TFlyioConnectionConfig,
|
||||
@@ -279,6 +285,7 @@ export type TAppConnection = { id: string } & (
|
||||
| TGitLabConnection
|
||||
| TCloudflareConnection
|
||||
| TBitbucketConnection
|
||||
| TDNSMadeEasyConnection
|
||||
| TZabbixConnection
|
||||
| TRailwayConnection
|
||||
| TChecklyConnection
|
||||
@@ -328,6 +335,7 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| TGitLabConnectionInput
|
||||
| TCloudflareConnectionInput
|
||||
| TBitbucketConnectionInput
|
||||
| TDNSMadeEasyConnectionInput
|
||||
| TZabbixConnectionInput
|
||||
| TRailwayConnectionInput
|
||||
| TChecklyConnectionInput
|
||||
@@ -395,6 +403,7 @@ export type TAppConnectionConfig =
|
||||
| TGitLabConnectionConfig
|
||||
| TCloudflareConnectionConfig
|
||||
| TBitbucketConnectionConfig
|
||||
| TDNSMadeEasyConnectionConfig
|
||||
| TZabbixConnectionConfig
|
||||
| TRailwayConnectionConfig
|
||||
| TChecklyConnectionConfig
|
||||
@@ -439,6 +448,7 @@ export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateGitLabConnectionCredentialsSchema
|
||||
| TValidateCloudflareConnectionCredentialsSchema
|
||||
| TValidateBitbucketConnectionCredentialsSchema
|
||||
| TValidateDNSMadeEasyConnectionCredentialsSchema
|
||||
| TValidateZabbixConnectionCredentialsSchema
|
||||
| TValidateRailwayConnectionCredentialsSchema
|
||||
| TValidateChecklyConnectionCredentialsSchema
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum DNSMadeEasyConnectionMethod {
|
||||
APIKeySecret = "api-key-secret"
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
|
||||
import { DNSMadeEasyConnectionMethod } from "./dns-made-easy-connection-enum";
|
||||
import {
|
||||
TDNSMadeEasyConnection,
|
||||
TDNSMadeEasyConnectionConfig,
|
||||
TDNSMadeEasyZone
|
||||
} from "./dns-made-easy-connection-types";
|
||||
|
||||
interface DNSMadeEasyApiResponse {
|
||||
totalRecords: number;
|
||||
totalPages: number;
|
||||
data: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
value: string;
|
||||
}>;
|
||||
page: number;
|
||||
}
|
||||
|
||||
export const getDNSMadeEasyUrl = (path: string) => {
|
||||
const appCfg = getConfig();
|
||||
return `${appCfg.DNS_MADE_EASY_SANDBOX_ENABLED ? IntegrationUrls.DNS_MADE_EASY_SANDBOX_API_URL : IntegrationUrls.DNS_MADE_EASY_API_URL}${path}`;
|
||||
};
|
||||
|
||||
export const makeDNSMadeEasyAuthHeaders = (
|
||||
apiKey: string,
|
||||
secretKey: string,
|
||||
currentDate?: Date
|
||||
): Record<string, string> => {
|
||||
// Format date as "Day, DD Mon YYYY HH:MM:SS GMT" (e.g., "Mon, 01 Jan 2024 12:00:00 GMT")
|
||||
const requestDate = (currentDate ?? new Date()).toUTCString();
|
||||
|
||||
// Generate HMAC-SHA1 signature
|
||||
const hmac = crypto.nativeCrypto.createHmac("sha1", secretKey);
|
||||
hmac.update(requestDate);
|
||||
const hmacSignature = hmac.digest("hex");
|
||||
|
||||
return {
|
||||
"x-dnsme-apiKey": apiKey,
|
||||
"x-dnsme-hmac": hmacSignature,
|
||||
"x-dnsme-requestDate": requestDate
|
||||
};
|
||||
};
|
||||
|
||||
export const getDNSMadeEasyConnectionListItem = () => {
|
||||
return {
|
||||
name: "DNS Made Easy" as const,
|
||||
app: AppConnection.DNSMadeEasy as const,
|
||||
methods: Object.values(DNSMadeEasyConnectionMethod) as [DNSMadeEasyConnectionMethod.APIKeySecret]
|
||||
};
|
||||
};
|
||||
|
||||
export const listDNSMadeEasyZones = async (appConnection: TDNSMadeEasyConnection): Promise<TDNSMadeEasyZone[]> => {
|
||||
if (appConnection.method !== DNSMadeEasyConnectionMethod.APIKeySecret) {
|
||||
throw new BadRequestError({ message: "Unsupported DNS Made Easy connection method" });
|
||||
}
|
||||
|
||||
const {
|
||||
credentials: { apiKey, secretKey }
|
||||
} = appConnection;
|
||||
|
||||
try {
|
||||
const allZones: TDNSMadeEasyZone[] = [];
|
||||
let currentPage = 0;
|
||||
let totalPages = 1;
|
||||
|
||||
// Fetch all pages of zones
|
||||
while (currentPage < totalPages) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const resp = await request.get<DNSMadeEasyApiResponse>(getDNSMadeEasyUrl("/V2.0/dns/managed/"), {
|
||||
headers: {
|
||||
...makeDNSMadeEasyAuthHeaders(apiKey, secretKey),
|
||||
Accept: "application/json"
|
||||
},
|
||||
params: {
|
||||
page: currentPage
|
||||
}
|
||||
});
|
||||
|
||||
if (resp.data?.data) {
|
||||
// Map the API response to TDNSMadeEasyZone format
|
||||
const zones = resp.data.data.map((zone) => ({
|
||||
id: String(zone.id),
|
||||
name: zone.name
|
||||
}));
|
||||
allZones.push(...zones);
|
||||
|
||||
// Update pagination info
|
||||
totalPages = resp.data.totalPages || 1;
|
||||
currentPage += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return allZones;
|
||||
} catch (error: unknown) {
|
||||
logger.error(error, "Error listing DNS Made Easy zones");
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
message: `Failed to list DNS Made Easy zones: ${error.response?.data?.error?.[0] || error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to list DNS Made Easy zones"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const listDNSMadeEasyRecords = async (
|
||||
appConnection: TDNSMadeEasyConnection,
|
||||
options: { zoneId: string; type?: string; name?: string }
|
||||
): Promise<DNSMadeEasyApiResponse["data"]> => {
|
||||
if (appConnection.method !== DNSMadeEasyConnectionMethod.APIKeySecret) {
|
||||
throw new BadRequestError({ message: "Unsupported DNS Made Easy connection method" });
|
||||
}
|
||||
const {
|
||||
credentials: { apiKey, secretKey }
|
||||
} = appConnection;
|
||||
const { zoneId, type, name } = options;
|
||||
|
||||
try {
|
||||
const allRecords: DNSMadeEasyApiResponse["data"] = [];
|
||||
let currentPage = 0;
|
||||
let totalPages = 1;
|
||||
|
||||
// Fetch all pages of records
|
||||
while (currentPage < totalPages) {
|
||||
// Build query parameters
|
||||
const queryParams: Record<string, string | number> = {};
|
||||
if (type) {
|
||||
queryParams.type = type;
|
||||
}
|
||||
if (name) {
|
||||
queryParams.recordName = name;
|
||||
}
|
||||
queryParams.page = currentPage;
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const resp = await request.get<DNSMadeEasyApiResponse>(
|
||||
getDNSMadeEasyUrl(`/V2.0/dns/managed/${encodeURIComponent(zoneId)}/records`),
|
||||
{
|
||||
headers: {
|
||||
...makeDNSMadeEasyAuthHeaders(apiKey, secretKey),
|
||||
Accept: "application/json"
|
||||
},
|
||||
params: queryParams
|
||||
}
|
||||
);
|
||||
|
||||
if (resp.data?.data) {
|
||||
allRecords.push(...resp.data.data);
|
||||
|
||||
// Update pagination info
|
||||
totalPages = resp.data.totalPages || 1;
|
||||
currentPage += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return allRecords;
|
||||
} catch (error: unknown) {
|
||||
logger.error(error, "Error listing DNS Made Easy records");
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
message: `Failed to list DNS Made Easy records: ${error.response?.data?.error?.[0] || error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to list DNS Made Easy records"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const validateDNSMadeEasyConnectionCredentials = async (config: TDNSMadeEasyConnectionConfig) => {
|
||||
if (config.method !== DNSMadeEasyConnectionMethod.APIKeySecret) {
|
||||
throw new BadRequestError({ message: "Unsupported DNS Made Easy connection method" });
|
||||
}
|
||||
|
||||
const { apiKey, secretKey } = config.credentials;
|
||||
|
||||
try {
|
||||
const resp = await request.get(getDNSMadeEasyUrl("/V2.0/dns/managed/"), {
|
||||
headers: {
|
||||
...makeDNSMadeEasyAuthHeaders(apiKey, secretKey),
|
||||
Accept: "application/json"
|
||||
}
|
||||
});
|
||||
if (resp.status !== 200) {
|
||||
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"}`
|
||||
});
|
||||
}
|
||||
logger.error(error, "Error validating DNS Made Easy connection credentials");
|
||||
throw new BadRequestError({
|
||||
message: "Unable to validate connection: verify credentials"
|
||||
});
|
||||
}
|
||||
|
||||
return config.credentials;
|
||||
};
|
||||
@@ -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"),
|
||||
secretKey: z.string().trim().min(1, "Secret key required").max(256, "Secret key cannot exceed 256 characters")
|
||||
});
|
||||
|
||||
const BaseDNSMadeEasyConnectionSchema = BaseAppConnectionSchema.extend({
|
||||
app: z.literal(AppConnection.DNSMadeEasy)
|
||||
});
|
||||
|
||||
export const DNSMadeEasyConnectionSchema = BaseDNSMadeEasyConnectionSchema.extend({
|
||||
method: z.literal(DNSMadeEasyConnectionMethod.APIKeySecret),
|
||||
credentials: DNSMadeEasyConnectionApiKeyCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedDNSMadeEasyConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseDNSMadeEasyConnectionSchema.extend({
|
||||
method: z.literal(DNSMadeEasyConnectionMethod.APIKeySecret),
|
||||
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.APIKeySecret)
|
||||
.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] }));
|
||||
@@ -0,0 +1,35 @@
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
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<TDNSMadeEasyConnection>;
|
||||
|
||||
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}]`
|
||||
);
|
||||
throw new BadRequestError({
|
||||
message: `Failed to list DNS Made Easy zones: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
listZones
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import z from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
CreateDNSMadeEasyConnectionSchema,
|
||||
DNSMadeEasyConnectionSchema,
|
||||
ValidateDNSMadeEasyConnectionCredentialsSchema
|
||||
} from "./dns-made-easy-connection-schema";
|
||||
|
||||
export type TDNSMadeEasyConnection = z.infer<typeof DNSMadeEasyConnectionSchema>;
|
||||
|
||||
export type TDNSMadeEasyConnectionInput = z.infer<typeof CreateDNSMadeEasyConnectionSchema> & {
|
||||
app: AppConnection.DNSMadeEasy;
|
||||
};
|
||||
|
||||
export type TValidateDNSMadeEasyConnectionCredentialsSchema = typeof ValidateDNSMadeEasyConnectionCredentialsSchema;
|
||||
|
||||
export type TDNSMadeEasyConnectionConfig = DiscriminativePick<
|
||||
TDNSMadeEasyConnectionInput,
|
||||
"method" | "app" | "credentials"
|
||||
> & {
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TDNSMadeEasyZone = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum AcmeDnsProvider {
|
||||
Route53 = "route53",
|
||||
Cloudflare = "cloudflare"
|
||||
Cloudflare = "cloudflare",
|
||||
DNSMadeEasy = "dns-made-easy"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { decryptAppConnection } from "@app/services/app-connection/app-connectio
|
||||
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||
import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types";
|
||||
import { TCloudflareConnection } from "@app/services/app-connection/cloudflare/cloudflare-connection-types";
|
||||
import { TDNSMadeEasyConnection } from "@app/services/app-connection/dns-made-easy/dns-made-easy-connection-types";
|
||||
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||
@@ -43,6 +44,7 @@ import {
|
||||
TUpdateAcmeCertificateAuthorityDTO
|
||||
} from "./acme-certificate-authority-types";
|
||||
import { cloudflareDeleteTxtRecord, cloudflareInsertTxtRecord } from "./dns-providers/cloudflare";
|
||||
import { dnsMadeEasyDeleteTxtRecord, dnsMadeEasyInsertTxtRecord } from "./dns-providers/dns-made-easy";
|
||||
import { route53DeleteTxtRecord, route53InsertTxtRecord } from "./dns-providers/route54";
|
||||
|
||||
type TAcmeCertificateAuthorityFnsDeps = {
|
||||
@@ -120,6 +122,22 @@ export const castDbEntryToAcmeCertificateAuthority = (
|
||||
};
|
||||
};
|
||||
|
||||
const getAcmeChallengeRecord = (
|
||||
provider: AcmeDnsProvider,
|
||||
identifierValue: string,
|
||||
keyAuthorization: string
|
||||
): { recordName: string; recordValue: string } => {
|
||||
let recordName: string;
|
||||
if (provider === AcmeDnsProvider.DNSMadeEasy) {
|
||||
// For DNS Made Easy, we don't need to provide the domain name in the record name.
|
||||
recordName = "_acme-challenge";
|
||||
} else {
|
||||
recordName = `_acme-challenge.${identifierValue}`; // e.g., "_acme-challenge.example.com"
|
||||
}
|
||||
const recordValue = `"${keyAuthorization}"`; // must be double quoted
|
||||
return { recordName, recordValue };
|
||||
};
|
||||
|
||||
export const orderCertificate = async (
|
||||
{
|
||||
caId,
|
||||
@@ -241,8 +259,11 @@ export const orderCertificate = async (
|
||||
throw new Error("Unsupported challenge type");
|
||||
}
|
||||
|
||||
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
|
||||
const recordValue = `"${keyAuthorization}"`; // must be double quoted
|
||||
const { recordName, recordValue } = getAcmeChallengeRecord(
|
||||
acmeCa.configuration.dnsProviderConfig.provider,
|
||||
authz.identifier.value,
|
||||
keyAuthorization
|
||||
);
|
||||
|
||||
switch (acmeCa.configuration.dnsProviderConfig.provider) {
|
||||
case AcmeDnsProvider.Route53: {
|
||||
@@ -263,14 +284,26 @@ export const orderCertificate = async (
|
||||
);
|
||||
break;
|
||||
}
|
||||
case AcmeDnsProvider.DNSMadeEasy: {
|
||||
await dnsMadeEasyInsertTxtRecord(
|
||||
connection as TDNSMadeEasyConnection,
|
||||
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
|
||||
recordName,
|
||||
recordValue
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
|
||||
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
|
||||
const recordValue = `"${keyAuthorization}"`; // must be double quoted
|
||||
const { recordName, recordValue } = getAcmeChallengeRecord(
|
||||
acmeCa.configuration.dnsProviderConfig.provider,
|
||||
authz.identifier.value,
|
||||
keyAuthorization
|
||||
);
|
||||
|
||||
switch (acmeCa.configuration.dnsProviderConfig.provider) {
|
||||
case AcmeDnsProvider.Route53: {
|
||||
@@ -291,6 +324,15 @@ export const orderCertificate = async (
|
||||
);
|
||||
break;
|
||||
}
|
||||
case AcmeDnsProvider.DNSMadeEasy: {
|
||||
await dnsMadeEasyDeleteTxtRecord(
|
||||
connection as TDNSMadeEasyConnection,
|
||||
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
|
||||
recordName,
|
||||
recordValue
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`);
|
||||
}
|
||||
@@ -413,6 +455,12 @@ export const AcmeCertificateAuthorityFns = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (dnsProviderConfig.provider === AcmeDnsProvider.DNSMadeEasy && appConnection.app !== AppConnection.DNSMadeEasy) {
|
||||
throw new BadRequestError({
|
||||
message: `App connection with ID '${dnsAppConnectionId}' is not a DNS Made Easy connection`
|
||||
});
|
||||
}
|
||||
|
||||
// validates permission to connect
|
||||
await appConnectionService.validateAppConnectionUsageById(
|
||||
appConnection.app as AppConnection,
|
||||
@@ -508,6 +556,15 @@ export const AcmeCertificateAuthorityFns = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
dnsProviderConfig.provider === AcmeDnsProvider.DNSMadeEasy &&
|
||||
appConnection.app !== AppConnection.DNSMadeEasy
|
||||
) {
|
||||
throw new BadRequestError({
|
||||
message: `App connection with ID '${dnsAppConnectionId}' is not a DNS Made Easy connection`
|
||||
});
|
||||
}
|
||||
|
||||
const ca = await certificateAuthorityDAL.findById(id);
|
||||
|
||||
if (!ca) {
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import axios from "axios";
|
||||
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import {
|
||||
getDNSMadeEasyUrl,
|
||||
listDNSMadeEasyRecords,
|
||||
makeDNSMadeEasyAuthHeaders
|
||||
} from "@app/services/app-connection/dns-made-easy/dns-made-easy-connection-fns";
|
||||
import { TDNSMadeEasyConnection } from "@app/services/app-connection/dns-made-easy/dns-made-easy-connection-types";
|
||||
|
||||
export const dnsMadeEasyInsertTxtRecord = async (
|
||||
connection: TDNSMadeEasyConnection,
|
||||
hostedZoneId: string,
|
||||
domain: string,
|
||||
value: string
|
||||
) => {
|
||||
const {
|
||||
credentials: { apiKey, secretKey }
|
||||
} = connection;
|
||||
|
||||
logger.info({ hostedZoneId, domain, value }, "Inserting TXT record for DNS Made Easy");
|
||||
try {
|
||||
await request.post(
|
||||
getDNSMadeEasyUrl(`/V2.0/dns/managed/${encodeURIComponent(hostedZoneId)}/records`),
|
||||
{
|
||||
type: "TXT",
|
||||
name: domain,
|
||||
value,
|
||||
ttl: 60
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
...makeDNSMadeEasyAuthHeaders(apiKey, secretKey),
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const errorMessage =
|
||||
(error.response?.data as { error?: string[] | string })?.error?.[0] ||
|
||||
(error.response?.data as { error?: string[] | string })?.error ||
|
||||
error.message ||
|
||||
"Unknown error";
|
||||
|
||||
if (error.status === 400 && error.message.includes("already exists")) {
|
||||
logger.info({ domain, value }, `Record already exists for domain: ${domain} and value: ${value}`);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(typeof errorMessage === "string" ? errorMessage : String(errorMessage));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const dnsMadeEasyDeleteTxtRecord = async (
|
||||
connection: TDNSMadeEasyConnection,
|
||||
hostedZoneId: string,
|
||||
domain: string,
|
||||
value: string
|
||||
) => {
|
||||
const {
|
||||
credentials: { apiKey, secretKey }
|
||||
} = connection;
|
||||
|
||||
logger.info({ hostedZoneId, domain, value }, "Deleting TXT record for DNS Made Easy");
|
||||
try {
|
||||
const dnsRecords = await listDNSMadeEasyRecords(connection, { zoneId: hostedZoneId, type: "TXT", name: domain });
|
||||
|
||||
let foundRecord = false;
|
||||
if (dnsRecords.length > 0) {
|
||||
const recordToDelete = dnsRecords.find(
|
||||
(record) => record.type === "TXT" && record.name === domain && record.value === value
|
||||
);
|
||||
|
||||
if (recordToDelete) {
|
||||
await request.delete(
|
||||
getDNSMadeEasyUrl(`/V2.0/dns/managed/${encodeURIComponent(hostedZoneId)}/records/${recordToDelete.id}`),
|
||||
{
|
||||
headers: {
|
||||
...makeDNSMadeEasyAuthHeaders(apiKey, secretKey),
|
||||
Accept: "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
foundRecord = true;
|
||||
}
|
||||
}
|
||||
if (!foundRecord) {
|
||||
logger.warn({ hostedZoneId, domain, value }, "Record to delete not found");
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const errorMessage =
|
||||
(error.response?.data as { error?: string[] | string })?.error?.[0] ||
|
||||
(error.response?.data as { error?: string[] | string })?.error ||
|
||||
error.message ||
|
||||
"Unknown error";
|
||||
throw new Error(typeof errorMessage === "string" ? errorMessage : String(errorMessage));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -105,7 +105,9 @@ export enum IntegrationUrls {
|
||||
GCP_CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform",
|
||||
|
||||
GITHUB_USER_INSTALLATIONS = "https://api.github.com/user/installations",
|
||||
CHEF_API_URL = "https://api.chef.io"
|
||||
CHEF_API_URL = "https://api.chef.io",
|
||||
DNS_MADE_EASY_API_URL = "https://api.dnsmadeeasy.com",
|
||||
DNS_MADE_EASY_SANDBOX_API_URL = "https://api.sandbox.dnsmadeeasy.com"
|
||||
}
|
||||
|
||||
export const getIntegrationOptions = async () => {
|
||||
|
||||
80
frontend/public/images/integrations/DNSMadeEasy.svg
Normal file
80
frontend/public/images/integrations/DNSMadeEasy.svg
Normal file
@@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 120 60" width="120" height="60" style="enable-background:new 0 0 120 60;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#808285;}
|
||||
.st1{fill:url(#SVGID_1_);}
|
||||
.st2{fill:#005B99;}
|
||||
.st3{enable-background:new ;}
|
||||
.st4{fill:#77787B;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st0" d="M34.1,42.6h9.4v-3.2C41.5,41.2,38.2,42.3,34.1,42.6L34.1,42.6z"/>
|
||||
<path class="st0" d="M17.3,40.1v2.5h11.9c-2.4-0.2-5-0.6-7.6-1.2C20.1,41,18.7,40.6,17.3,40.1L17.3,40.1z"/>
|
||||
|
||||
<radialGradient id="SVGID_1_" cx="-183.5882" cy="811.1324" r="47.498" gradientTransform="matrix(1 0 0 1 241.0864 -776.3726)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" style="stop-color:#0095DA"/>
|
||||
<stop offset="0.21" style="stop-color:#0095DA"/>
|
||||
<stop offset="0.33" style="stop-color:#00ACE4"/>
|
||||
<stop offset="0.9045" style="stop-color:#005093"/>
|
||||
<stop offset="0.9335" style="stop-color:#005396"/>
|
||||
<stop offset="0.954" style="stop-color:#005C9F"/>
|
||||
<stop offset="0.9718" style="stop-color:#006BAE"/>
|
||||
<stop offset="0.988" style="stop-color:#0080C4"/>
|
||||
<stop offset="1" style="stop-color:#0095DA"/>
|
||||
</radialGradient>
|
||||
<path class="st1" d="M43.5,18.5H29.2C23,17.1,15.4,17.1,10,18.2c2.4-0.3,5.1-0.3,7.8,0.1c0.7,0.1,1.3,0.2,2,0.3l0,0
|
||||
c2.9,0.5,5.7,1.2,8.1,2.2l0,0c0.3,0.1,0.5,0.2,0.8,0.3h0c0.1,0,0.2,0.1,0.3,0.1h0c0.1,0,0.2,0.1,0.3,0.1h0c0.1,0,0.2,0.1,0.2,0.1
|
||||
l0,0c0.1,0,0.1,0.1,0.2,0.1l0,0c0.1,0,0.1,0.1,0.2,0.1l0,0c0.1,0,0.2,0.1,0.2,0.1l0,0l0,0h0c0.1,0,0.1,0.1,0.2,0.1l0,0
|
||||
c0.1,0,0.1,0.1,0.2,0.1l0,0c0.1,0,0.1,0.1,0.2,0.1l0,0c0.1,0,0.1,0.1,0.2,0.1l0,0c0.1,0,0.1,0.1,0.2,0.1l0,0c0.1,0,0.1,0.1,0.2,0.1
|
||||
l0.1,0c0.1,0,0.1,0.1,0.2,0.1l0.1,0c0.1,0,0.1,0.1,0.2,0.1l0.1,0c0.1,0,0.1,0.1,0.2,0.1l0.1,0c0.1,0,0.1,0.1,0.1,0.1l0.1,0.1
|
||||
c0.1,0,0.1,0.1,0.1,0.1l0.1,0.1c0.1,0,0.1,0.1,0.1,0.1l0.1,0.1c0,0,0.1,0.1,0.1,0.1c3.2,2.2,5.1,4.9,5.1,7.6c0,3.1-2.6,5.7-6.8,7.2
|
||||
c2.9-1.5,4.6-3.6,4.6-6.1c0-3.1-2.7-6.3-7-8.7c-0.1,0-0.1-0.1-0.2-0.1c-0.1,0-0.1-0.1-0.2-0.1c-0.1,0-0.1-0.1-0.2-0.1
|
||||
c-0.1,0-0.1-0.1-0.2-0.1c-0.1,0-0.1-0.1-0.2-0.1c-0.1,0-0.1-0.1-0.2-0.1c-0.1,0-0.1-0.1-0.2-0.1l-0.2-0.1c-0.1,0-0.2-0.1-0.2-0.1
|
||||
l-0.1-0.1c-0.1,0-0.1-0.1-0.2-0.1c-0.1,0-0.1-0.1-0.2-0.1c-0.1,0-0.1-0.1-0.2-0.1c-0.1,0-0.1-0.1-0.2-0.1c-0.1,0-0.1-0.1-0.2-0.1
|
||||
L26,22c-0.1,0-0.2-0.1-0.3-0.1h0l-0.2-0.1l0,0c-0.1,0-0.2-0.1-0.3-0.1l-0.1,0c-0.1-0.1-0.2-0.1-0.4-0.1h0c-0.1,0-0.2-0.1-0.3-0.1h0
|
||||
c-0.1-0.1-0.3-0.1-0.5-0.2l-0.1,0c-0.1-0.1-0.3-0.1-0.5-0.1h0c-0.3-0.1-0.5-0.2-0.8-0.3l0,0c-0.3-0.1-0.6-0.2-0.8-0.3l0,0
|
||||
c-0.2-0.1-0.3-0.1-0.5-0.1l0,0c-0.7-0.2-1.5-0.4-2.3-0.5l0,0c-0.6-0.1-1.2-0.2-1.8-0.3v19.2c0.2,0.1,0.4,0.1,0.6,0.1
|
||||
C30.3,42,41,39.4,41.8,33.2c0.4-3.3-2-6.9-6.1-10c3.3,1.9,6,4.1,7.8,6.4c1,1.4,1.7,2.7,2,4.1c-0.1-1.9-0.9-4.8-2-6.9L43.5,18.5
|
||||
L43.5,18.5z"/>
|
||||
<g>
|
||||
<path class="st2" d="M67.5,26.9c0,1-0.2,2-0.7,2.9c-0.4,0.9-1,1.7-1.8,2.3c-0.8,0.7-1.7,1.2-2.8,1.6c-1.1,0.4-2.2,0.6-3.5,0.6H49
|
||||
v-9h4.5v5.3h5.2c0.6,0,1.2-0.1,1.7-0.3c0.5-0.2,1-0.4,1.4-0.7c0.4-0.3,0.7-0.7,0.9-1.1c0.2-0.4,0.3-0.9,0.3-1.4
|
||||
c0-0.5-0.1-1-0.3-1.4c-0.2-0.4-0.5-0.8-0.9-1.1c-0.4-0.3-0.8-0.6-1.4-0.7c-0.5-0.2-1.1-0.3-1.7-0.3H49l2.9-3.8h6.8
|
||||
c1.3,0,2.4,0.2,3.5,0.5c1.1,0.3,2,0.8,2.8,1.5s1.4,1.4,1.8,2.3C67.2,24.9,67.5,25.8,67.5,26.9z"/>
|
||||
<g>
|
||||
<path class="st2" d="M82.8,21.4v6.8l-8.9-8c-0.4-0.3-0.7-0.5-1-0.6c-0.3-0.1-0.6-0.1-0.8-0.1c-0.3,0-0.6,0.1-0.9,0.1
|
||||
c-0.3,0.1-0.6,0.3-0.8,0.5c-0.2,0.2-0.4,0.5-0.5,0.8c-0.1,0.3-0.2,0.8-0.2,1.2v12h4.1v-8.5l8.9,8c0.3,0.3,0.7,0.5,0.9,0.6
|
||||
c0.3,0.1,0.6,0.1,0.8,0.1c0.3,0,0.6-0.1,0.9-0.1c0.3-0.1,0.6-0.3,0.8-0.5c0.2-0.2,0.4-0.5,0.5-0.8c0.1-0.3,0.2-0.8,0.2-1.2V19.9
|
||||
L82.8,21.4z"/>
|
||||
</g>
|
||||
<path class="st2" d="M103,25.5c1.8,0,3.1,0.3,4,1c0.9,0.7,1.4,1.6,1.4,3c0,0.7-0.1,1.4-0.3,2c-0.2,0.6-0.6,1.1-1.1,1.5
|
||||
c-0.5,0.4-1.2,0.7-1.9,0.9c-0.8,0.2-1.7,0.3-2.8,0.3H88.7l2.9-3.7h11c0.5,0,0.9-0.1,1.2-0.3c0.3-0.2,0.4-0.4,0.4-0.8
|
||||
s-0.1-0.7-0.4-0.8c-0.3-0.2-0.6-0.2-1.2-0.2h-7.9c-0.9,0-1.8-0.1-2.4-0.3c-0.7-0.2-1.2-0.5-1.7-0.8c-0.5-0.4-0.8-0.8-1-1.3
|
||||
c-0.2-0.5-0.3-1.1-0.3-1.7c0-0.7,0.1-1.3,0.4-1.9c0.2-0.6,0.6-1,1.1-1.4c0.5-0.4,1.1-0.7,1.9-0.9c0.8-0.2,1.7-0.3,2.8-0.3h12.6
|
||||
l-2.9,3.8H95.1c-0.5,0-0.9,0.1-1.2,0.2c-0.3,0.1-0.4,0.4-0.4,0.8c0,0.4,0.1,0.6,0.4,0.8c0.3,0.1,0.6,0.2,1.2,0.2L103,25.5
|
||||
L103,25.5z"/>
|
||||
</g>
|
||||
<g class="st3">
|
||||
<path class="st4" d="M57.5,36.6v5h-1.4v-2.7v-0.7l0.1-0.4v-0.4h-0.1l-0.2,0.3l-0.2,0.3l-0.4,0.7l-1.7,2.8h-1.3l-1.7-2.8l-0.4-0.7
|
||||
l-0.2-0.3l-0.2-0.3h-0.1l0,0.4v0.4l0.1,0.7v2.7h-1.5v-5h2.4l1.4,2.3l0.4,0.7l0.2,0.3l0.2,0.3H53l0.2-0.3l0.2-0.3l0.4-0.7l1.4-2.3
|
||||
L57.5,36.6L57.5,36.6z"/>
|
||||
<path class="st4" d="M63.6,40.6h-3.3l-0.5,0.9h-1.6l2.6-5H63l2.6,5H64L63.6,40.6z M63.2,39.9l-1.3-2.6l-1.3,2.6H63.2z"/>
|
||||
<path class="st4" d="M66.2,41.6v-5H70c1,0,1.8,0.2,2.2,0.5s0.7,0.8,0.7,1.6c0,0.7,0,1.2-0.1,1.5c0,0.3-0.2,0.6-0.5,0.9
|
||||
c-0.3,0.3-1,0.5-2.3,0.5H66.2L66.2,41.6z M67.7,40.7h2.1c0.7,0,1.2-0.1,1.4-0.3s0.3-0.7,0.3-1.4c0-0.7-0.1-1.2-0.3-1.4
|
||||
s-0.6-0.3-1.3-0.3h-2.2L67.7,40.7L67.7,40.7z"/>
|
||||
<path class="st4" d="M75.3,37.4v1.3h3.6v0.7h-3.6v1.4h3.8v0.8h-5.3v-5h5.3v0.8L75.3,37.4L75.3,37.4z"/>
|
||||
<path class="st4" d="M84.4,37.4v1.3H88v0.7h-3.6v1.4h3.8v0.8H83v-5h5.3v0.8L84.4,37.4L84.4,37.4z"/>
|
||||
<path class="st4" d="M94,40.6h-3.3l-0.5,0.9h-1.6l2.6-5h2.2l2.6,5h-1.5L94,40.6z M93.7,39.9l-1.3-2.6L91,39.9H93.7z"/>
|
||||
<path class="st4" d="M102.4,38.1H101v-0.1c0-0.3-0.1-0.4-0.3-0.5c-0.2-0.1-0.6-0.1-1.2-0.1c-0.7,0-1.2,0-1.4,0.1
|
||||
c-0.2,0.1-0.3,0.3-0.3,0.5c0,0.3,0.1,0.5,0.3,0.6s0.8,0.1,1.7,0.1c1.1,0,1.9,0.1,2.2,0.3c0.3,0.2,0.5,0.5,0.5,1.1
|
||||
c0,0.7-0.2,1.1-0.6,1.3s-1.2,0.3-2.5,0.3c-1.3,0-2.2-0.1-2.5-0.3c-0.4-0.2-0.6-0.6-0.6-1.2V40h1.4v0.1c0,0.3,0.1,0.5,0.3,0.6
|
||||
c0.2,0.1,0.7,0.1,1.5,0.1c0.7,0,1.1,0,1.2-0.1s0.3-0.3,0.3-0.6c0-0.3-0.1-0.4-0.2-0.5c-0.1-0.1-0.4-0.1-0.9-0.1l-0.8,0
|
||||
c-1.2,0-2-0.1-2.3-0.3c-0.4-0.2-0.5-0.6-0.5-1.1c0-0.6,0.2-1,0.6-1.2c0.4-0.2,1.2-0.3,2.5-0.3c1.1,0,1.9,0.1,2.3,0.3
|
||||
c0.4,0.2,0.6,0.5,0.6,1L102.4,38.1L102.4,38.1z"/>
|
||||
<path class="st4" d="M110,36.6l-2.9,3.1v1.9h-1.5v-1.9l-2.8-3.1h1.7l1.2,1.3l0.3,0.4l0.2,0.2l0.2,0.2l0.2-0.2l0.2-0.2l0.3-0.4
|
||||
l1.2-1.3H110z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
@@ -57,6 +57,7 @@ import { OCIConnectionMethod } from "@app/hooks/api/appConnections/types/oci-con
|
||||
import { RailwayConnectionMethod } from "@app/hooks/api/appConnections/types/railway-connection";
|
||||
import { RenderConnectionMethod } from "@app/hooks/api/appConnections/types/render-connection";
|
||||
import { SupabaseConnectionMethod } from "@app/hooks/api/appConnections/types/supabase-connection";
|
||||
import { DNSMadeEasyConnectionMethod } from "@app/hooks/api/appConnections/types/dns-made-easy-connection";
|
||||
|
||||
export const APP_CONNECTION_MAP: Record<
|
||||
AppConnection,
|
||||
@@ -111,6 +112,7 @@ export const APP_CONNECTION_MAP: Record<
|
||||
[AppConnection.Flyio]: { name: "Fly.io", image: "Flyio.svg" },
|
||||
[AppConnection.GitLab]: { name: "GitLab", image: "GitLab.png" },
|
||||
[AppConnection.Cloudflare]: { name: "Cloudflare", image: "Cloudflare.png" },
|
||||
[AppConnection.DNSMadeEasy]: { name: "DNS Made Easy", image: "DNSMadeEasy.svg", size: 120 },
|
||||
[AppConnection.Zabbix]: { name: "Zabbix", image: "Zabbix.png" },
|
||||
[AppConnection.Railway]: { name: "Railway", image: "Railway.png" },
|
||||
[AppConnection.Bitbucket]: { name: "Bitbucket", image: "Bitbucket.png" },
|
||||
@@ -214,6 +216,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
|
||||
return { name: "Client Secret", icon: faKey };
|
||||
case AzureClientSecretsConnectionMethod.Certificate:
|
||||
return { name: "Certificate", icon: faCertificate };
|
||||
case DNSMadeEasyConnectionMethod.APIKeySecret:
|
||||
return { name: "API Key & Secret", icon: faKey };
|
||||
default:
|
||||
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./queries";
|
||||
export * from "./types";
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { appConnectionKeys } from "../queries";
|
||||
import { TDNSMadeEasyZone } from "./types";
|
||||
|
||||
const dnsMadeEasyConnectionKeys = {
|
||||
all: [...appConnectionKeys.all, "dns-made-easy"] as const,
|
||||
listZones: (connectionId: string) =>
|
||||
[...dnsMadeEasyConnectionKeys.all, "zones", connectionId] as const
|
||||
};
|
||||
|
||||
export const useDNSMadeEasyConnectionListZones = (
|
||||
connectionId: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TDNSMadeEasyZone[],
|
||||
unknown,
|
||||
TDNSMadeEasyZone[],
|
||||
ReturnType<typeof dnsMadeEasyConnectionKeys.listZones>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: dnsMadeEasyConnectionKeys.listZones(connectionId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TDNSMadeEasyZone[]>(
|
||||
`/api/v1/app-connections/dns-made-easy/${connectionId}/dns-made-easy-zones`
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export type TDNSMadeEasyZone = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
@@ -29,6 +29,7 @@ export enum AppConnection {
|
||||
Flyio = "flyio",
|
||||
GitLab = "gitlab",
|
||||
Cloudflare = "cloudflare",
|
||||
DNSMadeEasy = "dns-made-easy",
|
||||
Bitbucket = "bitbucket",
|
||||
Zabbix = "zabbix",
|
||||
Railway = "railway",
|
||||
|
||||
@@ -184,6 +184,10 @@ export type TRedisConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.Redis;
|
||||
};
|
||||
|
||||
export type TDNSMadeEasyConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.DNSMadeEasy;
|
||||
};
|
||||
|
||||
export type TAppConnectionOption =
|
||||
| TAwsConnectionOption
|
||||
| TGitHubConnectionOption
|
||||
@@ -225,7 +229,8 @@ export type TAppConnectionOption =
|
||||
| TOktaConnectionOption
|
||||
| TAzureAdCsConnectionOption
|
||||
| TLaravelForgeConnectionOption
|
||||
| TChefConnectionOption;
|
||||
| TChefConnectionOption
|
||||
| TDNSMadeEasyConnectionOption;
|
||||
|
||||
export type TAppConnectionOptionMap = {
|
||||
[AppConnection.AWS]: TAwsConnectionOption;
|
||||
@@ -257,6 +262,7 @@ export type TAppConnectionOptionMap = {
|
||||
[AppConnection.Flyio]: TFlyioConnectionOption;
|
||||
[AppConnection.GitLab]: TGitlabConnectionOption;
|
||||
[AppConnection.Cloudflare]: TCloudflareConnectionOption;
|
||||
[AppConnection.DNSMadeEasy]: TDNSMadeEasyConnectionOption;
|
||||
[AppConnection.Bitbucket]: TBitbucketConnectionOption;
|
||||
[AppConnection.Zabbix]: TZabbixConnectionOption;
|
||||
[AppConnection.Railway]: TRailwayConnectionOption;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
|
||||
|
||||
export enum DNSMadeEasyConnectionMethod {
|
||||
APIKeySecret = "api-key-secret"
|
||||
}
|
||||
|
||||
export type TDNSMadeEasyConnection = TRootAppConnection & { app: AppConnection.DNSMadeEasy } & {
|
||||
method: DNSMadeEasyConnectionMethod.APIKeySecret;
|
||||
credentials: {
|
||||
apiKey: string;
|
||||
secretKey: string;
|
||||
};
|
||||
};
|
||||
@@ -15,6 +15,7 @@ import { TChefConnection } from "./chef-connection";
|
||||
import { TCloudflareConnection } from "./cloudflare-connection";
|
||||
import { TDatabricksConnection } from "./databricks-connection";
|
||||
import { TDigitalOceanConnection } from "./digital-ocean";
|
||||
import { TDNSMadeEasyConnection } from "./dns-made-easy-connection";
|
||||
import { TFlyioConnection } from "./flyio-connection";
|
||||
import { TGcpConnection } from "./gcp-connection";
|
||||
import { TGitHubConnection } from "./github-connection";
|
||||
@@ -56,6 +57,7 @@ export * from "./camunda-connection";
|
||||
export * from "./checkly-connection";
|
||||
export * from "./chef-connection";
|
||||
export * from "./cloudflare-connection";
|
||||
export * from "./dns-made-easy-connection";
|
||||
export * from "./databricks-connection";
|
||||
export * from "./flyio-connection";
|
||||
export * from "./gcp-connection";
|
||||
@@ -127,7 +129,8 @@ export type TAppConnection =
|
||||
| TNorthflankConnection
|
||||
| TOktaConnection
|
||||
| TRedisConnection
|
||||
| TChefConnection;
|
||||
| TChefConnection
|
||||
| TDNSMadeEasyConnection;
|
||||
|
||||
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id" | "projectId">;
|
||||
|
||||
|
||||
@@ -16,12 +16,14 @@ export const caStatusToNameMap: { [K in CaStatus]: string } = {
|
||||
|
||||
export const ACME_DNS_PROVIDER_NAME_MAP: Record<AcmeDnsProvider, string> = {
|
||||
[AcmeDnsProvider.ROUTE53]: "Route53",
|
||||
[AcmeDnsProvider.Cloudflare]: "Cloudflare"
|
||||
[AcmeDnsProvider.Cloudflare]: "Cloudflare",
|
||||
[AcmeDnsProvider.DNSMadeEasy]: "DNS Made Easy"
|
||||
};
|
||||
|
||||
export const ACME_DNS_PROVIDER_APP_CONNECTION_MAP: Record<AcmeDnsProvider, AppConnection> = {
|
||||
[AcmeDnsProvider.ROUTE53]: AppConnection.AWS,
|
||||
[AcmeDnsProvider.Cloudflare]: AppConnection.Cloudflare
|
||||
[AcmeDnsProvider.Cloudflare]: AppConnection.Cloudflare,
|
||||
[AcmeDnsProvider.DNSMadeEasy]: AppConnection.DNSMadeEasy
|
||||
};
|
||||
|
||||
export const CA_TYPE_CAPABILITIES_MAP: Record<CaType, CaCapability[]> = {
|
||||
|
||||
@@ -21,7 +21,8 @@ export enum CaRenewalType {
|
||||
|
||||
export enum AcmeDnsProvider {
|
||||
ROUTE53 = "route53",
|
||||
Cloudflare = "cloudflare"
|
||||
Cloudflare = "cloudflare",
|
||||
DNSMadeEasy = "dns-made-easy"
|
||||
}
|
||||
|
||||
export enum CaCapability {
|
||||
|
||||
@@ -27,6 +27,10 @@ import {
|
||||
TCloudflareZone,
|
||||
useCloudflareConnectionListZones
|
||||
} from "@app/hooks/api/appConnections/cloudflare";
|
||||
import {
|
||||
TDNSMadeEasyZone,
|
||||
useDNSMadeEasyConnectionListZones
|
||||
} from "@app/hooks/api/appConnections/dns-made-easy";
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import {
|
||||
AcmeDnsProvider,
|
||||
@@ -211,6 +215,11 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
enabled: caType === CaType.ACME
|
||||
});
|
||||
|
||||
const { data: availableDNSMadeEasyConnections, isPending: isDNSMadeEasyPending } =
|
||||
useListAvailableAppConnections(AppConnection.DNSMadeEasy, currentProject.id, {
|
||||
enabled: caType === CaType.ACME
|
||||
});
|
||||
|
||||
const { data: availableAzureConnections, isPending: isAzurePending } =
|
||||
useListAvailableAppConnections(AppConnection.AzureADCS, currentProject.id, {
|
||||
enabled: caType === CaType.AZURE_AD_CS
|
||||
@@ -220,16 +229,24 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
if (caType === CaType.AZURE_AD_CS) {
|
||||
return availableAzureConnections || [];
|
||||
}
|
||||
return [...(availableRoute53Connections || []), ...(availableCloudflareConnections || [])];
|
||||
return [
|
||||
...(availableRoute53Connections || []),
|
||||
...(availableCloudflareConnections || []),
|
||||
...(availableDNSMadeEasyConnections || [])
|
||||
];
|
||||
}, [
|
||||
caType,
|
||||
availableRoute53Connections,
|
||||
availableCloudflareConnections,
|
||||
availableDNSMadeEasyConnections,
|
||||
availableAzureConnections
|
||||
]);
|
||||
|
||||
const isPending =
|
||||
isRoute53Pending || isCloudflarePending || (isAzurePending && caType === CaType.AZURE_AD_CS);
|
||||
isRoute53Pending ||
|
||||
isCloudflarePending ||
|
||||
isDNSMadeEasyPending ||
|
||||
(isAzurePending && caType === CaType.AZURE_AD_CS);
|
||||
|
||||
const dnsAppConnection =
|
||||
caType === CaType.ACME && configuration && "dnsAppConnection" in configuration
|
||||
@@ -241,6 +258,11 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
enabled: dnsProvider === AcmeDnsProvider.Cloudflare && !!dnsAppConnection.id
|
||||
});
|
||||
|
||||
const { data: dnsMadeEasyZones = [], isPending: isDNSMadeEasyZonesPending } =
|
||||
useDNSMadeEasyConnectionListZones(dnsAppConnection.id, {
|
||||
enabled: dnsProvider === AcmeDnsProvider.DNSMadeEasy && !!dnsAppConnection.id
|
||||
});
|
||||
|
||||
// Populate form with CA data when editing
|
||||
useEffect(() => {
|
||||
if (ca && !isCaLoading) {
|
||||
@@ -500,6 +522,32 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{dnsProvider === AcmeDnsProvider.DNSMadeEasy && (
|
||||
<Controller
|
||||
name="configuration.dnsProviderConfig.hostedZoneId"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Zone"
|
||||
>
|
||||
<FilterableSelect
|
||||
isLoading={isDNSMadeEasyZonesPending && !!dnsAppConnection.id}
|
||||
isDisabled={!dnsAppConnection.id}
|
||||
value={dnsMadeEasyZones.find((zone) => zone.id === value)}
|
||||
onChange={(option) => {
|
||||
onChange((option as SingleValue<TDNSMadeEasyZone>)?.id ?? null);
|
||||
}}
|
||||
options={dnsMadeEasyZones}
|
||||
placeholder="Select a zone..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.id}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
|
||||
@@ -24,6 +24,7 @@ import { ChefConnectionForm } from "./ChefConnectionForm";
|
||||
import { CloudflareConnectionForm } from "./CloudflareConnectionForm";
|
||||
import { DatabricksConnectionForm } from "./DatabricksConnectionForm";
|
||||
import { DigitalOceanConnectionForm } from "./DigitalOceanConnectionForm";
|
||||
import { DNSMadeEasyConnectionForm } from "./DNSMadeEasyConnectionForm";
|
||||
import { FlyioConnectionForm } from "./FlyioConnectionForm";
|
||||
import { GcpConnectionForm } from "./GcpConnectionForm";
|
||||
import { GitHubConnectionForm } from "./GitHubConnectionForm";
|
||||
@@ -148,6 +149,8 @@ const CreateForm = ({ app, onComplete, projectId }: CreateFormProps) => {
|
||||
return <GitLabConnectionForm onSubmit={onSubmit} projectId={projectId} />;
|
||||
case AppConnection.Cloudflare:
|
||||
return <CloudflareConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.DNSMadeEasy:
|
||||
return <DNSMadeEasyConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Bitbucket:
|
||||
return <BitbucketConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Zabbix:
|
||||
@@ -306,6 +309,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
|
||||
);
|
||||
case AppConnection.Cloudflare:
|
||||
return <CloudflareConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.DNSMadeEasy:
|
||||
return <DNSMadeEasyConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.Bitbucket:
|
||||
return <BitbucketConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.Zabbix:
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
ModalClose,
|
||||
SecretInput,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||
import { TDNSMadeEasyConnection } from "@app/hooks/api/appConnections";
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
|
||||
import { DNSMadeEasyConnectionMethod } from "@app/hooks/api/appConnections/types/dns-made-easy-connection";
|
||||
import {
|
||||
genericAppConnectionFieldsSchema,
|
||||
GenericAppConnectionsFields
|
||||
} from "./GenericAppConnectionFields";
|
||||
|
||||
type Props = {
|
||||
appConnection?: TDNSMadeEasyConnection;
|
||||
onSubmit: (formData: FormData) => Promise<void>;
|
||||
};
|
||||
|
||||
const rootSchema = genericAppConnectionFieldsSchema.extend({
|
||||
app: z.literal(AppConnection.DNSMadeEasy)
|
||||
});
|
||||
|
||||
const formSchema = z.discriminatedUnion("method", [
|
||||
rootSchema.extend({
|
||||
method: z.literal(DNSMadeEasyConnectionMethod.APIKeySecret),
|
||||
credentials: z.object({
|
||||
apiKey: z.string().trim().min(1, "API Key required"),
|
||||
secretKey: z.string().trim().min(1, "Secret Key required")
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const DNSMadeEasyConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
||||
const isUpdate = Boolean(appConnection);
|
||||
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: appConnection ?? {
|
||||
app: AppConnection.DNSMadeEasy,
|
||||
method: DNSMadeEasyConnectionMethod.APIKeySecret,
|
||||
credentials: {
|
||||
apiKey: "",
|
||||
secretKey: ""
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { isSubmitting, isDirty }
|
||||
} = form;
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
{!isUpdate && <GenericAppConnectionsFields />}
|
||||
<Controller
|
||||
name="method"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
tooltipText={`The method you would like to use to connect with ${
|
||||
APP_CONNECTION_MAP[AppConnection.DNSMadeEasy].name
|
||||
}. This field cannot be changed after creation.`}
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Method"
|
||||
>
|
||||
<Select
|
||||
isDisabled={isUpdate}
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
position="popper"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
>
|
||||
{Object.values(DNSMadeEasyConnectionMethod).map((method) => {
|
||||
return (
|
||||
<SelectItem value={method} key={method}>
|
||||
{getAppConnectionMethodDetails(method).name}{" "}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.apiKey"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="API Key"
|
||||
>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder="af1b628f-3272-46aa-9cde-837d0c59155d"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="credentials.secretKey"
|
||||
control={control}
|
||||
shouldUnregister
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Secret Key"
|
||||
>
|
||||
<SecretInput
|
||||
containerClassName="text-gray-400 group-focus-within:border-primary-400/50! border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
colorSchema="secondary"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting || !isDirty}
|
||||
>
|
||||
{isUpdate ? "Update Credentials" : "Connect to DNS Made Easy"}
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user