Merge branch 'main' of https://github.com/Infisical/infisical into ENG-4117
@@ -1,6 +1,7 @@
|
||||
Feature: External CA
|
||||
|
||||
Scenario: Issue a certificate from an external CA
|
||||
@cloudflare
|
||||
Scenario Outline: Issue a certificate from an external CA with Cloudflare
|
||||
Given I create a Cloudflare connection as cloudflare
|
||||
Then I memorize cloudflare with jq ".appConnection.id" as app_conn_id
|
||||
Given I create a external ACME CA with the following config as ext_ca
|
||||
@@ -92,9 +93,7 @@ Feature: External CA
|
||||
When I create certificate signing request as csr
|
||||
Then I add names to certificate signing request csr
|
||||
"""
|
||||
{
|
||||
"COMMON_NAME": "localhost"
|
||||
}
|
||||
<subject>
|
||||
"""
|
||||
# Pebble has a strict rule to only takes SANs
|
||||
Then I add subject alternative name to certificate signing request csr
|
||||
@@ -177,4 +176,196 @@ Feature: External CA
|
||||
[
|
||||
"localhost"
|
||||
]
|
||||
"""
|
||||
"""
|
||||
|
||||
Examples:
|
||||
| subject |
|
||||
| {"COMMON_NAME": "localhost"} |
|
||||
| {} |
|
||||
|
||||
@dnsme
|
||||
Scenario Outline: Issue a certificate from an external CA with DNS Made Easy
|
||||
Given I create a DNS Made Easy connection as dnsme
|
||||
Then I memorize dnsme with jq ".appConnection.id" as app_conn_id
|
||||
Given I create a external ACME CA with the following config as ext_ca
|
||||
"""
|
||||
{
|
||||
"dnsProviderConfig": {
|
||||
"provider": "dns-made-easy",
|
||||
"hostedZoneId": "MOCK_ZONE_ID"
|
||||
},
|
||||
"directoryUrl": "{PEBBLE_URL}",
|
||||
"accountEmail": "fangpen@infisical.com",
|
||||
"dnsAppConnectionId": "{app_conn_id}",
|
||||
"eabKid": "",
|
||||
"eabHmacKey": ""
|
||||
}
|
||||
"""
|
||||
Then I memorize ext_ca with jq ".id" as ext_ca_id
|
||||
Given I create a certificate template with the following config as cert_template
|
||||
"""
|
||||
{
|
||||
"subject": [
|
||||
{
|
||||
"type": "common_name",
|
||||
"allowed": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"sans": [
|
||||
{
|
||||
"type": "dns_name",
|
||||
"allowed": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"keyUsages": {
|
||||
"required": [],
|
||||
"allowed": [
|
||||
"digital_signature",
|
||||
"key_encipherment",
|
||||
"non_repudiation",
|
||||
"data_encipherment",
|
||||
"key_agreement",
|
||||
"key_cert_sign",
|
||||
"crl_sign",
|
||||
"encipher_only",
|
||||
"decipher_only"
|
||||
]
|
||||
},
|
||||
"extendedKeyUsages": {
|
||||
"required": [],
|
||||
"allowed": [
|
||||
"client_auth",
|
||||
"server_auth",
|
||||
"code_signing",
|
||||
"email_protection",
|
||||
"ocsp_signing",
|
||||
"time_stamping"
|
||||
]
|
||||
},
|
||||
"algorithms": {
|
||||
"signature": [
|
||||
"SHA256-RSA",
|
||||
"SHA512-RSA",
|
||||
"SHA384-ECDSA",
|
||||
"SHA384-RSA",
|
||||
"SHA256-ECDSA",
|
||||
"SHA512-ECDSA"
|
||||
],
|
||||
"keyAlgorithm": [
|
||||
"RSA-2048",
|
||||
"RSA-4096",
|
||||
"ECDSA-P384",
|
||||
"RSA-3072",
|
||||
"ECDSA-P256",
|
||||
"ECDSA-P521"
|
||||
]
|
||||
},
|
||||
"validity": {
|
||||
"max": "365d"
|
||||
}
|
||||
}
|
||||
"""
|
||||
Then I memorize cert_template with jq ".certificateTemplate.id" as cert_template_id
|
||||
Given I create an ACME profile with ca {ext_ca_id} and template {cert_template_id} as "acme_profile"
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
|
||||
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
|
||||
When I create certificate signing request as csr
|
||||
Then I add names to certificate signing request csr
|
||||
"""
|
||||
<subject>
|
||||
"""
|
||||
# Pebble has a strict rule to only takes SANs
|
||||
Then I add subject alternative name to certificate signing request csr
|
||||
"""
|
||||
[
|
||||
"localhost"
|
||||
]
|
||||
"""
|
||||
And I create a RSA private key pair as cert_key
|
||||
And I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format
|
||||
And I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order
|
||||
And I select challenge with type http-01 for domain localhost from order in order as challenge
|
||||
And I serve challenge response for challenge at localhost
|
||||
And I tell ACME server that challenge is ready to be verified
|
||||
Given I intercept outgoing requests
|
||||
"""
|
||||
[
|
||||
{
|
||||
"scope": "https://api.dnsmadeeasy.com:443",
|
||||
"method": "POST",
|
||||
"path": "/V2.0/dns/managed/MOCK_ZONE_ID/records",
|
||||
"status": 201,
|
||||
"response": {
|
||||
"gtdLocation": "DEFAULT",
|
||||
"failed": false,
|
||||
"monitor": false,
|
||||
"failover": false,
|
||||
"sourceId": 895364,
|
||||
"dynamicDns": false,
|
||||
"hardLink": false,
|
||||
"ttl": 60,
|
||||
"source": 1,
|
||||
"name": "_acme-challenge",
|
||||
"value": "\"MOCK_HTTP_01_VALUE\"",
|
||||
"id": 12345678,
|
||||
"type": "TXT"
|
||||
},
|
||||
"responseIsBinary": false
|
||||
},
|
||||
{
|
||||
"scope": "https://api.dnsmadeeasy.com:443",
|
||||
"method": "GET",
|
||||
"path": "/V2.0/dns/managed/MOCK_ZONE_ID/records?type=TXT&recordName=_acme-challenge&page=0",
|
||||
"status": 200,
|
||||
"response": {
|
||||
"totalRecords": 1,
|
||||
"totalPages": 1,
|
||||
"data": [
|
||||
{
|
||||
"gtdLocation": "DEFAULT",
|
||||
"failed": false,
|
||||
"monitor": false,
|
||||
"failover": false,
|
||||
"sourceId": 895364,
|
||||
"dynamicDns": false,
|
||||
"hardLink": false,
|
||||
"ttl": 60,
|
||||
"source": 1,
|
||||
"name": "_acme-challenge",
|
||||
"value": "\"MOCK_CHALLENGE_VALUE\"",
|
||||
"id": 1111111,
|
||||
"type": "TXT"
|
||||
}
|
||||
],
|
||||
"page": 0
|
||||
},
|
||||
"responseIsBinary": false
|
||||
},
|
||||
{
|
||||
"scope": "https://api.dnsmadeeasy.com:443",
|
||||
"method": "DELETE",
|
||||
"path": "/V2.0/dns/managed/MOCK_ZONE_ID/records/1111111",
|
||||
"status": 200,
|
||||
"response": "",
|
||||
"responseIsBinary": false
|
||||
}
|
||||
]
|
||||
"""
|
||||
Then I poll and finalize the ACME order order as finalized_order
|
||||
And the value finalized_order.body with jq ".status" should be equal to "valid"
|
||||
And I parse the full-chain certificate from order finalized_order as cert
|
||||
And the value cert with jq "[.extensions.subjectAltName.general_names.[].value] | sort" should be equal to json
|
||||
"""
|
||||
[
|
||||
"localhost"
|
||||
]
|
||||
"""
|
||||
|
||||
Examples:
|
||||
| subject |
|
||||
| {"COMMON_NAME": "localhost"} |
|
||||
| {} |
|
||||
|
||||
@@ -147,6 +147,40 @@ def step_impl(context: Context, var_name: str):
|
||||
context.vars[var_name] = response
|
||||
|
||||
|
||||
@given("I create a DNS Made Easy connection as {var_name}")
|
||||
def step_impl(context: Context, var_name: str):
|
||||
jwt_token = context.vars["AUTH_TOKEN"]
|
||||
conn_slug = faker.slug()
|
||||
with with_nocks(
|
||||
context,
|
||||
definitions=[
|
||||
{
|
||||
"scope": "https://api.dnsmadeeasy.com:443",
|
||||
"method": "GET",
|
||||
"path": "/V2.0/dns/managed/",
|
||||
"status": 200,
|
||||
"response": {"totalRecords": 0, "totalPages": 1, "data": [], "page": 0},
|
||||
"responseIsBinary": False,
|
||||
}
|
||||
],
|
||||
):
|
||||
response = context.http_client.post(
|
||||
"/api/v1/app-connections/dns-made-easy",
|
||||
headers=dict(authorization="Bearer {}".format(jwt_token)),
|
||||
json={
|
||||
"name": conn_slug,
|
||||
"description": "",
|
||||
"method": "api-key-secret",
|
||||
"credentials": {
|
||||
"apiKey": "MOCK_API_KEY",
|
||||
"secretKey": "MOCK_SECRET_KEY",
|
||||
},
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
context.vars[var_name] = response
|
||||
|
||||
|
||||
@given("I create a external ACME CA with the following config as {var_name}")
|
||||
def step_impl(context: Context, var_name: str):
|
||||
jwt_token = context.vars["AUTH_TOKEN"]
|
||||
|
||||
@@ -776,7 +776,9 @@ export const pkiAcmeServiceFactory = ({
|
||||
const cert = await orderCertificate(
|
||||
{
|
||||
caId: certificateAuthority!.id,
|
||||
commonName: certificateRequest.commonName!,
|
||||
// It is possible that the CSR does not have a common name, in which case we use an empty string
|
||||
// (more likely than not for a CSR from a modern ACME client like certbot, cert-manager, etc.)
|
||||
commonName: certificateRequest.commonName ?? "",
|
||||
altNames: certificateRequest.subjectAlternativeNames?.map((san) => san.value),
|
||||
csr: Buffer.from(csrPem),
|
||||
// TODO: not 100% sure what are these columns for, but let's put the values for common website SSL certs for now
|
||||
|
||||
@@ -119,6 +119,7 @@ const envSchema = z
|
||||
})
|
||||
.default("{}")
|
||||
),
|
||||
DNS_MADE_EASY_SANDBOX_ENABLED: zodStrBool.default("false").optional(),
|
||||
// smtp options
|
||||
SMTP_HOST: zpStr(z.string().optional()),
|
||||
SMTP_IGNORE_TLS: zodStrBool.default("false"),
|
||||
|
||||
@@ -61,6 +61,10 @@ import {
|
||||
DigitalOceanConnectionListItemSchema,
|
||||
SanitizedDigitalOceanConnectionSchema
|
||||
} from "@app/services/app-connection/digital-ocean";
|
||||
import {
|
||||
DNSMadeEasyConnectionListItemSchema,
|
||||
SanitizedDNSMadeEasyConnectionSchema
|
||||
} from "@app/services/app-connection/dns-made-easy/dns-made-easy-connection-schema";
|
||||
import { FlyioConnectionListItemSchema, SanitizedFlyioConnectionSchema } from "@app/services/app-connection/flyio";
|
||||
import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp";
|
||||
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
|
||||
@@ -170,7 +174,8 @@ const SanitizedAppConnectionSchema = z.union([
|
||||
...SanitizedAzureADCSConnectionSchema.options,
|
||||
...SanitizedRedisConnectionSchema.options,
|
||||
...SanitizedLaravelForgeConnectionSchema.options,
|
||||
...SanitizedChefConnectionSchema.options
|
||||
...SanitizedChefConnectionSchema.options,
|
||||
...SanitizedDNSMadeEasyConnectionSchema.options
|
||||
]);
|
||||
|
||||
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
@@ -215,7 +220,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
AzureADCSConnectionListItemSchema,
|
||||
RedisConnectionListItemSchema,
|
||||
LaravelForgeConnectionListItemSchema,
|
||||
ChefConnectionListItemSchema
|
||||
ChefConnectionListItemSchema,
|
||||
DNSMadeEasyConnectionListItemSchema
|
||||
]);
|
||||
|
||||
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import z from "zod";
|
||||
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreateDNSMadeEasyConnectionSchema,
|
||||
SanitizedDNSMadeEasyConnectionSchema,
|
||||
UpdateDNSMadeEasyConnectionSchema
|
||||
} from "@app/services/app-connection/dns-made-easy/dns-made-easy-connection-schema";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerDNSMadeEasyConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.DNSMadeEasy,
|
||||
server,
|
||||
sanitizedResponseSchema: SanitizedDNSMadeEasyConnectionSchema,
|
||||
createSchema: CreateDNSMadeEasyConnectionSchema,
|
||||
updateSchema: UpdateDNSMadeEasyConnectionSchema
|
||||
});
|
||||
|
||||
// The below endpoints are not exposed and for Infisical App use
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/dns-made-easy-zones`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
const zones = await server.services.appConnection.dnsMadeEasy.listZones(connectionId, req.permission);
|
||||
return zones;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -16,6 +16,7 @@ import { registerCamundaConnectionRouter } from "./camunda-connection-router";
|
||||
import { registerChecklyConnectionRouter } from "./checkly-connection-router";
|
||||
import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router";
|
||||
import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
|
||||
import { registerDNSMadeEasyConnectionRouter } from "./dns-made-easy-connection-router";
|
||||
import { registerDigitalOceanConnectionRouter } from "./digital-ocean-connection-router";
|
||||
import { registerFlyioConnectionRouter } from "./flyio-connection-router";
|
||||
import { registerGcpConnectionRouter } from "./gcp-connection-router";
|
||||
@@ -78,6 +79,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
||||
[AppConnection.Flyio]: registerFlyioConnectionRouter,
|
||||
[AppConnection.GitLab]: registerGitLabConnectionRouter,
|
||||
[AppConnection.Cloudflare]: registerCloudflareConnectionRouter,
|
||||
[AppConnection.DNSMadeEasy]: registerDNSMadeEasyConnectionRouter,
|
||||
[AppConnection.Bitbucket]: registerBitbucketConnectionRouter,
|
||||
[AppConnection.Zabbix]: registerZabbixConnectionRouter,
|
||||
[AppConnection.Railway]: registerRailwayConnectionRouter,
|
||||
|
||||
@@ -29,6 +29,7 @@ export enum AppConnection {
|
||||
Flyio = "flyio",
|
||||
GitLab = "gitlab",
|
||||
Cloudflare = "cloudflare",
|
||||
DNSMadeEasy = "dns-made-easy",
|
||||
Zabbix = "zabbix",
|
||||
Railway = "railway",
|
||||
Bitbucket = "bitbucket",
|
||||
|
||||
@@ -88,6 +88,11 @@ import {
|
||||
getDigitalOceanConnectionListItem,
|
||||
validateDigitalOceanConnectionCredentials
|
||||
} from "./digital-ocean";
|
||||
import { DNSMadeEasyConnectionMethod } from "./dns-made-easy/dns-made-easy-connection-enum";
|
||||
import {
|
||||
getDNSMadeEasyConnectionListItem,
|
||||
validateDNSMadeEasyConnectionCredentials
|
||||
} from "./dns-made-easy/dns-made-easy-connection-fns";
|
||||
import { FlyioConnectionMethod, getFlyioConnectionListItem, validateFlyioConnectionCredentials } from "./flyio";
|
||||
import { GcpConnectionMethod, getGcpConnectionListItem, validateGcpConnectionCredentials } from "./gcp";
|
||||
import { getGitHubConnectionListItem, GitHubConnectionMethod, validateGitHubConnectionCredentials } from "./github";
|
||||
@@ -171,7 +176,8 @@ const PKI_APP_CONNECTIONS = [
|
||||
AppConnection.Cloudflare,
|
||||
AppConnection.AzureADCS,
|
||||
AppConnection.AzureKeyVault,
|
||||
AppConnection.Chef
|
||||
AppConnection.Chef,
|
||||
AppConnection.DNSMadeEasy
|
||||
];
|
||||
|
||||
export const listAppConnectionOptions = (projectType?: ProjectType) => {
|
||||
@@ -207,6 +213,7 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => {
|
||||
getFlyioConnectionListItem(),
|
||||
getGitLabConnectionListItem(),
|
||||
getCloudflareConnectionListItem(),
|
||||
getDNSMadeEasyConnectionListItem(),
|
||||
getZabbixConnectionListItem(),
|
||||
getRailwayConnectionListItem(),
|
||||
getBitbucketConnectionListItem(),
|
||||
@@ -339,6 +346,7 @@ export const validateAppConnectionCredentials = async (
|
||||
[AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.DNSMadeEasy]: validateDNSMadeEasyConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Zabbix]: validateZabbixConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Railway]: validateRailwayConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
@@ -395,6 +403,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
case OktaConnectionMethod.ApiToken:
|
||||
case LaravelForgeConnectionMethod.ApiToken:
|
||||
return "API Token";
|
||||
case DNSMadeEasyConnectionMethod.APIKeySecret:
|
||||
return "API Key & Secret";
|
||||
case PostgresConnectionMethod.UsernameAndPassword:
|
||||
case MsSqlConnectionMethod.UsernameAndPassword:
|
||||
case MySqlConnectionMethod.UsernameAndPassword:
|
||||
@@ -483,6 +493,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
||||
[AppConnection.Flyio]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.GitLab]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Cloudflare]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.DNSMadeEasy]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Zabbix]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Railway]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Bitbucket]: platformManagedCredentialsNotSupported,
|
||||
|
||||
@@ -32,6 +32,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.Flyio]: "Fly.io",
|
||||
[AppConnection.GitLab]: "GitLab",
|
||||
[AppConnection.Cloudflare]: "Cloudflare",
|
||||
[AppConnection.DNSMadeEasy]: "DNS Made Easy",
|
||||
[AppConnection.Zabbix]: "Zabbix",
|
||||
[AppConnection.Railway]: "Railway",
|
||||
[AppConnection.Bitbucket]: "Bitbucket",
|
||||
@@ -77,6 +78,7 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
|
||||
[AppConnection.Flyio]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.GitLab]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Cloudflare]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.DNSMadeEasy]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Zabbix]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Railway]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Bitbucket]: AppConnectionPlanType.Regular,
|
||||
|
||||
@@ -72,6 +72,8 @@ import { checklyConnectionService } from "./checkly/checkly-connection-service";
|
||||
import { ValidateCloudflareConnectionCredentialsSchema } from "./cloudflare/cloudflare-connection-schema";
|
||||
import { cloudflareConnectionService } from "./cloudflare/cloudflare-connection-service";
|
||||
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
|
||||
import { ValidateDNSMadeEasyConnectionCredentialsSchema } from "./dns-made-easy/dns-made-easy-connection-schema";
|
||||
import { dnsMadeEasyConnectionService } from "./dns-made-easy/dns-made-easy-connection-service";
|
||||
import { databricksConnectionService } from "./databricks/databricks-connection-service";
|
||||
import { ValidateDigitalOceanConnectionCredentialsSchema } from "./digital-ocean";
|
||||
import { digitalOceanAppPlatformConnectionService } from "./digital-ocean/digital-ocean-connection-service";
|
||||
@@ -167,6 +169,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.Flyio]: ValidateFlyioConnectionCredentialsSchema,
|
||||
[AppConnection.GitLab]: ValidateGitLabConnectionCredentialsSchema,
|
||||
[AppConnection.Cloudflare]: ValidateCloudflareConnectionCredentialsSchema,
|
||||
[AppConnection.DNSMadeEasy]: ValidateDNSMadeEasyConnectionCredentialsSchema,
|
||||
[AppConnection.Zabbix]: ValidateZabbixConnectionCredentialsSchema,
|
||||
[AppConnection.Railway]: ValidateRailwayConnectionCredentialsSchema,
|
||||
[AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema,
|
||||
@@ -875,6 +878,7 @@ export const appConnectionServiceFactory = ({
|
||||
flyio: flyioConnectionService(connectAppConnectionById),
|
||||
gitlab: gitlabConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
|
||||
cloudflare: cloudflareConnectionService(connectAppConnectionById),
|
||||
dnsMadeEasy: dnsMadeEasyConnectionService(connectAppConnectionById),
|
||||
zabbix: zabbixConnectionService(connectAppConnectionById),
|
||||
railway: railwayConnectionService(connectAppConnectionById),
|
||||
bitbucket: bitbucketConnectionService(connectAppConnectionById),
|
||||
|
||||
@@ -15,8 +15,8 @@ import {
|
||||
TOracleDBConnectionInput,
|
||||
TValidateOracleDBConnectionCredentialsSchema
|
||||
} from "@app/ee/services/app-connections/oracledb";
|
||||
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||
import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service";
|
||||
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
@@ -106,6 +106,12 @@ import {
|
||||
TDigitalOceanConnectionInput,
|
||||
TValidateDigitalOceanCredentialsSchema
|
||||
} from "./digital-ocean";
|
||||
import {
|
||||
TDNSMadeEasyConnection,
|
||||
TDNSMadeEasyConnectionConfig,
|
||||
TDNSMadeEasyConnectionInput,
|
||||
TValidateDNSMadeEasyConnectionCredentialsSchema
|
||||
} from "./dns-made-easy/dns-made-easy-connection-types";
|
||||
import {
|
||||
TFlyioConnection,
|
||||
TFlyioConnectionConfig,
|
||||
@@ -279,6 +285,7 @@ export type TAppConnection = { id: string } & (
|
||||
| TGitLabConnection
|
||||
| TCloudflareConnection
|
||||
| TBitbucketConnection
|
||||
| TDNSMadeEasyConnection
|
||||
| TZabbixConnection
|
||||
| TRailwayConnection
|
||||
| TChecklyConnection
|
||||
@@ -328,6 +335,7 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| TGitLabConnectionInput
|
||||
| TCloudflareConnectionInput
|
||||
| TBitbucketConnectionInput
|
||||
| TDNSMadeEasyConnectionInput
|
||||
| TZabbixConnectionInput
|
||||
| TRailwayConnectionInput
|
||||
| TChecklyConnectionInput
|
||||
@@ -395,6 +403,7 @@ export type TAppConnectionConfig =
|
||||
| TGitLabConnectionConfig
|
||||
| TCloudflareConnectionConfig
|
||||
| TBitbucketConnectionConfig
|
||||
| TDNSMadeEasyConnectionConfig
|
||||
| TZabbixConnectionConfig
|
||||
| TRailwayConnectionConfig
|
||||
| TChecklyConnectionConfig
|
||||
@@ -439,6 +448,7 @@ export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateGitLabConnectionCredentialsSchema
|
||||
| TValidateCloudflareConnectionCredentialsSchema
|
||||
| TValidateBitbucketConnectionCredentialsSchema
|
||||
| TValidateDNSMadeEasyConnectionCredentialsSchema
|
||||
| TValidateZabbixConnectionCredentialsSchema
|
||||
| TValidateRailwayConnectionCredentialsSchema
|
||||
| TValidateChecklyConnectionCredentialsSchema
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum DNSMadeEasyConnectionMethod {
|
||||
APIKeySecret = "api-key-secret"
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
|
||||
import { DNSMadeEasyConnectionMethod } from "./dns-made-easy-connection-enum";
|
||||
import {
|
||||
TDNSMadeEasyConnection,
|
||||
TDNSMadeEasyConnectionConfig,
|
||||
TDNSMadeEasyZone
|
||||
} from "./dns-made-easy-connection-types";
|
||||
|
||||
interface DNSMadeEasyApiResponse {
|
||||
totalRecords: number;
|
||||
totalPages: number;
|
||||
data: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
value: string;
|
||||
}>;
|
||||
page: number;
|
||||
}
|
||||
|
||||
export const getDNSMadeEasyUrl = (path: string) => {
|
||||
const appCfg = getConfig();
|
||||
return `${appCfg.DNS_MADE_EASY_SANDBOX_ENABLED ? IntegrationUrls.DNS_MADE_EASY_SANDBOX_API_URL : IntegrationUrls.DNS_MADE_EASY_API_URL}${path}`;
|
||||
};
|
||||
|
||||
export const makeDNSMadeEasyAuthHeaders = (
|
||||
apiKey: string,
|
||||
secretKey: string,
|
||||
currentDate?: Date
|
||||
): Record<string, string> => {
|
||||
// Format date as "Day, DD Mon YYYY HH:MM:SS GMT" (e.g., "Mon, 01 Jan 2024 12:00:00 GMT")
|
||||
const requestDate = (currentDate ?? new Date()).toUTCString();
|
||||
|
||||
// Generate HMAC-SHA1 signature
|
||||
const hmac = crypto.nativeCrypto.createHmac("sha1", secretKey);
|
||||
hmac.update(requestDate);
|
||||
const hmacSignature = hmac.digest("hex");
|
||||
|
||||
return {
|
||||
"x-dnsme-apiKey": apiKey,
|
||||
"x-dnsme-hmac": hmacSignature,
|
||||
"x-dnsme-requestDate": requestDate
|
||||
};
|
||||
};
|
||||
|
||||
export const getDNSMadeEasyConnectionListItem = () => {
|
||||
return {
|
||||
name: "DNS Made Easy" as const,
|
||||
app: AppConnection.DNSMadeEasy as const,
|
||||
methods: Object.values(DNSMadeEasyConnectionMethod) as [DNSMadeEasyConnectionMethod.APIKeySecret]
|
||||
};
|
||||
};
|
||||
|
||||
export const listDNSMadeEasyZones = async (appConnection: TDNSMadeEasyConnection): Promise<TDNSMadeEasyZone[]> => {
|
||||
if (appConnection.method !== DNSMadeEasyConnectionMethod.APIKeySecret) {
|
||||
throw new BadRequestError({ message: "Unsupported DNS Made Easy connection method" });
|
||||
}
|
||||
|
||||
const {
|
||||
credentials: { apiKey, secretKey }
|
||||
} = appConnection;
|
||||
|
||||
try {
|
||||
const allZones: TDNSMadeEasyZone[] = [];
|
||||
let currentPage = 0;
|
||||
let totalPages = 1;
|
||||
|
||||
// Fetch all pages of zones
|
||||
while (currentPage < totalPages) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const resp = await request.get<DNSMadeEasyApiResponse>(getDNSMadeEasyUrl("/V2.0/dns/managed/"), {
|
||||
headers: {
|
||||
...makeDNSMadeEasyAuthHeaders(apiKey, secretKey),
|
||||
Accept: "application/json"
|
||||
},
|
||||
params: {
|
||||
page: currentPage
|
||||
}
|
||||
});
|
||||
|
||||
if (resp.data?.data) {
|
||||
// Map the API response to TDNSMadeEasyZone format
|
||||
const zones = resp.data.data.map((zone) => ({
|
||||
id: String(zone.id),
|
||||
name: zone.name
|
||||
}));
|
||||
allZones.push(...zones);
|
||||
|
||||
// Update pagination info
|
||||
totalPages = resp.data.totalPages || 1;
|
||||
currentPage += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return allZones;
|
||||
} catch (error: unknown) {
|
||||
logger.error(error, "Error listing DNS Made Easy zones");
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
message: `Failed to list DNS Made Easy zones: ${error.response?.data?.error?.[0] || error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to list DNS Made Easy zones"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const listDNSMadeEasyRecords = async (
|
||||
appConnection: TDNSMadeEasyConnection,
|
||||
options: { zoneId: string; type?: string; name?: string }
|
||||
): Promise<DNSMadeEasyApiResponse["data"]> => {
|
||||
if (appConnection.method !== DNSMadeEasyConnectionMethod.APIKeySecret) {
|
||||
throw new BadRequestError({ message: "Unsupported DNS Made Easy connection method" });
|
||||
}
|
||||
const {
|
||||
credentials: { apiKey, secretKey }
|
||||
} = appConnection;
|
||||
const { zoneId, type, name } = options;
|
||||
|
||||
try {
|
||||
const allRecords: DNSMadeEasyApiResponse["data"] = [];
|
||||
let currentPage = 0;
|
||||
let totalPages = 1;
|
||||
|
||||
// Fetch all pages of records
|
||||
while (currentPage < totalPages) {
|
||||
// Build query parameters
|
||||
const queryParams: Record<string, string | number> = {};
|
||||
if (type) {
|
||||
queryParams.type = type;
|
||||
}
|
||||
if (name) {
|
||||
queryParams.recordName = name;
|
||||
}
|
||||
queryParams.page = currentPage;
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const resp = await request.get<DNSMadeEasyApiResponse>(
|
||||
getDNSMadeEasyUrl(`/V2.0/dns/managed/${encodeURIComponent(zoneId)}/records`),
|
||||
{
|
||||
headers: {
|
||||
...makeDNSMadeEasyAuthHeaders(apiKey, secretKey),
|
||||
Accept: "application/json"
|
||||
},
|
||||
params: queryParams
|
||||
}
|
||||
);
|
||||
|
||||
if (resp.data?.data) {
|
||||
allRecords.push(...resp.data.data);
|
||||
|
||||
// Update pagination info
|
||||
totalPages = resp.data.totalPages || 1;
|
||||
currentPage += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return allRecords;
|
||||
} catch (error: unknown) {
|
||||
logger.error(error, "Error listing DNS Made Easy records");
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
message: `Failed to list DNS Made Easy records: ${error.response?.data?.error?.[0] || error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to list DNS Made Easy records"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const validateDNSMadeEasyConnectionCredentials = async (config: TDNSMadeEasyConnectionConfig) => {
|
||||
if (config.method !== DNSMadeEasyConnectionMethod.APIKeySecret) {
|
||||
throw new BadRequestError({ message: "Unsupported DNS Made Easy connection method" });
|
||||
}
|
||||
|
||||
const { apiKey, secretKey } = config.credentials;
|
||||
|
||||
try {
|
||||
const resp = await request.get(getDNSMadeEasyUrl("/V2.0/dns/managed/"), {
|
||||
headers: {
|
||||
...makeDNSMadeEasyAuthHeaders(apiKey, secretKey),
|
||||
Accept: "application/json"
|
||||
}
|
||||
});
|
||||
if (resp.status !== 200) {
|
||||
throw new BadRequestError({
|
||||
message: "Unable to validate connection: Invalid API credentials provided."
|
||||
});
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
message: `Failed to validate credentials: ${error.response?.data?.error?.[0] || error.message || "Unknown error"}`
|
||||
});
|
||||
}
|
||||
logger.error(error, "Error validating DNS Made Easy connection credentials");
|
||||
throw new BadRequestError({
|
||||
message: "Unable to validate connection: verify credentials"
|
||||
});
|
||||
}
|
||||
|
||||
return config.credentials;
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import z from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
GenericCreateAppConnectionFieldsSchema,
|
||||
GenericUpdateAppConnectionFieldsSchema
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { APP_CONNECTION_NAME_MAP } from "../app-connection-maps";
|
||||
import { DNSMadeEasyConnectionMethod } from "./dns-made-easy-connection-enum";
|
||||
|
||||
export const DNSMadeEasyConnectionApiKeyCredentialsSchema = z.object({
|
||||
apiKey: z.string().trim().min(1, "API key required").max(256, "API key cannot exceed 256 characters"),
|
||||
secretKey: z.string().trim().min(1, "Secret key required").max(256, "Secret key cannot exceed 256 characters")
|
||||
});
|
||||
|
||||
const BaseDNSMadeEasyConnectionSchema = BaseAppConnectionSchema.extend({
|
||||
app: z.literal(AppConnection.DNSMadeEasy)
|
||||
});
|
||||
|
||||
export const DNSMadeEasyConnectionSchema = BaseDNSMadeEasyConnectionSchema.extend({
|
||||
method: z.literal(DNSMadeEasyConnectionMethod.APIKeySecret),
|
||||
credentials: DNSMadeEasyConnectionApiKeyCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedDNSMadeEasyConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseDNSMadeEasyConnectionSchema.extend({
|
||||
method: z.literal(DNSMadeEasyConnectionMethod.APIKeySecret),
|
||||
credentials: DNSMadeEasyConnectionApiKeyCredentialsSchema.pick({ apiKey: true })
|
||||
}).describe(JSON.stringify({ title: `${APP_CONNECTION_NAME_MAP[AppConnection.DNSMadeEasy]} (API Key)` }))
|
||||
]);
|
||||
|
||||
export const ValidateDNSMadeEasyConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z
|
||||
.literal(DNSMadeEasyConnectionMethod.APIKeySecret)
|
||||
.describe(AppConnections.CREATE(AppConnection.DNSMadeEasy).method),
|
||||
credentials: DNSMadeEasyConnectionApiKeyCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.DNSMadeEasy).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateDNSMadeEasyConnectionSchema = ValidateDNSMadeEasyConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.DNSMadeEasy)
|
||||
);
|
||||
|
||||
export const UpdateDNSMadeEasyConnectionSchema = z
|
||||
.object({
|
||||
credentials: DNSMadeEasyConnectionApiKeyCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.DNSMadeEasy).credentials
|
||||
)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.DNSMadeEasy));
|
||||
|
||||
export const DNSMadeEasyConnectionListItemSchema = z
|
||||
.object({
|
||||
name: z.literal("DNS Made Easy"),
|
||||
app: z.literal(AppConnection.DNSMadeEasy),
|
||||
methods: z.nativeEnum(DNSMadeEasyConnectionMethod).array()
|
||||
})
|
||||
.describe(JSON.stringify({ title: APP_CONNECTION_NAME_MAP[AppConnection.DNSMadeEasy] }));
|
||||
@@ -0,0 +1,35 @@
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { listDNSMadeEasyZones } from "./dns-made-easy-connection-fns";
|
||||
import { TDNSMadeEasyConnection } from "./dns-made-easy-connection-types";
|
||||
|
||||
type TGetAppConnectionFunc = (
|
||||
app: AppConnection,
|
||||
connectionId: string,
|
||||
actor: OrgServiceActor
|
||||
) => Promise<TDNSMadeEasyConnection>;
|
||||
|
||||
export const dnsMadeEasyConnectionService = (getAppConnection: TGetAppConnectionFunc) => {
|
||||
const listZones = async (connectionId: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.DNSMadeEasy, connectionId, actor);
|
||||
try {
|
||||
const zones = await listDNSMadeEasyZones(appConnection);
|
||||
return zones;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
error,
|
||||
`Failed to list DNS Made Easy zones for DNS Made Easy connection [connectionId=${connectionId}]`
|
||||
);
|
||||
throw new BadRequestError({
|
||||
message: `Failed to list DNS Made Easy zones: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
listZones
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import z from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
CreateDNSMadeEasyConnectionSchema,
|
||||
DNSMadeEasyConnectionSchema,
|
||||
ValidateDNSMadeEasyConnectionCredentialsSchema
|
||||
} from "./dns-made-easy-connection-schema";
|
||||
|
||||
export type TDNSMadeEasyConnection = z.infer<typeof DNSMadeEasyConnectionSchema>;
|
||||
|
||||
export type TDNSMadeEasyConnectionInput = z.infer<typeof CreateDNSMadeEasyConnectionSchema> & {
|
||||
app: AppConnection.DNSMadeEasy;
|
||||
};
|
||||
|
||||
export type TValidateDNSMadeEasyConnectionCredentialsSchema = typeof ValidateDNSMadeEasyConnectionCredentialsSchema;
|
||||
|
||||
export type TDNSMadeEasyConnectionConfig = DiscriminativePick<
|
||||
TDNSMadeEasyConnectionInput,
|
||||
"method" | "app" | "credentials"
|
||||
> & {
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TDNSMadeEasyZone = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum AcmeDnsProvider {
|
||||
Route53 = "route53",
|
||||
Cloudflare = "cloudflare"
|
||||
Cloudflare = "cloudflare",
|
||||
DNSMadeEasy = "dns-made-easy"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { decryptAppConnection } from "@app/services/app-connection/app-connectio
|
||||
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||
import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types";
|
||||
import { TCloudflareConnection } from "@app/services/app-connection/cloudflare/cloudflare-connection-types";
|
||||
import { TDNSMadeEasyConnection } from "@app/services/app-connection/dns-made-easy/dns-made-easy-connection-types";
|
||||
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||
@@ -43,6 +44,7 @@ import {
|
||||
TUpdateAcmeCertificateAuthorityDTO
|
||||
} from "./acme-certificate-authority-types";
|
||||
import { cloudflareDeleteTxtRecord, cloudflareInsertTxtRecord } from "./dns-providers/cloudflare";
|
||||
import { dnsMadeEasyDeleteTxtRecord, dnsMadeEasyInsertTxtRecord } from "./dns-providers/dns-made-easy";
|
||||
import { route53DeleteTxtRecord, route53InsertTxtRecord } from "./dns-providers/route54";
|
||||
|
||||
type TAcmeCertificateAuthorityFnsDeps = {
|
||||
@@ -120,6 +122,22 @@ export const castDbEntryToAcmeCertificateAuthority = (
|
||||
};
|
||||
};
|
||||
|
||||
const getAcmeChallengeRecord = (
|
||||
provider: AcmeDnsProvider,
|
||||
identifierValue: string,
|
||||
keyAuthorization: string
|
||||
): { recordName: string; recordValue: string } => {
|
||||
let recordName: string;
|
||||
if (provider === AcmeDnsProvider.DNSMadeEasy) {
|
||||
// For DNS Made Easy, we don't need to provide the domain name in the record name.
|
||||
recordName = "_acme-challenge";
|
||||
} else {
|
||||
recordName = `_acme-challenge.${identifierValue}`; // e.g., "_acme-challenge.example.com"
|
||||
}
|
||||
const recordValue = `"${keyAuthorization}"`; // must be double quoted
|
||||
return { recordName, recordValue };
|
||||
};
|
||||
|
||||
export const orderCertificate = async (
|
||||
{
|
||||
caId,
|
||||
@@ -241,8 +259,11 @@ export const orderCertificate = async (
|
||||
throw new Error("Unsupported challenge type");
|
||||
}
|
||||
|
||||
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
|
||||
const recordValue = `"${keyAuthorization}"`; // must be double quoted
|
||||
const { recordName, recordValue } = getAcmeChallengeRecord(
|
||||
acmeCa.configuration.dnsProviderConfig.provider,
|
||||
authz.identifier.value,
|
||||
keyAuthorization
|
||||
);
|
||||
|
||||
switch (acmeCa.configuration.dnsProviderConfig.provider) {
|
||||
case AcmeDnsProvider.Route53: {
|
||||
@@ -263,14 +284,26 @@ export const orderCertificate = async (
|
||||
);
|
||||
break;
|
||||
}
|
||||
case AcmeDnsProvider.DNSMadeEasy: {
|
||||
await dnsMadeEasyInsertTxtRecord(
|
||||
connection as TDNSMadeEasyConnection,
|
||||
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
|
||||
recordName,
|
||||
recordValue
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
|
||||
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com"
|
||||
const recordValue = `"${keyAuthorization}"`; // must be double quoted
|
||||
const { recordName, recordValue } = getAcmeChallengeRecord(
|
||||
acmeCa.configuration.dnsProviderConfig.provider,
|
||||
authz.identifier.value,
|
||||
keyAuthorization
|
||||
);
|
||||
|
||||
switch (acmeCa.configuration.dnsProviderConfig.provider) {
|
||||
case AcmeDnsProvider.Route53: {
|
||||
@@ -291,6 +324,15 @@ export const orderCertificate = async (
|
||||
);
|
||||
break;
|
||||
}
|
||||
case AcmeDnsProvider.DNSMadeEasy: {
|
||||
await dnsMadeEasyDeleteTxtRecord(
|
||||
connection as TDNSMadeEasyConnection,
|
||||
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
|
||||
recordName,
|
||||
recordValue
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`);
|
||||
}
|
||||
@@ -413,6 +455,12 @@ export const AcmeCertificateAuthorityFns = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (dnsProviderConfig.provider === AcmeDnsProvider.DNSMadeEasy && appConnection.app !== AppConnection.DNSMadeEasy) {
|
||||
throw new BadRequestError({
|
||||
message: `App connection with ID '${dnsAppConnectionId}' is not a DNS Made Easy connection`
|
||||
});
|
||||
}
|
||||
|
||||
// validates permission to connect
|
||||
await appConnectionService.validateAppConnectionUsageById(
|
||||
appConnection.app as AppConnection,
|
||||
@@ -508,6 +556,15 @@ export const AcmeCertificateAuthorityFns = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
dnsProviderConfig.provider === AcmeDnsProvider.DNSMadeEasy &&
|
||||
appConnection.app !== AppConnection.DNSMadeEasy
|
||||
) {
|
||||
throw new BadRequestError({
|
||||
message: `App connection with ID '${dnsAppConnectionId}' is not a DNS Made Easy connection`
|
||||
});
|
||||
}
|
||||
|
||||
const ca = await certificateAuthorityDAL.findById(id);
|
||||
|
||||
if (!ca) {
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import axios from "axios";
|
||||
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import {
|
||||
getDNSMadeEasyUrl,
|
||||
listDNSMadeEasyRecords,
|
||||
makeDNSMadeEasyAuthHeaders
|
||||
} from "@app/services/app-connection/dns-made-easy/dns-made-easy-connection-fns";
|
||||
import { TDNSMadeEasyConnection } from "@app/services/app-connection/dns-made-easy/dns-made-easy-connection-types";
|
||||
|
||||
export const dnsMadeEasyInsertTxtRecord = async (
|
||||
connection: TDNSMadeEasyConnection,
|
||||
hostedZoneId: string,
|
||||
domain: string,
|
||||
value: string
|
||||
) => {
|
||||
const {
|
||||
credentials: { apiKey, secretKey }
|
||||
} = connection;
|
||||
|
||||
logger.info({ hostedZoneId, domain, value }, "Inserting TXT record for DNS Made Easy");
|
||||
try {
|
||||
await request.post(
|
||||
getDNSMadeEasyUrl(`/V2.0/dns/managed/${encodeURIComponent(hostedZoneId)}/records`),
|
||||
{
|
||||
type: "TXT",
|
||||
name: domain,
|
||||
value,
|
||||
ttl: 60
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
...makeDNSMadeEasyAuthHeaders(apiKey, secretKey),
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const errorMessage =
|
||||
(error.response?.data as { error?: string[] | string })?.error?.[0] ||
|
||||
(error.response?.data as { error?: string[] | string })?.error ||
|
||||
error.message ||
|
||||
"Unknown error";
|
||||
|
||||
if (error.status === 400 && error.message.includes("already exists")) {
|
||||
logger.info({ domain, value }, `Record already exists for domain: ${domain} and value: ${value}`);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(typeof errorMessage === "string" ? errorMessage : String(errorMessage));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const dnsMadeEasyDeleteTxtRecord = async (
|
||||
connection: TDNSMadeEasyConnection,
|
||||
hostedZoneId: string,
|
||||
domain: string,
|
||||
value: string
|
||||
) => {
|
||||
const {
|
||||
credentials: { apiKey, secretKey }
|
||||
} = connection;
|
||||
|
||||
logger.info({ hostedZoneId, domain, value }, "Deleting TXT record for DNS Made Easy");
|
||||
try {
|
||||
const dnsRecords = await listDNSMadeEasyRecords(connection, { zoneId: hostedZoneId, type: "TXT", name: domain });
|
||||
|
||||
let foundRecord = false;
|
||||
if (dnsRecords.length > 0) {
|
||||
const recordToDelete = dnsRecords.find(
|
||||
(record) => record.type === "TXT" && record.name === domain && record.value === value
|
||||
);
|
||||
|
||||
if (recordToDelete) {
|
||||
await request.delete(
|
||||
getDNSMadeEasyUrl(`/V2.0/dns/managed/${encodeURIComponent(hostedZoneId)}/records/${recordToDelete.id}`),
|
||||
{
|
||||
headers: {
|
||||
...makeDNSMadeEasyAuthHeaders(apiKey, secretKey),
|
||||
Accept: "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
foundRecord = true;
|
||||
}
|
||||
}
|
||||
if (!foundRecord) {
|
||||
logger.warn({ hostedZoneId, domain, value }, "Record to delete not found");
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const errorMessage =
|
||||
(error.response?.data as { error?: string[] | string })?.error?.[0] ||
|
||||
(error.response?.data as { error?: string[] | string })?.error ||
|
||||
error.message ||
|
||||
"Unknown error";
|
||||
throw new Error(typeof errorMessage === "string" ? errorMessage : String(errorMessage));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -105,7 +105,9 @@ export enum IntegrationUrls {
|
||||
GCP_CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform",
|
||||
|
||||
GITHUB_USER_INSTALLATIONS = "https://api.github.com/user/installations",
|
||||
CHEF_API_URL = "https://api.chef.io"
|
||||
CHEF_API_URL = "https://api.chef.io",
|
||||
DNS_MADE_EASY_API_URL = "https://api.dnsmadeeasy.com",
|
||||
DNS_MADE_EASY_SANDBOX_API_URL = "https://api.sandbox.dnsmadeeasy.com"
|
||||
}
|
||||
|
||||
export const getIntegrationOptions = async () => {
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
"integrations/app-connections/cloudflare",
|
||||
"integrations/app-connections/databricks",
|
||||
"integrations/app-connections/digital-ocean",
|
||||
"integrations/app-connections/dns-made-easy",
|
||||
"integrations/app-connections/flyio",
|
||||
"integrations/app-connections/gcp",
|
||||
"integrations/app-connections/github",
|
||||
|
||||
@@ -17,7 +17,7 @@ their **ACME Directory URL** such as:
|
||||
- ZeroSSL: `https://acme.zerossl.com/v2/DV90`.
|
||||
- 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.
|
||||
|
||||
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.
|
||||
</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>
|
||||
</Step>
|
||||
<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:
|
||||

|
||||
|
||||
<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.
|
||||

|
||||
|
||||
|
||||
@@ -1,70 +1,388 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import React, { useState, useMemo } from "react";
|
||||
|
||||
export const AppConnectionsBrowser = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState('All');
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
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 = [
|
||||
{"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": "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"},
|
||||
{"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"},
|
||||
{"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"},
|
||||
{"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"},
|
||||
{"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": "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());
|
||||
{
|
||||
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: "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",
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
{
|
||||
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(() => {
|
||||
let filtered = connections;
|
||||
|
||||
if (selectedCategory !== 'All') {
|
||||
filtered = filtered.filter(connection => connection.category === selectedCategory);
|
||||
|
||||
if (selectedCategory !== "All") {
|
||||
filtered = filtered.filter(
|
||||
(connection) => connection.category === selectedCategory
|
||||
);
|
||||
}
|
||||
|
||||
if (searchTerm) {
|
||||
filtered = filtered.filter(connection =>
|
||||
connection.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
connection.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
connection.category.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
filtered = filtered.filter(
|
||||
(connection) =>
|
||||
connection.name.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="relative w-full">
|
||||
<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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
<svg
|
||||
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>
|
||||
</div>
|
||||
<input
|
||||
@@ -94,14 +422,14 @@ export const AppConnectionsBrowser = () => {
|
||||
{/* Category Filter */}
|
||||
<div className="mb-6">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{categories.map(category => (
|
||||
{categories.map((category) => (
|
||||
<button
|
||||
key={category}
|
||||
onClick={() => setSelectedCategory(category)}
|
||||
className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors shadow-sm ${
|
||||
selectedCategory === category
|
||||
? '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-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"
|
||||
}`}
|
||||
>
|
||||
{category}
|
||||
@@ -113,8 +441,9 @@ export const AppConnectionsBrowser = () => {
|
||||
{/* Results Count */}
|
||||
<div className="mb-4">
|
||||
<p className="text-sm text-gray-600">
|
||||
{filteredConnections.length} app connection{filteredConnections.length !== 1 ? 's' : ''} found
|
||||
{selectedCategory !== 'All' && ` in ${selectedCategory}`}
|
||||
{filteredConnections.length} app connection
|
||||
{filteredConnections.length !== 1 ? "s" : ""} found
|
||||
{selectedCategory !== "All" && ` in ${selectedCategory}`}
|
||||
{searchTerm && ` for "${searchTerm}"`}
|
||||
</p>
|
||||
</div>
|
||||
@@ -147,13 +476,17 @@ export const AppConnectionsBrowser = () => {
|
||||
) : (
|
||||
<div className="text-center py-8">
|
||||
<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 && (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
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": {
|
||||
"title": "Settings",
|
||||
"title": "Project Settings",
|
||||
"description": "These settings only apply to the currently selected Project.",
|
||||
"danger-zone": "Danger Zone",
|
||||
"delete-project": "Delete Project",
|
||||
|
||||
@@ -3,14 +3,7 @@ import { ReactNode } from "@tanstack/react-router";
|
||||
import { LucideIcon } from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import {
|
||||
Badge,
|
||||
InstanceIcon,
|
||||
OrgIcon,
|
||||
ProjectIcon,
|
||||
SubOrgIcon,
|
||||
TBadgeProps
|
||||
} from "@app/components/v3";
|
||||
import { InstanceIcon, OrgIcon, ProjectIcon, SubOrgIcon } from "@app/components/v3";
|
||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||
|
||||
type Props = {
|
||||
@@ -21,41 +14,40 @@ type Props = {
|
||||
scope: "org" | "namespace" | "instance" | ProjectType | null;
|
||||
};
|
||||
|
||||
const SCOPE_NAME: Record<NonNullable<Props["scope"]>, { label: string; icon: LucideIcon }> = {
|
||||
org: { label: "Organization", icon: OrgIcon },
|
||||
[ProjectType.SecretManager]: { label: "Project", icon: ProjectIcon },
|
||||
[ProjectType.CertificateManager]: { label: "Project", icon: ProjectIcon },
|
||||
[ProjectType.SSH]: { label: "Project", icon: ProjectIcon },
|
||||
[ProjectType.KMS]: { label: "Project", icon: ProjectIcon },
|
||||
[ProjectType.PAM]: { label: "Project", icon: ProjectIcon },
|
||||
[ProjectType.SecretScanning]: { label: "Project", icon: ProjectIcon },
|
||||
namespace: { label: "Sub-Organization", icon: SubOrgIcon },
|
||||
instance: { label: "Server", 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"
|
||||
const SCOPE_BADGE: Record<NonNullable<Props["scope"]>, { icon: LucideIcon; className: string }> = {
|
||||
org: { className: "text-org", icon: OrgIcon },
|
||||
[ProjectType.SecretManager]: { className: "text-project", icon: ProjectIcon },
|
||||
[ProjectType.CertificateManager]: { className: "text-project", icon: ProjectIcon },
|
||||
[ProjectType.SSH]: { className: "text-project", icon: ProjectIcon },
|
||||
[ProjectType.KMS]: { className: "text-project", icon: ProjectIcon },
|
||||
[ProjectType.PAM]: { className: "text-project", icon: ProjectIcon },
|
||||
[ProjectType.SecretScanning]: { className: "text-project", icon: ProjectIcon },
|
||||
namespace: { className: "text-sub-org", icon: SubOrgIcon },
|
||||
instance: { className: "text-neutral", icon: InstanceIcon }
|
||||
};
|
||||
|
||||
export const PageHeader = ({ title, description, children, className, scope }: Props) => (
|
||||
<div className={twMerge("mb-10 w-full", className)}>
|
||||
<div className="flex w-full justify-between">
|
||||
<div className="mr-4 flex w-full items-center">
|
||||
<h1 className="text-3xl font-medium text-white capitalize">{title}</h1>
|
||||
{scope && (
|
||||
<Badge variant={SCOPE_VARIANT[scope]} className="mt-1 ml-2.5">
|
||||
{createElement(SCOPE_NAME[scope].icon)}
|
||||
{SCOPE_NAME[scope].label}
|
||||
</Badge>
|
||||
)}
|
||||
<h1
|
||||
className={twMerge(
|
||||
"text-3xl font-medium text-white capitalize underline decoration-2 underline-offset-4",
|
||||
scope === "org" && "decoration-org/90",
|
||||
scope === "instance" && "decoration-neutral/90",
|
||||
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 className="flex items-center gap-2">{children}</div>
|
||||
</div>
|
||||
|
||||
@@ -47,8 +47,8 @@ export const Tab = ({
|
||||
}) => (
|
||||
<TabsPrimitive.Trigger
|
||||
className={twMerge(
|
||||
"flex h-10 cursor-pointer items-center justify-center border-transparent",
|
||||
"px-3 text-sm font-medium whitespace-nowrap text-mineshaft-400 transition-all select-none",
|
||||
"flex h-11 cursor-pointer items-center justify-center border-transparent",
|
||||
"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",
|
||||
"border-b hover:text-mineshaft-200",
|
||||
"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 { ChefConnectionMethod } from "@app/hooks/api/appConnections/types/chef-connection";
|
||||
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 { LaravelForgeConnectionMethod } from "@app/hooks/api/appConnections/types/laravel-forge-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.GitLab]: { name: "GitLab", image: "GitLab.png" },
|
||||
[AppConnection.Cloudflare]: { name: "Cloudflare", image: "Cloudflare.png" },
|
||||
[AppConnection.DNSMadeEasy]: { name: "DNS Made Easy", image: "DNSMadeEasy.svg", size: 120 },
|
||||
[AppConnection.Zabbix]: { name: "Zabbix", image: "Zabbix.png" },
|
||||
[AppConnection.Railway]: { name: "Railway", image: "Railway.png" },
|
||||
[AppConnection.Bitbucket]: { name: "Bitbucket", image: "Bitbucket.png" },
|
||||
@@ -214,6 +216,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
|
||||
return { name: "Client Secret", icon: faKey };
|
||||
case AzureClientSecretsConnectionMethod.Certificate:
|
||||
return { name: "Certificate", icon: faCertificate };
|
||||
case DNSMadeEasyConnectionMethod.APIKeySecret:
|
||||
return { name: "API Key & Secret", icon: faKey };
|
||||
default:
|
||||
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./queries";
|
||||
export * from "./types";
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { appConnectionKeys } from "../queries";
|
||||
import { TDNSMadeEasyZone } from "./types";
|
||||
|
||||
const dnsMadeEasyConnectionKeys = {
|
||||
all: [...appConnectionKeys.all, "dns-made-easy"] as const,
|
||||
listZones: (connectionId: string) =>
|
||||
[...dnsMadeEasyConnectionKeys.all, "zones", connectionId] as const
|
||||
};
|
||||
|
||||
export const useDNSMadeEasyConnectionListZones = (
|
||||
connectionId: string,
|
||||
options?: Omit<
|
||||
UseQueryOptions<
|
||||
TDNSMadeEasyZone[],
|
||||
unknown,
|
||||
TDNSMadeEasyZone[],
|
||||
ReturnType<typeof dnsMadeEasyConnectionKeys.listZones>
|
||||
>,
|
||||
"queryKey" | "queryFn"
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: dnsMadeEasyConnectionKeys.listZones(connectionId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TDNSMadeEasyZone[]>(
|
||||
`/api/v1/app-connections/dns-made-easy/${connectionId}/dns-made-easy-zones`
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export type TDNSMadeEasyZone = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
@@ -29,6 +29,7 @@ export enum AppConnection {
|
||||
Flyio = "flyio",
|
||||
GitLab = "gitlab",
|
||||
Cloudflare = "cloudflare",
|
||||
DNSMadeEasy = "dns-made-easy",
|
||||
Bitbucket = "bitbucket",
|
||||
Zabbix = "zabbix",
|
||||
Railway = "railway",
|
||||
|
||||
@@ -184,6 +184,10 @@ export type TRedisConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.Redis;
|
||||
};
|
||||
|
||||
export type TDNSMadeEasyConnectionOption = TAppConnectionOptionBase & {
|
||||
app: AppConnection.DNSMadeEasy;
|
||||
};
|
||||
|
||||
export type TAppConnectionOption =
|
||||
| TAwsConnectionOption
|
||||
| TGitHubConnectionOption
|
||||
@@ -225,7 +229,8 @@ export type TAppConnectionOption =
|
||||
| TOktaConnectionOption
|
||||
| TAzureAdCsConnectionOption
|
||||
| TLaravelForgeConnectionOption
|
||||
| TChefConnectionOption;
|
||||
| TChefConnectionOption
|
||||
| TDNSMadeEasyConnectionOption;
|
||||
|
||||
export type TAppConnectionOptionMap = {
|
||||
[AppConnection.AWS]: TAwsConnectionOption;
|
||||
@@ -257,6 +262,7 @@ export type TAppConnectionOptionMap = {
|
||||
[AppConnection.Flyio]: TFlyioConnectionOption;
|
||||
[AppConnection.GitLab]: TGitlabConnectionOption;
|
||||
[AppConnection.Cloudflare]: TCloudflareConnectionOption;
|
||||
[AppConnection.DNSMadeEasy]: TDNSMadeEasyConnectionOption;
|
||||
[AppConnection.Bitbucket]: TBitbucketConnectionOption;
|
||||
[AppConnection.Zabbix]: TZabbixConnectionOption;
|
||||
[AppConnection.Railway]: TRailwayConnectionOption;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection";
|
||||
|
||||
export enum DNSMadeEasyConnectionMethod {
|
||||
APIKeySecret = "api-key-secret"
|
||||
}
|
||||
|
||||
export type TDNSMadeEasyConnection = TRootAppConnection & { app: AppConnection.DNSMadeEasy } & {
|
||||
method: DNSMadeEasyConnectionMethod.APIKeySecret;
|
||||
credentials: {
|
||||
apiKey: string;
|
||||
secretKey: string;
|
||||
};
|
||||
};
|
||||
@@ -15,6 +15,7 @@ import { TChefConnection } from "./chef-connection";
|
||||
import { TCloudflareConnection } from "./cloudflare-connection";
|
||||
import { TDatabricksConnection } from "./databricks-connection";
|
||||
import { TDigitalOceanConnection } from "./digital-ocean";
|
||||
import { TDNSMadeEasyConnection } from "./dns-made-easy-connection";
|
||||
import { TFlyioConnection } from "./flyio-connection";
|
||||
import { TGcpConnection } from "./gcp-connection";
|
||||
import { TGitHubConnection } from "./github-connection";
|
||||
@@ -57,6 +58,7 @@ export * from "./checkly-connection";
|
||||
export * from "./chef-connection";
|
||||
export * from "./cloudflare-connection";
|
||||
export * from "./databricks-connection";
|
||||
export * from "./dns-made-easy-connection";
|
||||
export * from "./flyio-connection";
|
||||
export * from "./gcp-connection";
|
||||
export * from "./github-connection";
|
||||
@@ -127,7 +129,8 @@ export type TAppConnection =
|
||||
| TNorthflankConnection
|
||||
| TOktaConnection
|
||||
| TRedisConnection
|
||||
| TChefConnection;
|
||||
| TChefConnection
|
||||
| TDNSMadeEasyConnection;
|
||||
|
||||
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id" | "projectId">;
|
||||
|
||||
|
||||
@@ -16,12 +16,14 @@ export const caStatusToNameMap: { [K in CaStatus]: string } = {
|
||||
|
||||
export const ACME_DNS_PROVIDER_NAME_MAP: Record<AcmeDnsProvider, string> = {
|
||||
[AcmeDnsProvider.ROUTE53]: "Route53",
|
||||
[AcmeDnsProvider.Cloudflare]: "Cloudflare"
|
||||
[AcmeDnsProvider.Cloudflare]: "Cloudflare",
|
||||
[AcmeDnsProvider.DNSMadeEasy]: "DNS Made Easy"
|
||||
};
|
||||
|
||||
export const ACME_DNS_PROVIDER_APP_CONNECTION_MAP: Record<AcmeDnsProvider, AppConnection> = {
|
||||
[AcmeDnsProvider.ROUTE53]: AppConnection.AWS,
|
||||
[AcmeDnsProvider.Cloudflare]: AppConnection.Cloudflare
|
||||
[AcmeDnsProvider.Cloudflare]: AppConnection.Cloudflare,
|
||||
[AcmeDnsProvider.DNSMadeEasy]: AppConnection.DNSMadeEasy
|
||||
};
|
||||
|
||||
export const CA_TYPE_CAPABILITIES_MAP: Record<CaType, CaCapability[]> = {
|
||||
|
||||
@@ -21,7 +21,8 @@ export enum CaRenewalType {
|
||||
|
||||
export enum AcmeDnsProvider {
|
||||
ROUTE53 = "route53",
|
||||
Cloudflare = "cloudflare"
|
||||
Cloudflare = "cloudflare",
|
||||
DNSMadeEasy = "dns-made-easy"
|
||||
}
|
||||
|
||||
export enum CaCapability {
|
||||
|
||||
@@ -14,8 +14,8 @@ export const KmsLayout = () => {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden">
|
||||
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
|
||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden 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
|
||||
key="menu-project-items"
|
||||
initial={{ x: -150 }}
|
||||
|
||||
@@ -22,8 +22,8 @@ import {
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Link, useLocation, useNavigate, useRouter, useRouterState } from "@tanstack/react-router";
|
||||
import { UserPlusIcon } from "lucide-react";
|
||||
import { Link, useLocation, useNavigate, useRouter } from "@tanstack/react-router";
|
||||
import { ChevronRight, UserPlusIcon } from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Mfa } from "@app/components/auth/Mfa";
|
||||
@@ -31,7 +31,6 @@ import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
import {
|
||||
BreadcrumbContainer,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -43,7 +42,6 @@ import {
|
||||
IconButton,
|
||||
Modal,
|
||||
ModalContent,
|
||||
TBreadcrumbFormat,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
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 { Organization, SubscriptionPlan } from "@app/hooks/api/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 { ServerAdminsPanel } from "../ServerAdminsPanel/ServerAdminsPanel";
|
||||
@@ -183,9 +182,6 @@ export const Navbar = () => {
|
||||
}
|
||||
}, [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) => {
|
||||
queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
|
||||
queryClient.removeQueries({ queryKey: projectKeys.getAllUserProjects() });
|
||||
@@ -249,7 +245,9 @@ export const Navbar = () => {
|
||||
|
||||
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) => {
|
||||
if (currentOrg?.id === org.id) return;
|
||||
@@ -279,64 +277,59 @@ export const Navbar = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="z-10 flex min-h-12 items-center bg-mineshaft-900 px-4 pt-1">
|
||||
<div className="mr-auto flex items-center overflow-hidden">
|
||||
<div className="shrink-0">
|
||||
<div className="z-10 flex min-h-12 items-center bg-mineshaft-900 px-4">
|
||||
<div className="mr-auto flex h-full min-w-34 items-center">
|
||||
<div className="mt-0.5 shrink-0">
|
||||
<Link to="/organizations/$orgId/projects" params={{ orgId: currentOrg.id }}>
|
||||
<img alt="infisical logo" src="/images/logotransparent.png" className="h-4" />
|
||||
</Link>
|
||||
</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 ? (
|
||||
<>
|
||||
<Link
|
||||
to="/admin"
|
||||
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" />
|
||||
<div className="whitespace-nowrap">Server Console</div>
|
||||
</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}
|
||||
</>
|
||||
<Link
|
||||
to="/admin"
|
||||
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" />
|
||||
<div className="whitespace-nowrap">Server Console</div>
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
<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}>
|
||||
<div className="group flex cursor-pointer items-center gap-2 overflow-hidden text-sm text-white transition-all duration-100 hover:text-primary">
|
||||
<Badge
|
||||
asChild
|
||||
variant="org"
|
||||
isTruncatable
|
||||
// TODO(scott): either add badge size/style variant or create designated component for namespace/org nav bar
|
||||
className={twMerge(
|
||||
"gap-x-1.5 text-sm",
|
||||
(isProjectScope || isSubOrganization) &&
|
||||
"bg-transparent text-mineshaft-200 hover:!bg-transparent hover:underline [&>svg]:!text-org"
|
||||
)}
|
||||
<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">
|
||||
<button
|
||||
className="flex cursor-pointer items-center gap-x-2 truncate whitespace-nowrap"
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
navigate({
|
||||
to: "/organizations/$orgId/projects",
|
||||
params: { orgId: currentOrg.id }
|
||||
});
|
||||
if (isSubOrganization) {
|
||||
await router.invalidate({ sync: true }).catch(() => null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
navigate({
|
||||
to: "/organizations/$orgId/projects",
|
||||
params: { orgId: currentOrg.id }
|
||||
});
|
||||
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>
|
||||
<OrgIcon className={twMerge("size-[14px] shrink-0 text-org")} />
|
||||
<span className="truncate">{currentOrg?.name}</span>
|
||||
<Badge variant="org" className="hidden lg:inline-flex">
|
||||
Organization
|
||||
</Badge>
|
||||
</button>
|
||||
{subscription.cardDeclined && (
|
||||
<Tooltip
|
||||
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 && (
|
||||
<>
|
||||
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
|
||||
{breadcrumbs ? (
|
||||
<BreadcrumbContainer
|
||||
className="min-w-[15rem] flex-1"
|
||||
breadcrumbs={[breadcrumbs[0]] as TBreadcrumbFormat[]}
|
||||
/>
|
||||
) : null}
|
||||
<ChevronRight size={18} className="mx-3 mt-[3px] text-mineshaft-400/70" />
|
||||
<ProjectSelect />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{subscription && subscription.slug === "starter" && !subscription.has_used_trial && (
|
||||
|
||||
{subscription && subscription.slug === "starter" && !subscription.has_used_trial ? (
|
||||
<Tooltip content="Start Free Pro Trial">
|
||||
<Button
|
||||
variant="plain"
|
||||
@@ -576,6 +565,10 @@ export const Navbar = () => {
|
||||
Free Pro Trial
|
||||
</Button>
|
||||
</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 */}
|
||||
{!location.pathname.startsWith("/admin") ? (
|
||||
@@ -601,7 +594,7 @@ export const Navbar = () => {
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
) : null
|
||||
}
|
||||
@@ -705,7 +698,7 @@ export const Navbar = () => {
|
||||
}}
|
||||
>
|
||||
<DropdownMenuItem icon={<FontAwesomeIcon icon={faUserPlus} />}>
|
||||
Invite Members
|
||||
Invite Users
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
) : null
|
||||
|
||||
@@ -19,9 +19,9 @@ export const OrgNavBar = ({ isHidden }: Props) => {
|
||||
const variant = isRootOrganization ? "org" : "namespace";
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-mineshaft-900">
|
||||
{!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
|
||||
key="menu-org-items"
|
||||
initial={{ x: -150 }}
|
||||
@@ -114,6 +114,6 @@ export const OrgNavBar = ({ isHidden }: Props) => {
|
||||
isOpen={popUp?.createOrg?.isOpen}
|
||||
onClose={() => handlePopUpToggle("createOrg", false)}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -29,8 +29,8 @@ export const PamLayout = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden">
|
||||
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
|
||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden 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
|
||||
key="menu-project-items"
|
||||
initial={{ x: -150 }}
|
||||
|
||||
@@ -29,8 +29,8 @@ export const PkiManagerLayout = () => {
|
||||
|
||||
const location = useLocation();
|
||||
return (
|
||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden">
|
||||
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
|
||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden 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
|
||||
key="menu-project-items"
|
||||
initial={{ x: -150 }}
|
||||
|
||||
@@ -92,7 +92,11 @@ export const ProjectSelect = () => {
|
||||
}, [projects, projectFavorites, currentWorkspace]);
|
||||
|
||||
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}>
|
||||
<Link
|
||||
to={getProjectHomePage(currentWorkspace.type, currentWorkspace.environments)}
|
||||
@@ -100,16 +104,12 @@ export const ProjectSelect = () => {
|
||||
projectId: currentWorkspace.id,
|
||||
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">
|
||||
{currentWorkspace?.name}
|
||||
</p>
|
||||
<Badge variant="project">
|
||||
<ProjectIcon />
|
||||
<span className="hidden sm:inline-block">
|
||||
{currentWorkspace.type ? PROJECT_TYPE_NAME[currentWorkspace.type] : "Project"}
|
||||
</span>
|
||||
<ProjectIcon className="size-[14px] shrink-0 text-project" />
|
||||
<span className="truncate">{currentWorkspace?.name}</span>
|
||||
<Badge variant="project" className="hidden lg:inline-flex">
|
||||
{currentWorkspace.type ? PROJECT_TYPE_NAME[currentWorkspace.type] : "Project"}
|
||||
</Badge>
|
||||
</Link>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -118,7 +118,7 @@ export const ProjectSelect = () => {
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
ariaLabel="switch-project"
|
||||
className="px-2 py-1"
|
||||
className="top-px px-2 py-1"
|
||||
>
|
||||
<FontAwesomeIcon icon={faCaretDown} className="text-xs text-bunker-300" />
|
||||
</IconButton>
|
||||
|
||||
@@ -39,8 +39,8 @@ export const SecretManagerLayout = () => {
|
||||
(secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
|
||||
|
||||
return (
|
||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden">
|
||||
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
|
||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden 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
|
||||
key="menu-project-items"
|
||||
initial={{ x: -150 }}
|
||||
|
||||
@@ -38,8 +38,8 @@ export const SecretScanningLayout = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden">
|
||||
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
|
||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden 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
|
||||
key="menu-project-items"
|
||||
initial={{ x: -150 }}
|
||||
|
||||
@@ -20,8 +20,8 @@ export const SshLayout = () => {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden">
|
||||
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
|
||||
<div className="dark flex h-full w-full flex-col overflow-x-hidden 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
|
||||
key="menu-project-items"
|
||||
initial={{ x: -150 }}
|
||||
|
||||
@@ -17,7 +17,7 @@ export const AccessManagementPage = () => {
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope="instance"
|
||||
title="Access Control"
|
||||
title="Server Admin Access Control"
|
||||
description="Manage server admins within your Infisical instance."
|
||||
/>
|
||||
<ServerAdminsTable />
|
||||
|
||||
@@ -350,8 +350,8 @@ const ViewMembersModalContent = ({
|
||||
className="my-auto bg-mineshaft-700"
|
||||
title={
|
||||
members.length
|
||||
? "No organization members match search..."
|
||||
: "No organization members found"
|
||||
? "No organization users match search..."
|
||||
: "No organization users found"
|
||||
}
|
||||
icon={faUsers}
|
||||
/>
|
||||
|
||||
@@ -27,6 +27,10 @@ import {
|
||||
TCloudflareZone,
|
||||
useCloudflareConnectionListZones
|
||||
} from "@app/hooks/api/appConnections/cloudflare";
|
||||
import {
|
||||
TDNSMadeEasyZone,
|
||||
useDNSMadeEasyConnectionListZones
|
||||
} from "@app/hooks/api/appConnections/dns-made-easy";
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
import {
|
||||
AcmeDnsProvider,
|
||||
@@ -211,6 +215,11 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
enabled: caType === CaType.ACME
|
||||
});
|
||||
|
||||
const { data: availableDNSMadeEasyConnections, isPending: isDNSMadeEasyPending } =
|
||||
useListAvailableAppConnections(AppConnection.DNSMadeEasy, currentProject.id, {
|
||||
enabled: caType === CaType.ACME
|
||||
});
|
||||
|
||||
const { data: availableAzureConnections, isPending: isAzurePending } =
|
||||
useListAvailableAppConnections(AppConnection.AzureADCS, currentProject.id, {
|
||||
enabled: caType === CaType.AZURE_AD_CS
|
||||
@@ -220,16 +229,24 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
if (caType === CaType.AZURE_AD_CS) {
|
||||
return availableAzureConnections || [];
|
||||
}
|
||||
return [...(availableRoute53Connections || []), ...(availableCloudflareConnections || [])];
|
||||
return [
|
||||
...(availableRoute53Connections || []),
|
||||
...(availableCloudflareConnections || []),
|
||||
...(availableDNSMadeEasyConnections || [])
|
||||
];
|
||||
}, [
|
||||
caType,
|
||||
availableRoute53Connections,
|
||||
availableCloudflareConnections,
|
||||
availableDNSMadeEasyConnections,
|
||||
availableAzureConnections
|
||||
]);
|
||||
|
||||
const isPending =
|
||||
isRoute53Pending || isCloudflarePending || (isAzurePending && caType === CaType.AZURE_AD_CS);
|
||||
isRoute53Pending ||
|
||||
isCloudflarePending ||
|
||||
isDNSMadeEasyPending ||
|
||||
(isAzurePending && caType === CaType.AZURE_AD_CS);
|
||||
|
||||
const dnsAppConnection =
|
||||
caType === CaType.ACME && configuration && "dnsAppConnection" in configuration
|
||||
@@ -241,6 +258,11 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
enabled: dnsProvider === AcmeDnsProvider.Cloudflare && !!dnsAppConnection.id
|
||||
});
|
||||
|
||||
const { data: dnsMadeEasyZones = [], isPending: isDNSMadeEasyZonesPending } =
|
||||
useDNSMadeEasyConnectionListZones(dnsAppConnection.id, {
|
||||
enabled: dnsProvider === AcmeDnsProvider.DNSMadeEasy && !!dnsAppConnection.id
|
||||
});
|
||||
|
||||
// Populate form with CA data when editing
|
||||
useEffect(() => {
|
||||
if (ca && !isCaLoading) {
|
||||
@@ -500,6 +522,32 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{dnsProvider === AcmeDnsProvider.DNSMadeEasy && (
|
||||
<Controller
|
||||
name="configuration.dnsProviderConfig.hostedZoneId"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error?.message)}
|
||||
label="Zone"
|
||||
>
|
||||
<FilterableSelect
|
||||
isLoading={isDNSMadeEasyZonesPending && !!dnsAppConnection.id}
|
||||
isDisabled={!dnsAppConnection.id}
|
||||
value={dnsMadeEasyZones.find((zone) => zone.id === value)}
|
||||
onChange={(option) => {
|
||||
onChange((option as SingleValue<TDNSMadeEasyZone>)?.id ?? null);
|
||||
}}
|
||||
options={dnsMadeEasyZones}
|
||||
placeholder="Select a zone..."
|
||||
getOptionLabel={(option) => option.name}
|
||||
getOptionValue={(option) => option.id}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
|
||||
@@ -49,7 +49,7 @@ export const IntegrationsListPage = () => {
|
||||
<div className="mb-8">
|
||||
<PageHeader
|
||||
scope={ProjectType.CertificateManager}
|
||||
title="Integrations"
|
||||
title="Project Integrations"
|
||||
description="Manage integrations with third-party certificate services."
|
||||
/>
|
||||
<Tabs orientation="vertical" value={currentTab} onValueChange={updateSelectedTab}>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
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 { useOrganization } from "@app/context";
|
||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||
import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab";
|
||||
|
||||
@@ -15,6 +18,7 @@ const tabs = [
|
||||
|
||||
export const SettingsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
return (
|
||||
<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>
|
||||
</Helmet>
|
||||
<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}>
|
||||
<TabList>
|
||||
{tabs.map((tab) => (
|
||||
|
||||
@@ -20,7 +20,7 @@ export const OverviewPage = () => {
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope={ProjectType.KMS}
|
||||
title="Overview"
|
||||
title="Project Overview"
|
||||
description="Manage keys and perform cryptographic operations."
|
||||
/>
|
||||
<ProjectPermissionCan
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
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 { useOrganization } from "@app/context";
|
||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||
import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab";
|
||||
|
||||
@@ -16,6 +19,8 @@ const tabs = [
|
||||
export const SettingsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
||||
<Helmet>
|
||||
@@ -24,9 +29,19 @@ export const SettingsPage = () => {
|
||||
<div className="w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope={ProjectType.KMS}
|
||||
title="Settings"
|
||||
title="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}>
|
||||
<TabList>
|
||||
{tabs.map((tab) => (
|
||||
|
||||
@@ -83,7 +83,7 @@ export const AccessManagementPage = () => {
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<PageHeader
|
||||
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."
|
||||
/>
|
||||
{!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-4 flex flex-wrap items-center justify-between gap-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" />
|
||||
</div>
|
||||
<OrgPermissionCan I={OrgPermissionGroupActions.Create} a={OrgPermissionSubjects.Groups}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handleAddGroupModal()}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create Group
|
||||
Create Organization Group
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
|
||||
@@ -159,7 +159,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search groups..."
|
||||
placeholder="Search organization groups..."
|
||||
/>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
@@ -205,7 +205,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
|
||||
</Th>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Role
|
||||
Organization Role
|
||||
<IconButton
|
||||
variant="plain"
|
||||
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="mb-4 flex flex-wrap items-center justify-between gap-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" />
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
@@ -129,7 +131,9 @@ export const IdentitySection = withPermission(
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Add Machine Identity
|
||||
{isSubOrganization
|
||||
? "Add Machine Identity to Sub-Organization"
|
||||
: "Create Organization Machine Identity"}
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
|
||||
@@ -200,7 +200,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
</DropdownSubMenuTrigger>
|
||||
<DropdownSubMenuContent className="max-h-80 thin-scrollbar overflow-y-auto rounded-l-none">
|
||||
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
||||
Filter Machine Identities by Role
|
||||
Filter Organization Machine Identities by Role
|
||||
</DropdownMenuLabel>
|
||||
{roles?.map(({ id, slug, name }) => (
|
||||
<DropdownMenuItem
|
||||
@@ -258,7 +258,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
</Th>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Role
|
||||
Organization Role
|
||||
<IconButton
|
||||
variant="plain"
|
||||
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-4 flex flex-wrap items-center justify-between gap-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" />
|
||||
</div>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
leftIcon={<UserPlusIcon size={16} />}
|
||||
onClick={() =>
|
||||
@@ -219,7 +219,9 @@ export const OrgMembersSection = () => {
|
||||
}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
{isSubOrganization ? "Add Members" : "Invite Members"}
|
||||
{isSubOrganization
|
||||
? "Add Users to Sub-Organization"
|
||||
: "Invite Users to Organization"}
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
|
||||
@@ -336,7 +336,7 @@ export const OrgMembersTable = ({
|
||||
</DropdownSubMenuTrigger>
|
||||
<DropdownSubMenuContent className="max-h-80 thin-scrollbar overflow-y-auto rounded-l-none">
|
||||
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
||||
Apply Roles to Filter Users
|
||||
Filter Organization Users by Role
|
||||
</DropdownMenuLabel>
|
||||
{roles?.map(({ id, slug, name }) => (
|
||||
<DropdownMenuItem
|
||||
@@ -365,7 +365,7 @@ export const OrgMembersTable = ({
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search members..."
|
||||
placeholder="Search organization users..."
|
||||
/>
|
||||
</div>
|
||||
<TableContainer className="mt-4">
|
||||
@@ -434,7 +434,7 @@ export const OrgMembersTable = ({
|
||||
</Th>
|
||||
<Th className="w-1/3">
|
||||
<div className="flex items-center">
|
||||
Role
|
||||
Organization Role
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={`ml-2 ${orderBy === OrgMembersOrderBy.Role ? "" : "opacity-30"}`}
|
||||
@@ -727,8 +727,8 @@ export const OrgMembersTable = ({
|
||||
<EmptyState
|
||||
title={
|
||||
members.length
|
||||
? "No organization members match search..."
|
||||
: "No organization members found"
|
||||
? "No organization users match search..."
|
||||
: "No organization users found"
|
||||
}
|
||||
icon={members.length ? faSearch : faUsers}
|
||||
/>
|
||||
|
||||
@@ -199,7 +199,7 @@ export const OrgRoleTable = () => {
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Role}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
@@ -207,7 +207,7 @@ export const OrgRoleTable = () => {
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Add Role
|
||||
Add Organization Role
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
@@ -216,7 +216,7 @@ export const OrgRoleTable = () => {
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search roles..."
|
||||
placeholder="Search organization roles..."
|
||||
className="flex-1"
|
||||
containerClassName="mb-4"
|
||||
/>
|
||||
@@ -441,7 +441,7 @@ export const OrgRoleTable = () => {
|
||||
<EmptyState
|
||||
title={
|
||||
roles?.length
|
||||
? "No roles match search..."
|
||||
? "No organization roles match search..."
|
||||
: "This organization does not have any roles"
|
||||
}
|
||||
icon={roles?.length ? faSearch : undefined}
|
||||
|
||||
@@ -27,7 +27,7 @@ export const AppConnectionsPage = withPermission(
|
||||
<PageHeader
|
||||
scope={isSubOrganization ? "namespace" : "org"}
|
||||
className="w-full"
|
||||
title="App Connections"
|
||||
title={`${isSubOrganization ? "Sub-Organization" : "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">
|
||||
|
||||
@@ -24,6 +24,7 @@ import { ChefConnectionForm } from "./ChefConnectionForm";
|
||||
import { CloudflareConnectionForm } from "./CloudflareConnectionForm";
|
||||
import { DatabricksConnectionForm } from "./DatabricksConnectionForm";
|
||||
import { DigitalOceanConnectionForm } from "./DigitalOceanConnectionForm";
|
||||
import { DNSMadeEasyConnectionForm } from "./DNSMadeEasyConnectionForm";
|
||||
import { FlyioConnectionForm } from "./FlyioConnectionForm";
|
||||
import { GcpConnectionForm } from "./GcpConnectionForm";
|
||||
import { GitHubConnectionForm } from "./GitHubConnectionForm";
|
||||
@@ -148,6 +149,8 @@ const CreateForm = ({ app, onComplete, projectId }: CreateFormProps) => {
|
||||
return <GitLabConnectionForm onSubmit={onSubmit} projectId={projectId} />;
|
||||
case AppConnection.Cloudflare:
|
||||
return <CloudflareConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.DNSMadeEasy:
|
||||
return <DNSMadeEasyConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Bitbucket:
|
||||
return <BitbucketConnectionForm onSubmit={onSubmit} />;
|
||||
case AppConnection.Zabbix:
|
||||
@@ -306,6 +309,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
|
||||
);
|
||||
case AppConnection.Cloudflare:
|
||||
return <CloudflareConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.DNSMadeEasy:
|
||||
return <DNSMadeEasyConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.Bitbucket:
|
||||
return <BitbucketConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
|
||||
case AppConnection.Zabbix:
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import { 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">
|
||||
<PageHeader
|
||||
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."
|
||||
/>
|
||||
<LogsSection pageView />
|
||||
|
||||
@@ -88,7 +88,7 @@ const Page = () => {
|
||||
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronLeft} />
|
||||
Groups
|
||||
Organization Groups
|
||||
</Link>
|
||||
<PageHeader
|
||||
scope={isSubOrganization ? "namespace" : "org"}
|
||||
|
||||
@@ -82,7 +82,7 @@ const Page = () => {
|
||||
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronLeft} />
|
||||
Machine Identities
|
||||
Organization Machine Identities
|
||||
</Link>
|
||||
<PageHeader
|
||||
scope={isSubOrganization ? "namespace" : "org"}
|
||||
|
||||
@@ -81,7 +81,7 @@ export const ProjectsPage = () => {
|
||||
</Helmet>
|
||||
<PageHeader
|
||||
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."
|
||||
/>
|
||||
{projectListView === ProjectListView.MyProjects ? (
|
||||
|
||||
@@ -20,7 +20,7 @@ export const SettingsPage = () => {
|
||||
<PageHeader
|
||||
scope={isSubOrganization ? "namespace" : "org"}
|
||||
description="Configure organization-wide settings"
|
||||
title={t("settings.org.title")}
|
||||
title={isSubOrganization ? "Sub-Organization Settings" : "Organization Settings"}
|
||||
/>
|
||||
<OrgTabGroup />
|
||||
</div>
|
||||
|
||||
@@ -112,7 +112,7 @@ const Page = withPermission(
|
||||
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronLeft} />
|
||||
Users
|
||||
Organization Users
|
||||
</Link>
|
||||
<PageHeader
|
||||
scope={isSubOrganization ? "namespace" : "org"}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
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 { useOrganization } from "@app/context";
|
||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||
import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab";
|
||||
|
||||
export const SettingsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
||||
<Helmet>
|
||||
@@ -16,9 +21,19 @@ export const SettingsPage = () => {
|
||||
<div className="w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope={ProjectType.PAM}
|
||||
title="Settings"
|
||||
title="Project Settings"
|
||||
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">
|
||||
<TabList>
|
||||
<Tab variant="project" value="tab-project-general">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
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 { useOrganization, useProject } from "@app/context";
|
||||
@@ -43,9 +44,19 @@ const Page = () => {
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<PageHeader
|
||||
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."
|
||||
/>
|
||||
>
|
||||
<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}>
|
||||
<TabList>
|
||||
<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-4 flex items-center justify-between">
|
||||
<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" />
|
||||
</div>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Groups}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handleAddGroupModal()}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Add Group
|
||||
Add Group to Project
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
|
||||
@@ -122,7 +122,7 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => {
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search members..."
|
||||
placeholder="Search project groups..."
|
||||
/>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
@@ -143,7 +143,7 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => {
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>Role</Th>
|
||||
<Th>Project Role</Th>
|
||||
<Th>Added on</Th>
|
||||
<Th className="w-5" />
|
||||
</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-4 flex items-center justify-between">
|
||||
<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" />
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
@@ -212,7 +212,7 @@ export const IdentityTab = withProjectPermission(
|
||||
onClick={() => handlePopUpOpen("createIdentity")}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Add Machine Identity
|
||||
Add Machine Identity to Project
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
@@ -223,7 +223,7 @@ export const IdentityTab = withProjectPermission(
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search machine identities by name..."
|
||||
placeholder="Search project machine identities by name..."
|
||||
/>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
@@ -251,7 +251,7 @@ export const IdentityTab = withProjectPermission(
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-1/3">Role</Th>
|
||||
<Th className="w-1/3">Project Role</Th>
|
||||
<Th>Managed by</Th>
|
||||
<Th className="w-5">{isFetching ? <Spinner size="xs" /> : null}</Th>
|
||||
</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-4 flex items-center justify-between">
|
||||
<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" />
|
||||
</div>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Member}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("addMember")}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Add Member
|
||||
Add Users to Project
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
|
||||
@@ -208,7 +208,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
|
||||
</DropdownSubMenuTrigger>
|
||||
<DropdownSubMenuContent className="max-h-80 thin-scrollbar overflow-y-auto rounded-l-none">
|
||||
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
||||
Apply Roles to Filter Users
|
||||
Filter Project Users by Role
|
||||
</DropdownMenuLabel>
|
||||
{projectRoles?.map(({ id, slug, name }) => (
|
||||
<DropdownMenuItem
|
||||
@@ -237,7 +237,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search members..."
|
||||
placeholder="Search project users..."
|
||||
/>
|
||||
</div>
|
||||
<TableContainer className="mt-4">
|
||||
@@ -282,7 +282,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>Role</Th>
|
||||
<Th>Project Role</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
@@ -462,9 +462,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
|
||||
)}
|
||||
{!isMembersLoading && !filteredUsers?.length && (
|
||||
<EmptyState
|
||||
title={
|
||||
members.length ? "No project members match search..." : "No project members found"
|
||||
}
|
||||
title={members.length ? "No project users match search..." : "No project users found"}
|
||||
icon={members.length ? faSearch : faUsers}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -170,13 +170,13 @@ export const ProjectRoleList = () => {
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Role}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("role")}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Add Role
|
||||
Add Project Role
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
|
||||
@@ -57,7 +57,7 @@ export const ServiceTokenSection = withProjectPermission(
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("createAPIToken");
|
||||
|
||||
@@ -25,7 +25,7 @@ export const AppConnectionsPage = withProjectPermission(
|
||||
<PageHeader
|
||||
scope={currentProject.type}
|
||||
className="w-full"
|
||||
title="App Connections"
|
||||
title="Project App Connections"
|
||||
description="Manage project App Connections"
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
|
||||
import { PageHeader } from "@app/components/v2";
|
||||
import { useProject } from "@app/context";
|
||||
@@ -17,9 +19,19 @@ export const AuditLogsPage = () => {
|
||||
<div className="w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope={currentProject.type}
|
||||
title="Audit logs"
|
||||
title="Project Audit logs"
|
||||
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} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -57,7 +57,7 @@ const Page = () => {
|
||||
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronLeft} />
|
||||
Groups
|
||||
Project Groups
|
||||
</Link>
|
||||
<PageHeader
|
||||
scope={currentProject.type}
|
||||
|
||||
@@ -149,7 +149,7 @@ const Page = () => {
|
||||
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronLeft} />
|
||||
Machine Identities
|
||||
Project Machine Identities
|
||||
</Link>
|
||||
<PageHeader
|
||||
scope={currentProject.type}
|
||||
|
||||
@@ -128,7 +128,7 @@ export const Page = () => {
|
||||
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronLeft} />
|
||||
Users
|
||||
Project Users
|
||||
</Link>
|
||||
<PageHeader
|
||||
scope={currentProject.type}
|
||||
|
||||
@@ -104,7 +104,7 @@ const Page = () => {
|
||||
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronLeft} />
|
||||
Roles
|
||||
Project Roles
|
||||
</Link>
|
||||
<PageHeader
|
||||
scope={currentProject.type}
|
||||
|
||||
@@ -57,7 +57,7 @@ export const IntegrationsListPage = () => {
|
||||
<div className="mb-8">
|
||||
<PageHeader
|
||||
scope={ProjectType.SecretManager}
|
||||
title="Integrations"
|
||||
title="Project Integrations"
|
||||
description="Manage integrations with third-party services."
|
||||
/>
|
||||
<Tabs orientation="vertical" value={selectedTab} onValueChange={updateSelectedTab}>
|
||||
|
||||
@@ -933,12 +933,12 @@ export const OverviewPage = () => {
|
||||
<div className="flex w-full items-baseline justify-between">
|
||||
<PageHeader
|
||||
scope={ProjectType.SecretManager}
|
||||
title="Overview"
|
||||
title="Project Overview"
|
||||
description={
|
||||
<p className="text-md text-bunker-300">
|
||||
Inject your secrets using
|
||||
<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"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -947,7 +947,7 @@ export const OverviewPage = () => {
|
||||
</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"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -956,7 +956,7 @@ export const OverviewPage = () => {
|
||||
</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"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -965,7 +965,7 @@ export const OverviewPage = () => {
|
||||
</a>
|
||||
, and
|
||||
<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"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
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 { useProject } from "@app/context";
|
||||
@@ -43,9 +45,19 @@ export const SettingsPage = () => {
|
||||
<div className="w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope={ProjectType.SecretManager}
|
||||
title="Settings"
|
||||
title="Project Settings"
|
||||
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}>
|
||||
<TabList>
|
||||
{tabs
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -31,7 +32,13 @@ type ContentProps = {
|
||||
const Content = ({ onComplete }: ContentProps) => {
|
||||
const { currentProject } = useProject();
|
||||
const { mutateAsync, isPending } = useCreateWsEnvironment();
|
||||
const { control, handleSubmit } = useForm<FormData>({
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
getValues,
|
||||
formState: { dirtyFields }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema)
|
||||
});
|
||||
|
||||
@@ -52,15 +59,28 @@ const Content = ({ onComplete }: ContentProps) => {
|
||||
onComplete(env);
|
||||
};
|
||||
|
||||
const handleEnvironmentNameChange = () => {
|
||||
if (dirtyFields.environmentSlug) return;
|
||||
|
||||
const value = getValues("environmentName");
|
||||
setValue("environmentSlug", slugify(value, { lowercase: true }));
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="environmentName"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="Environment Name" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} />
|
||||
<Input
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
onChange(e);
|
||||
handleEnvironmentNameChange();
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
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 { ProjectType } from "@app/hooks/api/projects/types";
|
||||
import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab";
|
||||
@@ -12,6 +14,7 @@ import { ProjectScanningConfigTab } from "./components/ProjectScanningConfigTab"
|
||||
|
||||
export const SettingsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
return (
|
||||
<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">
|
||||
<PageHeader
|
||||
scope={ProjectType.SecretScanning}
|
||||
title="Settings"
|
||||
title="Project Settings"
|
||||
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">
|
||||
<TabList>
|
||||
<Tab variant="project" value="tab-project-general">
|
||||
|
||||