Merge branch 'main' of https://github.com/Infisical/infisical into ENG-4117

This commit is contained in:
Piyush Gupta
2025-11-26 17:53:16 +05:30
101 changed files with 2025 additions and 309 deletions

View File

@@ -1,6 +1,7 @@
Feature: External CA Feature: External CA
Scenario: Issue a certificate from an external CA @cloudflare
Scenario Outline: Issue a certificate from an external CA with Cloudflare
Given I create a Cloudflare connection as cloudflare Given I create a Cloudflare connection as cloudflare
Then I memorize cloudflare with jq ".appConnection.id" as app_conn_id Then I memorize cloudflare with jq ".appConnection.id" as app_conn_id
Given I create a external ACME CA with the following config as ext_ca Given I create a external ACME CA with the following config as ext_ca
@@ -92,9 +93,7 @@ Feature: External CA
When I create certificate signing request as csr When I create certificate signing request as csr
Then I add names to certificate signing request csr Then I add names to certificate signing request csr
""" """
{ <subject>
"COMMON_NAME": "localhost"
}
""" """
# Pebble has a strict rule to only takes SANs # Pebble has a strict rule to only takes SANs
Then I add subject alternative name to certificate signing request csr Then I add subject alternative name to certificate signing request csr
@@ -177,4 +176,196 @@ Feature: External CA
[ [
"localhost" "localhost"
] ]
""" """
Examples:
| subject |
| {"COMMON_NAME": "localhost"} |
| {} |
@dnsme
Scenario Outline: Issue a certificate from an external CA with DNS Made Easy
Given I create a DNS Made Easy connection as dnsme
Then I memorize dnsme with jq ".appConnection.id" as app_conn_id
Given I create a external ACME CA with the following config as ext_ca
"""
{
"dnsProviderConfig": {
"provider": "dns-made-easy",
"hostedZoneId": "MOCK_ZONE_ID"
},
"directoryUrl": "{PEBBLE_URL}",
"accountEmail": "fangpen@infisical.com",
"dnsAppConnectionId": "{app_conn_id}",
"eabKid": "",
"eabHmacKey": ""
}
"""
Then I memorize ext_ca with jq ".id" as ext_ca_id
Given I create a certificate template with the following config as cert_template
"""
{
"subject": [
{
"type": "common_name",
"allowed": [
"*"
]
}
],
"sans": [
{
"type": "dns_name",
"allowed": [
"*"
]
}
],
"keyUsages": {
"required": [],
"allowed": [
"digital_signature",
"key_encipherment",
"non_repudiation",
"data_encipherment",
"key_agreement",
"key_cert_sign",
"crl_sign",
"encipher_only",
"decipher_only"
]
},
"extendedKeyUsages": {
"required": [],
"allowed": [
"client_auth",
"server_auth",
"code_signing",
"email_protection",
"ocsp_signing",
"time_stamping"
]
},
"algorithms": {
"signature": [
"SHA256-RSA",
"SHA512-RSA",
"SHA384-ECDSA",
"SHA384-RSA",
"SHA256-ECDSA",
"SHA512-ECDSA"
],
"keyAlgorithm": [
"RSA-2048",
"RSA-4096",
"ECDSA-P384",
"RSA-3072",
"ECDSA-P256",
"ECDSA-P521"
]
},
"validity": {
"max": "365d"
}
}
"""
Then I memorize cert_template with jq ".certificateTemplate.id" as cert_template_id
Given I create an ACME profile with ca {ext_ca_id} and template {cert_template_id} as "acme_profile"
When I have an ACME client connecting to "{BASE_URL}/api/v1/pki/acme/profiles/{acme_profile.id}/directory"
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
When I create certificate signing request as csr
Then I add names to certificate signing request csr
"""
<subject>
"""
# Pebble has a strict rule to only takes SANs
Then I add subject alternative name to certificate signing request csr
"""
[
"localhost"
]
"""
And I create a RSA private key pair as cert_key
And I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format
And I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order
And I select challenge with type http-01 for domain localhost from order in order as challenge
And I serve challenge response for challenge at localhost
And I tell ACME server that challenge is ready to be verified
Given I intercept outgoing requests
"""
[
{
"scope": "https://api.dnsmadeeasy.com:443",
"method": "POST",
"path": "/V2.0/dns/managed/MOCK_ZONE_ID/records",
"status": 201,
"response": {
"gtdLocation": "DEFAULT",
"failed": false,
"monitor": false,
"failover": false,
"sourceId": 895364,
"dynamicDns": false,
"hardLink": false,
"ttl": 60,
"source": 1,
"name": "_acme-challenge",
"value": "\"MOCK_HTTP_01_VALUE\"",
"id": 12345678,
"type": "TXT"
},
"responseIsBinary": false
},
{
"scope": "https://api.dnsmadeeasy.com:443",
"method": "GET",
"path": "/V2.0/dns/managed/MOCK_ZONE_ID/records?type=TXT&recordName=_acme-challenge&page=0",
"status": 200,
"response": {
"totalRecords": 1,
"totalPages": 1,
"data": [
{
"gtdLocation": "DEFAULT",
"failed": false,
"monitor": false,
"failover": false,
"sourceId": 895364,
"dynamicDns": false,
"hardLink": false,
"ttl": 60,
"source": 1,
"name": "_acme-challenge",
"value": "\"MOCK_CHALLENGE_VALUE\"",
"id": 1111111,
"type": "TXT"
}
],
"page": 0
},
"responseIsBinary": false
},
{
"scope": "https://api.dnsmadeeasy.com:443",
"method": "DELETE",
"path": "/V2.0/dns/managed/MOCK_ZONE_ID/records/1111111",
"status": 200,
"response": "",
"responseIsBinary": false
}
]
"""
Then I poll and finalize the ACME order order as finalized_order
And the value finalized_order.body with jq ".status" should be equal to "valid"
And I parse the full-chain certificate from order finalized_order as cert
And the value cert with jq "[.extensions.subjectAltName.general_names.[].value] | sort" should be equal to json
"""
[
"localhost"
]
"""
Examples:
| subject |
| {"COMMON_NAME": "localhost"} |
| {} |

View File

@@ -147,6 +147,40 @@ def step_impl(context: Context, var_name: str):
context.vars[var_name] = response context.vars[var_name] = response
@given("I create a DNS Made Easy connection as {var_name}")
def step_impl(context: Context, var_name: str):
jwt_token = context.vars["AUTH_TOKEN"]
conn_slug = faker.slug()
with with_nocks(
context,
definitions=[
{
"scope": "https://api.dnsmadeeasy.com:443",
"method": "GET",
"path": "/V2.0/dns/managed/",
"status": 200,
"response": {"totalRecords": 0, "totalPages": 1, "data": [], "page": 0},
"responseIsBinary": False,
}
],
):
response = context.http_client.post(
"/api/v1/app-connections/dns-made-easy",
headers=dict(authorization="Bearer {}".format(jwt_token)),
json={
"name": conn_slug,
"description": "",
"method": "api-key-secret",
"credentials": {
"apiKey": "MOCK_API_KEY",
"secretKey": "MOCK_SECRET_KEY",
},
},
)
response.raise_for_status()
context.vars[var_name] = response
@given("I create a external ACME CA with the following config as {var_name}") @given("I create a external ACME CA with the following config as {var_name}")
def step_impl(context: Context, var_name: str): def step_impl(context: Context, var_name: str):
jwt_token = context.vars["AUTH_TOKEN"] jwt_token = context.vars["AUTH_TOKEN"]

View File

@@ -776,7 +776,9 @@ export const pkiAcmeServiceFactory = ({
const cert = await orderCertificate( const cert = await orderCertificate(
{ {
caId: certificateAuthority!.id, caId: certificateAuthority!.id,
commonName: certificateRequest.commonName!, // It is possible that the CSR does not have a common name, in which case we use an empty string
// (more likely than not for a CSR from a modern ACME client like certbot, cert-manager, etc.)
commonName: certificateRequest.commonName ?? "",
altNames: certificateRequest.subjectAlternativeNames?.map((san) => san.value), altNames: certificateRequest.subjectAlternativeNames?.map((san) => san.value),
csr: Buffer.from(csrPem), csr: Buffer.from(csrPem),
// TODO: not 100% sure what are these columns for, but let's put the values for common website SSL certs for now // TODO: not 100% sure what are these columns for, but let's put the values for common website SSL certs for now

View File

@@ -119,6 +119,7 @@ const envSchema = z
}) })
.default("{}") .default("{}")
), ),
DNS_MADE_EASY_SANDBOX_ENABLED: zodStrBool.default("false").optional(),
// smtp options // smtp options
SMTP_HOST: zpStr(z.string().optional()), SMTP_HOST: zpStr(z.string().optional()),
SMTP_IGNORE_TLS: zodStrBool.default("false"), SMTP_IGNORE_TLS: zodStrBool.default("false"),

View File

@@ -61,6 +61,10 @@ import {
DigitalOceanConnectionListItemSchema, DigitalOceanConnectionListItemSchema,
SanitizedDigitalOceanConnectionSchema SanitizedDigitalOceanConnectionSchema
} from "@app/services/app-connection/digital-ocean"; } from "@app/services/app-connection/digital-ocean";
import {
DNSMadeEasyConnectionListItemSchema,
SanitizedDNSMadeEasyConnectionSchema
} from "@app/services/app-connection/dns-made-easy/dns-made-easy-connection-schema";
import { FlyioConnectionListItemSchema, SanitizedFlyioConnectionSchema } from "@app/services/app-connection/flyio"; import { FlyioConnectionListItemSchema, SanitizedFlyioConnectionSchema } from "@app/services/app-connection/flyio";
import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp"; import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp";
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github"; import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github";
@@ -170,7 +174,8 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedAzureADCSConnectionSchema.options, ...SanitizedAzureADCSConnectionSchema.options,
...SanitizedRedisConnectionSchema.options, ...SanitizedRedisConnectionSchema.options,
...SanitizedLaravelForgeConnectionSchema.options, ...SanitizedLaravelForgeConnectionSchema.options,
...SanitizedChefConnectionSchema.options ...SanitizedChefConnectionSchema.options,
...SanitizedDNSMadeEasyConnectionSchema.options
]); ]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -215,7 +220,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
AzureADCSConnectionListItemSchema, AzureADCSConnectionListItemSchema,
RedisConnectionListItemSchema, RedisConnectionListItemSchema,
LaravelForgeConnectionListItemSchema, LaravelForgeConnectionListItemSchema,
ChefConnectionListItemSchema ChefConnectionListItemSchema,
DNSMadeEasyConnectionListItemSchema
]); ]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => { export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {

View File

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

View File

@@ -16,6 +16,7 @@ import { registerCamundaConnectionRouter } from "./camunda-connection-router";
import { registerChecklyConnectionRouter } from "./checkly-connection-router"; import { registerChecklyConnectionRouter } from "./checkly-connection-router";
import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router"; import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router";
import { registerDatabricksConnectionRouter } from "./databricks-connection-router"; import { registerDatabricksConnectionRouter } from "./databricks-connection-router";
import { registerDNSMadeEasyConnectionRouter } from "./dns-made-easy-connection-router";
import { registerDigitalOceanConnectionRouter } from "./digital-ocean-connection-router"; import { registerDigitalOceanConnectionRouter } from "./digital-ocean-connection-router";
import { registerFlyioConnectionRouter } from "./flyio-connection-router"; import { registerFlyioConnectionRouter } from "./flyio-connection-router";
import { registerGcpConnectionRouter } from "./gcp-connection-router"; import { registerGcpConnectionRouter } from "./gcp-connection-router";
@@ -78,6 +79,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.Flyio]: registerFlyioConnectionRouter, [AppConnection.Flyio]: registerFlyioConnectionRouter,
[AppConnection.GitLab]: registerGitLabConnectionRouter, [AppConnection.GitLab]: registerGitLabConnectionRouter,
[AppConnection.Cloudflare]: registerCloudflareConnectionRouter, [AppConnection.Cloudflare]: registerCloudflareConnectionRouter,
[AppConnection.DNSMadeEasy]: registerDNSMadeEasyConnectionRouter,
[AppConnection.Bitbucket]: registerBitbucketConnectionRouter, [AppConnection.Bitbucket]: registerBitbucketConnectionRouter,
[AppConnection.Zabbix]: registerZabbixConnectionRouter, [AppConnection.Zabbix]: registerZabbixConnectionRouter,
[AppConnection.Railway]: registerRailwayConnectionRouter, [AppConnection.Railway]: registerRailwayConnectionRouter,

View File

@@ -29,6 +29,7 @@ export enum AppConnection {
Flyio = "flyio", Flyio = "flyio",
GitLab = "gitlab", GitLab = "gitlab",
Cloudflare = "cloudflare", Cloudflare = "cloudflare",
DNSMadeEasy = "dns-made-easy",
Zabbix = "zabbix", Zabbix = "zabbix",
Railway = "railway", Railway = "railway",
Bitbucket = "bitbucket", Bitbucket = "bitbucket",

View File

@@ -88,6 +88,11 @@ import {
getDigitalOceanConnectionListItem, getDigitalOceanConnectionListItem,
validateDigitalOceanConnectionCredentials validateDigitalOceanConnectionCredentials
} from "./digital-ocean"; } from "./digital-ocean";
import { DNSMadeEasyConnectionMethod } from "./dns-made-easy/dns-made-easy-connection-enum";
import {
getDNSMadeEasyConnectionListItem,
validateDNSMadeEasyConnectionCredentials
} from "./dns-made-easy/dns-made-easy-connection-fns";
import { FlyioConnectionMethod, getFlyioConnectionListItem, validateFlyioConnectionCredentials } from "./flyio"; import { FlyioConnectionMethod, getFlyioConnectionListItem, validateFlyioConnectionCredentials } from "./flyio";
import { GcpConnectionMethod, getGcpConnectionListItem, validateGcpConnectionCredentials } from "./gcp"; import { GcpConnectionMethod, getGcpConnectionListItem, validateGcpConnectionCredentials } from "./gcp";
import { getGitHubConnectionListItem, GitHubConnectionMethod, validateGitHubConnectionCredentials } from "./github"; import { getGitHubConnectionListItem, GitHubConnectionMethod, validateGitHubConnectionCredentials } from "./github";
@@ -171,7 +176,8 @@ const PKI_APP_CONNECTIONS = [
AppConnection.Cloudflare, AppConnection.Cloudflare,
AppConnection.AzureADCS, AppConnection.AzureADCS,
AppConnection.AzureKeyVault, AppConnection.AzureKeyVault,
AppConnection.Chef AppConnection.Chef,
AppConnection.DNSMadeEasy
]; ];
export const listAppConnectionOptions = (projectType?: ProjectType) => { export const listAppConnectionOptions = (projectType?: ProjectType) => {
@@ -207,6 +213,7 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => {
getFlyioConnectionListItem(), getFlyioConnectionListItem(),
getGitLabConnectionListItem(), getGitLabConnectionListItem(),
getCloudflareConnectionListItem(), getCloudflareConnectionListItem(),
getDNSMadeEasyConnectionListItem(),
getZabbixConnectionListItem(), getZabbixConnectionListItem(),
getRailwayConnectionListItem(), getRailwayConnectionListItem(),
getBitbucketConnectionListItem(), getBitbucketConnectionListItem(),
@@ -339,6 +346,7 @@ export const validateAppConnectionCredentials = async (
[AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Flyio]: validateFlyioConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.GitLab]: validateGitLabConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Cloudflare]: validateCloudflareConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.DNSMadeEasy]: validateDNSMadeEasyConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Zabbix]: validateZabbixConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Zabbix]: validateZabbixConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Railway]: validateRailwayConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Railway]: validateRailwayConnectionCredentials as TAppConnectionCredentialsValidator,
[AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Bitbucket]: validateBitbucketConnectionCredentials as TAppConnectionCredentialsValidator,
@@ -395,6 +403,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
case OktaConnectionMethod.ApiToken: case OktaConnectionMethod.ApiToken:
case LaravelForgeConnectionMethod.ApiToken: case LaravelForgeConnectionMethod.ApiToken:
return "API Token"; return "API Token";
case DNSMadeEasyConnectionMethod.APIKeySecret:
return "API Key & Secret";
case PostgresConnectionMethod.UsernameAndPassword: case PostgresConnectionMethod.UsernameAndPassword:
case MsSqlConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword:
case MySqlConnectionMethod.UsernameAndPassword: case MySqlConnectionMethod.UsernameAndPassword:
@@ -483,6 +493,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
[AppConnection.Flyio]: platformManagedCredentialsNotSupported, [AppConnection.Flyio]: platformManagedCredentialsNotSupported,
[AppConnection.GitLab]: platformManagedCredentialsNotSupported, [AppConnection.GitLab]: platformManagedCredentialsNotSupported,
[AppConnection.Cloudflare]: platformManagedCredentialsNotSupported, [AppConnection.Cloudflare]: platformManagedCredentialsNotSupported,
[AppConnection.DNSMadeEasy]: platformManagedCredentialsNotSupported,
[AppConnection.Zabbix]: platformManagedCredentialsNotSupported, [AppConnection.Zabbix]: platformManagedCredentialsNotSupported,
[AppConnection.Railway]: platformManagedCredentialsNotSupported, [AppConnection.Railway]: platformManagedCredentialsNotSupported,
[AppConnection.Bitbucket]: platformManagedCredentialsNotSupported, [AppConnection.Bitbucket]: platformManagedCredentialsNotSupported,

