Merge branch 'main' of https://github.com/Infisical/infisical into ENG-4117
@@ -1,6 +1,7 @@
|
|||||||
Feature: External CA
|
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
|
Given I create a Cloudflare connection as cloudflare
|
||||||
Then I memorize cloudflare with jq ".appConnection.id" as app_conn_id
|
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
|
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
|
When I create certificate signing request as csr
|
||||||
Then I add names to certificate signing request csr
|
Then I add names to certificate signing request csr
|
||||||
"""
|
"""
|
||||||
{
|
<subject>
|
||||||
"COMMON_NAME": "localhost"
|
|
||||||
}
|
|
||||||
"""
|
"""
|
||||||
# Pebble has a strict rule to only takes SANs
|
# Pebble has a strict rule to only takes SANs
|
||||||
Then I add subject alternative name to certificate signing request csr
|
Then I add subject alternative name to certificate signing request csr
|
||||||
@@ -177,4 +176,196 @@ Feature: External CA
|
|||||||
[
|
[
|
||||||
"localhost"
|
"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
|
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}")
|
@given("I create a external ACME CA with the following config as {var_name}")
|
||||||
def step_impl(context: Context, var_name: str):
|
def step_impl(context: Context, var_name: str):
|
||||||
jwt_token = context.vars["AUTH_TOKEN"]
|
jwt_token = context.vars["AUTH_TOKEN"]
|
||||||
|
|||||||
@@ -776,7 +776,9 @@ export const pkiAcmeServiceFactory = ({
|
|||||||
const cert = await orderCertificate(
|
const cert = await orderCertificate(
|
||||||
{
|
{
|
||||||
caId: certificateAuthority!.id,
|
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),
|
altNames: certificateRequest.subjectAlternativeNames?.map((san) => san.value),
|
||||||
csr: Buffer.from(csrPem),
|
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
|
// 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("{}")
|
.default("{}")
|
||||||
),
|
),
|
||||||
|
DNS_MADE_EASY_SANDBOX_ENABLED: zodStrBool.default("false").optional(),
|
||||||
// smtp options
|
// smtp options
|
||||||
SMTP_HOST: zpStr(z.string().optional()),
|
SMTP_HOST: zpStr(z.string().optional()),
|
||||||
SMTP_IGNORE_TLS: zodStrBool.default("false"),
|
SMTP_IGNORE_TLS: zodStrBool.default("false"),
|
||||||
|
|||||||
@@ -61,6 +61,10 @@ import {
|
|||||||
DigitalOceanConnectionListItemSchema,
|
DigitalOceanConnectionListItemSchema,
|
||||||
SanitizedDigitalOceanConnectionSchema
|
SanitizedDigitalOceanConnectionSchema
|
||||||
} from "@app/services/app-connection/digital-ocean";
|
} 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 { FlyioConnectionListItemSchema, SanitizedFlyioConnectionSchema } from "@app/services/app-connection/flyio";
|
||||||
import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp";
|
import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp";
|
||||||
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
|
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
|
||||||
@@ -170,7 +174,8 @@ const SanitizedAppConnectionSchema = z.union([
|
|||||||
...SanitizedAzureADCSConnectionSchema.options,
|
...SanitizedAzureADCSConnectionSchema.options,
|
||||||
...SanitizedRedisConnectionSchema.options,
|
...SanitizedRedisConnectionSchema.options,
|
||||||
...SanitizedLaravelForgeConnectionSchema.options,
|
...SanitizedLaravelForgeConnectionSchema.options,
|
||||||
...SanitizedChefConnectionSchema.options
|
...SanitizedChefConnectionSchema.options,
|
||||||
|
...SanitizedDNSMadeEasyConnectionSchema.options
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||||
@@ -215,7 +220,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
|||||||
AzureADCSConnectionListItemSchema,
|
AzureADCSConnectionListItemSchema,
|
||||||
RedisConnectionListItemSchema,
|
RedisConnectionListItemSchema,
|
||||||
LaravelForgeConnectionListItemSchema,
|
LaravelForgeConnectionListItemSchema,
|
||||||
ChefConnectionListItemSchema
|
ChefConnectionListItemSchema,
|
||||||
|
DNSMadeEasyConnectionListItemSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
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 { registerChecklyConnectionRouter } from "./checkly-connection-router";
|
||||||
import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router";
|
import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router";
|
||||||
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
|
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
|
||||||
|
import { registerDNSMadeEasyConnectionRouter } from "./dns-made-easy-connection-router";
|
||||||
import { registerDigitalOceanConnectionRouter } from "./digital-ocean-connection-router";
|
import { registerDigitalOceanConnectionRouter } from "./digital-ocean-connection-router";
|
||||||
import { registerFlyioConnectionRouter } from "./flyio-connection-router";
|
import { registerFlyioConnectionRouter } from "./flyio-connection-router";
|
||||||
import { registerGcpConnectionRouter } from "./gcp-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.Flyio]: registerFlyioConnectionRouter,
|
||||||
[AppConnection.GitLab]: registerGitLabConnectionRouter,
|
[AppConnection.GitLab]: registerGitLabConnectionRouter,
|
||||||
[AppConnection.Cloudflare]: registerCloudflareConnectionRouter,
|
[AppConnection.Cloudflare]: registerCloudflareConnectionRouter,
|
||||||
|
[AppConnection.DNSMadeEasy]: registerDNSMadeEasyConnectionRouter,
|
||||||
[AppConnection.Bitbucket]: registerBitbucketConnectionRouter,
|
[AppConnection.Bitbucket]: registerBitbucketConnectionRouter,
|
||||||
[AppConnection.Zabbix]: registerZabbixConnectionRouter,
|
[AppConnection.Zabbix]: registerZabbixConnectionRouter,
|
||||||
[AppConnection.Railway]: registerRailwayConnectionRouter,
|
[AppConnection.Railway]: registerRailwayConnectionRouter,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export enum AppConnection {
|
|||||||
Flyio = "flyio",
|
Flyio = "flyio",
|
||||||
GitLab = "gitlab",
|
GitLab = "gitlab",
|
||||||
Cloudflare = "cloudflare",
|
Cloudflare = "cloudflare",
|
||||||
|
DNSMadeEasy = "dns-made-easy",
|
||||||
Zabbix = "zabbix",
|
Zabbix = "zabbix",
|
||||||
Railway = "railway",
|
Railway = "railway",
|
||||||
Bitbucket = "bitbucket",
|
Bitbucket = "bitbucket",
|
||||||
|
|||||||
@@ -88,6 +88,11 @@ import {
|
|||||||
getDigitalOceanConnectionListItem,
|
getDigitalOceanConnectionListItem,
|
||||||
validateDigitalOceanConnectionCredentials
|
validateDigitalOceanConnectionCredentials
|
||||||
} from "./digital-ocean";
|
} 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 { FlyioConnectionMethod, getFlyioConnectionListItem, validateFlyioConnectionCredentials } from "./flyio";
|
||||||
import { GcpConnectionMethod, getGcpConnectionListItem, validateGcpConnectionCredentials } from "./gcp";
|
import { GcpConnectionMethod, getGcpConnectionListItem, validateGcpConnectionCredentials } from "./gcp";
|
||||||
import { getGitHubConnectionListItem, GitHubConnectionMethod, validateGitHubConnectionCredentials } from "./github";
|
import { getGitHubConnectionListItem, GitHubConnectionMethod, validateGitHubConnectionCredentials } from "./github";
|
||||||
@@ -171,7 +176,8 @@ const PKI_APP_CONNECTIONS = [
|
|||||||
AppConnection.Cloudflare,
|
AppConnection.Cloudflare,
|
||||||
AppConnection.AzureADCS,
|
AppConnection.AzureADCS,
|
||||||
AppConnection.AzureKeyVault,
|
AppConnection.AzureKeyVault,
|
||||||
AppConnection.Chef
|
AppConnection.Chef,
|
||||||
|
AppConnection.DNSMadeEasy
|
||||||
];
|
];
|
||||||
|
|
||||||
export const listAppConnectionOptions = (projectType?: ProjectType) => {
|
export const listAppConnectionOptions = (projectType?: ProjectType) => {
|
||||||
@@ -207,6 +213,7 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => {
|
|||||||
getFlyioConnectionListItem(),
|
getFlyioConnectionListItem(),
|
||||||
getGitLabConnectionListItem(),
|
getGitLabConnectionListItem(),
|
||||||
getCloudflareConnectionListItem(),
|
getCloudflareConnectionListItem(),
|
||||||
|
getDNSMadeEasyConnectionListItem(),
|
||||||
getZabbixConnectionListItem(),
|
getZabbixConnectionListItem(),
|
||||||
getRailwayConnectionListItem(),
|
getRailwayConnectionListItem(),
|
||||||
getBitbucketConnectionListItem(),
|
getBitbucketConnectionListItem(),
|
||||||
@@ -339,6 +346,7 @@ export const validateAppConnectionCredentials = async (
|
|||||||
[AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
|
[AppConnection.DNSMadeEasy]: validateDNSMadeEasyConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.Zabbix]: validateZabbixConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.Zabbix]: validateZabbixConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.Railway]: validateRailwayConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.Railway]: validateRailwayConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
[AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
@@ -395,6 +403,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
|||||||
case OktaConnectionMethod.ApiToken:
|
case OktaConnectionMethod.ApiToken:
|
||||||
case LaravelForgeConnectionMethod.ApiToken:
|
case LaravelForgeConnectionMethod.ApiToken:
|
||||||
return "API Token";
|
return "API Token";
|
||||||
|
case DNSMadeEasyConnectionMethod.APIKeySecret:
|
||||||
|
return "API Key & Secret";
|
||||||
case PostgresConnectionMethod.UsernameAndPassword:
|
case PostgresConnectionMethod.UsernameAndPassword:
|
||||||
case MsSqlConnectionMethod.UsernameAndPassword:
|
case MsSqlConnectionMethod.UsernameAndPassword:
|
||||||
case MySqlConnectionMethod.UsernameAndPassword:
|
case MySqlConnectionMethod.UsernameAndPassword:
|
||||||
@@ -483,6 +493,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
|||||||
[AppConnection.Flyio]: platformManagedCredentialsNotSupported,
|
[AppConnection.Flyio]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.GitLab]: platformManagedCredentialsNotSupported,
|
[AppConnection.GitLab]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Cloudflare]: platformManagedCredentialsNotSupported,
|
[AppConnection.Cloudflare]: platformManagedCredentialsNotSupported,
|
||||||
|
[AppConnection.DNSMadeEasy]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Zabbix]: platformManagedCredentialsNotSupported,
|
[AppConnection.Zabbix]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Railway]: platformManagedCredentialsNotSupported,
|
[AppConnection.Railway]: platformManagedCredentialsNotSupported,
|
||||||
[AppConnection.Bitbucket]: platformManagedCredentialsNotSupported,
|
[AppConnection.Bitbucket]: platformManagedCredentialsNotSupported,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
|||||||
[AppConnection.Flyio]: "Fly.io",
|
[AppConnection.Flyio]: "Fly.io",
|
||||||
[AppConnection.GitLab]: "GitLab",
|
[AppConnection.GitLab]: "GitLab",
|
||||||
[AppConnection.Cloudflare]: "Cloudflare",
|
[AppConnection.Cloudflare]: "Cloudflare",
|
||||||
|
[AppConnection.DNSMadeEasy]: "DNS Made Easy",
|
||||||
[AppConnection.Zabbix]: "Zabbix",
|
[AppConnection.Zabbix]: "Zabbix",
|
||||||
[AppConnection.Railway]: "Railway",
|
[AppConnection.Railway]: "Railway",
|
||||||
[AppConnection.Bitbucket]: "Bitbucket",
|
[AppConnection.Bitbucket]: "Bitbucket",
|
||||||
@@ -77,6 +78,7 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
|
|||||||
[AppConnection.Flyio]: AppConnectionPlanType.Regular,
|
[AppConnection.Flyio]: AppConnectionPlanType.Regular,
|
||||||
[AppConnection.GitLab]: AppConnectionPlanType.Regular,
|
[AppConnection.GitLab]: AppConnectionPlanType.Regular,
|
||||||
[AppConnection.Cloudflare]: AppConnectionPlanType.Regular,
|
[AppConnection.Cloudflare]: AppConnectionPlanType.Regular,
|
||||||
|
[AppConnection.DNSMadeEasy]: AppConnectionPlanType.Regular,
|
||||||
[AppConnection.Zabbix]: AppConnectionPlanType.Regular,
|
[AppConnection.Zabbix]: AppConnectionPlanType.Regular,
|
||||||
[AppConnection.Railway]: AppConnectionPlanType.Regular,
|
[AppConnection.Railway]: AppConnectionPlanType.Regular,
|
||||||
[AppConnection.Bitbucket]: 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 { ValidateCloudflareConnectionCredentialsSchema } from "./cloudflare/cloudflare-connection-schema";
|
||||||
import { cloudflareConnectionService } from "./cloudflare/cloudflare-connection-service";
|
import { cloudflareConnectionService } from "./cloudflare/cloudflare-connection-service";
|
||||||
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
|
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 { databricksConnectionService } from "./databricks/databricks-connection-service";
|
||||||
import { ValidateDigitalOceanConnectionCredentialsSchema } from "./digital-ocean";
|
import { ValidateDigitalOceanConnectionCredentialsSchema } from "./digital-ocean";
|
||||||
import { digitalOceanAppPlatformConnectionService } from "./digital-ocean/digital-ocean-connection-service";
|
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.Flyio]: ValidateFlyioConnectionCredentialsSchema,
|
||||||
[AppConnection.GitLab]: ValidateGitLabConnectionCredentialsSchema,
|
[AppConnection.GitLab]: ValidateGitLabConnectionCredentialsSchema,
|
||||||
[AppConnection.Cloudflare]: ValidateCloudflareConnectionCredentialsSchema,
|
[AppConnection.Cloudflare]: ValidateCloudflareConnectionCredentialsSchema,
|
||||||
|
[AppConnection.DNSMadeEasy]: ValidateDNSMadeEasyConnectionCredentialsSchema,
|
||||||
[AppConnection.Zabbix]: ValidateZabbixConnectionCredentialsSchema,
|
[AppConnection.Zabbix]: ValidateZabbixConnectionCredentialsSchema,
|
||||||
[AppConnection.Railway]: ValidateRailwayConnectionCredentialsSchema,
|
[AppConnection.Railway]: ValidateRailwayConnectionCredentialsSchema,
|
||||||
[AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema,
|
[AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema,
|
||||||
@@ -875,6 +878,7 @@ export const appConnectionServiceFactory = ({
|
|||||||
flyio: flyioConnectionService(connectAppConnectionById),
|
flyio: flyioConnectionService(connectAppConnectionById),
|
||||||
gitlab: gitlabConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
gitlab: gitlabConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||||
cloudflare: cloudflareConnectionService(connectAppConnectionById),
|
cloudflare: cloudflareConnectionService(connectAppConnectionById),
|
||||||
|
dnsMadeEasy: dnsMadeEasyConnectionService(connectAppConnectionById),
|
||||||
zabbix: zabbixConnectionService(connectAppConnectionById),
|
zabbix: zabbixConnectionService(connectAppConnectionById),
|
||||||
railway: railwayConnectionService(connectAppConnectionById),
|
railway: railwayConnectionService(connectAppConnectionById),
|
||||||
bitbucket: bitbucketConnectionService(connectAppConnectionById),
|
bitbucket: bitbucketConnectionService(connectAppConnectionById),
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import {
|
|||||||
TOracleDBConnectionInput,
|
TOracleDBConnectionInput,
|
||||||
TValidateOracleDBConnectionCredentialsSchema
|
TValidateOracleDBConnectionCredentialsSchema
|
||||||
} from "@app/ee/services/app-connections/oracledb";
|
} 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 { 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 { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||||
import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types";
|
import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types";
|
||||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||||
@@ -106,6 +106,12 @@ import {
|
|||||||
TDigitalOceanConnectionInput,
|
TDigitalOceanConnectionInput,
|
||||||
TValidateDigitalOceanCredentialsSchema
|
TValidateDigitalOceanCredentialsSchema
|
||||||
} from "./digital-ocean";
|
} from "./digital-ocean";
|
||||||
|
import {
|
||||||
|
TDNSMadeEasyConnection,
|
||||||
|
TDNSMadeEasyConnectionConfig,
|
||||||
|
TDNSMadeEasyConnectionInput,
|
||||||
|
TValidateDNSMadeEasyConnectionCredentialsSchema
|
||||||
|
} from "./dns-made-easy/dns-made-easy-connection-types";
|
||||||
import {
|
import {
|
||||||
TFlyioConnection,
|
TFlyioConnection,
|
||||||
TFlyioConnectionConfig,
|
TFlyioConnectionConfig,
|
||||||
@@ -279,6 +285,7 @@ export type TAppConnection = { id: string } & (
|
|||||||
| TGitLabConnection
|
| TGitLabConnection
|
||||||
| TCloudflareConnection
|
| TCloudflareConnection
|
||||||
| TBitbucketConnection
|
| TBitbucketConnection
|
||||||
|
| TDNSMadeEasyConnection
|
||||||
| TZabbixConnection
|
| TZabbixConnection
|
||||||
| TRailwayConnection
|
| TRailwayConnection
|
||||||
| TChecklyConnection
|
| TChecklyConnection
|
||||||
@@ -328,6 +335,7 @@ export type TAppConnectionInput = { id: string } & (
|
|||||||
| TGitLabConnectionInput
|
| TGitLabConnectionInput
|
||||||
| TCloudflareConnectionInput
|
| TCloudflareConnectionInput
|
||||||
| TBitbucketConnectionInput
|
| TBitbucketConnectionInput
|
||||||
|
| TDNSMadeEasyConnectionInput
|
||||||
| TZabbixConnectionInput
|
| TZabbixConnectionInput
|
||||||
| TRailwayConnectionInput
|
| TRailwayConnectionInput
|
||||||
| TChecklyConnectionInput
|
| TChecklyConnectionInput
|
||||||
@@ -395,6 +403,7 @@ export type TAppConnectionConfig =
|
|||||||
| TGitLabConnectionConfig
|
| TGitLabConnectionConfig
|
||||||
| TCloudflareConnectionConfig
|
| TCloudflareConnectionConfig
|
||||||
| TBitbucketConnectionConfig
|
| TBitbucketConnectionConfig
|
||||||
|
| TDNSMadeEasyConnectionConfig
|
||||||
| TZabbixConnectionConfig
|
| TZabbixConnectionConfig
|
||||||
| TRailwayConnectionConfig
|
| TRailwayConnectionConfig
|
||||||
| TChecklyConnectionConfig
|
| TChecklyConnectionConfig
|
||||||
@@ -439,6 +448,7 @@ export type TValidateAppConnectionCredentialsSchema =
|
|||||||
| TValidateGitLabConnectionCredentialsSchema
|
| TValidateGitLabConnectionCredentialsSchema
|
||||||
| TValidateCloudflareConnectionCredentialsSchema
|
| TValidateCloudflareConnectionCredentialsSchema
|
||||||
| TValidateBitbucketConnectionCredentialsSchema
|
| TValidateBitbucketConnectionCredentialsSchema
|
||||||
|
| TValidateDNSMadeEasyConnectionCredentialsSchema
|
||||||
| TValidateZabbixConnectionCredentialsSchema
|
| TValidateZabbixConnectionCredentialsSchema
|
||||||
| TValidateRailwayConnectionCredentialsSchema
|
| TValidateRailwayConnectionCredentialsSchema
|
||||||
| TValidateChecklyConnectionCredentialsSchema
|
| 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 {
|
export enum AcmeDnsProvider {
|
||||||
Route53 = "route53",
|
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 { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||||
import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types";
|
import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types";
|
||||||
import { TCloudflareConnection } from "@app/services/app-connection/cloudflare/cloudflare-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 { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||||
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||||
@@ -43,6 +44,7 @@ import {
|
|||||||
TUpdateAcmeCertificateAuthorityDTO
|
TUpdateAcmeCertificateAuthorityDTO
|
||||||
} from "./acme-certificate-authority-types";
|
} from "./acme-certificate-authority-types";
|
||||||
import { cloudflareDeleteTxtRecord, cloudflareInsertTxtRecord } from "./dns-providers/cloudflare";
|
import { cloudflareDeleteTxtRecord, cloudflareInsertTxtRecord } from "./dns-providers/cloudflare";
|
||||||
|
import { dnsMadeEasyDeleteTxtRecord, dnsMadeEasyInsertTxtRecord } from "./dns-providers/dns-made-easy";
|
||||||
import { route53DeleteTxtRecord, route53InsertTxtRecord } from "./dns-providers/route54";
|
import { route53DeleteTxtRecord, route53InsertTxtRecord } from "./dns-providers/route54";
|
||||||
|
|
||||||
type TAcmeCertificateAuthorityFnsDeps = {
|
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 (
|
export const orderCertificate = async (
|
||||||
{
|
{
|
||||||
caId,
|
caId,
|
||||||
@@ -241,8 +259,11 @@ export const orderCertificate = async (
|
|||||||
throw new Error("Unsupported challenge type");
|
throw new Error("Unsupported challenge type");
|
||||||
}
|
}
|
||||||
|
|
||||||
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
|
const { recordName, recordValue } = getAcmeChallengeRecord(
|
||||||
const recordValue = `"${keyAuthorization}"`; // must be double quoted
|
acmeCa.configuration.dnsProviderConfig.provider,
|
||||||
|
authz.identifier.value,
|
||||||
|
keyAuthorization
|
||||||
|
);
|
||||||
|
|
||||||
switch (acmeCa.configuration.dnsProviderConfig.provider) {
|
switch (acmeCa.configuration.dnsProviderConfig.provider) {
|
||||||
case AcmeDnsProvider.Route53: {
|
case AcmeDnsProvider.Route53: {
|
||||||
@@ -263,14 +284,26 @@ export const orderCertificate = async (
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case AcmeDnsProvider.DNSMadeEasy: {
|
||||||
|
await dnsMadeEasyInsertTxtRecord(
|
||||||
|
connection as TDNSMadeEasyConnection,
|
||||||
|
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
|
||||||
|
recordName,
|
||||||
|
recordValue
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`);
|
throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
|
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
|
||||||
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
|
const { recordName, recordValue } = getAcmeChallengeRecord(
|
||||||
const recordValue = `"${keyAuthorization}"`; // must be double quoted
|
acmeCa.configuration.dnsProviderConfig.provider,
|
||||||
|
authz.identifier.value,
|
||||||
|
keyAuthorization
|
||||||
|
);
|
||||||
|
|
||||||
switch (acmeCa.configuration.dnsProviderConfig.provider) {
|
switch (acmeCa.configuration.dnsProviderConfig.provider) {
|
||||||
case AcmeDnsProvider.Route53: {
|
case AcmeDnsProvider.Route53: {
|
||||||
@@ -291,6 +324,15 @@ export const orderCertificate = async (
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case AcmeDnsProvider.DNSMadeEasy: {
|
||||||
|
await dnsMadeEasyDeleteTxtRecord(
|
||||||
|
connection as TDNSMadeEasyConnection,
|
||||||
|
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
|
||||||
|
recordName,
|
||||||
|
recordValue
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`);
|
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
|
// validates permission to connect
|
||||||
await appConnectionService.validateAppConnectionUsageById(
|
await appConnectionService.validateAppConnectionUsageById(
|
||||||
appConnection.app as AppConnection,
|
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);
|
const ca = await certificateAuthorityDAL.findById(id);
|
||||||
|
|
||||||
if (!ca) {
|
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",
|
GCP_CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform",
|
||||||
|
|
||||||
GITHUB_USER_INSTALLATIONS = "https://api.github.com/user/installations",
|
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 () => {
|
export const getIntegrationOptions = async () => {
|
||||||
|
|||||||
@@ -118,6 +118,7 @@
|
|||||||
"integrations/app-connections/cloudflare",
|
"integrations/app-connections/cloudflare",
|
||||||
"integrations/app-connections/databricks",
|
"integrations/app-connections/databricks",
|
||||||
"integrations/app-connections/digital-ocean",
|
"integrations/app-connections/digital-ocean",
|
||||||
|
"integrations/app-connections/dns-made-easy",
|
||||||
"integrations/app-connections/flyio",
|
"integrations/app-connections/flyio",
|
||||||
"integrations/app-connections/gcp",
|
"integrations/app-connections/gcp",
|
||||||
"integrations/app-connections/github",
|
"integrations/app-connections/github",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ their **ACME Directory URL** such as:
|
|||||||
- ZeroSSL: `https://acme.zerossl.com/v2/DV90`.
|
- ZeroSSL: `https://acme.zerossl.com/v2/DV90`.
|
||||||
- SSL.com: `https://acme.ssl.com/sslcom-dv-rsa`.
|
- SSL.com: `https://acme.ssl.com/sslcom-dv-rsa`.
|
||||||
|
|
||||||
When Infisical requests a certificate from an ACME-compatible CA, it creates a TXT record at `_acme-challenge.{your-domain}` in your configured DNS provider (e.g. Route53, Cloudflare, etc.); this TXT record contains the challenge token issued by the ACME-compatible CA to validate domain control for the requested certificate.
|
When Infisical requests a certificate from an ACME-compatible CA, it creates a TXT record at `_acme-challenge.{your-domain}` in your configured DNS provider (e.g. Route53, Cloudflare, DNS Made Easy, etc.); this TXT record contains the challenge token issued by the ACME-compatible CA to validate domain control for the requested certificate.
|
||||||
The ACME provider checks for the existence of this TXT record to verify domain control before issuing the certificate back to Infisical.
|
The ACME provider checks for the existence of this TXT record to verify domain control before issuing the certificate back to Infisical.
|
||||||
|
|
||||||
After validation completes successfully, Infisical automatically removes the TXT record from your DNS provider.
|
After validation completes successfully, Infisical automatically removes the TXT record from your DNS provider.
|
||||||
@@ -120,6 +120,11 @@ In the following steps, we explore how to connect Infisical to an ACME-compatibl
|
|||||||
|
|
||||||
For detailed instructions on setting up a Cloudflare connection, see the [Cloudflare Connection](/integrations/app-connections/cloudflare) documentation.
|
For detailed instructions on setting up a Cloudflare connection, see the [Cloudflare Connection](/integrations/app-connections/cloudflare) documentation.
|
||||||
</Tab>
|
</Tab>
|
||||||
|
<Tab title="DNS Made Easy">
|
||||||
|
Navigate to your Certificate Management Project > App Connections and create a new DNS Made Easy connection.
|
||||||
|
|
||||||
|
For detailed instructions on setting up a DNS Made Easy connection, see the [DNS Made Easy Connection](/integrations/app-connections/dns-made-easy) documentation.
|
||||||
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Register an ACME-compatible CA">
|
<Step title="Register an ACME-compatible CA">
|
||||||
|
|||||||
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 149 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 407 KiB After Width: | Height: | Size: 166 KiB |
59
docs/integrations/app-connections/dns-made-easy.mdx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
title: "DNS Made Easy"
|
||||||
|
description: "Learn how to configure a DNS Made Easy Connection for Infisical."
|
||||||
|
---
|
||||||
|
|
||||||
|
Infisical supports connecting to DNS Made Easy using API key and secret key for secure access to your DNS Made Easy service.
|
||||||
|
|
||||||
|
## Configure API key and secret Key for Infisical
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Generate API key and secret key">
|
||||||
|
Navigate to your DNS Made Easy dashboard and go to **Account Information** under the **Config** top menu.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
If your **API Key** and **Secret Key** are already available, proceed to step 2.
|
||||||
|
|
||||||
|
Otherwise, check the **Generate New API Credentials** then click the **Save** button to generate the new API credentials.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Copy Your API Key and Secret Key">
|
||||||
|
After creation, copy your API key and secret key.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
Keep your API key and secret key secure and do not share it.
|
||||||
|
Anyone with access to this token can manage your DNS Made Easy resources.
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Setup DNS Made Easy Connection in Infisical
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Navigate to App Connections">
|
||||||
|
Navigate to the **App Connections** page in the desired project. 
|
||||||
|
</Step>
|
||||||
|
<Step title="Add Connection">
|
||||||
|
Select the **DNS Made Easy Connection** option from the connection options
|
||||||
|
modal. 
|
||||||
|
</Step>
|
||||||
|
<Step title="Input Credentials">
|
||||||
|
Enter your DNS Made Easy API key and secret key in the provided fields and
|
||||||
|
click **Connect to DNS Made Easy** to establish the connection. 
|
||||||
|
</Step>
|
||||||
|
<Step title="Connection Created">
|
||||||
|
Your **DNS Made Easy Connection** is now available for use in your Infisical
|
||||||
|
projects. 
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
@@ -50,6 +50,11 @@ Prerequisites:
|
|||||||

|
team](/images/integrations/octopus-deploy/integrations-octopus-deploy-create-team.png)
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
If you need to sync only one space, assign the service account to that space. Otherwise, allow access to all spaces for multi-space syncs.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
|
||||||
On the **Members** tab, click on the **Add Member** button, add your **Infisical Service Account** and click on the **Add** button.
|
On the **Members** tab, click on the **Add Member** button, add your **Infisical Service Account** and click on the **Add** button.
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -1,70 +1,388 @@
|
|||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useMemo } from "react";
|
||||||
|
|
||||||
export const AppConnectionsBrowser = () => {
|
export const AppConnectionsBrowser = () => {
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [selectedCategory, setSelectedCategory] = useState('All');
|
const [selectedCategory, setSelectedCategory] = useState("All");
|
||||||
|
|
||||||
const categories = ['All', 'Cloud Providers', 'Databases', 'CI/CD', 'Monitoring', 'Directory Services', 'Identity & Auth', 'Data Analytics', 'Hosting', 'DevOps Tools', 'Security'];
|
const categories = [
|
||||||
|
"All",
|
||||||
|
"Cloud Providers",
|
||||||
|
"Databases",
|
||||||
|
"CI/CD",
|
||||||
|
"Monitoring",
|
||||||
|
"Directory Services",
|
||||||
|
"Identity & Auth",
|
||||||
|
"Data Analytics",
|
||||||
|
"Hosting",
|
||||||
|
"DevOps Tools",
|
||||||
|
"Security",
|
||||||
|
"Networking & DNS",
|
||||||
|
];
|
||||||
|
|
||||||
const connections = [
|
const connections = [
|
||||||
{"name": "AWS", "slug": "aws", "path": "/integrations/app-connections/aws", "description": "Learn how to connect your AWS applications to pull secrets from Infisical.", "category": "Cloud Providers"},
|
{
|
||||||
{"name": "Azure Key Vault", "slug": "azure-key-vault", "path": "/integrations/app-connections/azure-key-vault", "description": "Learn how to connect your Azure Key Vault to pull secrets from Infisical.", "category": "Cloud Providers"},
|
name: "AWS",
|
||||||
{"name": "Azure App Configuration", "slug": "azure-app-configuration", "path": "/integrations/app-connections/azure-app-configuration", "description": "Learn how to connect your Azure App Configuration to pull secrets from Infisical.", "category": "Cloud Providers"},
|
slug: "aws",
|
||||||
{"name": "Azure Client Secrets", "slug": "azure-client-secrets", "path": "/integrations/app-connections/azure-client-secrets", "description": "Learn how to connect your Azure Client Secrets to pull secrets from Infisical.", "category": "Cloud Providers"},
|
path: "/integrations/app-connections/aws",
|
||||||
{"name": "Azure DevOps", "slug": "azure-devops", "path": "/integrations/app-connections/azure-devops", "description": "Learn how to connect your Azure DevOps to pull secrets from Infisical.", "category": "CI/CD"},
|
description:
|
||||||
{"name": "Azure ADCS", "slug": "azure-adcs", "path": "/integrations/app-connections/azure-adcs", "description": "Learn how to connect your Azure ADCS to pull secrets from Infisical.", "category": "Cloud Providers"},
|
"Learn how to connect your AWS applications to pull secrets from Infisical.",
|
||||||
{"name": "GCP", "slug": "gcp", "path": "/integrations/app-connections/gcp", "description": "Learn how to connect your GCP applications to pull secrets from Infisical.", "category": "Cloud Providers"},
|
category: "Cloud Providers",
|
||||||
{"name": "HashiCorp Vault", "slug": "hashicorp-vault", "path": "/integrations/app-connections/hashicorp-vault", "description": "Learn how to connect your HashiCorp Vault to pull secrets from Infisical.", "category": "Security"},
|
},
|
||||||
{"name": "1Password", "slug": "1password", "path": "/integrations/app-connections/1password", "description": "Learn how to connect your 1Password to pull secrets from Infisical.", "category": "Security"},
|
{
|
||||||
{"name": "Vercel", "slug": "vercel", "path": "/integrations/app-connections/vercel", "description": "Learn how to connect your Vercel application to pull secrets from Infisical.", "category": "Hosting"},
|
name: "Azure Key Vault",
|
||||||
{"name": "Netlify", "slug": "netlify", "path": "/integrations/app-connections/netlify", "description": "Learn how to connect your Netlify application to pull secrets from Infisical.", "category": "Hosting"},
|
slug: "azure-key-vault",
|
||||||
{"name": "Railway", "slug": "railway", "path": "/integrations/app-connections/railway", "description": "Learn how to connect your Railway application to pull secrets from Infisical.", "category": "Hosting"},
|
path: "/integrations/app-connections/azure-key-vault",
|
||||||
{"name": "Fly.io", "slug": "flyio", "path": "/integrations/app-connections/flyio", "description": "Learn how to connect your Fly.io application to pull secrets from Infisical.", "category": "Hosting"},
|
description:
|
||||||
{"name": "Render", "slug": "render", "path": "/integrations/app-connections/render", "description": "Learn how to connect your Render application to pull secrets from Infisical.", "category": "Hosting"},
|
"Learn how to connect your Azure Key Vault to pull secrets from Infisical.",
|
||||||
{"name": "Heroku", "slug": "heroku", "path": "/integrations/app-connections/heroku", "description": "Learn how to connect your Heroku application to pull secrets from Infisical.", "category": "Hosting"},
|
category: "Cloud Providers",
|
||||||
{"name": "DigitalOcean", "slug": "digital-ocean", "path": "/integrations/app-connections/digital-ocean", "description": "Learn how to connect your DigitalOcean application to pull secrets from Infisical.", "category": "Hosting"},
|
},
|
||||||
{"name": "Supabase", "slug": "supabase", "path": "/integrations/app-connections/supabase", "description": "Learn how to connect your Supabase application to pull secrets from Infisical.", "category": "Databases"},
|
{
|
||||||
{"name": "Checkly", "slug": "checkly", "path": "/integrations/app-connections/checkly", "description": "Learn how to connect your Checkly application to pull secrets from Infisical.", "category": "Monitoring"},
|
name: "Azure App Configuration",
|
||||||
{"name": "GitHub", "slug": "github", "path": "/integrations/app-connections/github", "description": "Learn how to connect your GitHub application to pull secrets from Infisical.", "category": "CI/CD"},
|
slug: "azure-app-configuration",
|
||||||
{"name": "GitHub Radar", "slug": "github-radar", "path": "/integrations/app-connections/github-radar", "description": "Learn how to connect your GitHub Radar to pull secrets from Infisical.", "category": "CI/CD"},
|
path: "/integrations/app-connections/azure-app-configuration",
|
||||||
{"name": "GitLab", "slug": "gitlab", "path": "/integrations/app-connections/gitlab", "description": "Learn how to connect your GitLab application to pull secrets from Infisical.", "category": "CI/CD"},
|
description:
|
||||||
{"name": "TeamCity", "slug": "teamcity", "path": "/integrations/app-connections/teamcity", "description": "Learn how to connect your TeamCity to pull secrets from Infisical.", "category": "CI/CD"},
|
"Learn how to connect your Azure App Configuration to pull secrets from Infisical.",
|
||||||
{"name": "Bitbucket", "slug": "bitbucket", "path": "/integrations/app-connections/bitbucket", "description": "Learn how to connect your Bitbucket to pull secrets from Infisical.", "category": "CI/CD"},
|
category: "Cloud Providers",
|
||||||
{"name": "Terraform Cloud", "slug": "terraform-cloud", "path": "/integrations/app-connections/terraform-cloud", "description": "Learn how to connect your Terraform Cloud to pull secrets from Infisical.", "category": "DevOps Tools"},
|
},
|
||||||
{"name": "Cloudflare", "slug": "cloudflare", "path": "/integrations/app-connections/cloudflare", "description": "Learn how to connect your Cloudflare application to pull secrets from Infisical.", "category": "Cloud Providers"},
|
{
|
||||||
{"name": "Databricks", "slug": "databricks", "path": "/integrations/app-connections/databricks", "description": "Learn how to connect your Databricks to pull secrets from Infisical.", "category": "Data Analytics"},
|
name: "Azure Client Secrets",
|
||||||
{"name": "Windmill", "slug": "windmill", "path": "/integrations/app-connections/windmill", "description": "Learn how to connect your Windmill to pull secrets from Infisical.", "category": "DevOps Tools"},
|
slug: "azure-client-secrets",
|
||||||
{"name": "Camunda", "slug": "camunda", "path": "/integrations/app-connections/camunda", "description": "Learn how to connect your Camunda to pull secrets from Infisical.", "category": "DevOps Tools"},
|
path: "/integrations/app-connections/azure-client-secrets",
|
||||||
{"name": "Humanitec", "slug": "humanitec", "path": "/integrations/app-connections/humanitec", "description": "Learn how to connect your Humanitec to pull secrets from Infisical.", "category": "DevOps Tools"},
|
description:
|
||||||
{"name": "OCI", "slug": "oci", "path": "/integrations/app-connections/oci", "description": "Learn how to connect your OCI applications to pull secrets from Infisical.", "category": "Cloud Providers"},
|
"Learn how to connect your Azure Client Secrets to pull secrets from Infisical.",
|
||||||
{"name": "Zabbix", "slug": "zabbix", "path": "/integrations/app-connections/zabbix", "description": "Learn how to connect your Zabbix to pull secrets from Infisical.", "category": "Monitoring"},
|
category: "Cloud Providers",
|
||||||
{"name": "MySQL", "slug": "mysql", "path": "/integrations/app-connections/mysql", "description": "Learn how to connect your MySQL database to pull secrets from Infisical.", "category": "Databases"},
|
},
|
||||||
{"name": "PostgreSQL", "slug": "postgres", "path": "/integrations/app-connections/postgres", "description": "Learn how to connect your PostgreSQL database to pull secrets from Infisical.", "category": "Databases"},
|
{
|
||||||
{"name": "Microsoft SQL Server", "slug": "mssql", "path": "/integrations/app-connections/mssql", "description": "Learn how to connect your SQL Server database to pull secrets from Infisical.", "category": "Databases"},
|
name: "Azure DevOps",
|
||||||
{"name": "Oracle Database", "slug": "oracledb", "path": "/integrations/app-connections/oracledb", "description": "Learn how to connect your Oracle database to pull secrets from Infisical.", "category": "Databases"},
|
slug: "azure-devops",
|
||||||
{"name": "Redis", "slug": "redis", "path": "/integrations/app-connections/redis", "description": "Learn how to connect Redis to pull secrets from Infisical.", "category": "Databases"},
|
path: "/integrations/app-connections/azure-devops",
|
||||||
{"name": "LDAP", "slug": "ldap", "path": "/integrations/app-connections/ldap", "description": "Learn how to connect your LDAP to pull secrets from Infisical.", "category": "Directory Services"},
|
description:
|
||||||
{"name": "Auth0", "slug": "auth0", "path": "/integrations/app-connections/auth0", "description": "Learn how to connect your Auth0 to pull secrets from Infisical.", "category": "Identity & Auth"},
|
"Learn how to connect your Azure DevOps to pull secrets from Infisical.",
|
||||||
{"name": "Okta", "slug": "okta", "path": "/integrations/app-connections/okta", "description": "Learn how to connect your Okta to pull secrets from Infisical.", "category": "Identity & Auth"},
|
category: "CI/CD",
|
||||||
{"name": "Laravel Forge", "slug": "laravel-forge", "path": "/integrations/app-connections/laravel-forge", "description": "Learn how to connect your Laravel Forge to pull secrets from Infisical.", "category": "Hosting"},
|
},
|
||||||
{"name": "Chef", "slug": "chef", "path": "/integrations/app-connections/chef", "description": "Learn how to connect your Chef to pull secrets from Infisical.", "category": "DevOps Tools"},
|
{
|
||||||
{"name": "Northflank", "slug": "northflank", "path": "/integrations/app-connections/northflank", "description": "Learn how to connect your Northflank projects to pull secrets from Infisical.", "category": "Hosting"}
|
name: "Azure ADCS",
|
||||||
].sort(function(a, b) {
|
slug: "azure-adcs",
|
||||||
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
path: "/integrations/app-connections/azure-adcs",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Azure ADCS to pull secrets from Infisical.",
|
||||||
|
category: "Cloud Providers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GCP",
|
||||||
|
slug: "gcp",
|
||||||
|
path: "/integrations/app-connections/gcp",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your GCP applications to pull secrets from Infisical.",
|
||||||
|
category: "Cloud Providers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "HashiCorp Vault",
|
||||||
|
slug: "hashicorp-vault",
|
||||||
|
path: "/integrations/app-connections/hashicorp-vault",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your HashiCorp Vault to pull secrets from Infisical.",
|
||||||
|
category: "Security",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "1Password",
|
||||||
|
slug: "1password",
|
||||||
|
path: "/integrations/app-connections/1password",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your 1Password to pull secrets from Infisical.",
|
||||||
|
category: "Security",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Vercel",
|
||||||
|
slug: "vercel",
|
||||||
|
path: "/integrations/app-connections/vercel",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Vercel application to pull secrets from Infisical.",
|
||||||
|
category: "Hosting",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Netlify",
|
||||||
|
slug: "netlify",
|
||||||
|
path: "/integrations/app-connections/netlify",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Netlify application to pull secrets from Infisical.",
|
||||||
|
category: "Hosting",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Railway",
|
||||||
|
slug: "railway",
|
||||||
|
path: "/integrations/app-connections/railway",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Railway application to pull secrets from Infisical.",
|
||||||
|
category: "Hosting",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Fly.io",
|
||||||
|
slug: "flyio",
|
||||||
|
path: "/integrations/app-connections/flyio",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Fly.io application to pull secrets from Infisical.",
|
||||||
|
category: "Hosting",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Render",
|
||||||
|
slug: "render",
|
||||||
|
path: "/integrations/app-connections/render",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Render application to pull secrets from Infisical.",
|
||||||
|
category: "Hosting",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Heroku",
|
||||||
|
slug: "heroku",
|
||||||
|
path: "/integrations/app-connections/heroku",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Heroku application to pull secrets from Infisical.",
|
||||||
|
category: "Hosting",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "DigitalOcean",
|
||||||
|
slug: "digital-ocean",
|
||||||
|
path: "/integrations/app-connections/digital-ocean",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your DigitalOcean application to pull secrets from Infisical.",
|
||||||
|
category: "Hosting",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Supabase",
|
||||||
|
slug: "supabase",
|
||||||
|
path: "/integrations/app-connections/supabase",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Supabase application to pull secrets from Infisical.",
|
||||||
|
category: "Databases",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Checkly",
|
||||||
|
slug: "checkly",
|
||||||
|
path: "/integrations/app-connections/checkly",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Checkly application to pull secrets from Infisical.",
|
||||||
|
category: "Monitoring",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GitHub",
|
||||||
|
slug: "github",
|
||||||
|
path: "/integrations/app-connections/github",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your GitHub application to pull secrets from Infisical.",
|
||||||
|
category: "CI/CD",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GitHub Radar",
|
||||||
|
slug: "github-radar",
|
||||||
|
path: "/integrations/app-connections/github-radar",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your GitHub Radar to pull secrets from Infisical.",
|
||||||
|
category: "CI/CD",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GitLab",
|
||||||
|
slug: "gitlab",
|
||||||
|
path: "/integrations/app-connections/gitlab",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your GitLab application to pull secrets from Infisical.",
|
||||||
|
category: "CI/CD",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "TeamCity",
|
||||||
|
slug: "teamcity",
|
||||||
|
path: "/integrations/app-connections/teamcity",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your TeamCity to pull secrets from Infisical.",
|
||||||
|
category: "CI/CD",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Bitbucket",
|
||||||
|
slug: "bitbucket",
|
||||||
|
path: "/integrations/app-connections/bitbucket",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Bitbucket to pull secrets from Infisical.",
|
||||||
|
category: "CI/CD",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Terraform Cloud",
|
||||||
|
slug: "terraform-cloud",
|
||||||
|
path: "/integrations/app-connections/terraform-cloud",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Terraform Cloud to pull secrets from Infisical.",
|
||||||
|
category: "DevOps Tools",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Cloudflare",
|
||||||
|
slug: "cloudflare",
|
||||||
|
path: "/integrations/app-connections/cloudflare",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Cloudflare application to pull secrets from Infisical.",
|
||||||
|
category: "Cloud Providers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Databricks",
|
||||||
|
slug: "databricks",
|
||||||
|
path: "/integrations/app-connections/databricks",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Databricks to pull secrets from Infisical.",
|
||||||
|
category: "Data Analytics",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "DNS Made Easy",
|
||||||
|
slug: "dns-made-easy",
|
||||||
|
path: "/integrations/app-connections/dns-made-easy",
|
||||||
|
description: "Learn how to connect Infisical to DNS Made Easy.",
|
||||||
|
category: "Networking & DNS",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Windmill",
|
||||||
|
slug: "windmill",
|
||||||
|
path: "/integrations/app-connections/windmill",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Windmill to pull secrets from Infisical.",
|
||||||
|
category: "DevOps Tools",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Camunda",
|
||||||
|
slug: "camunda",
|
||||||
|
path: "/integrations/app-connections/camunda",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Camunda to pull secrets from Infisical.",
|
||||||
|
category: "DevOps Tools",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Humanitec",
|
||||||
|
slug: "humanitec",
|
||||||
|
path: "/integrations/app-connections/humanitec",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Humanitec to pull secrets from Infisical.",
|
||||||
|
category: "DevOps Tools",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OCI",
|
||||||
|
slug: "oci",
|
||||||
|
path: "/integrations/app-connections/oci",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your OCI applications to pull secrets from Infisical.",
|
||||||
|
category: "Cloud Providers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Zabbix",
|
||||||
|
slug: "zabbix",
|
||||||
|
path: "/integrations/app-connections/zabbix",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Zabbix to pull secrets from Infisical.",
|
||||||
|
category: "Monitoring",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MySQL",
|
||||||
|
slug: "mysql",
|
||||||
|
path: "/integrations/app-connections/mysql",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your MySQL database to pull secrets from Infisical.",
|
||||||
|
category: "Databases",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "PostgreSQL",
|
||||||
|
slug: "postgres",
|
||||||
|
path: "/integrations/app-connections/postgres",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your PostgreSQL database to pull secrets from Infisical.",
|
||||||
|
category: "Databases",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Microsoft SQL Server",
|
||||||
|
slug: "mssql",
|
||||||
|
path: "/integrations/app-connections/mssql",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your SQL Server database to pull secrets from Infisical.",
|
||||||
|
category: "Databases",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Oracle Database",
|
||||||
|
slug: "oracledb",
|
||||||
|
path: "/integrations/app-connections/oracledb",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Oracle database to pull secrets from Infisical.",
|
||||||
|
category: "Databases",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Redis",
|
||||||
|
slug: "redis",
|
||||||
|
path: "/integrations/app-connections/redis",
|
||||||
|
description: "Learn how to connect Redis to pull secrets from Infisical.",
|
||||||
|
category: "Databases",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "LDAP",
|
||||||
|
slug: "ldap",
|
||||||
|
path: "/integrations/app-connections/ldap",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your LDAP to pull secrets from Infisical.",
|
||||||
|
category: "Directory Services",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Auth0",
|
||||||
|
slug: "auth0",
|
||||||
|
path: "/integrations/app-connections/auth0",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Auth0 to pull secrets from Infisical.",
|
||||||
|
category: "Identity & Auth",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Okta",
|
||||||
|
slug: "okta",
|
||||||
|
path: "/integrations/app-connections/okta",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Okta to pull secrets from Infisical.",
|
||||||
|
category: "Identity & Auth",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Laravel Forge",
|
||||||
|
slug: "laravel-forge",
|
||||||
|
path: "/integrations/app-connections/laravel-forge",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Laravel Forge to pull secrets from Infisical.",
|
||||||
|
category: "Hosting",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Chef",
|
||||||
|
slug: "chef",
|
||||||
|
path: "/integrations/app-connections/chef",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Chef to pull secrets from Infisical.",
|
||||||
|
category: "DevOps Tools",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Northflank",
|
||||||
|
slug: "northflank",
|
||||||
|
path: "/integrations/app-connections/northflank",
|
||||||
|
description:
|
||||||
|
"Learn how to connect your Northflank projects to pull secrets from Infisical.",
|
||||||
|
category: "Hosting",
|
||||||
|
},
|
||||||
|
].sort(function (a, b) {
|
||||||
|
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredConnections = useMemo(() => {
|
const filteredConnections = useMemo(() => {
|
||||||
let filtered = connections;
|
let filtered = connections;
|
||||||
|
|
||||||
if (selectedCategory !== 'All') {
|
if (selectedCategory !== "All") {
|
||||||
filtered = filtered.filter(connection => connection.category === selectedCategory);
|
filtered = filtered.filter(
|
||||||
|
(connection) => connection.category === selectedCategory
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
filtered = filtered.filter(connection =>
|
filtered = filtered.filter(
|
||||||
connection.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
(connection) =>
|
||||||
connection.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
connection.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
connection.category.toLowerCase().includes(searchTerm.toLowerCase())
|
connection.description
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchTerm.toLowerCase()) ||
|
||||||
|
connection.category.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,8 +395,18 @@ export const AppConnectionsBrowser = () => {
|
|||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<div className="relative w-full">
|
<div className="relative w-full">
|
||||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
<svg className="h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
className="h-4 w-4 text-gray-400"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
@@ -94,14 +422,14 @@ export const AppConnectionsBrowser = () => {
|
|||||||
{/* Category Filter */}
|
{/* Category Filter */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{categories.map(category => (
|
{categories.map((category) => (
|
||||||
<button
|
<button
|
||||||
key={category}
|
key={category}
|
||||||
onClick={() => setSelectedCategory(category)}
|
onClick={() => setSelectedCategory(category)}
|
||||||
className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors shadow-sm ${
|
className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors shadow-sm ${
|
||||||
selectedCategory === category
|
selectedCategory === category
|
||||||
? 'bg-yellow-100 text-yellow-700 border border-yellow-200'
|
? "bg-yellow-100 text-yellow-700 border border-yellow-200"
|
||||||
: 'bg-white text-gray-700 border border-gray-200 hover:bg-yellow-50 hover:border-yellow-200'
|
: "bg-white text-gray-700 border border-gray-200 hover:bg-yellow-50 hover:border-yellow-200"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{category}
|
{category}
|
||||||
@@ -113,8 +441,9 @@ export const AppConnectionsBrowser = () => {
|
|||||||
{/* Results Count */}
|
{/* Results Count */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
{filteredConnections.length} app connection{filteredConnections.length !== 1 ? 's' : ''} found
|
{filteredConnections.length} app connection
|
||||||
{selectedCategory !== 'All' && ` in ${selectedCategory}`}
|
{filteredConnections.length !== 1 ? "s" : ""} found
|
||||||
|
{selectedCategory !== "All" && ` in ${selectedCategory}`}
|
||||||
{searchTerm && ` for "${searchTerm}"`}
|
{searchTerm && ` for "${searchTerm}"`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -147,13 +476,17 @@ export const AppConnectionsBrowser = () => {
|
|||||||
) : (
|
) : (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<div className="flex flex-col items-center space-y-2">
|
<div className="flex flex-col items-center space-y-2">
|
||||||
<p className="text-gray-500">No app connections found matching your criteria</p>
|
<p className="text-gray-500">
|
||||||
|
No app connections found matching your criteria
|
||||||
|
</p>
|
||||||
{searchTerm && (
|
{searchTerm && (
|
||||||
<p className="text-gray-400 text-sm">Try adjusting your search terms or filters</p>
|
<p className="text-gray-400 text-sm">
|
||||||
|
Try adjusting your search terms or filters
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
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 |
@@ -289,7 +289,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"project": {
|
"project": {
|
||||||
"title": "Settings",
|
"title": "Project Settings",
|
||||||
"description": "These settings only apply to the currently selected Project.",
|
"description": "These settings only apply to the currently selected Project.",
|
||||||
"danger-zone": "Danger Zone",
|
"danger-zone": "Danger Zone",
|
||||||
"delete-project": "Delete Project",
|
"delete-project": "Delete Project",
|
||||||
|
|||||||
@@ -3,14 +3,7 @@ import { ReactNode } from "@tanstack/react-router";
|
|||||||
import { LucideIcon } from "lucide-react";
|
import { LucideIcon } from "lucide-react";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import {
|
import { InstanceIcon, OrgIcon, ProjectIcon, SubOrgIcon } from "@app/components/v3";
|
||||||
Badge,
|
|
||||||
InstanceIcon,
|
|
||||||
OrgIcon,
|
|
||||||
ProjectIcon,
|
|
||||||
SubOrgIcon,
|
|
||||||
TBadgeProps
|
|
||||||
} from "@app/components/v3";
|
|
||||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -21,41 +14,40 @@ type Props = {
|
|||||||
scope: "org" | "namespace" | "instance" | ProjectType | null;
|
scope: "org" | "namespace" | "instance" | ProjectType | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SCOPE_NAME: Record<NonNullable<Props["scope"]>, { label: string; icon: LucideIcon }> = {
|
const SCOPE_BADGE: Record<NonNullable<Props["scope"]>, { icon: LucideIcon; className: string }> = {
|
||||||
org: { label: "Organization", icon: OrgIcon },
|
org: { className: "text-org", icon: OrgIcon },
|
||||||
[ProjectType.SecretManager]: { label: "Project", icon: ProjectIcon },
|
[ProjectType.SecretManager]: { className: "text-project", icon: ProjectIcon },
|
||||||
[ProjectType.CertificateManager]: { label: "Project", icon: ProjectIcon },
|
[ProjectType.CertificateManager]: { className: "text-project", icon: ProjectIcon },
|
||||||
[ProjectType.SSH]: { label: "Project", icon: ProjectIcon },
|
[ProjectType.SSH]: { className: "text-project", icon: ProjectIcon },
|
||||||
[ProjectType.KMS]: { label: "Project", icon: ProjectIcon },
|
[ProjectType.KMS]: { className: "text-project", icon: ProjectIcon },
|
||||||
[ProjectType.PAM]: { label: "Project", icon: ProjectIcon },
|
[ProjectType.PAM]: { className: "text-project", icon: ProjectIcon },
|
||||||
[ProjectType.SecretScanning]: { label: "Project", icon: ProjectIcon },
|
[ProjectType.SecretScanning]: { className: "text-project", icon: ProjectIcon },
|
||||||
namespace: { label: "Sub-Organization", icon: SubOrgIcon },
|
namespace: { className: "text-sub-org", icon: SubOrgIcon },
|
||||||
instance: { label: "Server", icon: InstanceIcon }
|
instance: { className: "text-neutral", icon: InstanceIcon }
|
||||||
};
|
|
||||||
|
|
||||||
const SCOPE_VARIANT: Record<NonNullable<Props["scope"]>, TBadgeProps["variant"]> = {
|
|
||||||
org: "org",
|
|
||||||
[ProjectType.SecretManager]: "project",
|
|
||||||
[ProjectType.CertificateManager]: "project",
|
|
||||||
[ProjectType.SSH]: "project",
|
|
||||||
[ProjectType.KMS]: "project",
|
|
||||||
[ProjectType.PAM]: "project",
|
|
||||||
[ProjectType.SecretScanning]: "project",
|
|
||||||
namespace: "sub-org",
|
|
||||||
instance: "neutral"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageHeader = ({ title, description, children, className, scope }: Props) => (
|
export const PageHeader = ({ title, description, children, className, scope }: Props) => (
|
||||||
<div className={twMerge("mb-10 w-full", className)}>
|
<div className={twMerge("mb-10 w-full", className)}>
|
||||||
<div className="flex w-full justify-between">
|
<div className="flex w-full justify-between">
|
||||||
<div className="mr-4 flex w-full items-center">
|
<div className="mr-4 flex w-full items-center">
|
||||||
<h1 className="text-3xl font-medium text-white capitalize">{title}</h1>
|
<h1
|
||||||
{scope && (
|
className={twMerge(
|
||||||
<Badge variant={SCOPE_VARIANT[scope]} className="mt-1 ml-2.5">
|
"text-3xl font-medium text-white capitalize underline decoration-2 underline-offset-4",
|
||||||
{createElement(SCOPE_NAME[scope].icon)}
|
scope === "org" && "decoration-org/90",
|
||||||
{SCOPE_NAME[scope].label}
|
scope === "instance" && "decoration-neutral/90",
|
||||||
</Badge>
|
scope === "namespace" && "decoration-sub-org/90",
|
||||||
)}
|
Object.values(ProjectType).includes((scope as ProjectType) ?? "") &&
|
||||||
|
"decoration-project/90",
|
||||||
|
!scope && "no-underline"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{scope &&
|
||||||
|
createElement(SCOPE_BADGE[scope].icon, {
|
||||||
|
size: 26,
|
||||||
|
className: twMerge(SCOPE_BADGE[scope].className, "mr-3 mb-1 inline-block")
|
||||||
|
})}
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">{children}</div>
|
<div className="flex items-center gap-2">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ export const Tab = ({
|
|||||||
}) => (
|
}) => (
|
||||||
<TabsPrimitive.Trigger
|
<TabsPrimitive.Trigger
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"flex h-10 cursor-pointer items-center justify-center border-transparent",
|
"flex h-11 cursor-pointer items-center justify-center border-transparent",
|
||||||
"px-3 text-sm font-medium whitespace-nowrap text-mineshaft-400 transition-all select-none",
|
"px-3 text-sm font-medium whitespace-nowrap text-mineshaft-300/75 transition-all select-none",
|
||||||
"data-[orientation=vertical]:xl:h-5 data-[orientation=vertical]:xl:border-b-0 data-[orientation=vertical]:xl:border-l",
|
"data-[orientation=vertical]:xl:h-5 data-[orientation=vertical]:xl:border-b-0 data-[orientation=vertical]:xl:border-l",
|
||||||
"border-b hover:text-mineshaft-200",
|
"border-b hover:text-mineshaft-200",
|
||||||
"data-[state=active]:border-mineshaft-400 data-[state=active]:text-white",
|
"data-[state=active]:border-mineshaft-400 data-[state=active]:text-white",
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import { BitbucketConnectionMethod } from "@app/hooks/api/appConnections/types/b
|
|||||||
import { ChecklyConnectionMethod } from "@app/hooks/api/appConnections/types/checkly-connection";
|
import { ChecklyConnectionMethod } from "@app/hooks/api/appConnections/types/checkly-connection";
|
||||||
import { ChefConnectionMethod } from "@app/hooks/api/appConnections/types/chef-connection";
|
import { ChefConnectionMethod } from "@app/hooks/api/appConnections/types/chef-connection";
|
||||||
import { DigitalOceanConnectionMethod } from "@app/hooks/api/appConnections/types/digital-ocean";
|
import { DigitalOceanConnectionMethod } from "@app/hooks/api/appConnections/types/digital-ocean";
|
||||||
|
import { DNSMadeEasyConnectionMethod } from "@app/hooks/api/appConnections/types/dns-made-easy-connection";
|
||||||
import { HerokuConnectionMethod } from "@app/hooks/api/appConnections/types/heroku-connection";
|
import { HerokuConnectionMethod } from "@app/hooks/api/appConnections/types/heroku-connection";
|
||||||
import { LaravelForgeConnectionMethod } from "@app/hooks/api/appConnections/types/laravel-forge-connection";
|
import { LaravelForgeConnectionMethod } from "@app/hooks/api/appConnections/types/laravel-forge-connection";
|
||||||
import { NetlifyConnectionMethod } from "@app/hooks/api/appConnections/types/netlify-connection";
|
import { NetlifyConnectionMethod } from "@app/hooks/api/appConnections/types/netlify-connection";
|
||||||
@@ -111,6 +112,7 @@ export const APP_CONNECTION_MAP: Record<
|
|||||||
[AppConnection.Flyio]: { name: "Fly.io", image: "Flyio.svg" },
|
[AppConnection.Flyio]: { name: "Fly.io", image: "Flyio.svg" },
|
||||||
[AppConnection.GitLab]: { name: "GitLab", image: "GitLab.png" },
|
[AppConnection.GitLab]: { name: "GitLab", image: "GitLab.png" },
|
||||||
[AppConnection.Cloudflare]: { name: "Cloudflare", image: "Cloudflare.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.Zabbix]: { name: "Zabbix", image: "Zabbix.png" },
|
||||||
[AppConnection.Railway]: { name: "Railway", image: "Railway.png" },
|
[AppConnection.Railway]: { name: "Railway", image: "Railway.png" },
|
||||||
[AppConnection.Bitbucket]: { name: "Bitbucket", image: "Bitbucket.png" },
|
[AppConnection.Bitbucket]: { name: "Bitbucket", image: "Bitbucket.png" },
|
||||||
@@ -214,6 +216,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
|
|||||||
return { name: "Client Secret", icon: faKey };
|
return { name: "Client Secret", icon: faKey };
|
||||||
case AzureClientSecretsConnectionMethod.Certificate:
|
case AzureClientSecretsConnectionMethod.Certificate:
|
||||||
return { name: "Certificate", icon: faCertificate };
|
return { name: "Certificate", icon: faCertificate };
|
||||||
|
case DNSMadeEasyConnectionMethod.APIKeySecret:
|
||||||
|
return { name: "API Key & Secret", icon: faKey };
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled App Connection Method: ${method}`);
|
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",
|
Flyio = "flyio",
|
||||||
GitLab = "gitlab",
|
GitLab = "gitlab",
|
||||||
Cloudflare = "cloudflare",
|
Cloudflare = "cloudflare",
|
||||||
|
DNSMadeEasy = "dns-made-easy",
|
||||||
Bitbucket = "bitbucket",
|
Bitbucket = "bitbucket",
|
||||||
Zabbix = "zabbix",
|
Zabbix = "zabbix",
|
||||||
Railway = "railway",
|
Railway = "railway",
|
||||||
|
|||||||
@@ -184,6 +184,10 @@ export type TRedisConnectionOption = TAppConnectionOptionBase & {
|
|||||||
app: AppConnection.Redis;
|
app: AppConnection.Redis;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TDNSMadeEasyConnectionOption = TAppConnectionOptionBase & {
|
||||||
|
app: AppConnection.DNSMadeEasy;
|
||||||
|
};
|
||||||
|
|
||||||
export type TAppConnectionOption =
|
export type TAppConnectionOption =
|
||||||
| TAwsConnectionOption
|
| TAwsConnectionOption
|
||||||
| TGitHubConnectionOption
|
| TGitHubConnectionOption
|
||||||
@@ -225,7 +229,8 @@ export type TAppConnectionOption =
|
|||||||
| TOktaConnectionOption
|
| TOktaConnectionOption
|
||||||
| TAzureAdCsConnectionOption
|
| TAzureAdCsConnectionOption
|
||||||
| TLaravelForgeConnectionOption
|
| TLaravelForgeConnectionOption
|
||||||
| TChefConnectionOption;
|
| TChefConnectionOption
|
||||||
|
| TDNSMadeEasyConnectionOption;
|
||||||
|
|
||||||
export type TAppConnectionOptionMap = {
|
export type TAppConnectionOptionMap = {
|
||||||
[AppConnection.AWS]: TAwsConnectionOption;
|
[AppConnection.AWS]: TAwsConnectionOption;
|
||||||
@@ -257,6 +262,7 @@ export type TAppConnectionOptionMap = {
|
|||||||
[AppConnection.Flyio]: TFlyioConnectionOption;
|
[AppConnection.Flyio]: TFlyioConnectionOption;
|
||||||
[AppConnection.GitLab]: TGitlabConnectionOption;
|
[AppConnection.GitLab]: TGitlabConnectionOption;
|
||||||
[AppConnection.Cloudflare]: TCloudflareConnectionOption;
|
[AppConnection.Cloudflare]: TCloudflareConnectionOption;
|
||||||
|
[AppConnection.DNSMadeEasy]: TDNSMadeEasyConnectionOption;
|
||||||
[AppConnection.Bitbucket]: TBitbucketConnectionOption;
|
[AppConnection.Bitbucket]: TBitbucketConnectionOption;
|
||||||
[AppConnection.Zabbix]: TZabbixConnectionOption;
|
[AppConnection.Zabbix]: TZabbixConnectionOption;
|
||||||
[AppConnection.Railway]: TRailwayConnectionOption;
|
[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 { TCloudflareConnection } from "./cloudflare-connection";
|
||||||
import { TDatabricksConnection } from "./databricks-connection";
|
import { TDatabricksConnection } from "./databricks-connection";
|
||||||
import { TDigitalOceanConnection } from "./digital-ocean";
|
import { TDigitalOceanConnection } from "./digital-ocean";
|
||||||
|
import { TDNSMadeEasyConnection } from "./dns-made-easy-connection";
|
||||||
import { TFlyioConnection } from "./flyio-connection";
|
import { TFlyioConnection } from "./flyio-connection";
|
||||||
import { TGcpConnection } from "./gcp-connection";
|
import { TGcpConnection } from "./gcp-connection";
|
||||||
import { TGitHubConnection } from "./github-connection";
|
import { TGitHubConnection } from "./github-connection";
|
||||||
@@ -57,6 +58,7 @@ export * from "./checkly-connection";
|
|||||||
export * from "./chef-connection";
|
export * from "./chef-connection";
|
||||||
export * from "./cloudflare-connection";
|
export * from "./cloudflare-connection";
|
||||||
export * from "./databricks-connection";
|
export * from "./databricks-connection";
|
||||||
|
export * from "./dns-made-easy-connection";
|
||||||
export * from "./flyio-connection";
|
export * from "./flyio-connection";
|
||||||
export * from "./gcp-connection";
|
export * from "./gcp-connection";
|
||||||
export * from "./github-connection";
|
export * from "./github-connection";
|
||||||
@@ -127,7 +129,8 @@ export type TAppConnection =
|
|||||||
| TNorthflankConnection
|
| TNorthflankConnection
|
||||||
| TOktaConnection
|
| TOktaConnection
|
||||||
| TRedisConnection
|
| TRedisConnection
|
||||||
| TChefConnection;
|
| TChefConnection
|
||||||
|
| TDNSMadeEasyConnection;
|
||||||
|
|
||||||
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id" | "projectId">;
|
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> = {
|
export const ACME_DNS_PROVIDER_NAME_MAP: Record<AcmeDnsProvider, string> = {
|
||||||
[AcmeDnsProvider.ROUTE53]: "Route53",
|
[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> = {
|
export const ACME_DNS_PROVIDER_APP_CONNECTION_MAP: Record<AcmeDnsProvider, AppConnection> = {
|
||||||
[AcmeDnsProvider.ROUTE53]: AppConnection.AWS,
|
[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[]> = {
|
export const CA_TYPE_CAPABILITIES_MAP: Record<CaType, CaCapability[]> = {
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ export enum CaRenewalType {
|
|||||||
|
|
||||||
export enum AcmeDnsProvider {
|
export enum AcmeDnsProvider {
|
||||||
ROUTE53 = "route53",
|
ROUTE53 = "route53",
|
||||||
Cloudflare = "cloudflare"
|
Cloudflare = "cloudflare",
|
||||||
|
DNSMadeEasy = "dns-made-easy"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CaCapability {
|
export enum CaCapability {
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ export const KmsLayout = () => {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden">
|
<div className="dark flex h-full w-full flex-col overflow-x-hidden bg-mineshaft-900">
|
||||||
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
|
<div className="border-y border-t-project/10 border-b-project/5 bg-gradient-to-b from-project/[0.075] to-project/[0.025] px-4 pt-0.5">
|
||||||
<motion.div
|
<motion.div
|
||||||
key="menu-project-items"
|
key="menu-project-items"
|
||||||
initial={{ x: -150 }}
|
initial={{ x: -150 }}
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ import {
|
|||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { Link, useLocation, useNavigate, useRouter, useRouterState } from "@tanstack/react-router";
|
import { Link, useLocation, useNavigate, useRouter } from "@tanstack/react-router";
|
||||||
import { UserPlusIcon } from "lucide-react";
|
import { ChevronRight, UserPlusIcon } from "lucide-react";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { Mfa } from "@app/components/auth/Mfa";
|
import { Mfa } from "@app/components/auth/Mfa";
|
||||||
@@ -31,7 +31,6 @@ import { createNotification } from "@app/components/notifications";
|
|||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||||
import {
|
import {
|
||||||
BreadcrumbContainer,
|
|
||||||
Button,
|
Button,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -43,7 +42,6 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
TBreadcrumbFormat,
|
|
||||||
Tooltip
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { Badge, InstanceIcon, OrgIcon, SubOrgIcon } from "@app/components/v3";
|
import { Badge, InstanceIcon, OrgIcon, SubOrgIcon } from "@app/components/v3";
|
||||||
@@ -69,6 +67,7 @@ import { MfaMethod } from "@app/hooks/api/auth/types";
|
|||||||
import { getAuthToken } from "@app/hooks/api/reactQuery";
|
import { getAuthToken } from "@app/hooks/api/reactQuery";
|
||||||
import { Organization, SubscriptionPlan } from "@app/hooks/api/types";
|
import { Organization, SubscriptionPlan } from "@app/hooks/api/types";
|
||||||
import { AuthMethod } from "@app/hooks/api/users/types";
|
import { AuthMethod } from "@app/hooks/api/users/types";
|
||||||
|
import { ProjectSelect } from "@app/layouts/ProjectLayout/components/ProjectSelect";
|
||||||
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
|
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
|
||||||
|
|
||||||
import { ServerAdminsPanel } from "../ServerAdminsPanel/ServerAdminsPanel";
|
import { ServerAdminsPanel } from "../ServerAdminsPanel/ServerAdminsPanel";
|
||||||
@@ -183,9 +182,6 @@ export const Navbar = () => {
|
|||||||
}
|
}
|
||||||
}, [subscription, isBillingPage, isModalIntrusive]);
|
}, [subscription, isBillingPage, isModalIntrusive]);
|
||||||
|
|
||||||
const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context });
|
|
||||||
const breadcrumbs = matches && "breadcrumbs" in matches ? matches.breadcrumbs : undefined;
|
|
||||||
|
|
||||||
const handleOrgChange = async (orgId: string) => {
|
const handleOrgChange = async (orgId: string) => {
|
||||||
queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
|
queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
|
||||||
queryClient.removeQueries({ queryKey: projectKeys.getAllUserProjects() });
|
queryClient.removeQueries({ queryKey: projectKeys.getAllUserProjects() });
|
||||||
@@ -249,7 +245,9 @@ export const Navbar = () => {
|
|||||||
|
|
||||||
const isServerAdminPanel = location.pathname.startsWith("/admin");
|
const isServerAdminPanel = location.pathname.startsWith("/admin");
|
||||||
|
|
||||||
const isProjectScope = location.pathname.startsWith(`/organizations/${currentOrg.id}/projects`);
|
const isProjectScope =
|
||||||
|
location.pathname.startsWith(`/organizations/${currentOrg.id}/projects`) &&
|
||||||
|
location.pathname !== `/organizations/${currentOrg.id}/projects`;
|
||||||
|
|
||||||
const handleOrgNav = async (org: Organization) => {
|
const handleOrgNav = async (org: Organization) => {
|
||||||
if (currentOrg?.id === org.id) return;
|
if (currentOrg?.id === org.id) return;
|
||||||
@@ -279,64 +277,59 @@ export const Navbar = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="z-10 flex min-h-12 items-center bg-mineshaft-900 px-4 pt-1">
|
<div className="z-10 flex min-h-12 items-center bg-mineshaft-900 px-4">
|
||||||
<div className="mr-auto flex items-center overflow-hidden">
|
<div className="mr-auto flex h-full min-w-34 items-center">
|
||||||
<div className="shrink-0">
|
<div className="mt-0.5 shrink-0">
|
||||||
<Link to="/organizations/$orgId/projects" params={{ orgId: currentOrg.id }}>
|
<Link to="/organizations/$orgId/projects" params={{ orgId: currentOrg.id }}>
|
||||||
<img alt="infisical logo" src="/images/logotransparent.png" className="h-4" />
|
<img alt="infisical logo" src="/images/logotransparent.png" className="h-4" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
|
<ChevronRight size={18} className="mx-3 mt-[3px] text-mineshaft-400/70" />
|
||||||
{isServerAdminPanel ? (
|
{isServerAdminPanel ? (
|
||||||
<>
|
<Link
|
||||||
<Link
|
to="/admin"
|
||||||
to="/admin"
|
className="group flex cursor-pointer items-center gap-2 text-sm text-white transition-all duration-100 hover:text-primary"
|
||||||
className="group flex cursor-pointer items-center gap-2 text-sm text-white transition-all duration-100 hover:text-primary"
|
>
|
||||||
>
|
<InstanceIcon className="size-3.5 text-xs text-bunker-300" />
|
||||||
<InstanceIcon className="size-3.5 text-xs text-bunker-300" />
|
<div className="whitespace-nowrap">Server Console</div>
|
||||||
<div className="whitespace-nowrap">Server Console</div>
|
</Link>
|
||||||
</Link>
|
|
||||||
<p className="pr-3 pl-3 text-lg text-mineshaft-400/70">/</p>
|
|
||||||
{breadcrumbs ? (
|
|
||||||
// scott: remove /admin as we show server console above
|
|
||||||
<BreadcrumbContainer breadcrumbs={breadcrumbs.slice(1) as TBreadcrumbFormat[]} />
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="flex min-w-12 items-center overflow-hidden">
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"relative flex min-w-16 items-center self-end rounded-t-md border-x border-t pt-1.5 pr-2 pb-2.5 pl-3",
|
||||||
|
!isProjectScope && !isSubOrganization
|
||||||
|
? "border-org/15 bg-gradient-to-b from-org/10 to-org/[0.075]"
|
||||||
|
: "border-transparent"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* scott: the below is used to hide the top border from the org nav bar */}
|
||||||
|
{!isProjectScope && !isSubOrganization && (
|
||||||
|
<div className="absolute -bottom-px left-0 h-px w-full bg-mineshaft-900">
|
||||||
|
<div className="h-full bg-org/[0.075]" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<DropdownMenu modal={false} open={isOrgSelectOpen} onOpenChange={setIsOrgSelectOpen}>
|
<DropdownMenu modal={false} open={isOrgSelectOpen} onOpenChange={setIsOrgSelectOpen}>
|
||||||
<div className="group flex cursor-pointer items-center gap-2 overflow-hidden text-sm text-white transition-all duration-100 hover:text-primary">
|
<div className="group mr-1 flex min-w-0 cursor-pointer items-center gap-2 overflow-hidden text-sm text-white transition-all duration-100">
|
||||||
<Badge
|
<button
|
||||||
asChild
|
className="flex cursor-pointer items-center gap-x-2 truncate whitespace-nowrap"
|
||||||
variant="org"
|
type="button"
|
||||||
isTruncatable
|
onClick={async () => {
|
||||||
// TODO(scott): either add badge size/style variant or create designated component for namespace/org nav bar
|
navigate({
|
||||||
className={twMerge(
|
to: "/organizations/$orgId/projects",
|
||||||
"gap-x-1.5 text-sm",
|
params: { orgId: currentOrg.id }
|
||||||
(isProjectScope || isSubOrganization) &&
|
});
|
||||||
"bg-transparent text-mineshaft-200 hover:!bg-transparent hover:underline [&>svg]:!text-org"
|
if (isSubOrganization) {
|
||||||
)}
|
await router.invalidate({ sync: true }).catch(() => null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<button
|
<OrgIcon className={twMerge("size-[14px] shrink-0 text-org")} />
|
||||||
type="button"
|
<span className="truncate">{currentOrg?.name}</span>
|
||||||
onClick={async () => {
|
<Badge variant="org" className="hidden lg:inline-flex">
|
||||||
navigate({
|
Organization
|
||||||
to: "/organizations/$orgId/projects",
|
</Badge>
|
||||||
params: { orgId: currentOrg.id }
|
</button>
|
||||||
});
|
|
||||||
if (isSubOrganization) {
|
|
||||||
await router.invalidate({ sync: true }).catch(() => null);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<OrgIcon className="size-[12px]" />
|
|
||||||
<span>{currentOrg?.name}</span>
|
|
||||||
</button>
|
|
||||||
</Badge>
|
|
||||||
<div className="mr-1 hidden rounded-sm border border-mineshaft-500 px-1 text-xs text-bunker-300 no-underline! md:inline-block">
|
|
||||||
{getPlan(subscription)}
|
|
||||||
</div>
|
|
||||||
{subscription.cardDeclined && (
|
{subscription.cardDeclined && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={`Your payment could not be processed${subscription.cardDeclinedReason ? `: ${subscription.cardDeclinedReason}` : ""}. Please update your payment method to continue enjoying premium features.`}
|
content={`Your payment could not be processed${subscription.cardDeclinedReason ? `: ${subscription.cardDeclinedReason}` : ""}. Please update your payment method to continue enjoying premium features.`}
|
||||||
@@ -543,19 +536,15 @@ export const Navbar = () => {
|
|||||||
)}
|
)}
|
||||||
{isProjectScope && (
|
{isProjectScope && (
|
||||||
<>
|
<>
|
||||||
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
|
<ChevronRight size={18} className="mx-3 mt-[3px] text-mineshaft-400/70" />
|
||||||
{breadcrumbs ? (
|
<ProjectSelect />
|
||||||
<BreadcrumbContainer
|
|
||||||
className="min-w-[15rem] flex-1"
|
|
||||||
breadcrumbs={[breadcrumbs[0]] as TBreadcrumbFormat[]}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{subscription && subscription.slug === "starter" && !subscription.has_used_trial && (
|
|
||||||
|
{subscription && subscription.slug === "starter" && !subscription.has_used_trial ? (
|
||||||
<Tooltip content="Start Free Pro Trial">
|
<Tooltip content="Start Free Pro Trial">
|
||||||
<Button
|
<Button
|
||||||
variant="plain"
|
variant="plain"
|
||||||
@@ -576,6 +565,10 @@ export const Navbar = () => {
|
|||||||
Free Pro Trial
|
Free Pro Trial
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<div className="mt-0.5 mr-3 hidden rounded-sm border border-mineshaft-400 px-1 text-xs text-mineshaft-100 no-underline! opacity-50 md:inline-block">
|
||||||
|
{getPlan(subscription)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* eslint-disable-next-line no-nested-ternary */}
|
{/* eslint-disable-next-line no-nested-ternary */}
|
||||||
{!location.pathname.startsWith("/admin") ? (
|
{!location.pathname.startsWith("/admin") ? (
|
||||||
@@ -601,7 +594,7 @@ export const Navbar = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<UserPlusIcon className="inline-block size-3.5" />
|
<UserPlusIcon className="inline-block size-3.5" />
|
||||||
<span className="ml-2 hidden md:inline-block">Invite Members</span>
|
<span className="ml-2 hidden md:inline-block">Invite Users</span>
|
||||||
</Link>
|
</Link>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
@@ -705,7 +698,7 @@ export const Navbar = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenuItem icon={<FontAwesomeIcon icon={faUserPlus} />}>
|
<DropdownMenuItem icon={<FontAwesomeIcon icon={faUserPlus} />}>
|
||||||
Invite Members
|
Invite Users
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</Link>
|
</Link>
|
||||||
) : null
|
) : null
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ export const OrgNavBar = ({ isHidden }: Props) => {
|
|||||||
const variant = isRootOrganization ? "org" : "namespace";
|
const variant = isRootOrganization ? "org" : "namespace";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="bg-mineshaft-900">
|
||||||
{!isHidden && (
|
{!isHidden && (
|
||||||
<div className="dark flex w-full flex-col overflow-x-hidden border-b border-mineshaft-600 bg-mineshaft-900 px-4">
|
<div className="dark flex w-full flex-col overflow-x-hidden border-y border-t-org/15 border-b-org/5 bg-gradient-to-b from-org/[0.075] to-org/[0.025] px-4 pt-0.5">
|
||||||
<motion.div
|
<motion.div
|
||||||
key="menu-org-items"
|
key="menu-org-items"
|
||||||
initial={{ x: -150 }}
|
initial={{ x: -150 }}
|
||||||
@@ -114,6 +114,6 @@ export const OrgNavBar = ({ isHidden }: Props) => {
|
|||||||
isOpen={popUp?.createOrg?.isOpen}
|
isOpen={popUp?.createOrg?.isOpen}
|
||||||
onClose={() => handlePopUpToggle("createOrg", false)}
|
onClose={() => handlePopUpToggle("createOrg", false)}
|
||||||
/>
|
/>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ export const PamLayout = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden">
|
<div className="dark flex h-full w-full flex-col overflow-x-hidden bg-mineshaft-900">
|
||||||
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
|
<div className="border-y border-t-project/10 border-b-project/5 bg-gradient-to-b from-project/[0.075] to-project/[0.025] px-4 pt-0.5">
|
||||||
<motion.div
|
<motion.div
|
||||||
key="menu-project-items"
|
key="menu-project-items"
|
||||||
initial={{ x: -150 }}
|
initial={{ x: -150 }}
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ export const PkiManagerLayout = () => {
|
|||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
return (
|
return (
|
||||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden">
|
<div className="dark flex h-full w-full flex-col overflow-x-hidden bg-mineshaft-900">
|
||||||
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
|
<div className="border-y border-t-project/10 border-b-project/5 bg-gradient-to-b from-project/[0.075] to-project/[0.025] px-4 pt-0.5">
|
||||||
<motion.div
|
<motion.div
|
||||||
key="menu-project-items"
|
key="menu-project-items"
|
||||||
initial={{ x: -150 }}
|
initial={{ x: -150 }}
|
||||||
|
|||||||
@@ -92,7 +92,11 @@ export const ProjectSelect = () => {
|
|||||||
}, [projects, projectFavorites, currentWorkspace]);
|
}, [projects, projectFavorites, currentWorkspace]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mr-2 flex items-center gap-1 overflow-hidden">
|
<div className="relative mr-2 flex min-w-16 items-center gap-1 self-end rounded-t-md border-x border-t border-project/10 bg-gradient-to-b from-project/10 to-project/[0.075] pt-1.5 pr-1 pb-2.5 pl-3">
|
||||||
|
{/* scott: the below is used to hide the top border from the org nav bar */}
|
||||||
|
<div className="absolute -bottom-px left-0 h-px w-full bg-mineshaft-900">
|
||||||
|
<div className="h-full bg-project/[0.075]" />
|
||||||
|
</div>
|
||||||
<DropdownMenu modal={false}>
|
<DropdownMenu modal={false}>
|
||||||
<Link
|
<Link
|
||||||
to={getProjectHomePage(currentWorkspace.type, currentWorkspace.environments)}
|
to={getProjectHomePage(currentWorkspace.type, currentWorkspace.environments)}
|
||||||
@@ -100,16 +104,12 @@ export const ProjectSelect = () => {
|
|||||||
projectId: currentWorkspace.id,
|
projectId: currentWorkspace.id,
|
||||||
orgId: currentWorkspace.orgId
|
orgId: currentWorkspace.orgId
|
||||||
}}
|
}}
|
||||||
className="group flex cursor-pointer items-center gap-x-1.5 overflow-hidden hover:text-white"
|
className="group flex cursor-pointer items-center gap-x-2 overflow-hidden pt-0.5 text-sm text-white"
|
||||||
>
|
>
|
||||||
<p className="inline-block truncate text-mineshaft-200 group-hover:underline">
|
<ProjectIcon className="size-[14px] shrink-0 text-project" />
|
||||||
{currentWorkspace?.name}
|
<span className="truncate">{currentWorkspace?.name}</span>
|
||||||
</p>
|
<Badge variant="project" className="hidden lg:inline-flex">
|
||||||
<Badge variant="project">
|
{currentWorkspace.type ? PROJECT_TYPE_NAME[currentWorkspace.type] : "Project"}
|
||||||
<ProjectIcon />
|
|
||||||
<span className="hidden sm:inline-block">
|
|
||||||
{currentWorkspace.type ? PROJECT_TYPE_NAME[currentWorkspace.type] : "Project"}
|
|
||||||
</span>
|
|
||||||
</Badge>
|
</Badge>
|
||||||
</Link>
|
</Link>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
@@ -118,7 +118,7 @@ export const ProjectSelect = () => {
|
|||||||
variant="plain"
|
variant="plain"
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
ariaLabel="switch-project"
|
ariaLabel="switch-project"
|
||||||
className="px-2 py-1"
|
className="top-px px-2 py-1"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faCaretDown} className="text-xs text-bunker-300" />
|
<FontAwesomeIcon icon={faCaretDown} className="text-xs text-bunker-300" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ export const SecretManagerLayout = () => {
|
|||||||
(secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
|
(secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden">
|
<div className="dark flex h-full w-full flex-col overflow-x-hidden bg-mineshaft-900">
|
||||||
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
|
<div className="border-y border-t-project/10 border-b-project/5 bg-gradient-to-b from-project/[0.075] to-project/[0.025] px-4 pt-0.5">
|
||||||
<motion.div
|
<motion.div
|
||||||
key="menu-project-items"
|
key="menu-project-items"
|
||||||
initial={{ x: -150 }}
|
initial={{ x: -150 }}
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ export const SecretScanningLayout = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden">
|
<div className="dark flex h-full w-full flex-col overflow-x-hidden bg-mineshaft-900">
|
||||||
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
|
<div className="border-y border-t-project/10 border-b-project/5 bg-gradient-to-b from-project/[0.075] to-project/[0.025] px-4 pt-0.5">
|
||||||
<motion.div
|
<motion.div
|
||||||
key="menu-project-items"
|
key="menu-project-items"
|
||||||
initial={{ x: -150 }}
|
initial={{ x: -150 }}
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ export const SshLayout = () => {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden">
|
<div className="dark flex h-full w-full flex-col overflow-x-hidden bg-mineshaft-900">
|
||||||
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
|
<div className="border-y border-t-project/10 border-b-project/5 bg-gradient-to-b from-project/[0.075] to-project/[0.025] px-4 pt-0.5">
|
||||||
<motion.div
|
<motion.div
|
||||||
key="menu-project-items"
|
key="menu-project-items"
|
||||||
initial={{ x: -150 }}
|
initial={{ x: -150 }}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const AccessManagementPage = () => {
|
|||||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
scope="instance"
|
scope="instance"
|
||||||
title="Access Control"
|
title="Server Admin Access Control"
|
||||||
description="Manage server admins within your Infisical instance."
|
description="Manage server admins within your Infisical instance."
|
||||||
/>
|
/>
|
||||||
<ServerAdminsTable />
|
<ServerAdminsTable />
|
||||||
|
|||||||
@@ -350,8 +350,8 @@ const ViewMembersModalContent = ({
|
|||||||
className="my-auto bg-mineshaft-700"
|
className="my-auto bg-mineshaft-700"
|
||||||
title={
|
title={
|
||||||
members.length
|
members.length
|
||||||
? "No organization members match search..."
|
? "No organization users match search..."
|
||||||
: "No organization members found"
|
: "No organization users found"
|
||||||
}
|
}
|
||||||
icon={faUsers}
|
icon={faUsers}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ import {
|
|||||||
TCloudflareZone,
|
TCloudflareZone,
|
||||||
useCloudflareConnectionListZones
|
useCloudflareConnectionListZones
|
||||||
} from "@app/hooks/api/appConnections/cloudflare";
|
} 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 { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
import {
|
import {
|
||||||
AcmeDnsProvider,
|
AcmeDnsProvider,
|
||||||
@@ -211,6 +215,11 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
enabled: caType === CaType.ACME
|
enabled: caType === CaType.ACME
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: availableDNSMadeEasyConnections, isPending: isDNSMadeEasyPending } =
|
||||||
|
useListAvailableAppConnections(AppConnection.DNSMadeEasy, currentProject.id, {
|
||||||
|
enabled: caType === CaType.ACME
|
||||||
|
});
|
||||||
|
|
||||||
const { data: availableAzureConnections, isPending: isAzurePending } =
|
const { data: availableAzureConnections, isPending: isAzurePending } =
|
||||||
useListAvailableAppConnections(AppConnection.AzureADCS, currentProject.id, {
|
useListAvailableAppConnections(AppConnection.AzureADCS, currentProject.id, {
|
||||||
enabled: caType === CaType.AZURE_AD_CS
|
enabled: caType === CaType.AZURE_AD_CS
|
||||||
@@ -220,16 +229,24 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
if (caType === CaType.AZURE_AD_CS) {
|
if (caType === CaType.AZURE_AD_CS) {
|
||||||
return availableAzureConnections || [];
|
return availableAzureConnections || [];
|
||||||
}
|
}
|
||||||
return [...(availableRoute53Connections || []), ...(availableCloudflareConnections || [])];
|
return [
|
||||||
|
...(availableRoute53Connections || []),
|
||||||
|
...(availableCloudflareConnections || []),
|
||||||
|
...(availableDNSMadeEasyConnections || [])
|
||||||
|
];
|
||||||
}, [
|
}, [
|
||||||
caType,
|
caType,
|
||||||
availableRoute53Connections,
|
availableRoute53Connections,
|
||||||
availableCloudflareConnections,
|
availableCloudflareConnections,
|
||||||
|
availableDNSMadeEasyConnections,
|
||||||
availableAzureConnections
|
availableAzureConnections
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isPending =
|
const isPending =
|
||||||
isRoute53Pending || isCloudflarePending || (isAzurePending && caType === CaType.AZURE_AD_CS);
|
isRoute53Pending ||
|
||||||
|
isCloudflarePending ||
|
||||||
|
isDNSMadeEasyPending ||
|
||||||
|
(isAzurePending && caType === CaType.AZURE_AD_CS);
|
||||||
|
|
||||||
const dnsAppConnection =
|
const dnsAppConnection =
|
||||||
caType === CaType.ACME && configuration && "dnsAppConnection" in configuration
|
caType === CaType.ACME && configuration && "dnsAppConnection" in configuration
|
||||||
@@ -241,6 +258,11 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
enabled: dnsProvider === AcmeDnsProvider.Cloudflare && !!dnsAppConnection.id
|
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
|
// Populate form with CA data when editing
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ca && !isCaLoading) {
|
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
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export const IntegrationsListPage = () => {
|
|||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
scope={ProjectType.CertificateManager}
|
scope={ProjectType.CertificateManager}
|
||||||
title="Integrations"
|
title="Project Integrations"
|
||||||
description="Manage integrations with third-party certificate services."
|
description="Manage integrations with third-party certificate services."
|
||||||
/>
|
/>
|
||||||
<Tabs orientation="vertical" value={currentTab} onValueChange={updateSelectedTab}>
|
<Tabs orientation="vertical" value={currentTab} onValueChange={updateSelectedTab}>
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
import { InfoIcon } from "lucide-react";
|
||||||
|
|
||||||
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||||
|
import { useOrganization } from "@app/context";
|
||||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||||
import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab";
|
import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab";
|
||||||
|
|
||||||
@@ -15,6 +18,7 @@ const tabs = [
|
|||||||
|
|
||||||
export const SettingsPage = () => {
|
export const SettingsPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
||||||
@@ -22,7 +26,17 @@ export const SettingsPage = () => {
|
|||||||
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
|
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="w-full max-w-8xl">
|
<div className="w-full max-w-8xl">
|
||||||
<PageHeader scope={ProjectType.CertificateManager} title={t("settings.project.title")} />
|
<PageHeader scope={ProjectType.CertificateManager} title={t("settings.project.title")}>
|
||||||
|
<Link
|
||||||
|
to="/organizations/$orgId/settings"
|
||||||
|
params={{
|
||||||
|
orgId: currentOrg.id
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
|
||||||
|
>
|
||||||
|
<InfoIcon size={12} /> Looking for organization settings?
|
||||||
|
</Link>
|
||||||
|
</PageHeader>
|
||||||
<Tabs orientation="vertical" defaultValue={tabs[0].key}>
|
<Tabs orientation="vertical" defaultValue={tabs[0].key}>
|
||||||
<TabList>
|
<TabList>
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const OverviewPage = () => {
|
|||||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
scope={ProjectType.KMS}
|
scope={ProjectType.KMS}
|
||||||
title="Overview"
|
title="Project Overview"
|
||||||
description="Manage keys and perform cryptographic operations."
|
description="Manage keys and perform cryptographic operations."
|
||||||
/>
|
/>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
import { InfoIcon } from "lucide-react";
|
||||||
|
|
||||||
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||||
|
import { useOrganization } from "@app/context";
|
||||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||||
import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab";
|
import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab";
|
||||||
|
|
||||||
@@ -16,6 +19,8 @@ const tabs = [
|
|||||||
export const SettingsPage = () => {
|
export const SettingsPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
@@ -24,9 +29,19 @@ export const SettingsPage = () => {
|
|||||||
<div className="w-full max-w-8xl">
|
<div className="w-full max-w-8xl">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
scope={ProjectType.KMS}
|
scope={ProjectType.KMS}
|
||||||
title="Settings"
|
title="Project Settings"
|
||||||
description="Configure general project settings"
|
description="Configure general project settings"
|
||||||
/>
|
>
|
||||||
|
<Link
|
||||||
|
to="/organizations/$orgId/settings"
|
||||||
|
params={{
|
||||||
|
orgId: currentOrg.id
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
|
||||||
|
>
|
||||||
|
<InfoIcon size={12} /> Looking for organization settings?
|
||||||
|
</Link>
|
||||||
|
</PageHeader>
|
||||||
<Tabs orientation="vertical" defaultValue={tabs[0].key}>
|
<Tabs orientation="vertical" defaultValue={tabs[0].key}>
|
||||||
<TabList>
|
<TabList>
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export const AccessManagementPage = () => {
|
|||||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
scope={isSubOrganization ? "namespace" : "org"}
|
scope={isSubOrganization ? "namespace" : "org"}
|
||||||
title="Access Control"
|
title={`${isSubOrganization ? "Sub-Organization" : "Organization"} Access Control`}
|
||||||
description="Manage fine-grained access for users, groups, roles, and machine identities within your organization resources."
|
description="Manage fine-grained access for users, groups, roles, and machine identities within your organization resources."
|
||||||
/>
|
/>
|
||||||
{!currentOrg.shouldUseNewPrivilegeSystem && (
|
{!currentOrg.shouldUseNewPrivilegeSystem && (
|
||||||
|
|||||||
@@ -51,19 +51,19 @@ export const OrgGroupsSection = () => {
|
|||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
<p className="text-xl font-medium text-mineshaft-100">Groups</p>
|
<p className="text-xl font-medium text-mineshaft-100">Organization Groups</p>
|
||||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/groups" />
|
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/groups" />
|
||||||
</div>
|
</div>
|
||||||
<OrgPermissionCan I={OrgPermissionGroupActions.Create} a={OrgPermissionSubjects.Groups}>
|
<OrgPermissionCan I={OrgPermissionGroupActions.Create} a={OrgPermissionSubjects.Groups}>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
colorSchema="secondary"
|
variant="outline_bg"
|
||||||
type="submit"
|
type="submit"
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
onClick={() => handleAddGroupModal()}
|
onClick={() => handleAddGroupModal()}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
Create Group
|
Create Organization Group
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</OrgPermissionCan>
|
</OrgPermissionCan>
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
placeholder="Search groups..."
|
placeholder="Search organization groups..."
|
||||||
/>
|
/>
|
||||||
<TableContainer className="mt-4">
|
<TableContainer className="mt-4">
|
||||||
<Table>
|
<Table>
|
||||||
@@ -205,7 +205,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
</Th>
|
</Th>
|
||||||
<Th>
|
<Th>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
Role
|
Organization Role
|
||||||
<IconButton
|
<IconButton
|
||||||
variant="plain"
|
variant="plain"
|
||||||
className={`ml-2 ${orderBy === GroupsOrderBy.Role ? "" : "opacity-30"}`}
|
className={`ml-2 ${orderBy === GroupsOrderBy.Role ? "" : "opacity-30"}`}
|
||||||
|
|||||||
@@ -99,7 +99,9 @@ export const IdentitySection = withPermission(
|
|||||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||||
<div className="flex flex-1 items-center gap-x-2">
|
<div className="flex flex-1 items-center gap-x-2">
|
||||||
<p className="text-xl font-medium text-mineshaft-100">Machine Identities</p>
|
<p className="text-xl font-medium text-mineshaft-100">
|
||||||
|
Organization Machine Identities
|
||||||
|
</p>
|
||||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/machine-identities" />
|
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/machine-identities" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
@@ -129,7 +131,9 @@ export const IdentitySection = withPermission(
|
|||||||
}}
|
}}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
Add Machine Identity
|
{isSubOrganization
|
||||||
|
? "Add Machine Identity to Sub-Organization"
|
||||||
|
: "Create Organization Machine Identity"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</OrgPermissionCan>
|
</OrgPermissionCan>
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
</DropdownSubMenuTrigger>
|
</DropdownSubMenuTrigger>
|
||||||
<DropdownSubMenuContent className="max-h-80 thin-scrollbar overflow-y-auto rounded-l-none">
|
<DropdownSubMenuContent className="max-h-80 thin-scrollbar overflow-y-auto rounded-l-none">
|
||||||
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
||||||
Filter Machine Identities by Role
|
Filter Organization Machine Identities by Role
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
{roles?.map(({ id, slug, name }) => (
|
{roles?.map(({ id, slug, name }) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@@ -258,7 +258,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
</Th>
|
</Th>
|
||||||
<Th>
|
<Th>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
Role
|
Organization Role
|
||||||
<IconButton
|
<IconButton
|
||||||
variant="plain"
|
variant="plain"
|
||||||
className={`ml-2 ${orderBy === OrgIdentityOrderBy.Role ? "" : "opacity-30"}`}
|
className={`ml-2 ${orderBy === OrgIdentityOrderBy.Role ? "" : "opacity-30"}`}
|
||||||
|
|||||||
@@ -205,13 +205,13 @@ export const OrgMembersSection = () => {
|
|||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
<div className="mb-4 flex flex-wrap items-center justify-between gap-2">
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
<p className="text-xl font-medium text-mineshaft-100">Users</p>
|
<p className="text-xl font-medium text-mineshaft-100">Organization Users</p>
|
||||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/user-identities" />
|
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/user-identities" />
|
||||||
</div>
|
</div>
|
||||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
|
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
colorSchema="secondary"
|
variant="outline_bg"
|
||||||
type="submit"
|
type="submit"
|
||||||
leftIcon={<UserPlusIcon size={16} />}
|
leftIcon={<UserPlusIcon size={16} />}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@@ -219,7 +219,9 @@ export const OrgMembersSection = () => {
|
|||||||
}
|
}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
{isSubOrganization ? "Add Members" : "Invite Members"}
|
{isSubOrganization
|
||||||
|
? "Add Users to Sub-Organization"
|
||||||
|
: "Invite Users to Organization"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</OrgPermissionCan>
|
</OrgPermissionCan>
|
||||||
|
|||||||
@@ -336,7 +336,7 @@ export const OrgMembersTable = ({
|
|||||||
</DropdownSubMenuTrigger>
|
</DropdownSubMenuTrigger>
|
||||||
<DropdownSubMenuContent className="max-h-80 thin-scrollbar overflow-y-auto rounded-l-none">
|
<DropdownSubMenuContent className="max-h-80 thin-scrollbar overflow-y-auto rounded-l-none">
|
||||||
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
||||||
Apply Roles to Filter Users
|
Filter Organization Users by Role
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
{roles?.map(({ id, slug, name }) => (
|
{roles?.map(({ id, slug, name }) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@@ -365,7 +365,7 @@ export const OrgMembersTable = ({
|
|||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
placeholder="Search members..."
|
placeholder="Search organization users..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<TableContainer className="mt-4">
|
<TableContainer className="mt-4">
|
||||||
@@ -434,7 +434,7 @@ export const OrgMembersTable = ({
|
|||||||
</Th>
|
</Th>
|
||||||
<Th className="w-1/3">
|
<Th className="w-1/3">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
Role
|
Organization Role
|
||||||
<IconButton
|
<IconButton
|
||||||
variant="plain"
|
variant="plain"
|
||||||
className={`ml-2 ${orderBy === OrgMembersOrderBy.Role ? "" : "opacity-30"}`}
|
className={`ml-2 ${orderBy === OrgMembersOrderBy.Role ? "" : "opacity-30"}`}
|
||||||
@@ -727,8 +727,8 @@ export const OrgMembersTable = ({
|
|||||||
<EmptyState
|
<EmptyState
|
||||||
title={
|
title={
|
||||||
members.length
|
members.length
|
||||||
? "No organization members match search..."
|
? "No organization users match search..."
|
||||||
: "No organization members found"
|
: "No organization users found"
|
||||||
}
|
}
|
||||||
icon={members.length ? faSearch : faUsers}
|
icon={members.length ? faSearch : faUsers}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ export const OrgRoleTable = () => {
|
|||||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Role}>
|
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Role}>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
colorSchema="secondary"
|
variant="outline_bg"
|
||||||
type="submit"
|
type="submit"
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -207,7 +207,7 @@ export const OrgRoleTable = () => {
|
|||||||
}}
|
}}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
Add Role
|
Add Organization Role
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</OrgPermissionCan>
|
</OrgPermissionCan>
|
||||||
@@ -216,7 +216,7 @@ export const OrgRoleTable = () => {
|
|||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
placeholder="Search roles..."
|
placeholder="Search organization roles..."
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
containerClassName="mb-4"
|
containerClassName="mb-4"
|
||||||
/>
|
/>
|
||||||
@@ -441,7 +441,7 @@ export const OrgRoleTable = () => {
|
|||||||
<EmptyState
|
<EmptyState
|
||||||
title={
|
title={
|
||||||
roles?.length
|
roles?.length
|
||||||
? "No roles match search..."
|
? "No organization roles match search..."
|
||||||
: "This organization does not have any roles"
|
: "This organization does not have any roles"
|
||||||
}
|
}
|
||||||
icon={roles?.length ? faSearch : undefined}
|
icon={roles?.length ? faSearch : undefined}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export const AppConnectionsPage = withPermission(
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
scope={isSubOrganization ? "namespace" : "org"}
|
scope={isSubOrganization ? "namespace" : "org"}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
title="App Connections"
|
title={`${isSubOrganization ? "Sub-Organization" : "Organization"} App Connections`}
|
||||||
description="Manage organization App Connections"
|
description="Manage organization App Connections"
|
||||||
/>
|
/>
|
||||||
<div className="mb-4 flex w-full flex-col rounded-md border border-blue-500/50 bg-blue-500/30 px-4 py-2 text-sm text-blue-200">
|
<div className="mb-4 flex w-full flex-col rounded-md border border-blue-500/50 bg-blue-500/30 px-4 py-2 text-sm text-blue-200">
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { ChefConnectionForm } from "./ChefConnectionForm";
|
|||||||
import { CloudflareConnectionForm } from "./CloudflareConnectionForm";
|
import { CloudflareConnectionForm } from "./CloudflareConnectionForm";
|
||||||
import { DatabricksConnectionForm } from "./DatabricksConnectionForm";
|
import { DatabricksConnectionForm } from "./DatabricksConnectionForm";
|
||||||
import { DigitalOceanConnectionForm } from "./DigitalOceanConnectionForm";
|
import { DigitalOceanConnectionForm } from "./DigitalOceanConnectionForm";
|
||||||
|
import { DNSMadeEasyConnectionForm } from "./DNSMadeEasyConnectionForm";
|
||||||
import { FlyioConnectionForm } from "./FlyioConnectionForm";
|
import { FlyioConnectionForm } from "./FlyioConnectionForm";
|
||||||
import { GcpConnectionForm } from "./GcpConnectionForm";
|
import { GcpConnectionForm } from "./GcpConnectionForm";
|
||||||
import { GitHubConnectionForm } from "./GitHubConnectionForm";
|
import { GitHubConnectionForm } from "./GitHubConnectionForm";
|
||||||
@@ -148,6 +149,8 @@ const CreateForm = ({ app, onComplete, projectId }: CreateFormProps) => {
|
|||||||
return <GitLabConnectionForm onSubmit={onSubmit} projectId={projectId} />;
|
return <GitLabConnectionForm onSubmit={onSubmit} projectId={projectId} />;
|
||||||
case AppConnection.Cloudflare:
|
case AppConnection.Cloudflare:
|
||||||
return <CloudflareConnectionForm onSubmit={onSubmit} />;
|
return <CloudflareConnectionForm onSubmit={onSubmit} />;
|
||||||
|
case AppConnection.DNSMadeEasy:
|
||||||
|
return <DNSMadeEasyConnectionForm onSubmit={onSubmit} />;
|
||||||
case AppConnection.Bitbucket:
|
case AppConnection.Bitbucket:
|
||||||
return <BitbucketConnectionForm onSubmit={onSubmit} />;
|
return <BitbucketConnectionForm onSubmit={onSubmit} />;
|
||||||
case AppConnection.Zabbix:
|
case AppConnection.Zabbix:
|
||||||
@@ -306,6 +309,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
|
|||||||
);
|
);
|
||||||
case AppConnection.Cloudflare:
|
case AppConnection.Cloudflare:
|
||||||
return <CloudflareConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
return <CloudflareConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||||
|
case AppConnection.DNSMadeEasy:
|
||||||
|
return <DNSMadeEasyConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||||
case AppConnection.Bitbucket:
|
case AppConnection.Bitbucket:
|
||||||
return <BitbucketConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
return <BitbucketConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||||
case AppConnection.Zabbix:
|
case AppConnection.Zabbix:
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -19,7 +19,7 @@ export const AuditLogsPage = () => {
|
|||||||
<div className="w-full max-w-8xl">
|
<div className="w-full max-w-8xl">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
scope={isSubOrganization ? "namespace" : "org"}
|
scope={isSubOrganization ? "namespace" : "org"}
|
||||||
title="Audit Logs"
|
title={`${isSubOrganization ? "Sub-Organization" : "Organization"} Audit Logs`}
|
||||||
description="Audit logs for security and compliance teams to monitor information access."
|
description="Audit logs for security and compliance teams to monitor information access."
|
||||||
/>
|
/>
|
||||||
<LogsSection pageView />
|
<LogsSection pageView />
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ const Page = () => {
|
|||||||
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faChevronLeft} />
|
<FontAwesomeIcon icon={faChevronLeft} />
|
||||||
Groups
|
Organization Groups
|
||||||
</Link>
|
</Link>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
scope={isSubOrganization ? "namespace" : "org"}
|
scope={isSubOrganization ? "namespace" : "org"}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ const Page = () => {
|
|||||||
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faChevronLeft} />
|
<FontAwesomeIcon icon={faChevronLeft} />
|
||||||
Machine Identities
|
Organization Machine Identities
|
||||||
</Link>
|
</Link>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
scope={isSubOrganization ? "namespace" : "org"}
|
scope={isSubOrganization ? "namespace" : "org"}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export const ProjectsPage = () => {
|
|||||||
</Helmet>
|
</Helmet>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
scope={isSubOrganization ? "namespace" : "org"}
|
scope={isSubOrganization ? "namespace" : "org"}
|
||||||
title="Overview"
|
title={`${isSubOrganization ? "Sub-Organization" : "Organization"} Overview`}
|
||||||
description="Your team's complete security toolkit - organized and ready when you need them."
|
description="Your team's complete security toolkit - organized and ready when you need them."
|
||||||
/>
|
/>
|
||||||
{projectListView === ProjectListView.MyProjects ? (
|
{projectListView === ProjectListView.MyProjects ? (
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const SettingsPage = () => {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
scope={isSubOrganization ? "namespace" : "org"}
|
scope={isSubOrganization ? "namespace" : "org"}
|
||||||
description="Configure organization-wide settings"
|
description="Configure organization-wide settings"
|
||||||
title={t("settings.org.title")}
|
title={isSubOrganization ? "Sub-Organization Settings" : "Organization Settings"}
|
||||||
/>
|
/>
|
||||||
<OrgTabGroup />
|
<OrgTabGroup />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ const Page = withPermission(
|
|||||||
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faChevronLeft} />
|
<FontAwesomeIcon icon={faChevronLeft} />
|
||||||
Users
|
Organization Users
|
||||||
</Link>
|
</Link>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
scope={isSubOrganization ? "namespace" : "org"}
|
scope={isSubOrganization ? "namespace" : "org"}
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
import { InfoIcon } from "lucide-react";
|
||||||
|
|
||||||
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||||
|
import { useOrganization } from "@app/context";
|
||||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||||
import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab";
|
import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab";
|
||||||
|
|
||||||
export const SettingsPage = () => {
|
export const SettingsPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
@@ -16,9 +21,19 @@ export const SettingsPage = () => {
|
|||||||
<div className="w-full max-w-8xl">
|
<div className="w-full max-w-8xl">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
scope={ProjectType.PAM}
|
scope={ProjectType.PAM}
|
||||||
title="Settings"
|
title="Project Settings"
|
||||||
description="Configure your PAM project."
|
description="Configure your PAM project."
|
||||||
/>
|
>
|
||||||
|
<Link
|
||||||
|
to="/organizations/$orgId/settings"
|
||||||
|
params={{
|
||||||
|
orgId: currentOrg.id
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
|
||||||
|
>
|
||||||
|
<InfoIcon size={12} /> Looking for organization settings?
|
||||||
|
</Link>
|
||||||
|
</PageHeader>
|
||||||
<Tabs orientation="vertical" defaultValue="tab-project-general">
|
<Tabs orientation="vertical" defaultValue="tab-project-general">
|
||||||
<TabList>
|
<TabList>
|
||||||
<Tab variant="project" value="tab-project-general">
|
<Tab variant="project" value="tab-project-general">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
import { Link, useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
import { InfoIcon } from "lucide-react";
|
||||||
|
|
||||||
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||||
import { useOrganization, useProject } from "@app/context";
|
import { useOrganization, useProject } from "@app/context";
|
||||||
@@ -43,9 +44,19 @@ const Page = () => {
|
|||||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
scope={currentProject.type}
|
scope={currentProject.type}
|
||||||
title="Access Control"
|
title="Project Access Control"
|
||||||
description="Manage fine-grained access for users, groups, roles, and machine identities within your project resources."
|
description="Manage fine-grained access for users, groups, roles, and machine identities within your project resources."
|
||||||
/>
|
>
|
||||||
|
<Link
|
||||||
|
to="/organizations/$orgId/access-management"
|
||||||
|
params={{
|
||||||
|
orgId: currentOrg.id
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
|
||||||
|
>
|
||||||
|
<InfoIcon size={12} /> Looking for organization access control?
|
||||||
|
</Link>
|
||||||
|
</PageHeader>
|
||||||
<Tabs orientation="vertical" value={selectedTab} onValueChange={updateSelectedTab}>
|
<Tabs orientation="vertical" value={selectedTab} onValueChange={updateSelectedTab}>
|
||||||
<TabList>
|
<TabList>
|
||||||
<Tab variant="project" value={ProjectAccessControlTabs.Member}>
|
<Tab variant="project" value={ProjectAccessControlTabs.Member}>
|
||||||
|
|||||||
@@ -59,19 +59,19 @@ export const GroupsSection = () => {
|
|||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
<p className="text-xl font-medium text-mineshaft-100">User Groups</p>
|
<p className="text-xl font-medium text-mineshaft-100">Project Groups</p>
|
||||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/groups#user-groups" />
|
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/groups#user-groups" />
|
||||||
</div>
|
</div>
|
||||||
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Groups}>
|
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Groups}>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
colorSchema="secondary"
|
variant="outline_bg"
|
||||||
type="submit"
|
type="submit"
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
onClick={() => handleAddGroupModal()}
|
onClick={() => handleAddGroupModal()}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
Add Group
|
Add Group to Project
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
placeholder="Search members..."
|
placeholder="Search project groups..."
|
||||||
/>
|
/>
|
||||||
<TableContainer className="mt-4">
|
<TableContainer className="mt-4">
|
||||||
<Table>
|
<Table>
|
||||||
@@ -143,7 +143,7 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
</Th>
|
</Th>
|
||||||
<Th>Role</Th>
|
<Th>Project Role</Th>
|
||||||
<Th>Added on</Th>
|
<Th>Added on</Th>
|
||||||
<Th className="w-5" />
|
<Th className="w-5" />
|
||||||
</Tr>
|
</Tr>
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ export const IdentityTab = withProjectPermission(
|
|||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
<p className="text-xl font-medium text-mineshaft-100">Machine Identities</p>
|
<p className="text-xl font-medium text-mineshaft-100">Project Machine Identities</p>
|
||||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/machine-identities" />
|
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/machine-identities" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
@@ -212,7 +212,7 @@ export const IdentityTab = withProjectPermission(
|
|||||||
onClick={() => handlePopUpOpen("createIdentity")}
|
onClick={() => handlePopUpOpen("createIdentity")}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
Add Machine Identity
|
Add Machine Identity to Project
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
@@ -223,7 +223,7 @@ export const IdentityTab = withProjectPermission(
|
|||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
placeholder="Search machine identities by name..."
|
placeholder="Search project machine identities by name..."
|
||||||
/>
|
/>
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table>
|
<Table>
|
||||||
@@ -251,7 +251,7 @@ export const IdentityTab = withProjectPermission(
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
</Th>
|
</Th>
|
||||||
<Th className="w-1/3">Role</Th>
|
<Th className="w-1/3">Project Role</Th>
|
||||||
<Th>Managed by</Th>
|
<Th>Managed by</Th>
|
||||||
<Th className="w-5">{isFetching ? <Spinner size="xs" /> : null}</Th>
|
<Th className="w-5">{isFetching ? <Spinner size="xs" /> : null}</Th>
|
||||||
</Tr>
|
</Tr>
|
||||||
|
|||||||
@@ -49,19 +49,19 @@ export const MembersSection = () => {
|
|||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-x-2">
|
<div className="flex items-center gap-x-2">
|
||||||
<p className="text-xl font-medium text-mineshaft-100">Users</p>
|
<p className="text-xl font-medium text-mineshaft-100">Project Users</p>
|
||||||
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/user-identities" />
|
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/user-identities" />
|
||||||
</div>
|
</div>
|
||||||
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Member}>
|
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Member}>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
colorSchema="secondary"
|
variant="outline_bg"
|
||||||
type="submit"
|
type="submit"
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
onClick={() => handlePopUpOpen("addMember")}
|
onClick={() => handlePopUpOpen("addMember")}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
Add Member
|
Add Users to Project
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
</DropdownSubMenuTrigger>
|
</DropdownSubMenuTrigger>
|
||||||
<DropdownSubMenuContent className="max-h-80 thin-scrollbar overflow-y-auto rounded-l-none">
|
<DropdownSubMenuContent className="max-h-80 thin-scrollbar overflow-y-auto rounded-l-none">
|
||||||
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
||||||
Apply Roles to Filter Users
|
Filter Project Users by Role
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
{projectRoles?.map(({ id, slug, name }) => (
|
{projectRoles?.map(({ id, slug, name }) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@@ -237,7 +237,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
placeholder="Search members..."
|
placeholder="Search project users..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<TableContainer className="mt-4">
|
<TableContainer className="mt-4">
|
||||||
@@ -282,7 +282,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
</Th>
|
</Th>
|
||||||
<Th>Role</Th>
|
<Th>Project Role</Th>
|
||||||
<Th className="w-5" />
|
<Th className="w-5" />
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
@@ -462,9 +462,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
)}
|
)}
|
||||||
{!isMembersLoading && !filteredUsers?.length && (
|
{!isMembersLoading && !filteredUsers?.length && (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
title={
|
title={members.length ? "No project users match search..." : "No project users found"}
|
||||||
members.length ? "No project members match search..." : "No project members found"
|
|
||||||
}
|
|
||||||
icon={members.length ? faSearch : faUsers}
|
icon={members.length ? faSearch : faUsers}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -170,13 +170,13 @@ export const ProjectRoleList = () => {
|
|||||||
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Role}>
|
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Role}>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
colorSchema="secondary"
|
variant="outline_bg"
|
||||||
type="submit"
|
type="submit"
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
onClick={() => handlePopUpOpen("role")}
|
onClick={() => handlePopUpOpen("role")}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
Add Role
|
Add Project Role
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export const ServiceTokenSection = withProjectPermission(
|
|||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
colorSchema="secondary"
|
variant="outline_bg"
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handlePopUpOpen("createAPIToken");
|
handlePopUpOpen("createAPIToken");
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const AppConnectionsPage = withProjectPermission(
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
scope={currentProject.type}
|
scope={currentProject.type}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
title="App Connections"
|
title="Project App Connections"
|
||||||
description="Manage project App Connections"
|
description="Manage project App Connections"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
import { InfoIcon } from "lucide-react";
|
||||||
|
|
||||||
import { PageHeader } from "@app/components/v2";
|
import { PageHeader } from "@app/components/v2";
|
||||||
import { useProject } from "@app/context";
|
import { useProject } from "@app/context";
|
||||||
@@ -17,9 +19,19 @@ export const AuditLogsPage = () => {
|
|||||||
<div className="w-full max-w-8xl">
|
<div className="w-full max-w-8xl">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
scope={currentProject.type}
|
scope={currentProject.type}
|
||||||
title="Audit logs"
|
title="Project Audit logs"
|
||||||
description="Audit logs for security and compliance teams to monitor information access."
|
description="Audit logs for security and compliance teams to monitor information access."
|
||||||
/>
|
>
|
||||||
|
<Link
|
||||||
|
to="/organizations/$orgId/audit-logs"
|
||||||
|
params={{
|
||||||
|
orgId: currentProject.orgId
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
|
||||||
|
>
|
||||||
|
<InfoIcon size={12} /> Looking for organization audit logs?
|
||||||
|
</Link>
|
||||||
|
</PageHeader>
|
||||||
<LogsSection pageView project={currentProject} />
|
<LogsSection pageView project={currentProject} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ const Page = () => {
|
|||||||
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faChevronLeft} />
|
<FontAwesomeIcon icon={faChevronLeft} />
|
||||||
Groups
|
Project Groups
|
||||||
</Link>
|
</Link>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
scope={currentProject.type}
|
scope={currentProject.type}
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ const Page = () => {
|
|||||||
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faChevronLeft} />
|
<FontAwesomeIcon icon={faChevronLeft} />
|
||||||
Machine Identities
|
Project Machine Identities
|
||||||
</Link>
|
</Link>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
scope={currentProject.type}
|
scope={currentProject.type}
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export const Page = () => {
|
|||||||
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faChevronLeft} />
|
<FontAwesomeIcon icon={faChevronLeft} />
|
||||||
Users
|
Project Users
|
||||||
</Link>
|
</Link>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
scope={currentProject.type}
|
scope={currentProject.type}
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ const Page = () => {
|
|||||||
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faChevronLeft} />
|
<FontAwesomeIcon icon={faChevronLeft} />
|
||||||
Roles
|
Project Roles
|
||||||
</Link>
|
</Link>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
scope={currentProject.type}
|
scope={currentProject.type}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export const IntegrationsListPage = () => {
|
|||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
scope={ProjectType.SecretManager}
|
scope={ProjectType.SecretManager}
|
||||||
title="Integrations"
|
title="Project Integrations"
|
||||||
description="Manage integrations with third-party services."
|
description="Manage integrations with third-party services."
|
||||||
/>
|
/>
|
||||||
<Tabs orientation="vertical" value={selectedTab} onValueChange={updateSelectedTab}>
|
<Tabs orientation="vertical" value={selectedTab} onValueChange={updateSelectedTab}>
|
||||||
|
|||||||
@@ -933,12 +933,12 @@ export const OverviewPage = () => {
|
|||||||
<div className="flex w-full items-baseline justify-between">
|
<div className="flex w-full items-baseline justify-between">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
scope={ProjectType.SecretManager}
|
scope={ProjectType.SecretManager}
|
||||||
title="Overview"
|
title="Project Overview"
|
||||||
description={
|
description={
|
||||||
<p className="text-md text-bunker-300">
|
<p className="text-md text-bunker-300">
|
||||||
Inject your secrets using
|
Inject your secrets using
|
||||||
<a
|
<a
|
||||||
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
|
className="ml-1 text-mineshaft-200 underline decoration-mineshaft-400/65 underline-offset-3 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
|
||||||
href="https://infisical.com/docs/cli/overview"
|
href="https://infisical.com/docs/cli/overview"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
@@ -947,7 +947,7 @@ export const OverviewPage = () => {
|
|||||||
</a>
|
</a>
|
||||||
,
|
,
|
||||||
<a
|
<a
|
||||||
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
|
className="ml-1 text-mineshaft-200 underline decoration-mineshaft-400/65 underline-offset-3 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
|
||||||
href="https://infisical.com/docs/documentation/getting-started/api"
|
href="https://infisical.com/docs/documentation/getting-started/api"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
@@ -956,7 +956,7 @@ export const OverviewPage = () => {
|
|||||||
</a>
|
</a>
|
||||||
,
|
,
|
||||||
<a
|
<a
|
||||||
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
|
className="ml-1 text-mineshaft-200 underline decoration-mineshaft-400/65 underline-offset-3 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
|
||||||
href="https://infisical.com/docs/sdks/overview"
|
href="https://infisical.com/docs/sdks/overview"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
@@ -965,7 +965,7 @@ export const OverviewPage = () => {
|
|||||||
</a>
|
</a>
|
||||||
, and
|
, and
|
||||||
<a
|
<a
|
||||||
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
|
className="ml-1 text-mineshaft-200 underline decoration-mineshaft-400/65 underline-offset-3 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
|
||||||
href="https://infisical.com/docs/documentation/getting-started/introduction"
|
href="https://infisical.com/docs/documentation/getting-started/introduction"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
import { InfoIcon } from "lucide-react";
|
||||||
|
|
||||||
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||||
import { useProject } from "@app/context";
|
import { useProject } from "@app/context";
|
||||||
@@ -43,9 +45,19 @@ export const SettingsPage = () => {
|
|||||||
<div className="w-full max-w-8xl">
|
<div className="w-full max-w-8xl">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
scope={ProjectType.SecretManager}
|
scope={ProjectType.SecretManager}
|
||||||
title="Settings"
|
title="Project Settings"
|
||||||
description="Configure your secret manager's encryption, environments, webhooks and other configurations."
|
description="Configure your secret manager's encryption, environments, webhooks and other configurations."
|
||||||
/>
|
>
|
||||||
|
<Link
|
||||||
|
to="/organizations/$orgId/settings"
|
||||||
|
params={{
|
||||||
|
orgId: currentProject.orgId
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
|
||||||
|
>
|
||||||
|
<InfoIcon size={12} /> Looking for organization settings?
|
||||||
|
</Link>
|
||||||
|
</PageHeader>
|
||||||
<Tabs orientation="vertical" defaultValue={tabs[0].key}>
|
<Tabs orientation="vertical" defaultValue={tabs[0].key}>
|
||||||
<TabList>
|
<TabList>
|
||||||
{tabs
|
{tabs
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import slugify from "@sindresorhus/slugify";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
@@ -31,7 +32,13 @@ type ContentProps = {
|
|||||||
const Content = ({ onComplete }: ContentProps) => {
|
const Content = ({ onComplete }: ContentProps) => {
|
||||||
const { currentProject } = useProject();
|
const { currentProject } = useProject();
|
||||||
const { mutateAsync, isPending } = useCreateWsEnvironment();
|
const { mutateAsync, isPending } = useCreateWsEnvironment();
|
||||||
const { control, handleSubmit } = useForm<FormData>({
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
setValue,
|
||||||
|
getValues,
|
||||||
|
formState: { dirtyFields }
|
||||||
|
} = useForm<FormData>({
|
||||||
resolver: zodResolver(schema)
|
resolver: zodResolver(schema)
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -52,15 +59,28 @@ const Content = ({ onComplete }: ContentProps) => {
|
|||||||
onComplete(env);
|
onComplete(env);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleEnvironmentNameChange = () => {
|
||||||
|
if (dirtyFields.environmentSlug) return;
|
||||||
|
|
||||||
|
const value = getValues("environmentName");
|
||||||
|
setValue("environmentSlug", slugify(value, { lowercase: true }));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
name="environmentName"
|
name="environmentName"
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
<FormControl label="Environment Name" isError={Boolean(error)} errorText={error?.message}>
|
<FormControl label="Environment Name" isError={Boolean(error)} errorText={error?.message}>
|
||||||
<Input {...field} />
|
<Input
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChange(e);
|
||||||
|
handleEnvironmentNameChange();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
import { InfoIcon } from "lucide-react";
|
||||||
|
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||||
import { ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionSub, useOrganization } from "@app/context";
|
||||||
import { ProjectPermissionSecretScanningConfigActions } from "@app/context/ProjectPermissionContext/types";
|
import { ProjectPermissionSecretScanningConfigActions } from "@app/context/ProjectPermissionContext/types";
|
||||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||||
import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab";
|
import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab";
|
||||||
@@ -12,6 +14,7 @@ import { ProjectScanningConfigTab } from "./components/ProjectScanningConfigTab"
|
|||||||
|
|
||||||
export const SettingsPage = () => {
|
export const SettingsPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
||||||
@@ -21,9 +24,19 @@ export const SettingsPage = () => {
|
|||||||
<div className="w-full max-w-8xl">
|
<div className="w-full max-w-8xl">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
scope={ProjectType.SecretScanning}
|
scope={ProjectType.SecretScanning}
|
||||||
title="Settings"
|
title="Project Settings"
|
||||||
description="Configure your Secret Scanning product's configurations."
|
description="Configure your Secret Scanning product's configurations."
|
||||||
/>
|
>
|
||||||
|
<Link
|
||||||
|
to="/organizations/$orgId/settings"
|
||||||
|
params={{
|
||||||
|
orgId: currentOrg.id
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
|
||||||
|
>
|
||||||
|
<InfoIcon size={12} /> Looking for organization settings?
|
||||||
|
</Link>
|
||||||
|
</PageHeader>
|
||||||
<Tabs orientation="vertical" defaultValue="tab-project-general">
|
<Tabs orientation="vertical" defaultValue="tab-project-general">
|
||||||
<TabList>
|
<TabList>
|
||||||
<Tab variant="project" value="tab-project-general">
|
<Tab variant="project" value="tab-project-general">
|
||||||
|
|||||||