View File

@@ -32,6 +32,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
[AppConnection.Flyio]: "Fly.io", [AppConnection.Flyio]: "Fly.io",
[AppConnection.GitLab]: "GitLab", [AppConnection.GitLab]: "GitLab",
[AppConnection.Cloudflare]: "Cloudflare", [AppConnection.Cloudflare]: "Cloudflare",
[AppConnection.DNSMadeEasy]: "DNS Made Easy",
[AppConnection.Zabbix]: "Zabbix", [AppConnection.Zabbix]: "Zabbix",
[AppConnection.Railway]: "Railway", [AppConnection.Railway]: "Railway",
[AppConnection.Bitbucket]: "Bitbucket", [AppConnection.Bitbucket]: "Bitbucket",
@@ -77,6 +78,7 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
[AppConnection.Flyio]: AppConnectionPlanType.Regular, [AppConnection.Flyio]: AppConnectionPlanType.Regular,
[AppConnection.GitLab]: AppConnectionPlanType.Regular, [AppConnection.GitLab]: AppConnectionPlanType.Regular,
[AppConnection.Cloudflare]: AppConnectionPlanType.Regular, [AppConnection.Cloudflare]: AppConnectionPlanType.Regular,
[AppConnection.DNSMadeEasy]: AppConnectionPlanType.Regular,
[AppConnection.Zabbix]: AppConnectionPlanType.Regular, [AppConnection.Zabbix]: AppConnectionPlanType.Regular,
[AppConnection.Railway]: AppConnectionPlanType.Regular, [AppConnection.Railway]: AppConnectionPlanType.Regular,
[AppConnection.Bitbucket]: AppConnectionPlanType.Regular, [AppConnection.Bitbucket]: AppConnectionPlanType.Regular,

View File

@@ -72,6 +72,8 @@ import { checklyConnectionService } from "./checkly/checkly-connection-service";
import { ValidateCloudflareConnectionCredentialsSchema } from "./cloudflare/cloudflare-connection-schema"; import { ValidateCloudflareConnectionCredentialsSchema } from "./cloudflare/cloudflare-connection-schema";
import { cloudflareConnectionService } from "./cloudflare/cloudflare-connection-service"; import { cloudflareConnectionService } from "./cloudflare/cloudflare-connection-service";
import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks"; import { ValidateDatabricksConnectionCredentialsSchema } from "./databricks";
import { ValidateDNSMadeEasyConnectionCredentialsSchema } from "./dns-made-easy/dns-made-easy-connection-schema";
import { dnsMadeEasyConnectionService } from "./dns-made-easy/dns-made-easy-connection-service";
import { databricksConnectionService } from "./databricks/databricks-connection-service"; import { databricksConnectionService } from "./databricks/databricks-connection-service";
import { ValidateDigitalOceanConnectionCredentialsSchema } from "./digital-ocean"; import { ValidateDigitalOceanConnectionCredentialsSchema } from "./digital-ocean";
import { digitalOceanAppPlatformConnectionService } from "./digital-ocean/digital-ocean-connection-service"; import { digitalOceanAppPlatformConnectionService } from "./digital-ocean/digital-ocean-connection-service";
@@ -167,6 +169,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
[AppConnection.Flyio]: ValidateFlyioConnectionCredentialsSchema, [AppConnection.Flyio]: ValidateFlyioConnectionCredentialsSchema,
[AppConnection.GitLab]: ValidateGitLabConnectionCredentialsSchema, [AppConnection.GitLab]: ValidateGitLabConnectionCredentialsSchema,
[AppConnection.Cloudflare]: ValidateCloudflareConnectionCredentialsSchema, [AppConnection.Cloudflare]: ValidateCloudflareConnectionCredentialsSchema,
[AppConnection.DNSMadeEasy]: ValidateDNSMadeEasyConnectionCredentialsSchema,
[AppConnection.Zabbix]: ValidateZabbixConnectionCredentialsSchema, [AppConnection.Zabbix]: ValidateZabbixConnectionCredentialsSchema,
[AppConnection.Railway]: ValidateRailwayConnectionCredentialsSchema, [AppConnection.Railway]: ValidateRailwayConnectionCredentialsSchema,
[AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema, [AppConnection.Bitbucket]: ValidateBitbucketConnectionCredentialsSchema,
@@ -875,6 +878,7 @@ export const appConnectionServiceFactory = ({
flyio: flyioConnectionService(connectAppConnectionById), flyio: flyioConnectionService(connectAppConnectionById),
gitlab: gitlabConnectionService(connectAppConnectionById, appConnectionDAL, kmsService), gitlab: gitlabConnectionService(connectAppConnectionById, appConnectionDAL, kmsService),
cloudflare: cloudflareConnectionService(connectAppConnectionById), cloudflare: cloudflareConnectionService(connectAppConnectionById),
dnsMadeEasy: dnsMadeEasyConnectionService(connectAppConnectionById),
zabbix: zabbixConnectionService(connectAppConnectionById), zabbix: zabbixConnectionService(connectAppConnectionById),
railway: railwayConnectionService(connectAppConnectionById), railway: railwayConnectionService(connectAppConnectionById),
bitbucket: bitbucketConnectionService(connectAppConnectionById), bitbucket: bitbucketConnectionService(connectAppConnectionById),

View File

@@ -15,8 +15,8 @@ import {
TOracleDBConnectionInput, TOracleDBConnectionInput,
TValidateOracleDBConnectionCredentialsSchema TValidateOracleDBConnectionCredentialsSchema
} from "@app/ee/services/app-connections/oracledb"; } from "@app/ee/services/app-connections/oracledb";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service"; import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types"; import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types";
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
@@ -106,6 +106,12 @@ import {
TDigitalOceanConnectionInput, TDigitalOceanConnectionInput,
TValidateDigitalOceanCredentialsSchema TValidateDigitalOceanCredentialsSchema
} from "./digital-ocean"; } from "./digital-ocean";
import {
TDNSMadeEasyConnection,
TDNSMadeEasyConnectionConfig,
TDNSMadeEasyConnectionInput,
TValidateDNSMadeEasyConnectionCredentialsSchema
} from "./dns-made-easy/dns-made-easy-connection-types";
import { import {
TFlyioConnection, TFlyioConnection,
TFlyioConnectionConfig, TFlyioConnectionConfig,
@@ -279,6 +285,7 @@ export type TAppConnection = { id: string } & (
| TGitLabConnection | TGitLabConnection
| TCloudflareConnection | TCloudflareConnection
| TBitbucketConnection | TBitbucketConnection
| TDNSMadeEasyConnection
| TZabbixConnection | TZabbixConnection
| TRailwayConnection | TRailwayConnection
| TChecklyConnection | TChecklyConnection
@@ -328,6 +335,7 @@ export type TAppConnectionInput = { id: string } & (
| TGitLabConnectionInput | TGitLabConnectionInput
| TCloudflareConnectionInput | TCloudflareConnectionInput
| TBitbucketConnectionInput | TBitbucketConnectionInput
| TDNSMadeEasyConnectionInput
| TZabbixConnectionInput | TZabbixConnectionInput
| TRailwayConnectionInput | TRailwayConnectionInput
| TChecklyConnectionInput | TChecklyConnectionInput
@@ -395,6 +403,7 @@ export type TAppConnectionConfig =
| TGitLabConnectionConfig | TGitLabConnectionConfig
| TCloudflareConnectionConfig | TCloudflareConnectionConfig
| TBitbucketConnectionConfig | TBitbucketConnectionConfig
| TDNSMadeEasyConnectionConfig
| TZabbixConnectionConfig | TZabbixConnectionConfig
| TRailwayConnectionConfig | TRailwayConnectionConfig
| TChecklyConnectionConfig | TChecklyConnectionConfig
@@ -439,6 +448,7 @@ export type TValidateAppConnectionCredentialsSchema =
| TValidateGitLabConnectionCredentialsSchema | TValidateGitLabConnectionCredentialsSchema
| TValidateCloudflareConnectionCredentialsSchema | TValidateCloudflareConnectionCredentialsSchema
| TValidateBitbucketConnectionCredentialsSchema | TValidateBitbucketConnectionCredentialsSchema
| TValidateDNSMadeEasyConnectionCredentialsSchema
| TValidateZabbixConnectionCredentialsSchema | TValidateZabbixConnectionCredentialsSchema
| TValidateRailwayConnectionCredentialsSchema | TValidateRailwayConnectionCredentialsSchema
| TValidateChecklyConnectionCredentialsSchema | TValidateChecklyConnectionCredentialsSchema

View File

@@ -0,0 +1,3 @@
export enum DNSMadeEasyConnectionMethod {
APIKeySecret = "api-key-secret"
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
export enum AcmeDnsProvider { export enum AcmeDnsProvider {
Route53 = "route53", Route53 = "route53",
Cloudflare = "cloudflare" Cloudflare = "cloudflare",
DNSMadeEasy = "dns-made-easy"
} }

View File

@@ -14,6 +14,7 @@ import { decryptAppConnection } from "@app/services/app-connection/app-connectio
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service"; import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types"; import { TAwsConnection } from "@app/services/app-connection/aws/aws-connection-types";
import { TCloudflareConnection } from "@app/services/app-connection/cloudflare/cloudflare-connection-types"; import { TCloudflareConnection } from "@app/services/app-connection/cloudflare/cloudflare-connection-types";
import { TDNSMadeEasyConnection } from "@app/services/app-connection/dns-made-easy/dns-made-easy-connection-types";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
@@ -43,6 +44,7 @@ import {
TUpdateAcmeCertificateAuthorityDTO TUpdateAcmeCertificateAuthorityDTO
} from "./acme-certificate-authority-types"; } from "./acme-certificate-authority-types";
import { cloudflareDeleteTxtRecord, cloudflareInsertTxtRecord } from "./dns-providers/cloudflare"; import { cloudflareDeleteTxtRecord, cloudflareInsertTxtRecord } from "./dns-providers/cloudflare";
import { dnsMadeEasyDeleteTxtRecord, dnsMadeEasyInsertTxtRecord } from "./dns-providers/dns-made-easy";
import { route53DeleteTxtRecord, route53InsertTxtRecord } from "./dns-providers/route54"; import { route53DeleteTxtRecord, route53InsertTxtRecord } from "./dns-providers/route54";
type TAcmeCertificateAuthorityFnsDeps = { type TAcmeCertificateAuthorityFnsDeps = {
@@ -120,6 +122,22 @@ export const castDbEntryToAcmeCertificateAuthority = (
}; };
}; };
const getAcmeChallengeRecord = (
provider: AcmeDnsProvider,
identifierValue: string,
keyAuthorization: string
): { recordName: string; recordValue: string } => {
let recordName: string;
if (provider === AcmeDnsProvider.DNSMadeEasy) {
// For DNS Made Easy, we don't need to provide the domain name in the record name.
recordName = "_acme-challenge";
} else {
recordName = `_acme-challenge.${identifierValue}`; // e.g., "_acme-challenge.example.com"
}
const recordValue = `"${keyAuthorization}"`; // must be double quoted
return { recordName, recordValue };
};
export const orderCertificate = async ( export const orderCertificate = async (
{ {
caId, caId,
@@ -241,8 +259,11 @@ export const orderCertificate = async (
throw new Error("Unsupported challenge type"); throw new Error("Unsupported challenge type");
} }
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com" const { recordName, recordValue } = getAcmeChallengeRecord(
const recordValue = `"${keyAuthorization}"`; // must be double quoted acmeCa.configuration.dnsProviderConfig.provider,
authz.identifier.value,
keyAuthorization
);
switch (acmeCa.configuration.dnsProviderConfig.provider) { switch (acmeCa.configuration.dnsProviderConfig.provider) {
case AcmeDnsProvider.Route53: { case AcmeDnsProvider.Route53: {
@@ -263,14 +284,26 @@ export const orderCertificate = async (
); );
break; break;
} }
case AcmeDnsProvider.DNSMadeEasy: {
await dnsMadeEasyInsertTxtRecord(
connection as TDNSMadeEasyConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
break;
}
default: { default: {
throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`); throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`);
} }
} }
}, },
challengeRemoveFn: async (authz, challenge, keyAuthorization) => { challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
const recordName = `_acme-challenge.${authz.identifier.value}`; // e.g., "_acme-challenge.example.com" const { recordName, recordValue } = getAcmeChallengeRecord(
const recordValue = `"${keyAuthorization}"`; // must be double quoted acmeCa.configuration.dnsProviderConfig.provider,
authz.identifier.value,
keyAuthorization
);
switch (acmeCa.configuration.dnsProviderConfig.provider) { switch (acmeCa.configuration.dnsProviderConfig.provider) {
case AcmeDnsProvider.Route53: { case AcmeDnsProvider.Route53: {
@@ -291,6 +324,15 @@ export const orderCertificate = async (
); );
break; break;
} }
case AcmeDnsProvider.DNSMadeEasy: {
await dnsMadeEasyDeleteTxtRecord(
connection as TDNSMadeEasyConnection,
acmeCa.configuration.dnsProviderConfig.hostedZoneId,
recordName,
recordValue
);
break;
}
default: { default: {
throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`); throw new Error(`Unsupported DNS provider: ${acmeCa.configuration.dnsProviderConfig.provider as string}`);
} }
@@ -413,6 +455,12 @@ export const AcmeCertificateAuthorityFns = ({
}); });
} }
if (dnsProviderConfig.provider === AcmeDnsProvider.DNSMadeEasy && appConnection.app !== AppConnection.DNSMadeEasy) {
throw new BadRequestError({
message: `App connection with ID '${dnsAppConnectionId}' is not a DNS Made Easy connection`
});
}
// validates permission to connect // validates permission to connect
await appConnectionService.validateAppConnectionUsageById( await appConnectionService.validateAppConnectionUsageById(
appConnection.app as AppConnection, appConnection.app as AppConnection,
@@ -508,6 +556,15 @@ export const AcmeCertificateAuthorityFns = ({
}); });
} }
if (
dnsProviderConfig.provider === AcmeDnsProvider.DNSMadeEasy &&
appConnection.app !== AppConnection.DNSMadeEasy
) {
throw new BadRequestError({
message: `App connection with ID '${dnsAppConnectionId}' is not a DNS Made Easy connection`
});
}
const ca = await certificateAuthorityDAL.findById(id); const ca = await certificateAuthorityDAL.findById(id);
if (!ca) { if (!ca) {

View File

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

View File

@@ -105,7 +105,9 @@ export enum IntegrationUrls {
GCP_CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform", GCP_CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform",
GITHUB_USER_INSTALLATIONS = "https://api.github.com/user/installations", GITHUB_USER_INSTALLATIONS = "https://api.github.com/user/installations",
CHEF_API_URL = "https://api.chef.io" CHEF_API_URL = "https://api.chef.io",
DNS_MADE_EASY_API_URL = "https://api.dnsmadeeasy.com",
DNS_MADE_EASY_SANDBOX_API_URL = "https://api.sandbox.dnsmadeeasy.com"
} }
export const getIntegrationOptions = async () => { export const getIntegrationOptions = async () => {

View File

@@ -118,6 +118,7 @@
"integrations/app-connections/cloudflare", "integrations/app-connections/cloudflare",
"integrations/app-connections/databricks", "integrations/app-connections/databricks",
"integrations/app-connections/digital-ocean", "integrations/app-connections/digital-ocean",
"integrations/app-connections/dns-made-easy",
"integrations/app-connections/flyio", "integrations/app-connections/flyio",
"integrations/app-connections/gcp", "integrations/app-connections/gcp",
"integrations/app-connections/github", "integrations/app-connections/github",

View File

@@ -17,7 +17,7 @@ their **ACME Directory URL** such as:
- ZeroSSL: `https://acme.zerossl.com/v2/DV90`. - ZeroSSL: `https://acme.zerossl.com/v2/DV90`.
- SSL.com: `https://acme.ssl.com/sslcom-dv-rsa`. - SSL.com: `https://acme.ssl.com/sslcom-dv-rsa`.
When Infisical requests a certificate from an ACME-compatible CA, it creates a TXT record at `_acme-challenge.{your-domain}` in your configured DNS provider (e.g. Route53, Cloudflare, etc.); this TXT record contains the challenge token issued by the ACME-compatible CA to validate domain control for the requested certificate. When Infisical requests a certificate from an ACME-compatible CA, it creates a TXT record at `_acme-challenge.{your-domain}` in your configured DNS provider (e.g. Route53, Cloudflare, DNS Made Easy, etc.); this TXT record contains the challenge token issued by the ACME-compatible CA to validate domain control for the requested certificate.
The ACME provider checks for the existence of this TXT record to verify domain control before issuing the certificate back to Infisical. The ACME provider checks for the existence of this TXT record to verify domain control before issuing the certificate back to Infisical.
After validation completes successfully, Infisical automatically removes the TXT record from your DNS provider. After validation completes successfully, Infisical automatically removes the TXT record from your DNS provider.
@@ -120,6 +120,11 @@ In the following steps, we explore how to connect Infisical to an ACME-compatibl
For detailed instructions on setting up a Cloudflare connection, see the [Cloudflare Connection](/integrations/app-connections/cloudflare) documentation. For detailed instructions on setting up a Cloudflare connection, see the [Cloudflare Connection](/integrations/app-connections/cloudflare) documentation.
</Tab> </Tab>
<Tab title="DNS Made Easy">
Navigate to your Certificate Management Project > App Connections and create a new DNS Made Easy connection.
For detailed instructions on setting up a DNS Made Easy connection, see the [DNS Made Easy Connection](/integrations/app-connections/dns-made-easy) documentation.
</Tab>
</Tabs> </Tabs>
</Step> </Step>
<Step title="Register an ACME-compatible CA"> <Step title="Register an ACME-compatible CA">

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 407 KiB

After

Width:  |  Height:  |  Size: 166 KiB

View 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.
![Navigate to Account Information](/images/app-connections/dns-made-easy/nav-to-account-info.png)
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.
![Generate API Credentials](/images/app-connections/dns-made-easy/generate-new-api-credentials.png)
</Step>
<Step title="Copy Your API Key and Secret Key">
After creation, copy your API key and secret key.
![Generated API Token](/images/app-connections/dns-made-easy/copy-api-credentials.png)
<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. ![App
Connections Tab](/images/app-connections/general/add-connection.png)
</Step>
<Step title="Add Connection">
Select the **DNS Made Easy Connection** option from the connection options
modal. ![Select DNS Made Easy
Connection](/images/app-connections/dns-made-easy/dns-made-easy-app-connection-select.png)
</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. ![Connect to
DNS Made
Easy](/images/app-connections/dns-made-easy/dns-made-easy-app-connection-form.png)
</Step>
<Step title="Connection Created">
Your **DNS Made Easy Connection** is now available for use in your Infisical
projects. ![DNS Made Easy Connection
Created](/images/app-connections/dns-made-easy/dns-made-easy-app-connection-created.png)
</Step>
</Steps>

View File

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

View File

@@ -1,70 +1,388 @@
import React, { useState, useMemo } from 'react'; import React, { useState, useMemo } from "react";
export const AppConnectionsBrowser = () => { export const AppConnectionsBrowser = () => {
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState("");
const [selectedCategory, setSelectedCategory] = useState('All'); const [selectedCategory, setSelectedCategory] = useState("All");
const categories = ['All', 'Cloud Providers', 'Databases', 'CI/CD', 'Monitoring', 'Directory Services', 'Identity & Auth', 'Data Analytics', 'Hosting', 'DevOps Tools', 'Security']; const categories = [
"All",
"Cloud Providers",
"Databases",
"CI/CD",
"Monitoring",
"Directory Services",
"Identity & Auth",
"Data Analytics",
"Hosting",
"DevOps Tools",
"Security",
"Networking & DNS",
];
const connections = [ const connections = [
{"name": "AWS", "slug": "aws", "path": "/integrations/app-connections/aws", "description": "Learn how to connect your AWS applications to pull secrets from Infisical.", "category": "Cloud Providers"}, {
{"name": "Azure Key Vault", "slug": "azure-key-vault", "path": "/integrations/app-connections/azure-key-vault", "description": "Learn how to connect your Azure Key Vault to pull secrets from Infisical.", "category": "Cloud Providers"}, name: "AWS",
{"name": "Azure App Configuration", "slug": "azure-app-configuration", "path": "/integrations/app-connections/azure-app-configuration", "description": "Learn how to connect your Azure App Configuration to pull secrets from Infisical.", "category": "Cloud Providers"}, slug: "aws",
{"name": "Azure Client Secrets", "slug": "azure-client-secrets", "path": "/integrations/app-connections/azure-client-secrets", "description": "Learn how to connect your Azure Client Secrets to pull secrets from Infisical.", "category": "Cloud Providers"}, path: "/integrations/app-connections/aws",
{"name": "Azure DevOps", "slug": "azure-devops", "path": "/integrations/app-connections/azure-devops", "description": "Learn how to connect your Azure DevOps to pull secrets from Infisical.", "category": "CI/CD"}, description:
{"name": "Azure ADCS", "slug": "azure-adcs", "path": "/integrations/app-connections/azure-adcs", "description": "Learn how to connect your Azure ADCS to pull secrets from Infisical.", "category": "Cloud Providers"}, "Learn how to connect your AWS applications to pull secrets from Infisical.",
{"name": "GCP", "slug": "gcp", "path": "/integrations/app-connections/gcp", "description": "Learn how to connect your GCP applications to pull secrets from Infisical.", "category": "Cloud Providers"}, category: "Cloud Providers",
{"name": "HashiCorp Vault", "slug": "hashicorp-vault", "path": "/integrations/app-connections/hashicorp-vault", "description": "Learn how to connect your HashiCorp Vault to pull secrets from Infisical.", "category": "Security"}, },
{"name": "1Password", "slug": "1password", "path": "/integrations/app-connections/1password", "description": "Learn how to connect your 1Password to pull secrets from Infisical.", "category": "Security"}, {
{"name": "Vercel", "slug": "vercel", "path": "/integrations/app-connections/vercel", "description": "Learn how to connect your Vercel application to pull secrets from Infisical.", "category": "Hosting"}, name: "Azure Key Vault",
{"name": "Netlify", "slug": "netlify", "path": "/integrations/app-connections/netlify", "description": "Learn how to connect your Netlify application to pull secrets from Infisical.", "category": "Hosting"}, slug: "azure-key-vault",
{"name": "Railway", "slug": "railway", "path": "/integrations/app-connections/railway", "description": "Learn how to connect your Railway application to pull secrets from Infisical.", "category": "Hosting"}, path: "/integrations/app-connections/azure-key-vault",
{"name": "Fly.io", "slug": "flyio", "path": "/integrations/app-connections/flyio", "description": "Learn how to connect your Fly.io application to pull secrets from Infisical.", "category": "Hosting"}, description:
{"name": "Render", "slug": "render", "path": "/integrations/app-connections/render", "description": "Learn how to connect your Render application to pull secrets from Infisical.", "category": "Hosting"}, "Learn how to connect your Azure Key Vault to pull secrets from Infisical.",
{"name": "Heroku", "slug": "heroku", "path": "/integrations/app-connections/heroku", "description": "Learn how to connect your Heroku application to pull secrets from Infisical.", "category": "Hosting"}, category: "Cloud Providers",
{"name": "DigitalOcean", "slug": "digital-ocean", "path": "/integrations/app-connections/digital-ocean", "description": "Learn how to connect your DigitalOcean application to pull secrets from Infisical.", "category": "Hosting"}, },
{"name": "Supabase", "slug": "supabase", "path": "/integrations/app-connections/supabase", "description": "Learn how to connect your Supabase application to pull secrets from Infisical.", "category": "Databases"}, {
{"name": "Checkly", "slug": "checkly", "path": "/integrations/app-connections/checkly", "description": "Learn how to connect your Checkly application to pull secrets from Infisical.", "category": "Monitoring"}, name: "Azure App Configuration",
{"name": "GitHub", "slug": "github", "path": "/integrations/app-connections/github", "description": "Learn how to connect your GitHub application to pull secrets from Infisical.", "category": "CI/CD"}, slug: "azure-app-configuration",
{"name": "GitHub Radar", "slug": "github-radar", "path": "/integrations/app-connections/github-radar", "description": "Learn how to connect your GitHub Radar to pull secrets from Infisical.", "category": "CI/CD"}, path: "/integrations/app-connections/azure-app-configuration",
{"name": "GitLab", "slug": "gitlab", "path": "/integrations/app-connections/gitlab", "description": "Learn how to connect your GitLab application to pull secrets from Infisical.", "category": "CI/CD"}, description:
{"name": "TeamCity", "slug": "teamcity", "path": "/integrations/app-connections/teamcity", "description": "Learn how to connect your TeamCity to pull secrets from Infisical.", "category": "CI/CD"}, "Learn how to connect your Azure App Configuration to pull secrets from Infisical.",
{"name": "Bitbucket", "slug": "bitbucket", "path": "/integrations/app-connections/bitbucket", "description": "Learn how to connect your Bitbucket to pull secrets from Infisical.", "category": "CI/CD"}, category: "Cloud Providers",
{"name": "Terraform Cloud", "slug": "terraform-cloud", "path": "/integrations/app-connections/terraform-cloud", "description": "Learn how to connect your Terraform Cloud to pull secrets from Infisical.", "category": "DevOps Tools"}, },
{"name": "Cloudflare", "slug": "cloudflare", "path": "/integrations/app-connections/cloudflare", "description": "Learn how to connect your Cloudflare application to pull secrets from Infisical.", "category": "Cloud Providers"}, {
{"name": "Databricks", "slug": "databricks", "path": "/integrations/app-connections/databricks", "description": "Learn how to connect your Databricks to pull secrets from Infisical.", "category": "Data Analytics"}, name: "Azure Client Secrets",
{"name": "Windmill", "slug": "windmill", "path": "/integrations/app-connections/windmill", "description": "Learn how to connect your Windmill to pull secrets from Infisical.", "category": "DevOps Tools"}, slug: "azure-client-secrets",
{"name": "Camunda", "slug": "camunda", "path": "/integrations/app-connections/camunda", "description": "Learn how to connect your Camunda to pull secrets from Infisical.", "category": "DevOps Tools"}, path: "/integrations/app-connections/azure-client-secrets",
{"name": "Humanitec", "slug": "humanitec", "path": "/integrations/app-connections/humanitec", "description": "Learn how to connect your Humanitec to pull secrets from Infisical.", "category": "DevOps Tools"}, description:
{"name": "OCI", "slug": "oci", "path": "/integrations/app-connections/oci", "description": "Learn how to connect your OCI applications to pull secrets from Infisical.", "category": "Cloud Providers"}, "Learn how to connect your Azure Client Secrets to pull secrets from Infisical.",
{"name": "Zabbix", "slug": "zabbix", "path": "/integrations/app-connections/zabbix", "description": "Learn how to connect your Zabbix to pull secrets from Infisical.", "category": "Monitoring"}, category: "Cloud Providers",
{"name": "MySQL", "slug": "mysql", "path": "/integrations/app-connections/mysql", "description": "Learn how to connect your MySQL database to pull secrets from Infisical.", "category": "Databases"}, },
{"name": "PostgreSQL", "slug": "postgres", "path": "/integrations/app-connections/postgres", "description": "Learn how to connect your PostgreSQL database to pull secrets from Infisical.", "category": "Databases"}, {
{"name": "Microsoft SQL Server", "slug": "mssql", "path": "/integrations/app-connections/mssql", "description": "Learn how to connect your SQL Server database to pull secrets from Infisical.", "category": "Databases"}, name: "Azure DevOps",
{"name": "Oracle Database", "slug": "oracledb", "path": "/integrations/app-connections/oracledb", "description": "Learn how to connect your Oracle database to pull secrets from Infisical.", "category": "Databases"}, slug: "azure-devops",
{"name": "Redis", "slug": "redis", "path": "/integrations/app-connections/redis", "description": "Learn how to connect Redis to pull secrets from Infisical.", "category": "Databases"}, path: "/integrations/app-connections/azure-devops",
{"name": "LDAP", "slug": "ldap", "path": "/integrations/app-connections/ldap", "description": "Learn how to connect your LDAP to pull secrets from Infisical.", "category": "Directory Services"}, description:
{"name": "Auth0", "slug": "auth0", "path": "/integrations/app-connections/auth0", "description": "Learn how to connect your Auth0 to pull secrets from Infisical.", "category": "Identity & Auth"}, "Learn how to connect your Azure DevOps to pull secrets from Infisical.",
{"name": "Okta", "slug": "okta", "path": "/integrations/app-connections/okta", "description": "Learn how to connect your Okta to pull secrets from Infisical.", "category": "Identity & Auth"}, category: "CI/CD",
{"name": "Laravel Forge", "slug": "laravel-forge", "path": "/integrations/app-connections/laravel-forge", "description": "Learn how to connect your Laravel Forge to pull secrets from Infisical.", "category": "Hosting"}, },
{"name": "Chef", "slug": "chef", "path": "/integrations/app-connections/chef", "description": "Learn how to connect your Chef to pull secrets from Infisical.", "category": "DevOps Tools"}, {
{"name": "Northflank", "slug": "northflank", "path": "/integrations/app-connections/northflank", "description": "Learn how to connect your Northflank projects to pull secrets from Infisical.", "category": "Hosting"} name: "Azure ADCS",
].sort(function(a, b) { slug: "azure-adcs",
return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); path: "/integrations/app-connections/azure-adcs",
description:
"Learn how to connect your Azure ADCS to pull secrets from Infisical.",
category: "Cloud Providers",
},
{
name: "GCP",
slug: "gcp",
path: "/integrations/app-connections/gcp",
description:
"Learn how to connect your GCP applications to pull secrets from Infisical.",
category: "Cloud Providers",
},
{
name: "HashiCorp Vault",
slug: "hashicorp-vault",
path: "/integrations/app-connections/hashicorp-vault",
description:
"Learn how to connect your HashiCorp Vault to pull secrets from Infisical.",
category: "Security",
},
{
name: "1Password",
slug: "1password",
path: "/integrations/app-connections/1password",
description:
"Learn how to connect your 1Password to pull secrets from Infisical.",
category: "Security",
},
{
name: "Vercel",
slug: "vercel",
path: "/integrations/app-connections/vercel",
description:
"Learn how to connect your Vercel application to pull secrets from Infisical.",
category: "Hosting",
},
{
name: "Netlify",
slug: "netlify",
path: "/integrations/app-connections/netlify",
description:
"Learn how to connect your Netlify application to pull secrets from Infisical.",
category: "Hosting",
},
{
name: "Railway",
slug: "railway",
path: "/integrations/app-connections/railway",
description:
"Learn how to connect your Railway application to pull secrets from Infisical.",
category: "Hosting",
},
{
name: "Fly.io",
slug: "flyio",
path: "/integrations/app-connections/flyio",
description:
"Learn how to connect your Fly.io application to pull secrets from Infisical.",
category: "Hosting",
},
{
name: "Render",
slug: "render",
path: "/integrations/app-connections/render",
description:
"Learn how to connect your Render application to pull secrets from Infisical.",
category: "Hosting",
},
{
name: "Heroku",
slug: "heroku",
path: "/integrations/app-connections/heroku",
description:
"Learn how to connect your Heroku application to pull secrets from Infisical.",
category: "Hosting",
},
{
name: "DigitalOcean",
slug: "digital-ocean",
path: "/integrations/app-connections/digital-ocean",
description:
"Learn how to connect your DigitalOcean application to pull secrets from Infisical.",
category: "Hosting",
},
{
name: "Supabase",
slug: "supabase",
path: "/integrations/app-connections/supabase",
description:
"Learn how to connect your Supabase application to pull secrets from Infisical.",
category: "Databases",
},
{
name: "Checkly",
slug: "checkly",
path: "/integrations/app-connections/checkly",
description:
"Learn how to connect your Checkly application to pull secrets from Infisical.",
category: "Monitoring",
},
{
name: "GitHub",
slug: "github",
path: "/integrations/app-connections/github",
description:
"Learn how to connect your GitHub application to pull secrets from Infisical.",
category: "CI/CD",
},
{
name: "GitHub Radar",
slug: "github-radar",
path: "/integrations/app-connections/github-radar",
description:
"Learn how to connect your GitHub Radar to pull secrets from Infisical.",
category: "CI/CD",
},
{
name: "GitLab",
slug: "gitlab",
path: "/integrations/app-connections/gitlab",
description:
"Learn how to connect your GitLab application to pull secrets from Infisical.",
category: "CI/CD",
},
{
name: "TeamCity",
slug: "teamcity",
path: "/integrations/app-connections/teamcity",
description:
"Learn how to connect your TeamCity to pull secrets from Infisical.",
category: "CI/CD",
},
{
name: "Bitbucket",
slug: "bitbucket",
path: "/integrations/app-connections/bitbucket",
description:
"Learn how to connect your Bitbucket to pull secrets from Infisical.",
category: "CI/CD",
},
{
name: "Terraform Cloud",
slug: "terraform-cloud",
path: "/integrations/app-connections/terraform-cloud",
description:
"Learn how to connect your Terraform Cloud to pull secrets from Infisical.",
category: "DevOps Tools",
},
{
name: "Cloudflare",
slug: "cloudflare",
path: "/integrations/app-connections/cloudflare",
description:
"Learn how to connect your Cloudflare application to pull secrets from Infisical.",
category: "Cloud Providers",
},
{
name: "Databricks",
slug: "databricks",
path: "/integrations/app-connections/databricks",
description:
"Learn how to connect your Databricks to pull secrets from Infisical.",
category: "Data Analytics",
},
{
name: "DNS Made Easy",
slug: "dns-made-easy",
path: "/integrations/app-connections/dns-made-easy",
description: "Learn how to connect Infisical to DNS Made Easy.",
category: "Networking & DNS",
},
{
name: "Windmill",
slug: "windmill",
path: "/integrations/app-connections/windmill",
description:
"Learn how to connect your Windmill to pull secrets from Infisical.",
category: "DevOps Tools",
},
{
name: "Camunda",
slug: "camunda",
path: "/integrations/app-connections/camunda",
description:
"Learn how to connect your Camunda to pull secrets from Infisical.",
category: "DevOps Tools",
},
{
name: "Humanitec",
slug: "humanitec",
path: "/integrations/app-connections/humanitec",
description:
"Learn how to connect your Humanitec to pull secrets from Infisical.",
category: "DevOps Tools",
},
{
name: "OCI",
slug: "oci",
path: "/integrations/app-connections/oci",
description:
"Learn how to connect your OCI applications to pull secrets from Infisical.",
category: "Cloud Providers",
},
{
name: "Zabbix",
slug: "zabbix",
path: "/integrations/app-connections/zabbix",
description:
"Learn how to connect your Zabbix to pull secrets from Infisical.",
category: "Monitoring",
},
{
name: "MySQL",
slug: "mysql",
path: "/integrations/app-connections/mysql",
description:
"Learn how to connect your MySQL database to pull secrets from Infisical.",
category: "Databases",
},
{
name: "PostgreSQL",
slug: "postgres",
path: "/integrations/app-connections/postgres",
description:
"Learn how to connect your PostgreSQL database to pull secrets from Infisical.",
category: "Databases",
},
{
name: "Microsoft SQL Server",
slug: "mssql",
path: "/integrations/app-connections/mssql",
description:
"Learn how to connect your SQL Server database to pull secrets from Infisical.",
category: "Databases",
},
{
name: "Oracle Database",
slug: "oracledb",
path: "/integrations/app-connections/oracledb",
description:
"Learn how to connect your Oracle database to pull secrets from Infisical.",
category: "Databases",
},
{
name: "Redis",
slug: "redis",
path: "/integrations/app-connections/redis",
description: "Learn how to connect Redis to pull secrets from Infisical.",
category: "Databases",
},
{
name: "LDAP",
slug: "ldap",
path: "/integrations/app-connections/ldap",
description:
"Learn how to connect your LDAP to pull secrets from Infisical.",
category: "Directory Services",
},
{
name: "Auth0",
slug: "auth0",
path: "/integrations/app-connections/auth0",
description:
"Learn how to connect your Auth0 to pull secrets from Infisical.",
category: "Identity & Auth",
},
{
name: "Okta",
slug: "okta",
path: "/integrations/app-connections/okta",
description:
"Learn how to connect your Okta to pull secrets from Infisical.",
category: "Identity & Auth",
},
{
name: "Laravel Forge",
slug: "laravel-forge",
path: "/integrations/app-connections/laravel-forge",
description:
"Learn how to connect your Laravel Forge to pull secrets from Infisical.",
category: "Hosting",
},
{
name: "Chef",
slug: "chef",
path: "/integrations/app-connections/chef",
description:
"Learn how to connect your Chef to pull secrets from Infisical.",
category: "DevOps Tools",
},
{
name: "Northflank",
slug: "northflank",
path: "/integrations/app-connections/northflank",
description:
"Learn how to connect your Northflank projects to pull secrets from Infisical.",
category: "Hosting",
},
].sort(function (a, b) {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
}); });
const filteredConnections = useMemo(() => { const filteredConnections = useMemo(() => {
let filtered = connections; let filtered = connections;
if (selectedCategory !== 'All') { if (selectedCategory !== "All") {
filtered = filtered.filter(connection => connection.category === selectedCategory); filtered = filtered.filter(
(connection) => connection.category === selectedCategory
);
} }
if (searchTerm) { if (searchTerm) {
filtered = filtered.filter(connection => filtered = filtered.filter(
connection.name.toLowerCase().includes(searchTerm.toLowerCase()) || (connection) =>
connection.description.toLowerCase().includes(searchTerm.toLowerCase()) || connection.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
connection.category.toLowerCase().includes(searchTerm.toLowerCase()) connection.description
.toLowerCase()
.includes(searchTerm.toLowerCase()) ||
connection.category.toLowerCase().includes(searchTerm.toLowerCase())
); );
} }
@@ -77,8 +395,18 @@ export const AppConnectionsBrowser = () => {
<div className="mb-6"> <div className="mb-6">
<div className="relative w-full"> <div className="relative w-full">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg className="h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> className="h-4 w-4 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg> </svg>
</div> </div>
<input <input
@@ -94,14 +422,14 @@ export const AppConnectionsBrowser = () => {
{/* Category Filter */} {/* Category Filter */}
<div className="mb-6"> <div className="mb-6">
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{categories.map(category => ( {categories.map((category) => (
<button <button
key={category} key={category}
onClick={() => setSelectedCategory(category)} onClick={() => setSelectedCategory(category)}
className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors shadow-sm ${ className={`px-3 py-1.5 text-sm font-medium rounded-lg transition-colors shadow-sm ${
selectedCategory === category selectedCategory === category
? 'bg-yellow-100 text-yellow-700 border border-yellow-200' ? "bg-yellow-100 text-yellow-700 border border-yellow-200"
: 'bg-white text-gray-700 border border-gray-200 hover:bg-yellow-50 hover:border-yellow-200' : "bg-white text-gray-700 border border-gray-200 hover:bg-yellow-50 hover:border-yellow-200"
}`} }`}
> >
{category} {category}
@@ -113,8 +441,9 @@ export const AppConnectionsBrowser = () => {
{/* Results Count */} {/* Results Count */}
<div className="mb-4"> <div className="mb-4">
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600">
{filteredConnections.length} app connection{filteredConnections.length !== 1 ? 's' : ''} found {filteredConnections.length} app connection
{selectedCategory !== 'All' && ` in ${selectedCategory}`} {filteredConnections.length !== 1 ? "s" : ""} found
{selectedCategory !== "All" && ` in ${selectedCategory}`}
{searchTerm && ` for "${searchTerm}"`} {searchTerm && ` for "${searchTerm}"`}
</p> </p>
</div> </div>
@@ -147,13 +476,17 @@ export const AppConnectionsBrowser = () => {
) : ( ) : (
<div className="text-center py-8"> <div className="text-center py-8">
<div className="flex flex-col items-center space-y-2"> <div className="flex flex-col items-center space-y-2">
<p className="text-gray-500">No app connections found matching your criteria</p> <p className="text-gray-500">
No app connections found matching your criteria
</p>
{searchTerm && ( {searchTerm && (
<p className="text-gray-400 text-sm">Try adjusting your search terms or filters</p> <p className="text-gray-400 text-sm">
Try adjusting your search terms or filters
</p>
)} )}
</div> </div>
</div> </div>
)} )}
</div> </div>
); );
}; };

View 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

View File

@@ -289,7 +289,7 @@
} }
}, },
"project": { "project": {
"title": "Settings", "title": "Project Settings",
"description": "These settings only apply to the currently selected Project.", "description": "These settings only apply to the currently selected Project.",
"danger-zone": "Danger Zone", "danger-zone": "Danger Zone",
"delete-project": "Delete Project", "delete-project": "Delete Project",

View File

@@ -3,14 +3,7 @@ import { ReactNode } from "@tanstack/react-router";
import { LucideIcon } from "lucide-react"; import { LucideIcon } from "lucide-react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { import { InstanceIcon, OrgIcon, ProjectIcon, SubOrgIcon } from "@app/components/v3";
Badge,
InstanceIcon,
OrgIcon,
ProjectIcon,
SubOrgIcon,
TBadgeProps
} from "@app/components/v3";
import { ProjectType } from "@app/hooks/api/projects/types"; import { ProjectType } from "@app/hooks/api/projects/types";
type Props = { type Props = {
@@ -21,41 +14,40 @@ type Props = {
scope: "org" | "namespace" | "instance" | ProjectType | null; scope: "org" | "namespace" | "instance" | ProjectType | null;
}; };
const SCOPE_NAME: Record<NonNullable<Props["scope"]>, { label: string; icon: LucideIcon }> = { const SCOPE_BADGE: Record<NonNullable<Props["scope"]>, { icon: LucideIcon; className: string }> = {
org: { label: "Organization", icon: OrgIcon }, org: { className: "text-org", icon: OrgIcon },
[ProjectType.SecretManager]: { label: "Project", icon: ProjectIcon }, [ProjectType.SecretManager]: { className: "text-project", icon: ProjectIcon },
[ProjectType.CertificateManager]: { label: "Project", icon: ProjectIcon }, [ProjectType.CertificateManager]: { className: "text-project", icon: ProjectIcon },
[ProjectType.SSH]: { label: "Project", icon: ProjectIcon }, [ProjectType.SSH]: { className: "text-project", icon: ProjectIcon },
[ProjectType.KMS]: { label: "Project", icon: ProjectIcon }, [ProjectType.KMS]: { className: "text-project", icon: ProjectIcon },
[ProjectType.PAM]: { label: "Project", icon: ProjectIcon }, [ProjectType.PAM]: { className: "text-project", icon: ProjectIcon },
[ProjectType.SecretScanning]: { label: "Project", icon: ProjectIcon }, [ProjectType.SecretScanning]: { className: "text-project", icon: ProjectIcon },
namespace: { label: "Sub-Organization", icon: SubOrgIcon }, namespace: { className: "text-sub-org", icon: SubOrgIcon },
instance: { label: "Server", icon: InstanceIcon } instance: { className: "text-neutral", icon: InstanceIcon }
};
const SCOPE_VARIANT: Record<NonNullable<Props["scope"]>, TBadgeProps["variant"]> = {
org: "org",
[ProjectType.SecretManager]: "project",
[ProjectType.CertificateManager]: "project",
[ProjectType.SSH]: "project",
[ProjectType.KMS]: "project",
[ProjectType.PAM]: "project",
[ProjectType.SecretScanning]: "project",
namespace: "sub-org",
instance: "neutral"
}; };
export const PageHeader = ({ title, description, children, className, scope }: Props) => ( export const PageHeader = ({ title, description, children, className, scope }: Props) => (
<div className={twMerge("mb-10 w-full", className)}> <div className={twMerge("mb-10 w-full", className)}>
<div className="flex w-full justify-between"> <div className="flex w-full justify-between">
<div className="mr-4 flex w-full items-center"> <div className="mr-4 flex w-full items-center">
<h1 className="text-3xl font-medium text-white capitalize">{title}</h1> <h1
{scope && ( className={twMerge(
<Badge variant={SCOPE_VARIANT[scope]} className="mt-1 ml-2.5"> "text-3xl font-medium text-white capitalize underline decoration-2 underline-offset-4",
{createElement(SCOPE_NAME[scope].icon)} scope === "org" && "decoration-org/90",
{SCOPE_NAME[scope].label} scope === "instance" && "decoration-neutral/90",
</Badge> scope === "namespace" && "decoration-sub-org/90",
)} Object.values(ProjectType).includes((scope as ProjectType) ?? "") &&
"decoration-project/90",
!scope && "no-underline"
)}
>
{scope &&
createElement(SCOPE_BADGE[scope].icon, {
size: 26,
className: twMerge(SCOPE_BADGE[scope].className, "mr-3 mb-1 inline-block")
})}
{title}
</h1>
</div> </div>
<div className="flex items-center gap-2">{children}</div> <div className="flex items-center gap-2">{children}</div>
</div> </div>

View File

@@ -47,8 +47,8 @@ export const Tab = ({
}) => ( }) => (
<TabsPrimitive.Trigger <TabsPrimitive.Trigger
className={twMerge( className={twMerge(
"flex h-10 cursor-pointer items-center justify-center border-transparent", "flex h-11 cursor-pointer items-center justify-center border-transparent",
"px-3 text-sm font-medium whitespace-nowrap text-mineshaft-400 transition-all select-none", "px-3 text-sm font-medium whitespace-nowrap text-mineshaft-300/75 transition-all select-none",
"data-[orientation=vertical]:xl:h-5 data-[orientation=vertical]:xl:border-b-0 data-[orientation=vertical]:xl:border-l", "data-[orientation=vertical]:xl:h-5 data-[orientation=vertical]:xl:border-b-0 data-[orientation=vertical]:xl:border-l",
"border-b hover:text-mineshaft-200", "border-b hover:text-mineshaft-200",
"data-[state=active]:border-mineshaft-400 data-[state=active]:text-white", "data-[state=active]:border-mineshaft-400 data-[state=active]:text-white",

View File

@@ -49,6 +49,7 @@ import { BitbucketConnectionMethod } from "@app/hooks/api/appConnections/types/b
import { ChecklyConnectionMethod } from "@app/hooks/api/appConnections/types/checkly-connection"; import { ChecklyConnectionMethod } from "@app/hooks/api/appConnections/types/checkly-connection";
import { ChefConnectionMethod } from "@app/hooks/api/appConnections/types/chef-connection"; import { ChefConnectionMethod } from "@app/hooks/api/appConnections/types/chef-connection";
import { DigitalOceanConnectionMethod } from "@app/hooks/api/appConnections/types/digital-ocean"; import { DigitalOceanConnectionMethod } from "@app/hooks/api/appConnections/types/digital-ocean";
import { DNSMadeEasyConnectionMethod } from "@app/hooks/api/appConnections/types/dns-made-easy-connection";
import { HerokuConnectionMethod } from "@app/hooks/api/appConnections/types/heroku-connection"; import { HerokuConnectionMethod } from "@app/hooks/api/appConnections/types/heroku-connection";
import { LaravelForgeConnectionMethod } from "@app/hooks/api/appConnections/types/laravel-forge-connection"; import { LaravelForgeConnectionMethod } from "@app/hooks/api/appConnections/types/laravel-forge-connection";
import { NetlifyConnectionMethod } from "@app/hooks/api/appConnections/types/netlify-connection"; import { NetlifyConnectionMethod } from "@app/hooks/api/appConnections/types/netlify-connection";
@@ -111,6 +112,7 @@ export const APP_CONNECTION_MAP: Record<
[AppConnection.Flyio]: { name: "Fly.io", image: "Flyio.svg" }, [AppConnection.Flyio]: { name: "Fly.io", image: "Flyio.svg" },
[AppConnection.GitLab]: { name: "GitLab", image: "GitLab.png" }, [AppConnection.GitLab]: { name: "GitLab", image: "GitLab.png" },
[AppConnection.Cloudflare]: { name: "Cloudflare", image: "Cloudflare.png" }, [AppConnection.Cloudflare]: { name: "Cloudflare", image: "Cloudflare.png" },
[AppConnection.DNSMadeEasy]: { name: "DNS Made Easy", image: "DNSMadeEasy.svg", size: 120 },
[AppConnection.Zabbix]: { name: "Zabbix", image: "Zabbix.png" }, [AppConnection.Zabbix]: { name: "Zabbix", image: "Zabbix.png" },
[AppConnection.Railway]: { name: "Railway", image: "Railway.png" }, [AppConnection.Railway]: { name: "Railway", image: "Railway.png" },
[AppConnection.Bitbucket]: { name: "Bitbucket", image: "Bitbucket.png" }, [AppConnection.Bitbucket]: { name: "Bitbucket", image: "Bitbucket.png" },
@@ -214,6 +216,8 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"])
return { name: "Client Secret", icon: faKey }; return { name: "Client Secret", icon: faKey };
case AzureClientSecretsConnectionMethod.Certificate: case AzureClientSecretsConnectionMethod.Certificate:
return { name: "Certificate", icon: faCertificate }; return { name: "Certificate", icon: faCertificate };
case DNSMadeEasyConnectionMethod.APIKeySecret:
return { name: "API Key & Secret", icon: faKey };
default: default:
throw new Error(`Unhandled App Connection Method: ${method}`); throw new Error(`Unhandled App Connection Method: ${method}`);
} }

View File

@@ -0,0 +1,2 @@
export * from "./queries";
export * from "./types";

View File

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

View File

@@ -0,0 +1,4 @@
export type TDNSMadeEasyZone = {
id: string;
name: string;
};

View File

@@ -29,6 +29,7 @@ export enum AppConnection {
Flyio = "flyio", Flyio = "flyio",
GitLab = "gitlab", GitLab = "gitlab",
Cloudflare = "cloudflare", Cloudflare = "cloudflare",
DNSMadeEasy = "dns-made-easy",
Bitbucket = "bitbucket", Bitbucket = "bitbucket",
Zabbix = "zabbix", Zabbix = "zabbix",
Railway = "railway", Railway = "railway",

View File

@@ -184,6 +184,10 @@ export type TRedisConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.Redis; app: AppConnection.Redis;
}; };
export type TDNSMadeEasyConnectionOption = TAppConnectionOptionBase & {
app: AppConnection.DNSMadeEasy;
};
export type TAppConnectionOption = export type TAppConnectionOption =
| TAwsConnectionOption | TAwsConnectionOption
| TGitHubConnectionOption | TGitHubConnectionOption
@@ -225,7 +229,8 @@ export type TAppConnectionOption =
| TOktaConnectionOption | TOktaConnectionOption
| TAzureAdCsConnectionOption | TAzureAdCsConnectionOption
| TLaravelForgeConnectionOption | TLaravelForgeConnectionOption
| TChefConnectionOption; | TChefConnectionOption
| TDNSMadeEasyConnectionOption;
export type TAppConnectionOptionMap = { export type TAppConnectionOptionMap = {
[AppConnection.AWS]: TAwsConnectionOption; [AppConnection.AWS]: TAwsConnectionOption;
@@ -257,6 +262,7 @@ export type TAppConnectionOptionMap = {
[AppConnection.Flyio]: TFlyioConnectionOption; [AppConnection.Flyio]: TFlyioConnectionOption;
[AppConnection.GitLab]: TGitlabConnectionOption; [AppConnection.GitLab]: TGitlabConnectionOption;
[AppConnection.Cloudflare]: TCloudflareConnectionOption; [AppConnection.Cloudflare]: TCloudflareConnectionOption;
[AppConnection.DNSMadeEasy]: TDNSMadeEasyConnectionOption;
[AppConnection.Bitbucket]: TBitbucketConnectionOption; [AppConnection.Bitbucket]: TBitbucketConnectionOption;
[AppConnection.Zabbix]: TZabbixConnectionOption; [AppConnection.Zabbix]: TZabbixConnectionOption;
[AppConnection.Railway]: TRailwayConnectionOption; [AppConnection.Railway]: TRailwayConnectionOption;

View File

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

View File

@@ -15,6 +15,7 @@ import { TChefConnection } from "./chef-connection";
import { TCloudflareConnection } from "./cloudflare-connection"; import { TCloudflareConnection } from "./cloudflare-connection";
import { TDatabricksConnection } from "./databricks-connection"; import { TDatabricksConnection } from "./databricks-connection";
import { TDigitalOceanConnection } from "./digital-ocean"; import { TDigitalOceanConnection } from "./digital-ocean";
import { TDNSMadeEasyConnection } from "./dns-made-easy-connection";
import { TFlyioConnection } from "./flyio-connection"; import { TFlyioConnection } from "./flyio-connection";
import { TGcpConnection } from "./gcp-connection"; import { TGcpConnection } from "./gcp-connection";
import { TGitHubConnection } from "./github-connection"; import { TGitHubConnection } from "./github-connection";
@@ -57,6 +58,7 @@ export * from "./checkly-connection";
export * from "./chef-connection"; export * from "./chef-connection";
export * from "./cloudflare-connection"; export * from "./cloudflare-connection";
export * from "./databricks-connection"; export * from "./databricks-connection";
export * from "./dns-made-easy-connection";
export * from "./flyio-connection"; export * from "./flyio-connection";
export * from "./gcp-connection"; export * from "./gcp-connection";
export * from "./github-connection"; export * from "./github-connection";
@@ -127,7 +129,8 @@ export type TAppConnection =
| TNorthflankConnection | TNorthflankConnection
| TOktaConnection | TOktaConnection
| TRedisConnection | TRedisConnection
| TChefConnection; | TChefConnection
| TDNSMadeEasyConnection;
export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id" | "projectId">; export type TAvailableAppConnection = Pick<TAppConnection, "name" | "id" | "projectId">;

View File

@@ -16,12 +16,14 @@ export const caStatusToNameMap: { [K in CaStatus]: string } = {
export const ACME_DNS_PROVIDER_NAME_MAP: Record<AcmeDnsProvider, string> = { export const ACME_DNS_PROVIDER_NAME_MAP: Record<AcmeDnsProvider, string> = {
[AcmeDnsProvider.ROUTE53]: "Route53", [AcmeDnsProvider.ROUTE53]: "Route53",
[AcmeDnsProvider.Cloudflare]: "Cloudflare" [AcmeDnsProvider.Cloudflare]: "Cloudflare",
[AcmeDnsProvider.DNSMadeEasy]: "DNS Made Easy"
}; };
export const ACME_DNS_PROVIDER_APP_CONNECTION_MAP: Record<AcmeDnsProvider, AppConnection> = { export const ACME_DNS_PROVIDER_APP_CONNECTION_MAP: Record<AcmeDnsProvider, AppConnection> = {
[AcmeDnsProvider.ROUTE53]: AppConnection.AWS, [AcmeDnsProvider.ROUTE53]: AppConnection.AWS,
[AcmeDnsProvider.Cloudflare]: AppConnection.Cloudflare [AcmeDnsProvider.Cloudflare]: AppConnection.Cloudflare,
[AcmeDnsProvider.DNSMadeEasy]: AppConnection.DNSMadeEasy
}; };
export const CA_TYPE_CAPABILITIES_MAP: Record<CaType, CaCapability[]> = { export const CA_TYPE_CAPABILITIES_MAP: Record<CaType, CaCapability[]> = {

View File

@@ -21,7 +21,8 @@ export enum CaRenewalType {
export enum AcmeDnsProvider { export enum AcmeDnsProvider {
ROUTE53 = "route53", ROUTE53 = "route53",
Cloudflare = "cloudflare" Cloudflare = "cloudflare",
DNSMadeEasy = "dns-made-easy"
} }
export enum CaCapability { export enum CaCapability {

View File

@@ -14,8 +14,8 @@ export const KmsLayout = () => {
const location = useLocation(); const location = useLocation();
return ( return (
<div className="dark flex h-full w-full flex-col overflow-x-hidden"> <div className="dark flex h-full w-full flex-col overflow-x-hidden bg-mineshaft-900">
<div className="border-b border-mineshaft-600 bg-mineshaft-900"> <div className="border-y border-t-project/10 border-b-project/5 bg-gradient-to-b from-project/[0.075] to-project/[0.025] px-4 pt-0.5">
<motion.div <motion.div
key="menu-project-items" key="menu-project-items"
initial={{ x: -150 }} initial={{ x: -150 }}

View File

@@ -22,8 +22,8 @@ import {
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useQuery, useQueryClient } from "@tanstack/react-query";
import { Link, useLocation, useNavigate, useRouter, useRouterState } from "@tanstack/react-router"; import { Link, useLocation, useNavigate, useRouter } from "@tanstack/react-router";
import { UserPlusIcon } from "lucide-react"; import { ChevronRight, UserPlusIcon } from "lucide-react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { Mfa } from "@app/components/auth/Mfa"; import { Mfa } from "@app/components/auth/Mfa";
@@ -31,7 +31,6 @@ import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions"; import { OrgPermissionCan } from "@app/components/permissions";
import SecurityClient from "@app/components/utilities/SecurityClient"; import SecurityClient from "@app/components/utilities/SecurityClient";
import { import {
BreadcrumbContainer,
Button, Button,
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -43,7 +42,6 @@ import {
IconButton, IconButton,
Modal, Modal,
ModalContent, ModalContent,
TBreadcrumbFormat,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { Badge, InstanceIcon, OrgIcon, SubOrgIcon } from "@app/components/v3"; import { Badge, InstanceIcon, OrgIcon, SubOrgIcon } from "@app/components/v3";
@@ -69,6 +67,7 @@ import { MfaMethod } from "@app/hooks/api/auth/types";
import { getAuthToken } from "@app/hooks/api/reactQuery"; import { getAuthToken } from "@app/hooks/api/reactQuery";
import { Organization, SubscriptionPlan } from "@app/hooks/api/types"; import { Organization, SubscriptionPlan } from "@app/hooks/api/types";
import { AuthMethod } from "@app/hooks/api/users/types"; import { AuthMethod } from "@app/hooks/api/users/types";
import { ProjectSelect } from "@app/layouts/ProjectLayout/components/ProjectSelect";
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils"; import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
import { ServerAdminsPanel } from "../ServerAdminsPanel/ServerAdminsPanel"; import { ServerAdminsPanel } from "../ServerAdminsPanel/ServerAdminsPanel";
@@ -183,9 +182,6 @@ export const Navbar = () => {
} }
}, [subscription, isBillingPage, isModalIntrusive]); }, [subscription, isBillingPage, isModalIntrusive]);
const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context });
const breadcrumbs = matches && "breadcrumbs" in matches ? matches.breadcrumbs : undefined;
const handleOrgChange = async (orgId: string) => { const handleOrgChange = async (orgId: string) => {
queryClient.removeQueries({ queryKey: authKeys.getAuthToken }); queryClient.removeQueries({ queryKey: authKeys.getAuthToken });
queryClient.removeQueries({ queryKey: projectKeys.getAllUserProjects() }); queryClient.removeQueries({ queryKey: projectKeys.getAllUserProjects() });
@@ -249,7 +245,9 @@ export const Navbar = () => {
const isServerAdminPanel = location.pathname.startsWith("/admin"); const isServerAdminPanel = location.pathname.startsWith("/admin");
const isProjectScope = location.pathname.startsWith(`/organizations/${currentOrg.id}/projects`); const isProjectScope =
location.pathname.startsWith(`/organizations/${currentOrg.id}/projects`) &&
location.pathname !== `/organizations/${currentOrg.id}/projects`;
const handleOrgNav = async (org: Organization) => { const handleOrgNav = async (org: Organization) => {
if (currentOrg?.id === org.id) return; if (currentOrg?.id === org.id) return;
@@ -279,64 +277,59 @@ export const Navbar = () => {
}; };
return ( return (
<div className="z-10 flex min-h-12 items-center bg-mineshaft-900 px-4 pt-1"> <div className="z-10 flex min-h-12 items-center bg-mineshaft-900 px-4">
<div className="mr-auto flex items-center overflow-hidden"> <div className="mr-auto flex h-full min-w-34 items-center">
<div className="shrink-0"> <div className="mt-0.5 shrink-0">
<Link to="/organizations/$orgId/projects" params={{ orgId: currentOrg.id }}> <Link to="/organizations/$orgId/projects" params={{ orgId: currentOrg.id }}>
<img alt="infisical logo" src="/images/logotransparent.png" className="h-4" /> <img alt="infisical logo" src="/images/logotransparent.png" className="h-4" />
</Link> </Link>
</div> </div>
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p> <ChevronRight size={18} className="mx-3 mt-[3px] text-mineshaft-400/70" />
{isServerAdminPanel ? ( {isServerAdminPanel ? (
<> <Link
<Link to="/admin"
to="/admin" className="group flex cursor-pointer items-center gap-2 text-sm text-white transition-all duration-100 hover:text-primary"
className="group flex cursor-pointer items-center gap-2 text-sm text-white transition-all duration-100 hover:text-primary" >
> <InstanceIcon className="size-3.5 text-xs text-bunker-300" />
<InstanceIcon className="size-3.5 text-xs text-bunker-300" /> <div className="whitespace-nowrap">Server Console</div>
<div className="whitespace-nowrap">Server Console</div> </Link>
</Link>
<p className="pr-3 pl-3 text-lg text-mineshaft-400/70">/</p>
{breadcrumbs ? (
// scott: remove /admin as we show server console above
<BreadcrumbContainer breadcrumbs={breadcrumbs.slice(1) as TBreadcrumbFormat[]} />
) : null}
</>
) : ( ) : (
<> <>
<div className="flex min-w-12 items-center overflow-hidden"> <div
className={twMerge(
"relative flex min-w-16 items-center self-end rounded-t-md border-x border-t pt-1.5 pr-2 pb-2.5 pl-3",
!isProjectScope && !isSubOrganization
? "border-org/15 bg-gradient-to-b from-org/10 to-org/[0.075]"
: "border-transparent"
)}
>
{/* scott: the below is used to hide the top border from the org nav bar */}
{!isProjectScope && !isSubOrganization && (
<div className="absolute -bottom-px left-0 h-px w-full bg-mineshaft-900">
<div className="h-full bg-org/[0.075]" />
</div>
)}
<DropdownMenu modal={false} open={isOrgSelectOpen} onOpenChange={setIsOrgSelectOpen}> <DropdownMenu modal={false} open={isOrgSelectOpen} onOpenChange={setIsOrgSelectOpen}>
<div className="group flex cursor-pointer items-center gap-2 overflow-hidden text-sm text-white transition-all duration-100 hover:text-primary"> <div className="group mr-1 flex min-w-0 cursor-pointer items-center gap-2 overflow-hidden text-sm text-white transition-all duration-100">
<Badge <button
asChild className="flex cursor-pointer items-center gap-x-2 truncate whitespace-nowrap"
variant="org" type="button"
isTruncatable onClick={async () => {
// TODO(scott): either add badge size/style variant or create designated component for namespace/org nav bar navigate({
className={twMerge( to: "/organizations/$orgId/projects",
"gap-x-1.5 text-sm", params: { orgId: currentOrg.id }
(isProjectScope || isSubOrganization) && });
"bg-transparent text-mineshaft-200 hover:!bg-transparent hover:underline [&>svg]:!text-org" if (isSubOrganization) {
)} await router.invalidate({ sync: true }).catch(() => null);
}
}}
> >
<button <OrgIcon className={twMerge("size-[14px] shrink-0 text-org")} />
type="button" <span className="truncate">{currentOrg?.name}</span>
onClick={async () => { <Badge variant="org" className="hidden lg:inline-flex">
navigate({ Organization
to: "/organizations/$orgId/projects", </Badge>
params: { orgId: currentOrg.id } </button>
});
if (isSubOrganization) {
await router.invalidate({ sync: true }).catch(() => null);
}
}}
>
<OrgIcon className="size-[12px]" />
<span>{currentOrg?.name}</span>
</button>
</Badge>
<div className="mr-1 hidden rounded-sm border border-mineshaft-500 px-1 text-xs text-bunker-300 no-underline! md:inline-block">
{getPlan(subscription)}
</div>
{subscription.cardDeclined && ( {subscription.cardDeclined && (
<Tooltip <Tooltip
content={`Your payment could not be processed${subscription.cardDeclinedReason ? `: ${subscription.cardDeclinedReason}` : ""}. Please update your payment method to continue enjoying premium features.`} content={`Your payment could not be processed${subscription.cardDeclinedReason ? `: ${subscription.cardDeclinedReason}` : ""}. Please update your payment method to continue enjoying premium features.`}
@@ -543,19 +536,15 @@ export const Navbar = () => {
)} )}
{isProjectScope && ( {isProjectScope && (
<> <>
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p> <ChevronRight size={18} className="mx-3 mt-[3px] text-mineshaft-400/70" />
{breadcrumbs ? ( <ProjectSelect />
<BreadcrumbContainer
className="min-w-[15rem] flex-1"
breadcrumbs={[breadcrumbs[0]] as TBreadcrumbFormat[]}
/>
) : null}
</> </>
)} )}
</> </>
)} )}
</div> </div>
{subscription && subscription.slug === "starter" && !subscription.has_used_trial && (
{subscription && subscription.slug === "starter" && !subscription.has_used_trial ? (
<Tooltip content="Start Free Pro Trial"> <Tooltip content="Start Free Pro Trial">
<Button <Button
variant="plain" variant="plain"
@@ -576,6 +565,10 @@ export const Navbar = () => {
Free Pro Trial Free Pro Trial
</Button> </Button>
</Tooltip> </Tooltip>
) : (
<div className="mt-0.5 mr-3 hidden rounded-sm border border-mineshaft-400 px-1 text-xs text-mineshaft-100 no-underline! opacity-50 md:inline-block">
{getPlan(subscription)}
</div>
)} )}
{/* eslint-disable-next-line no-nested-ternary */} {/* eslint-disable-next-line no-nested-ternary */}
{!location.pathname.startsWith("/admin") ? ( {!location.pathname.startsWith("/admin") ? (
@@ -601,7 +594,7 @@ export const Navbar = () => {
}} }}
> >
<UserPlusIcon className="inline-block size-3.5" /> <UserPlusIcon className="inline-block size-3.5" />
<span className="ml-2 hidden md:inline-block">Invite Members</span> <span className="ml-2 hidden md:inline-block">Invite Users</span>
</Link> </Link>
) : null ) : null
} }
@@ -705,7 +698,7 @@ export const Navbar = () => {
}} }}
> >
<DropdownMenuItem icon={<FontAwesomeIcon icon={faUserPlus} />}> <DropdownMenuItem icon={<FontAwesomeIcon icon={faUserPlus} />}>
Invite Members Invite Users
</DropdownMenuItem> </DropdownMenuItem>
</Link> </Link>
) : null ) : null

View File

@@ -19,9 +19,9 @@ export const OrgNavBar = ({ isHidden }: Props) => {
const variant = isRootOrganization ? "org" : "namespace"; const variant = isRootOrganization ? "org" : "namespace";
return ( return (
<> <div className="bg-mineshaft-900">
{!isHidden && ( {!isHidden && (
<div className="dark flex w-full flex-col overflow-x-hidden border-b border-mineshaft-600 bg-mineshaft-900 px-4"> <div className="dark flex w-full flex-col overflow-x-hidden border-y border-t-org/15 border-b-org/5 bg-gradient-to-b from-org/[0.075] to-org/[0.025] px-4 pt-0.5">
<motion.div <motion.div
key="menu-org-items" key="menu-org-items"
initial={{ x: -150 }} initial={{ x: -150 }}
@@ -114,6 +114,6 @@ export const OrgNavBar = ({ isHidden }: Props) => {
isOpen={popUp?.createOrg?.isOpen} isOpen={popUp?.createOrg?.isOpen}
onClose={() => handlePopUpToggle("createOrg", false)} onClose={() => handlePopUpToggle("createOrg", false)}
/> />
</> </div>
); );
}; };

View File

@@ -29,8 +29,8 @@ export const PamLayout = () => {
return ( return (
<> <>
<div className="dark flex h-full w-full flex-col overflow-x-hidden"> <div className="dark flex h-full w-full flex-col overflow-x-hidden bg-mineshaft-900">
<div className="border-b border-mineshaft-600 bg-mineshaft-900"> <div className="border-y border-t-project/10 border-b-project/5 bg-gradient-to-b from-project/[0.075] to-project/[0.025] px-4 pt-0.5">
<motion.div <motion.div
key="menu-project-items" key="menu-project-items"
initial={{ x: -150 }} initial={{ x: -150 }}

View File

@@ -29,8 +29,8 @@ export const PkiManagerLayout = () => {
const location = useLocation(); const location = useLocation();
return ( return (
<div className="dark flex h-full w-full flex-col overflow-x-hidden"> <div className="dark flex h-full w-full flex-col overflow-x-hidden bg-mineshaft-900">
<div className="border-b border-mineshaft-600 bg-mineshaft-900"> <div className="border-y border-t-project/10 border-b-project/5 bg-gradient-to-b from-project/[0.075] to-project/[0.025] px-4 pt-0.5">
<motion.div <motion.div
key="menu-project-items" key="menu-project-items"
initial={{ x: -150 }} initial={{ x: -150 }}

View File

@@ -92,7 +92,11 @@ export const ProjectSelect = () => {
}, [projects, projectFavorites, currentWorkspace]); }, [projects, projectFavorites, currentWorkspace]);
return ( return (
<div className="mr-2 flex items-center gap-1 overflow-hidden"> <div className="relative mr-2 flex min-w-16 items-center gap-1 self-end rounded-t-md border-x border-t border-project/10 bg-gradient-to-b from-project/10 to-project/[0.075] pt-1.5 pr-1 pb-2.5 pl-3">
{/* scott: the below is used to hide the top border from the org nav bar */}
<div className="absolute -bottom-px left-0 h-px w-full bg-mineshaft-900">
<div className="h-full bg-project/[0.075]" />
</div>
<DropdownMenu modal={false}> <DropdownMenu modal={false}>
<Link <Link
to={getProjectHomePage(currentWorkspace.type, currentWorkspace.environments)} to={getProjectHomePage(currentWorkspace.type, currentWorkspace.environments)}
@@ -100,16 +104,12 @@ export const ProjectSelect = () => {
projectId: currentWorkspace.id, projectId: currentWorkspace.id,
orgId: currentWorkspace.orgId orgId: currentWorkspace.orgId
}} }}
className="group flex cursor-pointer items-center gap-x-1.5 overflow-hidden hover:text-white" className="group flex cursor-pointer items-center gap-x-2 overflow-hidden pt-0.5 text-sm text-white"
> >
<p className="inline-block truncate text-mineshaft-200 group-hover:underline"> <ProjectIcon className="size-[14px] shrink-0 text-project" />
{currentWorkspace?.name} <span className="truncate">{currentWorkspace?.name}</span>
</p> <Badge variant="project" className="hidden lg:inline-flex">
<Badge variant="project"> {currentWorkspace.type ? PROJECT_TYPE_NAME[currentWorkspace.type] : "Project"}
<ProjectIcon />
<span className="hidden sm:inline-block">
{currentWorkspace.type ? PROJECT_TYPE_NAME[currentWorkspace.type] : "Project"}
</span>
</Badge> </Badge>
</Link> </Link>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@@ -118,7 +118,7 @@ export const ProjectSelect = () => {
variant="plain" variant="plain"
colorSchema="secondary" colorSchema="secondary"
ariaLabel="switch-project" ariaLabel="switch-project"
className="px-2 py-1" className="top-px px-2 py-1"
> >
<FontAwesomeIcon icon={faCaretDown} className="text-xs text-bunker-300" /> <FontAwesomeIcon icon={faCaretDown} className="text-xs text-bunker-300" />
</IconButton> </IconButton>

View File

@@ -39,8 +39,8 @@ export const SecretManagerLayout = () => {
(secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0); (secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
return ( return (
<div className="dark flex h-full w-full flex-col overflow-x-hidden"> <div className="dark flex h-full w-full flex-col overflow-x-hidden bg-mineshaft-900">
<div className="border-b border-mineshaft-600 bg-mineshaft-900"> <div className="border-y border-t-project/10 border-b-project/5 bg-gradient-to-b from-project/[0.075] to-project/[0.025] px-4 pt-0.5">
<motion.div <motion.div
key="menu-project-items" key="menu-project-items"
initial={{ x: -150 }} initial={{ x: -150 }}

View File

@@ -38,8 +38,8 @@ export const SecretScanningLayout = () => {
); );
return ( return (
<div className="dark flex h-full w-full flex-col overflow-x-hidden"> <div className="dark flex h-full w-full flex-col overflow-x-hidden bg-mineshaft-900">
<div className="border-b border-mineshaft-600 bg-mineshaft-900"> <div className="border-y border-t-project/10 border-b-project/5 bg-gradient-to-b from-project/[0.075] to-project/[0.025] px-4 pt-0.5">
<motion.div <motion.div
key="menu-project-items" key="menu-project-items"
initial={{ x: -150 }} initial={{ x: -150 }}

View File

@@ -20,8 +20,8 @@ export const SshLayout = () => {
const location = useLocation(); const location = useLocation();
return ( return (
<div className="dark flex h-full w-full flex-col overflow-x-hidden"> <div className="dark flex h-full w-full flex-col overflow-x-hidden bg-mineshaft-900">
<div className="border-b border-mineshaft-600 bg-mineshaft-900"> <div className="border-y border-t-project/10 border-b-project/5 bg-gradient-to-b from-project/[0.075] to-project/[0.025] px-4 pt-0.5">
<motion.div <motion.div
key="menu-project-items" key="menu-project-items"
initial={{ x: -150 }} initial={{ x: -150 }}

View File

@@ -17,7 +17,7 @@ export const AccessManagementPage = () => {
<div className="mx-auto mb-6 w-full max-w-8xl"> <div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader <PageHeader
scope="instance" scope="instance"
title="Access Control" title="Server Admin Access Control"
description="Manage server admins within your Infisical instance." description="Manage server admins within your Infisical instance."
/> />
<ServerAdminsTable /> <ServerAdminsTable />

View File

@@ -350,8 +350,8 @@ const ViewMembersModalContent = ({
className="my-auto bg-mineshaft-700" className="my-auto bg-mineshaft-700"
title={ title={
members.length members.length
? "No organization members match search..." ? "No organization users match search..."
: "No organization members found" : "No organization users found"
} }
icon={faUsers} icon={faUsers}
/> />

View File

@@ -27,6 +27,10 @@ import {
TCloudflareZone, TCloudflareZone,
useCloudflareConnectionListZones useCloudflareConnectionListZones
} from "@app/hooks/api/appConnections/cloudflare"; } from "@app/hooks/api/appConnections/cloudflare";
import {
TDNSMadeEasyZone,
useDNSMadeEasyConnectionListZones
} from "@app/hooks/api/appConnections/dns-made-easy";
import { AppConnection } from "@app/hooks/api/appConnections/enums"; import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { import {
AcmeDnsProvider, AcmeDnsProvider,
@@ -211,6 +215,11 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
enabled: caType === CaType.ACME enabled: caType === CaType.ACME
}); });
const { data: availableDNSMadeEasyConnections, isPending: isDNSMadeEasyPending } =
useListAvailableAppConnections(AppConnection.DNSMadeEasy, currentProject.id, {
enabled: caType === CaType.ACME
});
const { data: availableAzureConnections, isPending: isAzurePending } = const { data: availableAzureConnections, isPending: isAzurePending } =
useListAvailableAppConnections(AppConnection.AzureADCS, currentProject.id, { useListAvailableAppConnections(AppConnection.AzureADCS, currentProject.id, {
enabled: caType === CaType.AZURE_AD_CS enabled: caType === CaType.AZURE_AD_CS
@@ -220,16 +229,24 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
if (caType === CaType.AZURE_AD_CS) { if (caType === CaType.AZURE_AD_CS) {
return availableAzureConnections || []; return availableAzureConnections || [];
} }
return [...(availableRoute53Connections || []), ...(availableCloudflareConnections || [])]; return [
...(availableRoute53Connections || []),
...(availableCloudflareConnections || []),
...(availableDNSMadeEasyConnections || [])
];
}, [ }, [
caType, caType,
availableRoute53Connections, availableRoute53Connections,
availableCloudflareConnections, availableCloudflareConnections,
availableDNSMadeEasyConnections,
availableAzureConnections availableAzureConnections
]); ]);
const isPending = const isPending =
isRoute53Pending || isCloudflarePending || (isAzurePending && caType === CaType.AZURE_AD_CS); isRoute53Pending ||
isCloudflarePending ||
isDNSMadeEasyPending ||
(isAzurePending && caType === CaType.AZURE_AD_CS);
const dnsAppConnection = const dnsAppConnection =
caType === CaType.ACME && configuration && "dnsAppConnection" in configuration caType === CaType.ACME && configuration && "dnsAppConnection" in configuration
@@ -241,6 +258,11 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
enabled: dnsProvider === AcmeDnsProvider.Cloudflare && !!dnsAppConnection.id enabled: dnsProvider === AcmeDnsProvider.Cloudflare && !!dnsAppConnection.id
}); });
const { data: dnsMadeEasyZones = [], isPending: isDNSMadeEasyZonesPending } =
useDNSMadeEasyConnectionListZones(dnsAppConnection.id, {
enabled: dnsProvider === AcmeDnsProvider.DNSMadeEasy && !!dnsAppConnection.id
});
// Populate form with CA data when editing // Populate form with CA data when editing
useEffect(() => { useEffect(() => {
if (ca && !isCaLoading) { if (ca && !isCaLoading) {
@@ -500,6 +522,32 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
)} )}
/> />
)} )}
{dnsProvider === AcmeDnsProvider.DNSMadeEasy && (
<Controller
name="configuration.dnsProviderConfig.hostedZoneId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
errorText={error?.message}
isError={Boolean(error?.message)}
label="Zone"
>
<FilterableSelect
isLoading={isDNSMadeEasyZonesPending && !!dnsAppConnection.id}
isDisabled={!dnsAppConnection.id}
value={dnsMadeEasyZones.find((zone) => zone.id === value)}
onChange={(option) => {
onChange((option as SingleValue<TDNSMadeEasyZone>)?.id ?? null);
}}
options={dnsMadeEasyZones}
placeholder="Select a zone..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
/>
</FormControl>
)}
/>
)}
<Controller <Controller
control={control} control={control}
defaultValue="" defaultValue=""

View File

@@ -49,7 +49,7 @@ export const IntegrationsListPage = () => {
<div className="mb-8"> <div className="mb-8">
<PageHeader <PageHeader
scope={ProjectType.CertificateManager} scope={ProjectType.CertificateManager}
title="Integrations" title="Project Integrations"
description="Manage integrations with third-party certificate services." description="Manage integrations with third-party certificate services."
/> />
<Tabs orientation="vertical" value={currentTab} onValueChange={updateSelectedTab}> <Tabs orientation="vertical" value={currentTab} onValueChange={updateSelectedTab}>

View File

@@ -1,7 +1,10 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "@tanstack/react-router";
import { InfoIcon } from "lucide-react";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { ProjectType } from "@app/hooks/api/projects/types"; import { ProjectType } from "@app/hooks/api/projects/types";
import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab"; import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab";
@@ -15,6 +18,7 @@ const tabs = [
export const SettingsPage = () => { export const SettingsPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { currentOrg } = useOrganization();
return ( return (
<div className="flex h-full w-full justify-center bg-bunker-800 text-white"> <div className="flex h-full w-full justify-center bg-bunker-800 text-white">
@@ -22,7 +26,17 @@ export const SettingsPage = () => {
<title>{t("common.head-title", { title: t("settings.project.title") })}</title> <title>{t("common.head-title", { title: t("settings.project.title") })}</title>
</Helmet> </Helmet>
<div className="w-full max-w-8xl"> <div className="w-full max-w-8xl">
<PageHeader scope={ProjectType.CertificateManager} title={t("settings.project.title")} /> <PageHeader scope={ProjectType.CertificateManager} title={t("settings.project.title")}>
<Link
to="/organizations/$orgId/settings"
params={{
orgId: currentOrg.id
}}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
>
<InfoIcon size={12} /> Looking for organization settings?
</Link>
</PageHeader>
<Tabs orientation="vertical" defaultValue={tabs[0].key}> <Tabs orientation="vertical" defaultValue={tabs[0].key}>
<TabList> <TabList>
{tabs.map((tab) => ( {tabs.map((tab) => (

View File

@@ -20,7 +20,7 @@ export const OverviewPage = () => {
<div className="mx-auto mb-6 w-full max-w-8xl"> <div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader <PageHeader
scope={ProjectType.KMS} scope={ProjectType.KMS}
title="Overview" title="Project Overview"
description="Manage keys and perform cryptographic operations." description="Manage keys and perform cryptographic operations."
/> />
<ProjectPermissionCan <ProjectPermissionCan

View File

@@ -1,7 +1,10 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "@tanstack/react-router";
import { InfoIcon } from "lucide-react";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { ProjectType } from "@app/hooks/api/projects/types"; import { ProjectType } from "@app/hooks/api/projects/types";
import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab"; import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab";
@@ -16,6 +19,8 @@ const tabs = [
export const SettingsPage = () => { export const SettingsPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { currentOrg } = useOrganization();
return ( return (
<div className="flex h-full w-full justify-center bg-bunker-800 text-white"> <div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<Helmet> <Helmet>
@@ -24,9 +29,19 @@ export const SettingsPage = () => {
<div className="w-full max-w-8xl"> <div className="w-full max-w-8xl">
<PageHeader <PageHeader
scope={ProjectType.KMS} scope={ProjectType.KMS}
title="Settings" title="Project Settings"
description="Configure general project settings" description="Configure general project settings"
/> >
<Link
to="/organizations/$orgId/settings"
params={{
orgId: currentOrg.id
}}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
>
<InfoIcon size={12} /> Looking for organization settings?
</Link>
</PageHeader>
<Tabs orientation="vertical" defaultValue={tabs[0].key}> <Tabs orientation="vertical" defaultValue={tabs[0].key}>
<TabList> <TabList>
{tabs.map((tab) => ( {tabs.map((tab) => (

View File

@@ -83,7 +83,7 @@ export const AccessManagementPage = () => {
<div className="mx-auto mb-6 w-full max-w-8xl"> <div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader <PageHeader
scope={isSubOrganization ? "namespace" : "org"} scope={isSubOrganization ? "namespace" : "org"}
title="Access Control" title={`${isSubOrganization ? "Sub-Organization" : "Organization"} Access Control`}
description="Manage fine-grained access for users, groups, roles, and machine identities within your organization resources." description="Manage fine-grained access for users, groups, roles, and machine identities within your organization resources."
/> />
{!currentOrg.shouldUseNewPrivilegeSystem && ( {!currentOrg.shouldUseNewPrivilegeSystem && (

View File

@@ -51,19 +51,19 @@ export const OrgGroupsSection = () => {
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex flex-wrap items-center justify-between gap-2"> <div className="mb-4 flex flex-wrap items-center justify-between gap-2">
<div className="flex items-center gap-x-2"> <div className="flex items-center gap-x-2">
<p className="text-xl font-medium text-mineshaft-100">Groups</p> <p className="text-xl font-medium text-mineshaft-100">Organization Groups</p>
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/groups" /> <DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/groups" />
</div> </div>
<OrgPermissionCan I={OrgPermissionGroupActions.Create} a={OrgPermissionSubjects.Groups}> <OrgPermissionCan I={OrgPermissionGroupActions.Create} a={OrgPermissionSubjects.Groups}>
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
colorSchema="secondary" variant="outline_bg"
type="submit" type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handleAddGroupModal()} onClick={() => handleAddGroupModal()}
isDisabled={!isAllowed} isDisabled={!isAllowed}
> >
Create Group Create Organization Group
</Button> </Button>
)} )}
</OrgPermissionCan> </OrgPermissionCan>

View File

@@ -159,7 +159,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />} leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search groups..." placeholder="Search organization groups..."
/> />
<TableContainer className="mt-4"> <TableContainer className="mt-4">
<Table> <Table>
@@ -205,7 +205,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
</Th> </Th>
<Th> <Th>
<div className="flex items-center"> <div className="flex items-center">
Role Organization Role
<IconButton <IconButton
variant="plain" variant="plain"
className={`ml-2 ${orderBy === GroupsOrderBy.Role ? "" : "opacity-30"}`} className={`ml-2 ${orderBy === GroupsOrderBy.Role ? "" : "opacity-30"}`}

View File

@@ -99,7 +99,9 @@ export const IdentitySection = withPermission(
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex flex-wrap items-center justify-between gap-2"> <div className="mb-4 flex flex-wrap items-center justify-between gap-2">
<div className="flex flex-1 items-center gap-x-2"> <div className="flex flex-1 items-center gap-x-2">
<p className="text-xl font-medium text-mineshaft-100">Machine Identities</p> <p className="text-xl font-medium text-mineshaft-100">
Organization Machine Identities
</p>
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/machine-identities" /> <DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/machine-identities" />
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
@@ -129,7 +131,9 @@ export const IdentitySection = withPermission(
}} }}
isDisabled={!isAllowed} isDisabled={!isAllowed}
> >
Add Machine Identity {isSubOrganization
? "Add Machine Identity to Sub-Organization"
: "Create Organization Machine Identity"}
</Button> </Button>
)} )}
</OrgPermissionCan> </OrgPermissionCan>

View File

@@ -200,7 +200,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
</DropdownSubMenuTrigger> </DropdownSubMenuTrigger>
<DropdownSubMenuContent className="max-h-80 thin-scrollbar overflow-y-auto rounded-l-none"> <DropdownSubMenuContent className="max-h-80 thin-scrollbar overflow-y-auto rounded-l-none">
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900"> <DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
Filter Machine Identities by Role Filter Organization Machine Identities by Role
</DropdownMenuLabel> </DropdownMenuLabel>
{roles?.map(({ id, slug, name }) => ( {roles?.map(({ id, slug, name }) => (
<DropdownMenuItem <DropdownMenuItem
@@ -258,7 +258,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
</Th> </Th>
<Th> <Th>
<div className="flex items-center"> <div className="flex items-center">
Role Organization Role
<IconButton <IconButton
variant="plain" variant="plain"
className={`ml-2 ${orderBy === OrgIdentityOrderBy.Role ? "" : "opacity-30"}`} className={`ml-2 ${orderBy === OrgIdentityOrderBy.Role ? "" : "opacity-30"}`}

View File

@@ -205,13 +205,13 @@ export const OrgMembersSection = () => {
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex flex-wrap items-center justify-between gap-2"> <div className="mb-4 flex flex-wrap items-center justify-between gap-2">
<div className="flex items-center gap-x-2"> <div className="flex items-center gap-x-2">
<p className="text-xl font-medium text-mineshaft-100">Users</p> <p className="text-xl font-medium text-mineshaft-100">Organization Users</p>
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/user-identities" /> <DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/user-identities" />
</div> </div>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}> <OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
colorSchema="secondary" variant="outline_bg"
type="submit" type="submit"
leftIcon={<UserPlusIcon size={16} />} leftIcon={<UserPlusIcon size={16} />}
onClick={() => onClick={() =>
@@ -219,7 +219,9 @@ export const OrgMembersSection = () => {
} }
isDisabled={!isAllowed} isDisabled={!isAllowed}
> >
{isSubOrganization ? "Add Members" : "Invite Members"} {isSubOrganization
? "Add Users to Sub-Organization"
: "Invite Users to Organization"}
</Button> </Button>
)} )}
</OrgPermissionCan> </OrgPermissionCan>

View File

@@ -336,7 +336,7 @@ export const OrgMembersTable = ({
</DropdownSubMenuTrigger> </DropdownSubMenuTrigger>
<DropdownSubMenuContent className="max-h-80 thin-scrollbar overflow-y-auto rounded-l-none"> <DropdownSubMenuContent className="max-h-80 thin-scrollbar overflow-y-auto rounded-l-none">
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900"> <DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
Apply Roles to Filter Users Filter Organization Users by Role
</DropdownMenuLabel> </DropdownMenuLabel>
{roles?.map(({ id, slug, name }) => ( {roles?.map(({ id, slug, name }) => (
<DropdownMenuItem <DropdownMenuItem
@@ -365,7 +365,7 @@ export const OrgMembersTable = ({
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />} leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search members..." placeholder="Search organization users..."
/> />
</div> </div>
<TableContainer className="mt-4"> <TableContainer className="mt-4">
@@ -434,7 +434,7 @@ export const OrgMembersTable = ({
</Th> </Th>
<Th className="w-1/3"> <Th className="w-1/3">
<div className="flex items-center"> <div className="flex items-center">
Role Organization Role
<IconButton <IconButton
variant="plain" variant="plain"
className={`ml-2 ${orderBy === OrgMembersOrderBy.Role ? "" : "opacity-30"}`} className={`ml-2 ${orderBy === OrgMembersOrderBy.Role ? "" : "opacity-30"}`}
@@ -727,8 +727,8 @@ export const OrgMembersTable = ({
<EmptyState <EmptyState
title={ title={
members.length members.length
? "No organization members match search..." ? "No organization users match search..."
: "No organization members found" : "No organization users found"
} }
icon={members.length ? faSearch : faUsers} icon={members.length ? faSearch : faUsers}
/> />

View File

@@ -199,7 +199,7 @@ export const OrgRoleTable = () => {
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Role}> <OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Role}>
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
colorSchema="secondary" variant="outline_bg"
type="submit" type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => { onClick={() => {
@@ -207,7 +207,7 @@ export const OrgRoleTable = () => {
}} }}
isDisabled={!isAllowed} isDisabled={!isAllowed}
> >
Add Role Add Organization Role
</Button> </Button>
)} )}
</OrgPermissionCan> </OrgPermissionCan>
@@ -216,7 +216,7 @@ export const OrgRoleTable = () => {
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />} leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search roles..." placeholder="Search organization roles..."
className="flex-1" className="flex-1"
containerClassName="mb-4" containerClassName="mb-4"
/> />
@@ -441,7 +441,7 @@ export const OrgRoleTable = () => {
<EmptyState <EmptyState
title={ title={
roles?.length roles?.length
? "No roles match search..." ? "No organization roles match search..."
: "This organization does not have any roles" : "This organization does not have any roles"
} }
icon={roles?.length ? faSearch : undefined} icon={roles?.length ? faSearch : undefined}

View File

@@ -27,7 +27,7 @@ export const AppConnectionsPage = withPermission(
<PageHeader <PageHeader
scope={isSubOrganization ? "namespace" : "org"} scope={isSubOrganization ? "namespace" : "org"}
className="w-full" className="w-full"
title="App Connections" title={`${isSubOrganization ? "Sub-Organization" : "Organization"} App Connections`}
description="Manage organization App Connections" description="Manage organization App Connections"
/> />
<div className="mb-4 flex w-full flex-col rounded-md border border-blue-500/50 bg-blue-500/30 px-4 py-2 text-sm text-blue-200"> <div className="mb-4 flex w-full flex-col rounded-md border border-blue-500/50 bg-blue-500/30 px-4 py-2 text-sm text-blue-200">

View File

@@ -24,6 +24,7 @@ import { ChefConnectionForm } from "./ChefConnectionForm";
import { CloudflareConnectionForm } from "./CloudflareConnectionForm"; import { CloudflareConnectionForm } from "./CloudflareConnectionForm";
import { DatabricksConnectionForm } from "./DatabricksConnectionForm"; import { DatabricksConnectionForm } from "./DatabricksConnectionForm";
import { DigitalOceanConnectionForm } from "./DigitalOceanConnectionForm"; import { DigitalOceanConnectionForm } from "./DigitalOceanConnectionForm";
import { DNSMadeEasyConnectionForm } from "./DNSMadeEasyConnectionForm";
import { FlyioConnectionForm } from "./FlyioConnectionForm"; import { FlyioConnectionForm } from "./FlyioConnectionForm";
import { GcpConnectionForm } from "./GcpConnectionForm"; import { GcpConnectionForm } from "./GcpConnectionForm";
import { GitHubConnectionForm } from "./GitHubConnectionForm"; import { GitHubConnectionForm } from "./GitHubConnectionForm";
@@ -148,6 +149,8 @@ const CreateForm = ({ app, onComplete, projectId }: CreateFormProps) => {
return <GitLabConnectionForm onSubmit={onSubmit} projectId={projectId} />; return <GitLabConnectionForm onSubmit={onSubmit} projectId={projectId} />;
case AppConnection.Cloudflare: case AppConnection.Cloudflare:
return <CloudflareConnectionForm onSubmit={onSubmit} />; return <CloudflareConnectionForm onSubmit={onSubmit} />;
case AppConnection.DNSMadeEasy:
return <DNSMadeEasyConnectionForm onSubmit={onSubmit} />;
case AppConnection.Bitbucket: case AppConnection.Bitbucket:
return <BitbucketConnectionForm onSubmit={onSubmit} />; return <BitbucketConnectionForm onSubmit={onSubmit} />;
case AppConnection.Zabbix: case AppConnection.Zabbix:
@@ -306,6 +309,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
); );
case AppConnection.Cloudflare: case AppConnection.Cloudflare:
return <CloudflareConnectionForm onSubmit={onSubmit} appConnection={appConnection} />; return <CloudflareConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.DNSMadeEasy:
return <DNSMadeEasyConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Bitbucket: case AppConnection.Bitbucket:
return <BitbucketConnectionForm onSubmit={onSubmit} appConnection={appConnection} />; return <BitbucketConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Zabbix: case AppConnection.Zabbix:

View File

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

View File

@@ -19,7 +19,7 @@ export const AuditLogsPage = () => {
<div className="w-full max-w-8xl"> <div className="w-full max-w-8xl">
<PageHeader <PageHeader
scope={isSubOrganization ? "namespace" : "org"} scope={isSubOrganization ? "namespace" : "org"}
title="Audit Logs" title={`${isSubOrganization ? "Sub-Organization" : "Organization"} Audit Logs`}
description="Audit logs for security and compliance teams to monitor information access." description="Audit logs for security and compliance teams to monitor information access."
/> />
<LogsSection pageView /> <LogsSection pageView />

View File

@@ -88,7 +88,7 @@ const Page = () => {
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400" className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
> >
<FontAwesomeIcon icon={faChevronLeft} /> <FontAwesomeIcon icon={faChevronLeft} />
Groups Organization Groups
</Link> </Link>
<PageHeader <PageHeader
scope={isSubOrganization ? "namespace" : "org"} scope={isSubOrganization ? "namespace" : "org"}

View File

@@ -82,7 +82,7 @@ const Page = () => {
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400" className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
> >
<FontAwesomeIcon icon={faChevronLeft} /> <FontAwesomeIcon icon={faChevronLeft} />
Machine Identities Organization Machine Identities
</Link> </Link>
<PageHeader <PageHeader
scope={isSubOrganization ? "namespace" : "org"} scope={isSubOrganization ? "namespace" : "org"}

View File

@@ -81,7 +81,7 @@ export const ProjectsPage = () => {
</Helmet> </Helmet>
<PageHeader <PageHeader
scope={isSubOrganization ? "namespace" : "org"} scope={isSubOrganization ? "namespace" : "org"}
title="Overview" title={`${isSubOrganization ? "Sub-Organization" : "Organization"} Overview`}
description="Your team's complete security toolkit - organized and ready when you need them." description="Your team's complete security toolkit - organized and ready when you need them."
/> />
{projectListView === ProjectListView.MyProjects ? ( {projectListView === ProjectListView.MyProjects ? (

View File

@@ -20,7 +20,7 @@ export const SettingsPage = () => {
<PageHeader <PageHeader
scope={isSubOrganization ? "namespace" : "org"} scope={isSubOrganization ? "namespace" : "org"}
description="Configure organization-wide settings" description="Configure organization-wide settings"
title={t("settings.org.title")} title={isSubOrganization ? "Sub-Organization Settings" : "Organization Settings"}
/> />
<OrgTabGroup /> <OrgTabGroup />
</div> </div>

View File

@@ -112,7 +112,7 @@ const Page = withPermission(
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400" className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
> >
<FontAwesomeIcon icon={faChevronLeft} /> <FontAwesomeIcon icon={faChevronLeft} />
Users Organization Users
</Link> </Link>
<PageHeader <PageHeader
scope={isSubOrganization ? "namespace" : "org"} scope={isSubOrganization ? "namespace" : "org"}

View File

@@ -1,13 +1,18 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "@tanstack/react-router";
import { InfoIcon } from "lucide-react";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { ProjectType } from "@app/hooks/api/projects/types"; import { ProjectType } from "@app/hooks/api/projects/types";
import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab"; import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab";
export const SettingsPage = () => { export const SettingsPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { currentOrg } = useOrganization();
return ( return (
<div className="flex h-full w-full justify-center bg-bunker-800 text-white"> <div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<Helmet> <Helmet>
@@ -16,9 +21,19 @@ export const SettingsPage = () => {
<div className="w-full max-w-8xl"> <div className="w-full max-w-8xl">
<PageHeader <PageHeader
scope={ProjectType.PAM} scope={ProjectType.PAM}
title="Settings" title="Project Settings"
description="Configure your PAM project." description="Configure your PAM project."
/> >
<Link
to="/organizations/$orgId/settings"
params={{
orgId: currentOrg.id
}}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
>
<InfoIcon size={12} /> Looking for organization settings?
</Link>
</PageHeader>
<Tabs orientation="vertical" defaultValue="tab-project-general"> <Tabs orientation="vertical" defaultValue="tab-project-general">
<TabList> <TabList>
<Tab variant="project" value="tab-project-general"> <Tab variant="project" value="tab-project-general">

View File

@@ -1,6 +1,7 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate, useSearch } from "@tanstack/react-router"; import { Link, useNavigate, useSearch } from "@tanstack/react-router";
import { InfoIcon } from "lucide-react";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { useOrganization, useProject } from "@app/context"; import { useOrganization, useProject } from "@app/context";
@@ -43,9 +44,19 @@ const Page = () => {
<div className="mx-auto mb-6 w-full max-w-8xl"> <div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader <PageHeader
scope={currentProject.type} scope={currentProject.type}
title="Access Control" title="Project Access Control"
description="Manage fine-grained access for users, groups, roles, and machine identities within your project resources." description="Manage fine-grained access for users, groups, roles, and machine identities within your project resources."
/> >
<Link
to="/organizations/$orgId/access-management"
params={{
orgId: currentOrg.id
}}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
>
<InfoIcon size={12} /> Looking for organization access control?
</Link>
</PageHeader>
<Tabs orientation="vertical" value={selectedTab} onValueChange={updateSelectedTab}> <Tabs orientation="vertical" value={selectedTab} onValueChange={updateSelectedTab}>
<TabList> <TabList>
<Tab variant="project" value={ProjectAccessControlTabs.Member}> <Tab variant="project" value={ProjectAccessControlTabs.Member}>

View File

@@ -59,19 +59,19 @@ export const GroupsSection = () => {
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-x-2"> <div className="flex items-center gap-x-2">
<p className="text-xl font-medium text-mineshaft-100">User Groups</p> <p className="text-xl font-medium text-mineshaft-100">Project Groups</p>
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/groups#user-groups" /> <DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/groups#user-groups" />
</div> </div>
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Groups}> <ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Groups}>
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
colorSchema="secondary" variant="outline_bg"
type="submit" type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handleAddGroupModal()} onClick={() => handleAddGroupModal()}
isDisabled={!isAllowed} isDisabled={!isAllowed}
> >
Add Group Add Group to Project
</Button> </Button>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>

View File

@@ -122,7 +122,7 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => {
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />} leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search members..." placeholder="Search project groups..."
/> />
<TableContainer className="mt-4"> <TableContainer className="mt-4">
<Table> <Table>
@@ -143,7 +143,7 @@ export const GroupTable = ({ handlePopUpOpen }: Props) => {
</IconButton> </IconButton>
</div> </div>
</Th> </Th>
<Th>Role</Th> <Th>Project Role</Th>
<Th>Added on</Th> <Th>Added on</Th>
<Th className="w-5" /> <Th className="w-5" />
</Tr> </Tr>

View File

@@ -197,7 +197,7 @@ export const IdentityTab = withProjectPermission(
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-x-2"> <div className="flex items-center gap-x-2">
<p className="text-xl font-medium text-mineshaft-100">Machine Identities</p> <p className="text-xl font-medium text-mineshaft-100">Project Machine Identities</p>
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/machine-identities" /> <DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/machine-identities" />
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
@@ -212,7 +212,7 @@ export const IdentityTab = withProjectPermission(
onClick={() => handlePopUpOpen("createIdentity")} onClick={() => handlePopUpOpen("createIdentity")}
isDisabled={!isAllowed} isDisabled={!isAllowed}
> >
Add Machine Identity Add Machine Identity to Project
</Button> </Button>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
@@ -223,7 +223,7 @@ export const IdentityTab = withProjectPermission(
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />} leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search machine identities by name..." placeholder="Search project machine identities by name..."
/> />
<TableContainer> <TableContainer>
<Table> <Table>
@@ -251,7 +251,7 @@ export const IdentityTab = withProjectPermission(
</IconButton> </IconButton>
</div> </div>
</Th> </Th>
<Th className="w-1/3">Role</Th> <Th className="w-1/3">Project Role</Th>
<Th>Managed by</Th> <Th>Managed by</Th>
<Th className="w-5">{isFetching ? <Spinner size="xs" /> : null}</Th> <Th className="w-5">{isFetching ? <Spinner size="xs" /> : null}</Th>
</Tr> </Tr>

View File

@@ -49,19 +49,19 @@ export const MembersSection = () => {
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-x-2"> <div className="flex items-center gap-x-2">
<p className="text-xl font-medium text-mineshaft-100">Users</p> <p className="text-xl font-medium text-mineshaft-100">Project Users</p>
<DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/user-identities" /> <DocumentationLinkBadge href="https://infisical.com/docs/documentation/platform/identities/user-identities" />
</div> </div>
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Member}> <ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Member}>
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
colorSchema="secondary" variant="outline_bg"
type="submit" type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("addMember")} onClick={() => handlePopUpOpen("addMember")}
isDisabled={!isAllowed} isDisabled={!isAllowed}
> >
Add Member Add Users to Project
</Button> </Button>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>

View File

@@ -208,7 +208,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
</DropdownSubMenuTrigger> </DropdownSubMenuTrigger>
<DropdownSubMenuContent className="max-h-80 thin-scrollbar overflow-y-auto rounded-l-none"> <DropdownSubMenuContent className="max-h-80 thin-scrollbar overflow-y-auto rounded-l-none">
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900"> <DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
Apply Roles to Filter Users Filter Project Users by Role
</DropdownMenuLabel> </DropdownMenuLabel>
{projectRoles?.map(({ id, slug, name }) => ( {projectRoles?.map(({ id, slug, name }) => (
<DropdownMenuItem <DropdownMenuItem
@@ -237,7 +237,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />} leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search members..." placeholder="Search project users..."
/> />
</div> </div>
<TableContainer className="mt-4"> <TableContainer className="mt-4">
@@ -282,7 +282,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
</IconButton> </IconButton>
</div> </div>
</Th> </Th>
<Th>Role</Th> <Th>Project Role</Th>
<Th className="w-5" /> <Th className="w-5" />
</Tr> </Tr>
</THead> </THead>
@@ -462,9 +462,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
)} )}
{!isMembersLoading && !filteredUsers?.length && ( {!isMembersLoading && !filteredUsers?.length && (
<EmptyState <EmptyState
title={ title={members.length ? "No project users match search..." : "No project users found"}
members.length ? "No project members match search..." : "No project members found"
}
icon={members.length ? faSearch : faUsers} icon={members.length ? faSearch : faUsers}
/> />
)} )}

View File

@@ -170,13 +170,13 @@ export const ProjectRoleList = () => {
<ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Role}> <ProjectPermissionCan I={ProjectPermissionActions.Create} a={ProjectPermissionSub.Role}>
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
colorSchema="secondary" variant="outline_bg"
type="submit" type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("role")} onClick={() => handlePopUpOpen("role")}
isDisabled={!isAllowed} isDisabled={!isAllowed}
> >
Add Role Add Project Role
</Button> </Button>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>

View File

@@ -57,7 +57,7 @@ export const ServiceTokenSection = withProjectPermission(
> >
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
colorSchema="secondary" variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => { onClick={() => {
handlePopUpOpen("createAPIToken"); handlePopUpOpen("createAPIToken");

View File

@@ -25,7 +25,7 @@ export const AppConnectionsPage = withProjectPermission(
<PageHeader <PageHeader
scope={currentProject.type} scope={currentProject.type}
className="w-full" className="w-full"
title="App Connections" title="Project App Connections"
description="Manage project App Connections" description="Manage project App Connections"
/> />

View File

@@ -1,4 +1,6 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { Link } from "@tanstack/react-router";
import { InfoIcon } from "lucide-react";
import { PageHeader } from "@app/components/v2"; import { PageHeader } from "@app/components/v2";
import { useProject } from "@app/context"; import { useProject } from "@app/context";
@@ -17,9 +19,19 @@ export const AuditLogsPage = () => {
<div className="w-full max-w-8xl"> <div className="w-full max-w-8xl">
<PageHeader <PageHeader
scope={currentProject.type} scope={currentProject.type}
title="Audit logs" title="Project Audit logs"
description="Audit logs for security and compliance teams to monitor information access." description="Audit logs for security and compliance teams to monitor information access."
/> >
<Link
to="/organizations/$orgId/audit-logs"
params={{
orgId: currentProject.orgId
}}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
>
<InfoIcon size={12} /> Looking for organization audit logs?
</Link>
</PageHeader>
<LogsSection pageView project={currentProject} /> <LogsSection pageView project={currentProject} />
</div> </div>
</div> </div>

View File

@@ -57,7 +57,7 @@ const Page = () => {
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400" className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
> >
<FontAwesomeIcon icon={faChevronLeft} /> <FontAwesomeIcon icon={faChevronLeft} />
Groups Project Groups
</Link> </Link>
<PageHeader <PageHeader
scope={currentProject.type} scope={currentProject.type}

View File

@@ -149,7 +149,7 @@ const Page = () => {
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400" className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
> >
<FontAwesomeIcon icon={faChevronLeft} /> <FontAwesomeIcon icon={faChevronLeft} />
Machine Identities Project Machine Identities
</Link> </Link>
<PageHeader <PageHeader
scope={currentProject.type} scope={currentProject.type}

View File

@@ -128,7 +128,7 @@ export const Page = () => {
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400" className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
> >
<FontAwesomeIcon icon={faChevronLeft} /> <FontAwesomeIcon icon={faChevronLeft} />
Users Project Users
</Link> </Link>
<PageHeader <PageHeader
scope={currentProject.type} scope={currentProject.type}

View File

@@ -104,7 +104,7 @@ const Page = () => {
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400" className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
> >
<FontAwesomeIcon icon={faChevronLeft} /> <FontAwesomeIcon icon={faChevronLeft} />
Roles Project Roles
</Link> </Link>
<PageHeader <PageHeader
scope={currentProject.type} scope={currentProject.type}

View File

@@ -57,7 +57,7 @@ export const IntegrationsListPage = () => {
<div className="mb-8"> <div className="mb-8">
<PageHeader <PageHeader
scope={ProjectType.SecretManager} scope={ProjectType.SecretManager}
title="Integrations" title="Project Integrations"
description="Manage integrations with third-party services." description="Manage integrations with third-party services."
/> />
<Tabs orientation="vertical" value={selectedTab} onValueChange={updateSelectedTab}> <Tabs orientation="vertical" value={selectedTab} onValueChange={updateSelectedTab}>

View File

@@ -933,12 +933,12 @@ export const OverviewPage = () => {
<div className="flex w-full items-baseline justify-between"> <div className="flex w-full items-baseline justify-between">
<PageHeader <PageHeader
scope={ProjectType.SecretManager} scope={ProjectType.SecretManager}
title="Overview" title="Project Overview"
description={ description={
<p className="text-md text-bunker-300"> <p className="text-md text-bunker-300">
Inject your secrets using Inject your secrets using
<a <a
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600" className="ml-1 text-mineshaft-200 underline decoration-mineshaft-400/65 underline-offset-3 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
href="https://infisical.com/docs/cli/overview" href="https://infisical.com/docs/cli/overview"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
@@ -947,7 +947,7 @@ export const OverviewPage = () => {
</a> </a>
, ,
<a <a
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600" className="ml-1 text-mineshaft-200 underline decoration-mineshaft-400/65 underline-offset-3 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
href="https://infisical.com/docs/documentation/getting-started/api" href="https://infisical.com/docs/documentation/getting-started/api"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
@@ -956,7 +956,7 @@ export const OverviewPage = () => {
</a> </a>
, ,
<a <a
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600" className="ml-1 text-mineshaft-200 underline decoration-mineshaft-400/65 underline-offset-3 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
href="https://infisical.com/docs/sdks/overview" href="https://infisical.com/docs/sdks/overview"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
@@ -965,7 +965,7 @@ export const OverviewPage = () => {
</a> </a>
, and , and
<a <a
className="ml-1 text-mineshaft-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600" className="ml-1 text-mineshaft-200 underline decoration-mineshaft-400/65 underline-offset-3 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
href="https://infisical.com/docs/documentation/getting-started/introduction" href="https://infisical.com/docs/documentation/getting-started/introduction"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"

View File

@@ -1,5 +1,7 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "@tanstack/react-router";
import { InfoIcon } from "lucide-react";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { useProject } from "@app/context"; import { useProject } from "@app/context";
@@ -43,9 +45,19 @@ export const SettingsPage = () => {
<div className="w-full max-w-8xl"> <div className="w-full max-w-8xl">
<PageHeader <PageHeader
scope={ProjectType.SecretManager} scope={ProjectType.SecretManager}
title="Settings" title="Project Settings"
description="Configure your secret manager's encryption, environments, webhooks and other configurations." description="Configure your secret manager's encryption, environments, webhooks and other configurations."
/> >
<Link
to="/organizations/$orgId/settings"
params={{
orgId: currentProject.orgId
}}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
>
<InfoIcon size={12} /> Looking for organization settings?
</Link>
</PageHeader>
<Tabs orientation="vertical" defaultValue={tabs[0].key}> <Tabs orientation="vertical" defaultValue={tabs[0].key}>
<TabList> <TabList>
{tabs {tabs

View File

@@ -1,5 +1,6 @@
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import slugify from "@sindresorhus/slugify";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
@@ -31,7 +32,13 @@ type ContentProps = {
const Content = ({ onComplete }: ContentProps) => { const Content = ({ onComplete }: ContentProps) => {
const { currentProject } = useProject(); const { currentProject } = useProject();
const { mutateAsync, isPending } = useCreateWsEnvironment(); const { mutateAsync, isPending } = useCreateWsEnvironment();
const { control, handleSubmit } = useForm<FormData>({ const {
control,
handleSubmit,
setValue,
getValues,
formState: { dirtyFields }
} = useForm<FormData>({
resolver: zodResolver(schema) resolver: zodResolver(schema)
}); });
@@ -52,15 +59,28 @@ const Content = ({ onComplete }: ContentProps) => {
onComplete(env); onComplete(env);
}; };
const handleEnvironmentNameChange = () => {
if (dirtyFields.environmentSlug) return;
const value = getValues("environmentName");
setValue("environmentSlug", slugify(value, { lowercase: true }));
};
return ( return (
<form onSubmit={handleSubmit(onFormSubmit)}> <form onSubmit={handleSubmit(onFormSubmit)}>
<Controller <Controller
control={control} control={control}
defaultValue="" defaultValue=""
name="environmentName" name="environmentName"
render={({ field, fieldState: { error } }) => ( render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Environment Name" isError={Boolean(error)} errorText={error?.message}> <FormControl label="Environment Name" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} /> <Input
{...field}
onChange={(e) => {
onChange(e);
handleEnvironmentNameChange();
}}
/>
</FormControl> </FormControl>
)} )}
/> />

View File

@@ -1,9 +1,11 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link } from "@tanstack/react-router";
import { InfoIcon } from "lucide-react";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context"; import { ProjectPermissionSub, useOrganization } from "@app/context";
import { ProjectPermissionSecretScanningConfigActions } from "@app/context/ProjectPermissionContext/types"; import { ProjectPermissionSecretScanningConfigActions } from "@app/context/ProjectPermissionContext/types";
import { ProjectType } from "@app/hooks/api/projects/types"; import { ProjectType } from "@app/hooks/api/projects/types";
import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab"; import { ProjectGeneralTab } from "@app/pages/project/SettingsPage/components/ProjectGeneralTab";
@@ -12,6 +14,7 @@ import { ProjectScanningConfigTab } from "./components/ProjectScanningConfigTab"
export const SettingsPage = () => { export const SettingsPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { currentOrg } = useOrganization();
return ( return (
<div className="flex h-full w-full justify-center bg-bunker-800 text-white"> <div className="flex h-full w-full justify-center bg-bunker-800 text-white">
@@ -21,9 +24,19 @@ export const SettingsPage = () => {
<div className="w-full max-w-8xl"> <div className="w-full max-w-8xl">
<PageHeader <PageHeader
scope={ProjectType.SecretScanning} scope={ProjectType.SecretScanning}
title="Settings" title="Project Settings"
description="Configure your Secret Scanning product's configurations." description="Configure your Secret Scanning product's configurations."
/> >
<Link
to="/organizations/$orgId/settings"
params={{
orgId: currentOrg.id
}}
className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline"
>
<InfoIcon size={12} /> Looking for organization settings?
</Link>
</PageHeader>
<Tabs orientation="vertical" defaultValue="tab-project-general"> <Tabs orientation="vertical" defaultValue="tab-project-general">
<TabList> <TabList>
<Tab variant="project" value="tab-project-general"> <Tab variant="project" value="tab-project-general">

Some files were not shown because too many files have changed in this diff Show More