mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 09:28:06 +00:00
Merge branch 'main' into ENG-3139
This commit is contained in:
@@ -0,0 +1,19 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.AppConnection, "gatewayId"))) {
|
||||||
|
await knex.schema.alterTable(TableName.AppConnection, (t) => {
|
||||||
|
t.uuid("gatewayId").nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.AppConnection, "gatewayId")) {
|
||||||
|
await knex.schema.alterTable(TableName.AppConnection, (t) => {
|
||||||
|
t.dropColumn("gatewayId");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,8 @@ export const AppConnectionsSchema = z.object({
|
|||||||
orgId: z.string().uuid(),
|
orgId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
isPlatformManagedCredentials: z.boolean().default(false).nullable().optional()
|
isPlatformManagedCredentials: z.boolean().default(false).nullable().optional(),
|
||||||
|
gatewayId: z.string().uuid().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TAppConnections = z.infer<typeof AppConnectionsSchema>;
|
export type TAppConnections = z.infer<typeof AppConnectionsSchema>;
|
||||||
|
|||||||
@@ -45,7 +45,10 @@ export const ValidateOracleDBConnectionCredentialsSchema = z.discriminatedUnion(
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
export const CreateOracleDBConnectionSchema = ValidateOracleDBConnectionCredentialsSchema.and(
|
export const CreateOracleDBConnectionSchema = ValidateOracleDBConnectionCredentialsSchema.and(
|
||||||
GenericCreateAppConnectionFieldsSchema(AppConnection.OracleDB, { supportsPlatformManagedCredentials: true })
|
GenericCreateAppConnectionFieldsSchema(AppConnection.OracleDB, {
|
||||||
|
supportsPlatformManagedCredentials: true,
|
||||||
|
supportsGateways: true
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
export const UpdateOracleDBConnectionSchema = z
|
export const UpdateOracleDBConnectionSchema = z
|
||||||
@@ -54,7 +57,12 @@ export const UpdateOracleDBConnectionSchema = z
|
|||||||
AppConnections.UPDATE(AppConnection.OracleDB).credentials
|
AppConnections.UPDATE(AppConnection.OracleDB).credentials
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.OracleDB, { supportsPlatformManagedCredentials: true }));
|
.and(
|
||||||
|
GenericUpdateAppConnectionFieldsSchema(AppConnection.OracleDB, {
|
||||||
|
supportsPlatformManagedCredentials: true,
|
||||||
|
supportsGateways: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
export const OracleDBConnectionListItemSchema = z.object({
|
export const OracleDBConnectionListItemSchema = z.object({
|
||||||
name: z.literal("OracleDB"),
|
name: z.literal("OracleDB"),
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { ForbiddenError } from "@casl/ability";
|
|||||||
import { CronJob } from "cron";
|
import { CronJob } from "cron";
|
||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { verifyOfflineLicense } from "@app/lib/crypto";
|
import { verifyOfflineLicense } from "@app/lib/crypto";
|
||||||
import { NotFoundError } from "@app/lib/errors";
|
import { NotFoundError } from "@app/lib/errors";
|
||||||
@@ -47,7 +46,6 @@ type TLicenseServiceFactoryDep = {
|
|||||||
orgDAL: Pick<TOrgDALFactory, "findOrgById" | "countAllOrgMembers">;
|
orgDAL: Pick<TOrgDALFactory, "findOrgById" | "countAllOrgMembers">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
licenseDAL: TLicenseDALFactory;
|
licenseDAL: TLicenseDALFactory;
|
||||||
keyStore: Pick<TKeyStoreFactory, "setItemWithExpiry" | "getItem" | "deleteItem">;
|
|
||||||
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
||||||
projectDAL: TProjectDALFactory;
|
projectDAL: TProjectDALFactory;
|
||||||
};
|
};
|
||||||
@@ -57,14 +55,10 @@ export type TLicenseServiceFactory = ReturnType<typeof licenseServiceFactory>;
|
|||||||
const LICENSE_SERVER_CLOUD_LOGIN = "/api/auth/v1/license-server-login";
|
const LICENSE_SERVER_CLOUD_LOGIN = "/api/auth/v1/license-server-login";
|
||||||
const LICENSE_SERVER_ON_PREM_LOGIN = "/api/auth/v1/license-login";
|
const LICENSE_SERVER_ON_PREM_LOGIN = "/api/auth/v1/license-login";
|
||||||
|
|
||||||
const LICENSE_SERVER_CLOUD_PLAN_TTL = 5 * 60; // 5 mins
|
|
||||||
const FEATURE_CACHE_KEY = (orgId: string) => `infisical-cloud-plan-${orgId}`;
|
|
||||||
|
|
||||||
export const licenseServiceFactory = ({
|
export const licenseServiceFactory = ({
|
||||||
orgDAL,
|
orgDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
licenseDAL,
|
licenseDAL,
|
||||||
keyStore,
|
|
||||||
identityOrgMembershipDAL,
|
identityOrgMembershipDAL,
|
||||||
projectDAL
|
projectDAL
|
||||||
}: TLicenseServiceFactoryDep) => {
|
}: TLicenseServiceFactoryDep) => {
|
||||||
@@ -178,12 +172,6 @@ export const licenseServiceFactory = ({
|
|||||||
logger.info(`getPlan: attempting to fetch plan for [orgId=${orgId}] [projectId=${projectId}]`);
|
logger.info(`getPlan: attempting to fetch plan for [orgId=${orgId}] [projectId=${projectId}]`);
|
||||||
try {
|
try {
|
||||||
if (instanceType === InstanceType.Cloud) {
|
if (instanceType === InstanceType.Cloud) {
|
||||||
const cachedPlan = await keyStore.getItem(FEATURE_CACHE_KEY(orgId));
|
|
||||||
if (cachedPlan) {
|
|
||||||
logger.info(`getPlan: plan fetched from cache [orgId=${orgId}] [projectId=${projectId}]`);
|
|
||||||
return JSON.parse(cachedPlan) as TFeatureSet;
|
|
||||||
}
|
|
||||||
|
|
||||||
const org = await orgDAL.findOrgById(orgId);
|
const org = await orgDAL.findOrgById(orgId);
|
||||||
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
||||||
const {
|
const {
|
||||||
@@ -199,23 +187,12 @@ export const licenseServiceFactory = ({
|
|||||||
const identityUsed = await licenseDAL.countOrgUsersAndIdentities(orgId);
|
const identityUsed = await licenseDAL.countOrgUsersAndIdentities(orgId);
|
||||||
currentPlan.identitiesUsed = identityUsed;
|
currentPlan.identitiesUsed = identityUsed;
|
||||||
|
|
||||||
await keyStore.setItemWithExpiry(
|
|
||||||
FEATURE_CACHE_KEY(org.id),
|
|
||||||
LICENSE_SERVER_CLOUD_PLAN_TTL,
|
|
||||||
JSON.stringify(currentPlan)
|
|
||||||
);
|
|
||||||
|
|
||||||
return currentPlan;
|
return currentPlan;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
error,
|
error,
|
||||||
`getPlan: encountered an error when fetching pan [orgId=${orgId}] [projectId=${projectId}] [error]`
|
`getPlan: encountered an error when fetching plan [orgId=${orgId}] [projectId=${projectId}] [error]`
|
||||||
);
|
|
||||||
await keyStore.setItemWithExpiry(
|
|
||||||
FEATURE_CACHE_KEY(orgId),
|
|
||||||
LICENSE_SERVER_CLOUD_PLAN_TTL,
|
|
||||||
JSON.stringify(onPremFeatures)
|
|
||||||
);
|
);
|
||||||
return onPremFeatures;
|
return onPremFeatures;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -226,7 +203,6 @@ export const licenseServiceFactory = ({
|
|||||||
|
|
||||||
const refreshPlan = async (orgId: string) => {
|
const refreshPlan = async (orgId: string) => {
|
||||||
if (instanceType === InstanceType.Cloud) {
|
if (instanceType === InstanceType.Cloud) {
|
||||||
await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId));
|
|
||||||
await getPlan(orgId);
|
await getPlan(orgId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -264,7 +240,6 @@ export const licenseServiceFactory = ({
|
|||||||
quantityIdentities
|
quantityIdentities
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId));
|
|
||||||
} else if (instanceType === InstanceType.EnterpriseOnPrem) {
|
} else if (instanceType === InstanceType.EnterpriseOnPrem) {
|
||||||
const usedSeats = await licenseDAL.countOfOrgMembers(null, tx);
|
const usedSeats = await licenseDAL.countOfOrgMembers(null, tx);
|
||||||
const usedIdentitySeats = await licenseDAL.countOrgUsersAndIdentities(null, tx);
|
const usedIdentitySeats = await licenseDAL.countOrgUsersAndIdentities(null, tx);
|
||||||
@@ -328,7 +303,6 @@ export const licenseServiceFactory = ({
|
|||||||
`/api/license-server/v1/customers/${organization.customerId}/session/trial`,
|
`/api/license-server/v1/customers/${organization.customerId}/session/trial`,
|
||||||
{ success_url }
|
{ success_url }
|
||||||
);
|
);
|
||||||
await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId));
|
|
||||||
return { url };
|
return { url };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -705,10 +679,6 @@ export const licenseServiceFactory = ({
|
|||||||
return licenses;
|
return licenses;
|
||||||
};
|
};
|
||||||
|
|
||||||
const invalidateGetPlan = async (orgId: string) => {
|
|
||||||
await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId));
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
generateOrgCustomerId,
|
generateOrgCustomerId,
|
||||||
removeOrgCustomer,
|
removeOrgCustomer,
|
||||||
@@ -723,7 +693,6 @@ export const licenseServiceFactory = ({
|
|||||||
return onPremFeatures;
|
return onPremFeatures;
|
||||||
},
|
},
|
||||||
getPlan,
|
getPlan,
|
||||||
invalidateGetPlan,
|
|
||||||
updateSubscriptionOrgMemberCount,
|
updateSubscriptionOrgMemberCount,
|
||||||
refreshPlan,
|
refreshPlan,
|
||||||
getOrgPlan,
|
getOrgPlan,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import isEqual from "lodash.isequal";
|
|||||||
|
|
||||||
import { SecretType, TableName } from "@app/db/schemas";
|
import { SecretType, TableName } from "@app/db/schemas";
|
||||||
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { hasSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns";
|
import { hasSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||||
@@ -108,6 +109,7 @@ export type TSecretRotationV2ServiceFactoryDep = {
|
|||||||
queueService: Pick<TQueueServiceFactory, "queuePg">;
|
queueService: Pick<TQueueServiceFactory, "queuePg">;
|
||||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
|
||||||
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSecretRotationV2ServiceFactory = ReturnType<typeof secretRotationV2ServiceFactory>;
|
export type TSecretRotationV2ServiceFactory = ReturnType<typeof secretRotationV2ServiceFactory>;
|
||||||
@@ -150,7 +152,8 @@ export const secretRotationV2ServiceFactory = ({
|
|||||||
keyStore,
|
keyStore,
|
||||||
queueService,
|
queueService,
|
||||||
folderCommitService,
|
folderCommitService,
|
||||||
appConnectionDAL
|
appConnectionDAL,
|
||||||
|
gatewayService
|
||||||
}: TSecretRotationV2ServiceFactoryDep) => {
|
}: TSecretRotationV2ServiceFactoryDep) => {
|
||||||
const $queueSendSecretRotationStatusNotification = async (secretRotation: TSecretRotationV2Raw) => {
|
const $queueSendSecretRotationStatusNotification = async (secretRotation: TSecretRotationV2Raw) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
@@ -463,7 +466,8 @@ export const secretRotationV2ServiceFactory = ({
|
|||||||
rotationInterval: payload.rotationInterval
|
rotationInterval: payload.rotationInterval
|
||||||
} as TSecretRotationV2WithConnection,
|
} as TSecretRotationV2WithConnection,
|
||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
kmsService
|
kmsService,
|
||||||
|
gatewayService
|
||||||
);
|
);
|
||||||
|
|
||||||
// even though we have a db constraint we want to check before any rotation of credentials is attempted
|
// even though we have a db constraint we want to check before any rotation of credentials is attempted
|
||||||
@@ -826,7 +830,8 @@ export const secretRotationV2ServiceFactory = ({
|
|||||||
connection: appConnection
|
connection: appConnection
|
||||||
} as TSecretRotationV2WithConnection,
|
} as TSecretRotationV2WithConnection,
|
||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
kmsService
|
kmsService,
|
||||||
|
gatewayService
|
||||||
);
|
);
|
||||||
|
|
||||||
const generatedCredentials = await decryptSecretRotationCredentials({
|
const generatedCredentials = await decryptSecretRotationCredentials({
|
||||||
@@ -909,7 +914,8 @@ export const secretRotationV2ServiceFactory = ({
|
|||||||
connection: appConnection
|
connection: appConnection
|
||||||
} as TSecretRotationV2WithConnection,
|
} as TSecretRotationV2WithConnection,
|
||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
kmsService
|
kmsService,
|
||||||
|
gatewayService
|
||||||
);
|
);
|
||||||
|
|
||||||
const updatedRotation = await rotationFactory.rotateCredentials(
|
const updatedRotation = await rotationFactory.rotateCredentials(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { AuditLogInfo } from "@app/ee/services/audit-log/audit-log-types";
|
import { AuditLogInfo } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||||
import { TSqlCredentialsRotationGeneratedCredentials } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-types";
|
import { TSqlCredentialsRotationGeneratedCredentials } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-types";
|
||||||
import { OrderByDirection } from "@app/lib/types";
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||||
@@ -251,7 +252,8 @@ export type TRotationFactory<
|
|||||||
> = (
|
> = (
|
||||||
secretRotation: T,
|
secretRotation: T,
|
||||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">,
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">,
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">,
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
|
||||||
) => {
|
) => {
|
||||||
issueCredentials: TRotationFactoryIssueCredentials<C, P>;
|
issueCredentials: TRotationFactoryIssueCredentials<C, P>;
|
||||||
revokeCredentials: TRotationFactoryRevokeCredentials<C>;
|
revokeCredentials: TRotationFactoryRevokeCredentials<C>;
|
||||||
|
|||||||
+45
-35
@@ -1,3 +1,5 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
TRotationFactory,
|
TRotationFactory,
|
||||||
TRotationFactoryGetSecretsPayload,
|
TRotationFactoryGetSecretsPayload,
|
||||||
@@ -5,7 +7,10 @@ import {
|
|||||||
TRotationFactoryRevokeCredentials,
|
TRotationFactoryRevokeCredentials,
|
||||||
TRotationFactoryRotateCredentials
|
TRotationFactoryRotateCredentials
|
||||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||||
import { getSqlConnectionClient, SQL_CONNECTION_ALTER_LOGIN_STATEMENT } from "@app/services/app-connection/shared/sql";
|
import {
|
||||||
|
executeWithPotentialGateway,
|
||||||
|
SQL_CONNECTION_ALTER_LOGIN_STATEMENT
|
||||||
|
} from "@app/services/app-connection/shared/sql";
|
||||||
|
|
||||||
import { generatePassword } from "../utils";
|
import { generatePassword } from "../utils";
|
||||||
import {
|
import {
|
||||||
@@ -30,7 +35,7 @@ const redactPasswords = (e: unknown, credentials: TSqlCredentialsRotationGenerat
|
|||||||
export const sqlCredentialsRotationFactory: TRotationFactory<
|
export const sqlCredentialsRotationFactory: TRotationFactory<
|
||||||
TSqlCredentialsRotationWithConnection,
|
TSqlCredentialsRotationWithConnection,
|
||||||
TSqlCredentialsRotationGeneratedCredentials
|
TSqlCredentialsRotationGeneratedCredentials
|
||||||
> = (secretRotation) => {
|
> = (secretRotation, _appConnectionDAL, _kmsService, gatewayService) => {
|
||||||
const {
|
const {
|
||||||
connection,
|
connection,
|
||||||
parameters: { username1, username2 },
|
parameters: { username1, username2 },
|
||||||
@@ -38,29 +43,38 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
|
|||||||
secretsMapping
|
secretsMapping
|
||||||
} = secretRotation;
|
} = secretRotation;
|
||||||
|
|
||||||
const $validateCredentials = async (credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => {
|
const executeOperation = <T>(
|
||||||
const client = await getSqlConnectionClient({
|
operation: (client: Knex) => Promise<T>,
|
||||||
...connection,
|
credentialsOverride?: TSqlCredentialsRotationGeneratedCredentials[number]
|
||||||
credentials: {
|
) => {
|
||||||
...connection.credentials,
|
const finalCredentials = {
|
||||||
...credentials
|
...connection.credentials,
|
||||||
}
|
...credentialsOverride
|
||||||
});
|
};
|
||||||
|
|
||||||
|
return executeWithPotentialGateway(
|
||||||
|
{
|
||||||
|
...connection,
|
||||||
|
credentials: finalCredentials
|
||||||
|
},
|
||||||
|
gatewayService,
|
||||||
|
(client) => operation(client)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const $validateCredentials = async (credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => {
|
||||||
try {
|
try {
|
||||||
await client.raw("SELECT 1");
|
await executeOperation(async (client) => {
|
||||||
|
await client.raw("SELECT 1");
|
||||||
|
}, credentials);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(redactPasswords(error, [credentials]));
|
throw new Error(redactPasswords(error, [credentials]));
|
||||||
} finally {
|
|
||||||
await client.destroy();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const issueCredentials: TRotationFactoryIssueCredentials<TSqlCredentialsRotationGeneratedCredentials> = async (
|
const issueCredentials: TRotationFactoryIssueCredentials<TSqlCredentialsRotationGeneratedCredentials> = async (
|
||||||
callback
|
callback
|
||||||
) => {
|
) => {
|
||||||
const client = await getSqlConnectionClient(connection);
|
|
||||||
|
|
||||||
// For SQL, since we get existing users, we change both their passwords
|
// For SQL, since we get existing users, we change both their passwords
|
||||||
// on issue to invalidate their existing passwords
|
// on issue to invalidate their existing passwords
|
||||||
const credentialsSet = [
|
const credentialsSet = [
|
||||||
@@ -69,15 +83,15 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
|
|||||||
];
|
];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.transaction(async (tx) => {
|
await executeOperation(async (client) => {
|
||||||
for await (const credentials of credentialsSet) {
|
await client.transaction(async (tx) => {
|
||||||
await tx.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials));
|
for await (const credentials of credentialsSet) {
|
||||||
}
|
await tx.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials));
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(redactPasswords(error, credentialsSet));
|
throw new Error(redactPasswords(error, credentialsSet));
|
||||||
} finally {
|
|
||||||
await client.destroy();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for await (const credentials of credentialsSet) {
|
for await (const credentials of credentialsSet) {
|
||||||
@@ -91,21 +105,19 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
|
|||||||
credentialsToRevoke,
|
credentialsToRevoke,
|
||||||
callback
|
callback
|
||||||
) => {
|
) => {
|
||||||
const client = await getSqlConnectionClient(connection);
|
|
||||||
|
|
||||||
const revokedCredentials = credentialsToRevoke.map(({ username }) => ({ username, password: generatePassword() }));
|
const revokedCredentials = credentialsToRevoke.map(({ username }) => ({ username, password: generatePassword() }));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.transaction(async (tx) => {
|
await executeOperation(async (client) => {
|
||||||
for await (const credentials of revokedCredentials) {
|
await client.transaction(async (tx) => {
|
||||||
// invalidate previous passwords
|
for await (const credentials of revokedCredentials) {
|
||||||
await tx.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials));
|
// invalidate previous passwords
|
||||||
}
|
await tx.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials));
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(redactPasswords(error, revokedCredentials));
|
throw new Error(redactPasswords(error, revokedCredentials));
|
||||||
} finally {
|
|
||||||
await client.destroy();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return callback();
|
return callback();
|
||||||
@@ -115,17 +127,15 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
|
|||||||
_,
|
_,
|
||||||
callback
|
callback
|
||||||
) => {
|
) => {
|
||||||
const client = await getSqlConnectionClient(connection);
|
|
||||||
|
|
||||||
// generate new password for the next active user
|
// generate new password for the next active user
|
||||||
const credentials = { username: activeIndex === 0 ? username2 : username1, password: generatePassword() };
|
const credentials = { username: activeIndex === 0 ? username2 : username1, password: generatePassword() };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials));
|
await executeOperation(async (client) => {
|
||||||
|
await client.raw(...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[connection.app](credentials));
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(redactPasswords(error, [credentials]));
|
throw new Error(redactPasswords(error, [credentials]));
|
||||||
} finally {
|
|
||||||
await client.destroy();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await $validateCredentials(credentials);
|
await $validateCredentials(credentials);
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ import { FastifyReply } from "fastify";
|
|||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `aod` (Auth Origin Domain) cookie is used to store the origin domain of the application when user was last authenticated.
|
||||||
|
* This is useful for determining the target domain for authentication redirects, especially in cloud deployments.
|
||||||
|
* It is set only in cloud mode to ensure that the cookie is shared across subdomains.
|
||||||
|
*/
|
||||||
export function addAuthOriginDomainCookie(res: FastifyReply) {
|
export function addAuthOriginDomainCookie(res: FastifyReply) {
|
||||||
try {
|
try {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|||||||
@@ -500,7 +500,6 @@ export const registerRoutes = async (
|
|||||||
permissionService,
|
permissionService,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
licenseDAL,
|
licenseDAL,
|
||||||
keyStore,
|
|
||||||
identityOrgMembershipDAL,
|
identityOrgMembershipDAL,
|
||||||
projectDAL
|
projectDAL
|
||||||
});
|
});
|
||||||
@@ -1706,7 +1705,9 @@ export const registerRoutes = async (
|
|||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
kmsService,
|
kmsService,
|
||||||
licenseService
|
licenseService,
|
||||||
|
gatewayService,
|
||||||
|
gatewayDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretSyncService = secretSyncServiceFactory({
|
const secretSyncService = secretSyncServiceFactory({
|
||||||
@@ -1804,7 +1805,8 @@ export const registerRoutes = async (
|
|||||||
snapshotService,
|
snapshotService,
|
||||||
secretQueueService,
|
secretQueueService,
|
||||||
queueService,
|
queueService,
|
||||||
appConnectionDAL
|
appConnectionDAL,
|
||||||
|
gatewayService
|
||||||
});
|
});
|
||||||
|
|
||||||
const certificateAuthorityService = certificateAuthorityServiceFactory({
|
const certificateAuthorityService = certificateAuthorityServiceFactory({
|
||||||
|
|||||||
@@ -25,12 +25,14 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
|
|||||||
credentials: I["credentials"];
|
credentials: I["credentials"];
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
isPlatformManagedCredentials?: boolean;
|
isPlatformManagedCredentials?: boolean;
|
||||||
|
gatewayId?: string | null;
|
||||||
}>;
|
}>;
|
||||||
updateSchema: z.ZodType<{
|
updateSchema: z.ZodType<{
|
||||||
name?: string;
|
name?: string;
|
||||||
credentials?: I["credentials"];
|
credentials?: I["credentials"];
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
isPlatformManagedCredentials?: boolean;
|
isPlatformManagedCredentials?: boolean;
|
||||||
|
gatewayId?: string | null;
|
||||||
}>;
|
}>;
|
||||||
sanitizedResponseSchema: z.ZodTypeAny;
|
sanitizedResponseSchema: z.ZodTypeAny;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -224,10 +226,10 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { name, method, credentials, description, isPlatformManagedCredentials } = req.body;
|
const { name, method, credentials, description, isPlatformManagedCredentials, gatewayId } = req.body;
|
||||||
|
|
||||||
const appConnection = (await server.services.appConnection.createAppConnection(
|
const appConnection = (await server.services.appConnection.createAppConnection(
|
||||||
{ name, method, app, credentials, description, isPlatformManagedCredentials },
|
{ name, method, app, credentials, description, isPlatformManagedCredentials, gatewayId },
|
||||||
req.permission
|
req.permission
|
||||||
)) as T;
|
)) as T;
|
||||||
|
|
||||||
@@ -270,11 +272,11 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { name, credentials, description, isPlatformManagedCredentials } = req.body;
|
const { name, credentials, description, isPlatformManagedCredentials, gatewayId } = req.body;
|
||||||
const { connectionId } = req.params;
|
const { connectionId } = req.params;
|
||||||
|
|
||||||
const appConnection = (await server.services.appConnection.updateAppConnection(
|
const appConnection = (await server.services.appConnection.updateAppConnection(
|
||||||
{ name, credentials, connectionId, description, isPlatformManagedCredentials },
|
{ name, credentials, connectionId, description, isPlatformManagedCredentials, gatewayId },
|
||||||
req.permission
|
req.permission
|
||||||
)) as T;
|
)) as T;
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,14 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
|||||||
maxAge: 0
|
maxAge: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
|
void res.cookie("aod", "", {
|
||||||
|
httpOnly: false,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "lax",
|
||||||
|
secure: appCfg.HTTPS_ENABLED,
|
||||||
|
maxAge: 0
|
||||||
|
});
|
||||||
|
|
||||||
return { message: "Successfully logged out" };
|
return { message: "Successfully logged out" };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
validateOCIConnectionCredentials
|
validateOCIConnectionCredentials
|
||||||
} from "@app/ee/services/app-connections/oci";
|
} from "@app/ee/services/app-connections/oci";
|
||||||
import { getOracleDBConnectionListItem, OracleDBConnectionMethod } from "@app/ee/services/app-connections/oracledb";
|
import { getOracleDBConnectionListItem, OracleDBConnectionMethod } from "@app/ee/services/app-connections/oracledb";
|
||||||
|
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { crypto } from "@app/lib/crypto/cryptography";
|
import { crypto } from "@app/lib/crypto/cryptography";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
@@ -203,7 +204,8 @@ export const decryptAppConnectionCredentials = async ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const validateAppConnectionCredentials = async (
|
export const validateAppConnectionCredentials = async (
|
||||||
appConnection: TAppConnectionConfig
|
appConnection: TAppConnectionConfig,
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
|
||||||
): Promise<TAppConnection["credentials"]> => {
|
): Promise<TAppConnection["credentials"]> => {
|
||||||
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TAppConnectionCredentialsValidator> = {
|
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TAppConnectionCredentialsValidator> = {
|
||||||
[AppConnection.AWS]: validateAwsConnectionCredentials as TAppConnectionCredentialsValidator,
|
[AppConnection.AWS]: validateAwsConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||||
@@ -245,7 +247,7 @@ export const validateAppConnectionCredentials = async (
|
|||||||
[AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator
|
[AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator
|
||||||
};
|
};
|
||||||
|
|
||||||
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
|
return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAppConnectionMethodName = (method: TAppConnection["method"]) => {
|
export const getAppConnectionMethodName = (method: TAppConnection["method"]) => {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const BaseAppConnectionSchema = AppConnectionsSchema.omit({
|
|||||||
|
|
||||||
export const GenericCreateAppConnectionFieldsSchema = (
|
export const GenericCreateAppConnectionFieldsSchema = (
|
||||||
app: AppConnection,
|
app: AppConnection,
|
||||||
{ supportsPlatformManagedCredentials = false }: TAppConnectionBaseConfig = {}
|
{ supportsPlatformManagedCredentials = false, supportsGateways = false }: TAppConnectionBaseConfig = {}
|
||||||
) =>
|
) =>
|
||||||
z.object({
|
z.object({
|
||||||
name: slugSchema({ field: "name" }).describe(AppConnections.CREATE(app).name),
|
name: slugSchema({ field: "name" }).describe(AppConnections.CREATE(app).name),
|
||||||
@@ -30,12 +30,23 @@ export const GenericCreateAppConnectionFieldsSchema = (
|
|||||||
.describe(AppConnections.CREATE(app).description),
|
.describe(AppConnections.CREATE(app).description),
|
||||||
isPlatformManagedCredentials: supportsPlatformManagedCredentials
|
isPlatformManagedCredentials: supportsPlatformManagedCredentials
|
||||||
? z.boolean().optional().default(false).describe(AppConnections.CREATE(app).isPlatformManagedCredentials)
|
? z.boolean().optional().default(false).describe(AppConnections.CREATE(app).isPlatformManagedCredentials)
|
||||||
: z.literal(false).optional().describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`)
|
: z
|
||||||
|
.literal(false, {
|
||||||
|
errorMap: () => ({ message: `Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections` })
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`),
|
||||||
|
gatewayId: supportsGateways
|
||||||
|
? z.string().uuid().nullish().describe("The Gateway ID to use for this connection.")
|
||||||
|
: z
|
||||||
|
.undefined({ message: `Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections` })
|
||||||
|
.or(z.null({ message: `Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections` }))
|
||||||
|
.describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const GenericUpdateAppConnectionFieldsSchema = (
|
export const GenericUpdateAppConnectionFieldsSchema = (
|
||||||
app: AppConnection,
|
app: AppConnection,
|
||||||
{ supportsPlatformManagedCredentials = false }: TAppConnectionBaseConfig = {}
|
{ supportsPlatformManagedCredentials = false, supportsGateways = false }: TAppConnectionBaseConfig = {}
|
||||||
) =>
|
) =>
|
||||||
z.object({
|
z.object({
|
||||||
name: slugSchema({ field: "name" }).describe(AppConnections.UPDATE(app).name).optional(),
|
name: slugSchema({ field: "name" }).describe(AppConnections.UPDATE(app).name).optional(),
|
||||||
@@ -47,5 +58,16 @@ export const GenericUpdateAppConnectionFieldsSchema = (
|
|||||||
.describe(AppConnections.UPDATE(app).description),
|
.describe(AppConnections.UPDATE(app).description),
|
||||||
isPlatformManagedCredentials: supportsPlatformManagedCredentials
|
isPlatformManagedCredentials: supportsPlatformManagedCredentials
|
||||||
? z.boolean().optional().describe(AppConnections.UPDATE(app).isPlatformManagedCredentials)
|
? z.boolean().optional().describe(AppConnections.UPDATE(app).isPlatformManagedCredentials)
|
||||||
: z.literal(false).optional().describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`)
|
: z
|
||||||
|
.literal(false, {
|
||||||
|
errorMap: () => ({ message: `Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections` })
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`),
|
||||||
|
gatewayId: supportsGateways
|
||||||
|
? z.string().uuid().nullish().describe("The Gateway ID to use for this connection.")
|
||||||
|
: z
|
||||||
|
.undefined({ message: `Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections` })
|
||||||
|
.or(z.null({ message: `Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections` }))
|
||||||
|
.describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`)
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,8 +3,14 @@ import { ForbiddenError, subject } from "@casl/ability";
|
|||||||
import { ValidateOCIConnectionCredentialsSchema } from "@app/ee/services/app-connections/oci";
|
import { ValidateOCIConnectionCredentialsSchema } from "@app/ee/services/app-connections/oci";
|
||||||
import { ociConnectionService } from "@app/ee/services/app-connections/oci/oci-connection-service";
|
import { ociConnectionService } from "@app/ee/services/app-connections/oci/oci-connection-service";
|
||||||
import { ValidateOracleDBConnectionCredentialsSchema } from "@app/ee/services/app-connections/oracledb";
|
import { ValidateOracleDBConnectionCredentialsSchema } from "@app/ee/services/app-connections/oracledb";
|
||||||
|
import { TGatewayDALFactory } from "@app/ee/services/gateway/gateway-dal";
|
||||||
|
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { OrgPermissionAppConnectionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import {
|
||||||
|
OrgPermissionAppConnectionActions,
|
||||||
|
OrgPermissionGatewayActions,
|
||||||
|
OrgPermissionSubjects
|
||||||
|
} from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||||
import { crypto } from "@app/lib/crypto/cryptography";
|
import { crypto } from "@app/lib/crypto/cryptography";
|
||||||
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||||
@@ -98,6 +104,8 @@ export type TAppConnectionServiceFactoryDep = {
|
|||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
||||||
|
gatewayDAL: Pick<TGatewayDALFactory, "find">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServiceFactory>;
|
export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServiceFactory>;
|
||||||
@@ -144,7 +152,9 @@ export const appConnectionServiceFactory = ({
|
|||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
kmsService,
|
kmsService,
|
||||||
licenseService
|
licenseService,
|
||||||
|
gatewayService,
|
||||||
|
gatewayDAL
|
||||||
}: TAppConnectionServiceFactoryDep) => {
|
}: TAppConnectionServiceFactoryDep) => {
|
||||||
const listAppConnectionsByOrg = async (actor: OrgServiceActor, app?: AppConnection) => {
|
const listAppConnectionsByOrg = async (actor: OrgServiceActor, app?: AppConnection) => {
|
||||||
const { permission } = await permissionService.getOrgPermission(
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
@@ -225,7 +235,7 @@ export const appConnectionServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createAppConnection = async (
|
const createAppConnection = async (
|
||||||
{ method, app, credentials, ...params }: TCreateAppConnectionDTO,
|
{ method, app, credentials, gatewayId, ...params }: TCreateAppConnectionDTO,
|
||||||
actor: OrgServiceActor
|
actor: OrgServiceActor
|
||||||
) => {
|
) => {
|
||||||
const { permission } = await permissionService.getOrgPermission(
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
@@ -241,6 +251,20 @@ export const appConnectionServiceFactory = ({
|
|||||||
OrgPermissionSubjects.AppConnections
|
OrgPermissionSubjects.AppConnections
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (gatewayId) {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionGatewayActions.AttachGateways,
|
||||||
|
OrgPermissionSubjects.Gateway
|
||||||
|
);
|
||||||
|
|
||||||
|
const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actor.orgId });
|
||||||
|
if (!gateway) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Gateway with ID ${gatewayId} not found for org`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await enterpriseAppCheck(
|
await enterpriseAppCheck(
|
||||||
licenseService,
|
licenseService,
|
||||||
app,
|
app,
|
||||||
@@ -248,12 +272,16 @@ export const appConnectionServiceFactory = ({
|
|||||||
"Failed to create app connection due to plan restriction. Upgrade plan to access enterprise app connections."
|
"Failed to create app connection due to plan restriction. Upgrade plan to access enterprise app connections."
|
||||||
);
|
);
|
||||||
|
|
||||||
const validatedCredentials = await validateAppConnectionCredentials({
|
const validatedCredentials = await validateAppConnectionCredentials(
|
||||||
app,
|
{
|
||||||
credentials,
|
app,
|
||||||
method,
|
credentials,
|
||||||
orgId: actor.orgId
|
method,
|
||||||
} as TAppConnectionConfig);
|
orgId: actor.orgId,
|
||||||
|
gatewayId
|
||||||
|
} as TAppConnectionConfig,
|
||||||
|
gatewayService
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const createConnection = async (connectionCredentials: TAppConnection["credentials"]) => {
|
const createConnection = async (connectionCredentials: TAppConnection["credentials"]) => {
|
||||||
@@ -268,6 +296,7 @@ export const appConnectionServiceFactory = ({
|
|||||||
encryptedCredentials,
|
encryptedCredentials,
|
||||||
method,
|
method,
|
||||||
app,
|
app,
|
||||||
|
gatewayId,
|
||||||
...params
|
...params
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -280,9 +309,11 @@ export const appConnectionServiceFactory = ({
|
|||||||
app,
|
app,
|
||||||
orgId: actor.orgId,
|
orgId: actor.orgId,
|
||||||
credentials: validatedCredentials,
|
credentials: validatedCredentials,
|
||||||
method
|
method,
|
||||||
|
gatewayId
|
||||||
} as TAppConnectionConfig,
|
} as TAppConnectionConfig,
|
||||||
(platformCredentials) => createConnection(platformCredentials)
|
(platformCredentials) => createConnection(platformCredentials),
|
||||||
|
gatewayService
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
connection = await createConnection(validatedCredentials);
|
connection = await createConnection(validatedCredentials);
|
||||||
@@ -303,7 +334,7 @@ export const appConnectionServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateAppConnection = async (
|
const updateAppConnection = async (
|
||||||
{ connectionId, credentials, ...params }: TUpdateAppConnectionDTO,
|
{ connectionId, credentials, gatewayId, ...params }: TUpdateAppConnectionDTO,
|
||||||
actor: OrgServiceActor
|
actor: OrgServiceActor
|
||||||
) => {
|
) => {
|
||||||
const appConnection = await appConnectionDAL.findById(connectionId);
|
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||||
@@ -330,6 +361,22 @@ export const appConnectionServiceFactory = ({
|
|||||||
OrgPermissionSubjects.AppConnections
|
OrgPermissionSubjects.AppConnections
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (gatewayId !== appConnection.gatewayId) {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionGatewayActions.AttachGateways,
|
||||||
|
OrgPermissionSubjects.Gateway
|
||||||
|
);
|
||||||
|
|
||||||
|
if (gatewayId) {
|
||||||
|
const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actor.orgId });
|
||||||
|
if (!gateway) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Gateway with ID ${gatewayId} not found for org`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// prevent updating credentials or management status if platform managed
|
// prevent updating credentials or management status if platform managed
|
||||||
if (appConnection.isPlatformManagedCredentials && (params.isPlatformManagedCredentials === false || credentials)) {
|
if (appConnection.isPlatformManagedCredentials && (params.isPlatformManagedCredentials === false || credentials)) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
@@ -354,12 +401,16 @@ export const appConnectionServiceFactory = ({
|
|||||||
} Connection with method ${getAppConnectionMethodName(method)}`
|
} Connection with method ${getAppConnectionMethodName(method)}`
|
||||||
});
|
});
|
||||||
|
|
||||||
updatedCredentials = await validateAppConnectionCredentials({
|
updatedCredentials = await validateAppConnectionCredentials(
|
||||||
app,
|
{
|
||||||
orgId: actor.orgId,
|
app,
|
||||||
credentials,
|
orgId: actor.orgId,
|
||||||
method
|
credentials,
|
||||||
} as TAppConnectionConfig);
|
method,
|
||||||
|
gatewayId
|
||||||
|
} as TAppConnectionConfig,
|
||||||
|
gatewayService
|
||||||
|
);
|
||||||
|
|
||||||
if (!updatedCredentials)
|
if (!updatedCredentials)
|
||||||
throw new BadRequestError({ message: "Unable to validate connection - check credentials" });
|
throw new BadRequestError({ message: "Unable to validate connection - check credentials" });
|
||||||
@@ -378,6 +429,7 @@ export const appConnectionServiceFactory = ({
|
|||||||
return appConnectionDAL.updateById(connectionId, {
|
return appConnectionDAL.updateById(connectionId, {
|
||||||
orgId: actor.orgId,
|
orgId: actor.orgId,
|
||||||
encryptedCredentials,
|
encryptedCredentials,
|
||||||
|
gatewayId,
|
||||||
...params
|
...params
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -394,9 +446,11 @@ export const appConnectionServiceFactory = ({
|
|||||||
app,
|
app,
|
||||||
orgId: actor.orgId,
|
orgId: actor.orgId,
|
||||||
credentials: updatedCredentials,
|
credentials: updatedCredentials,
|
||||||
method
|
method,
|
||||||
|
gatewayId
|
||||||
} as TAppConnectionConfig,
|
} as TAppConnectionConfig,
|
||||||
(platformCredentials) => updateConnection(platformCredentials)
|
(platformCredentials) => updateConnection(platformCredentials),
|
||||||
|
gatewayService
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
updatedConnection = await updateConnection(updatedCredentials);
|
updatedConnection = await updateConnection(updatedCredentials);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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 { 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";
|
||||||
@@ -290,7 +291,7 @@ export type TSqlConnectionInput =
|
|||||||
|
|
||||||
export type TCreateAppConnectionDTO = Pick<
|
export type TCreateAppConnectionDTO = Pick<
|
||||||
TAppConnectionInput,
|
TAppConnectionInput,
|
||||||
"credentials" | "method" | "name" | "app" | "description" | "isPlatformManagedCredentials"
|
"credentials" | "method" | "name" | "app" | "description" | "isPlatformManagedCredentials" | "gatewayId"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type TUpdateAppConnectionDTO = Partial<Omit<TCreateAppConnectionDTO, "method" | "app">> & {
|
export type TUpdateAppConnectionDTO = Partial<Omit<TCreateAppConnectionDTO, "method" | "app">> & {
|
||||||
@@ -379,14 +380,17 @@ export type TListAwsConnectionIamUsers = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type TAppConnectionCredentialsValidator = (
|
export type TAppConnectionCredentialsValidator = (
|
||||||
appConnection: TAppConnectionConfig
|
appConnection: TAppConnectionConfig,
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
|
||||||
) => Promise<TAppConnection["credentials"]>;
|
) => Promise<TAppConnection["credentials"]>;
|
||||||
|
|
||||||
export type TAppConnectionTransitionCredentialsToPlatform = (
|
export type TAppConnectionTransitionCredentialsToPlatform = (
|
||||||
appConnection: TAppConnectionConfig,
|
appConnection: TAppConnectionConfig,
|
||||||
callback: (credentials: TAppConnection["credentials"]) => Promise<TAppConnectionRaw>
|
callback: (credentials: TAppConnection["credentials"]) => Promise<TAppConnectionRaw>,
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
|
||||||
) => Promise<TAppConnectionRaw>;
|
) => Promise<TAppConnectionRaw>;
|
||||||
|
|
||||||
export type TAppConnectionBaseConfig = {
|
export type TAppConnectionBaseConfig = {
|
||||||
supportsPlatformManagedCredentials?: boolean;
|
supportsPlatformManagedCredentials?: boolean;
|
||||||
|
supportsGateways?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { getAppConnectionMethodName } from "@app/services/app-connection/app-con
|
|||||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||||
|
|
||||||
import { AppConnection } from "../app-connection-enums";
|
import { AppConnection } from "../app-connection-enums";
|
||||||
|
import { GithubTokenRespData, isGithubErrorResponse } from "../github/github-connection-fns";
|
||||||
import { GitHubRadarConnectionMethod } from "./github-radar-connection-enums";
|
import { GitHubRadarConnectionMethod } from "./github-radar-connection-enums";
|
||||||
import {
|
import {
|
||||||
TGitHubRadarConnection,
|
TGitHubRadarConnection,
|
||||||
@@ -71,13 +72,6 @@ export const listGitHubRadarRepositories = async (appConnection: TGitHubRadarCon
|
|||||||
return repositories;
|
return repositories;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TokenRespData = {
|
|
||||||
access_token: string;
|
|
||||||
scope: string;
|
|
||||||
token_type: string;
|
|
||||||
error?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const validateGitHubRadarConnectionCredentials = async (config: TGitHubRadarConnectionConfig) => {
|
export const validateGitHubRadarConnectionCredentials = async (config: TGitHubRadarConnectionConfig) => {
|
||||||
const { credentials, method } = config;
|
const { credentials, method } = config;
|
||||||
|
|
||||||
@@ -93,10 +87,10 @@ export const validateGitHubRadarConnectionCredentials = async (config: TGitHubRa
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let tokenResp: AxiosResponse<TokenRespData>;
|
let tokenResp: AxiosResponse<GithubTokenRespData>;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
tokenResp = await request.get<TokenRespData>("https://github.com/login/oauth/access_token", {
|
tokenResp = await request.get<GithubTokenRespData>("https://github.com/login/oauth/access_token", {
|
||||||
params: {
|
params: {
|
||||||
client_id: INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_ID,
|
client_id: INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_ID,
|
||||||
client_secret: INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_SECRET,
|
client_secret: INF_APP_CONNECTION_GITHUB_RADAR_APP_CLIENT_SECRET,
|
||||||
@@ -108,19 +102,27 @@ export const validateGitHubRadarConnectionCredentials = async (config: TGitHubRa
|
|||||||
"Accept-Encoding": "application/json"
|
"Accept-Encoding": "application/json"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (isGithubErrorResponse(tokenResp?.data)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unable to validate credentials: GitHub responded with an error: ${tokenResp.data.error} - ${tokenResp.data.error_description}`
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof BadRequestError) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: `Unable to validate connection: verify credentials`
|
message: `Unable to validate connection: verify credentials`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tokenResp.status !== 200) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: `Unable to validate credentials: GitHub responded with a status code of ${tokenResp.status} (${tokenResp.statusText}). Verify credentials and try again.`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (method === GitHubRadarConnectionMethod.App) {
|
if (method === GitHubRadarConnectionMethod.App) {
|
||||||
|
if (!tokenResp.data.access_token) {
|
||||||
|
throw new InternalServerError({ message: `Missing access token: ${tokenResp.data.error}` });
|
||||||
|
}
|
||||||
|
|
||||||
const installationsResp = await request.get<{
|
const installationsResp = await request.get<{
|
||||||
installations: {
|
installations: {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -149,10 +151,6 @@ export const validateGitHubRadarConnectionCredentials = async (config: TGitHubRa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tokenResp.data.access_token) {
|
|
||||||
throw new InternalServerError({ message: `Missing access token: ${tokenResp.data.error}` });
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case GitHubRadarConnectionMethod.App:
|
case GitHubRadarConnectionMethod.App:
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -144,14 +144,14 @@ export const getGitHubEnvironments = async (appConnection: TGitHubConnection, ow
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
type TokenRespData = {
|
export type GithubTokenRespData = {
|
||||||
access_token?: string;
|
access_token?: string;
|
||||||
scope: string;
|
scope: string;
|
||||||
token_type: string;
|
token_type: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function isErrorResponse(data: TokenRespData): data is TokenRespData & {
|
export function isGithubErrorResponse(data: GithubTokenRespData): data is GithubTokenRespData & {
|
||||||
error: string;
|
error: string;
|
||||||
error_description: string;
|
error_description: string;
|
||||||
error_uri: string;
|
error_uri: string;
|
||||||
@@ -191,10 +191,10 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let tokenResp: AxiosResponse<TokenRespData>;
|
let tokenResp: AxiosResponse<GithubTokenRespData>;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
tokenResp = await request.get<TokenRespData>("https://github.com/login/oauth/access_token", {
|
tokenResp = await request.get<GithubTokenRespData>("https://github.com/login/oauth/access_token", {
|
||||||
params: {
|
params: {
|
||||||
client_id: clientId,
|
client_id: clientId,
|
||||||
client_secret: clientSecret,
|
client_secret: clientSecret,
|
||||||
@@ -207,7 +207,7 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isErrorResponse(tokenResp?.data)) {
|
if (isGithubErrorResponse(tokenResp?.data)) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: `Unable to validate credentials: GitHub responded with an error: ${tokenResp.data.error} - ${tokenResp.data.error_description}`
|
message: `Unable to validate credentials: GitHub responded with an error: ${tokenResp.data.error} - ${tokenResp.data.error_description}`
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -49,7 +49,10 @@ export const ValidateMsSqlConnectionCredentialsSchema = z.discriminatedUnion("me
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
export const CreateMsSqlConnectionSchema = ValidateMsSqlConnectionCredentialsSchema.and(
|
export const CreateMsSqlConnectionSchema = ValidateMsSqlConnectionCredentialsSchema.and(
|
||||||
GenericCreateAppConnectionFieldsSchema(AppConnection.MsSql, { supportsPlatformManagedCredentials: true })
|
GenericCreateAppConnectionFieldsSchema(AppConnection.MsSql, {
|
||||||
|
supportsPlatformManagedCredentials: true,
|
||||||
|
supportsGateways: true
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
export const UpdateMsSqlConnectionSchema = z
|
export const UpdateMsSqlConnectionSchema = z
|
||||||
@@ -58,7 +61,12 @@ export const UpdateMsSqlConnectionSchema = z
|
|||||||
AppConnections.UPDATE(AppConnection.MsSql).credentials
|
AppConnections.UPDATE(AppConnection.MsSql).credentials
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.MsSql, { supportsPlatformManagedCredentials: true }));
|
.and(
|
||||||
|
GenericUpdateAppConnectionFieldsSchema(AppConnection.MsSql, {
|
||||||
|
supportsPlatformManagedCredentials: true,
|
||||||
|
supportsGateways: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
export const MsSqlConnectionListItemSchema = z.object({
|
export const MsSqlConnectionListItemSchema = z.object({
|
||||||
name: z.literal("Microsoft SQL Server"),
|
name: z.literal("Microsoft SQL Server"),
|
||||||
|
|||||||
@@ -47,7 +47,10 @@ export const ValidateMySqlConnectionCredentialsSchema = z.discriminatedUnion("me
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
export const CreateMySqlConnectionSchema = ValidateMySqlConnectionCredentialsSchema.and(
|
export const CreateMySqlConnectionSchema = ValidateMySqlConnectionCredentialsSchema.and(
|
||||||
GenericCreateAppConnectionFieldsSchema(AppConnection.MySql, { supportsPlatformManagedCredentials: true })
|
GenericCreateAppConnectionFieldsSchema(AppConnection.MySql, {
|
||||||
|
supportsPlatformManagedCredentials: true,
|
||||||
|
supportsGateways: true
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
export const UpdateMySqlConnectionSchema = z
|
export const UpdateMySqlConnectionSchema = z
|
||||||
@@ -56,7 +59,12 @@ export const UpdateMySqlConnectionSchema = z
|
|||||||
AppConnections.UPDATE(AppConnection.MySql).credentials
|
AppConnections.UPDATE(AppConnection.MySql).credentials
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.MySql, { supportsPlatformManagedCredentials: true }));
|
.and(
|
||||||
|
GenericUpdateAppConnectionFieldsSchema(AppConnection.MySql, {
|
||||||
|
supportsPlatformManagedCredentials: true,
|
||||||
|
supportsGateways: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
export const MySqlConnectionListItemSchema = z.object({
|
export const MySqlConnectionListItemSchema = z.object({
|
||||||
name: z.literal("MySQL"),
|
name: z.literal("MySQL"),
|
||||||
|
|||||||
@@ -47,7 +47,10 @@ export const ValidatePostgresConnectionCredentialsSchema = z.discriminatedUnion(
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
export const CreatePostgresConnectionSchema = ValidatePostgresConnectionCredentialsSchema.and(
|
export const CreatePostgresConnectionSchema = ValidatePostgresConnectionCredentialsSchema.and(
|
||||||
GenericCreateAppConnectionFieldsSchema(AppConnection.Postgres, { supportsPlatformManagedCredentials: true })
|
GenericCreateAppConnectionFieldsSchema(AppConnection.Postgres, {
|
||||||
|
supportsPlatformManagedCredentials: true,
|
||||||
|
supportsGateways: true
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
export const UpdatePostgresConnectionSchema = z
|
export const UpdatePostgresConnectionSchema = z
|
||||||
@@ -56,7 +59,12 @@ export const UpdatePostgresConnectionSchema = z
|
|||||||
AppConnections.UPDATE(AppConnection.Postgres).credentials
|
AppConnections.UPDATE(AppConnection.Postgres).credentials
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Postgres, { supportsPlatformManagedCredentials: true }));
|
.and(
|
||||||
|
GenericUpdateAppConnectionFieldsSchema(AppConnection.Postgres, {
|
||||||
|
supportsPlatformManagedCredentials: true,
|
||||||
|
supportsGateways: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
export const PostgresConnectionListItemSchema = z.object({
|
export const PostgresConnectionListItemSchema = z.object({
|
||||||
name: z.literal("PostgreSQL"),
|
name: z.literal("PostgreSQL"),
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import knex, { Knex } from "knex";
|
import knex, { Knex } from "knex";
|
||||||
|
|
||||||
import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
|
import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
|
||||||
|
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||||
import {
|
import {
|
||||||
TSqlCredentialsRotationGeneratedCredentials,
|
TSqlCredentialsRotationGeneratedCredentials,
|
||||||
TSqlCredentialsRotationWithConnection
|
TSqlCredentialsRotationWithConnection
|
||||||
} from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-types";
|
} from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-types";
|
||||||
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
||||||
|
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
import { TAppConnectionRaw, TSqlConnection } from "@app/services/app-connection/app-connection-types";
|
import { TAppConnectionRaw, TSqlConnection } from "@app/services/app-connection/app-connection-types";
|
||||||
@@ -98,25 +100,80 @@ export const getSqlConnectionClient = async (appConnection: Pick<TSqlConnection,
|
|||||||
return client;
|
return client;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const validateSqlConnectionCredentials = async (config: TSqlConnectionConfig) => {
|
export const executeWithPotentialGateway = async <T>(
|
||||||
const { credentials, app } = config;
|
config: TSqlConnectionConfig,
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
|
||||||
|
operation: (client: Knex) => Promise<T>
|
||||||
|
): Promise<T> => {
|
||||||
|
const { credentials, app, gatewayId } = config;
|
||||||
|
|
||||||
let client: Knex | undefined;
|
if (gatewayId && gatewayService) {
|
||||||
|
const [targetHost] = await verifyHostInputValidity(credentials.host, true);
|
||||||
|
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(gatewayId);
|
||||||
|
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
|
||||||
|
|
||||||
|
return withGatewayProxy(
|
||||||
|
async (proxyPort) => {
|
||||||
|
const client = knex({
|
||||||
|
client: SQL_CONNECTION_CLIENT_MAP[app],
|
||||||
|
connection: {
|
||||||
|
database: credentials.database,
|
||||||
|
port: proxyPort,
|
||||||
|
host: "localhost",
|
||||||
|
user: credentials.username,
|
||||||
|
password: credentials.password,
|
||||||
|
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
|
||||||
|
...getConnectionConfig({ app, credentials })
|
||||||
|
}
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
return await operation(client);
|
||||||
|
} finally {
|
||||||
|
await client.destroy();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: GatewayProxyProtocol.Tcp,
|
||||||
|
targetHost,
|
||||||
|
targetPort: credentials.port,
|
||||||
|
relayHost,
|
||||||
|
relayPort: Number(relayPort),
|
||||||
|
identityId: relayDetails.identityId,
|
||||||
|
orgId: relayDetails.orgId,
|
||||||
|
tlsOptions: {
|
||||||
|
ca: relayDetails.certChain,
|
||||||
|
cert: relayDetails.certificate,
|
||||||
|
key: relayDetails.privateKey.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-gateway path
|
||||||
|
const client = await getSqlConnectionClient({ app, credentials });
|
||||||
try {
|
try {
|
||||||
client = await getSqlConnectionClient({ app, credentials });
|
return await operation(client);
|
||||||
|
} finally {
|
||||||
|
await client.destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
await client.raw(`Select 1`);
|
export const validateSqlConnectionCredentials = async (
|
||||||
|
config: TSqlConnectionConfig,
|
||||||
return credentials;
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
await executeWithPotentialGateway(config, gatewayService, async (client) => {
|
||||||
|
await client.raw(`Select 1`);
|
||||||
|
});
|
||||||
|
return config.credentials;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: `Unable to validate connection: ${
|
message: `Unable to validate connection: ${
|
||||||
(error as Error)?.message?.replaceAll(credentials.password, "********************") ?? "verify credentials"
|
(error as Error)?.message?.replaceAll(config.credentials.password, "********************") ??
|
||||||
|
"verify credentials"
|
||||||
}`
|
}`
|
||||||
});
|
});
|
||||||
} finally {
|
|
||||||
await client?.destroy();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -132,22 +189,23 @@ export const SQL_CONNECTION_ALTER_LOGIN_STATEMENT: Record<
|
|||||||
|
|
||||||
export const transferSqlConnectionCredentialsToPlatform = async (
|
export const transferSqlConnectionCredentialsToPlatform = async (
|
||||||
config: TSqlConnectionConfig,
|
config: TSqlConnectionConfig,
|
||||||
callback: (credentials: TSqlConnectionConfig["credentials"]) => Promise<TAppConnectionRaw>
|
callback: (credentials: TSqlConnectionConfig["credentials"]) => Promise<TAppConnectionRaw>,
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
|
||||||
) => {
|
) => {
|
||||||
const { credentials, app } = config;
|
const { credentials, app } = config;
|
||||||
|
|
||||||
const client = await getSqlConnectionClient({ app, credentials });
|
|
||||||
|
|
||||||
const newPassword = alphaNumericNanoId(32);
|
const newPassword = alphaNumericNanoId(32);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await client.transaction(async (tx) => {
|
return await executeWithPotentialGateway(config, gatewayService, (client) => {
|
||||||
await tx.raw(
|
return client.transaction(async (tx) => {
|
||||||
...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[app]({ username: credentials.username, password: newPassword })
|
await tx.raw(
|
||||||
);
|
...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[app]({ username: credentials.username, password: newPassword })
|
||||||
return callback({
|
);
|
||||||
...credentials,
|
return callback({
|
||||||
password: newPassword
|
...credentials,
|
||||||
|
password: newPassword
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -161,7 +219,5 @@ export const transferSqlConnectionCredentialsToPlatform = async (
|
|||||||
(error as Error)?.message?.replaceAll(newPassword, "********************") ??
|
(error as Error)?.message?.replaceAll(newPassword, "********************") ??
|
||||||
"Encountered an error transferring credentials to platform"
|
"Encountered an error transferring credentials to platform"
|
||||||
});
|
});
|
||||||
} finally {
|
|
||||||
await client.destroy();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { DiscriminativePick } from "@app/lib/types";
|
import { DiscriminativePick } from "@app/lib/types";
|
||||||
import { TSqlConnectionInput } from "@app/services/app-connection/app-connection-types";
|
import { TSqlConnectionInput } from "@app/services/app-connection/app-connection-types";
|
||||||
|
|
||||||
export type TSqlConnectionConfig = DiscriminativePick<TSqlConnectionInput, "method" | "app" | "credentials"> & {
|
export type TSqlConnectionConfig = DiscriminativePick<
|
||||||
|
TSqlConnectionInput,
|
||||||
|
"method" | "app" | "credentials" | "gatewayId"
|
||||||
|
> & {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -161,8 +161,8 @@ type TProjectServiceFactoryDep = {
|
|||||||
sshHostGroupDAL: Pick<TSshHostGroupDALFactory, "find" | "findSshHostGroupsWithLoginMappings">;
|
sshHostGroupDAL: Pick<TSshHostGroupDALFactory, "find" | "findSshHostGroupsWithLoginMappings">;
|
||||||
permissionService: TPermissionServiceFactory;
|
permissionService: TPermissionServiceFactory;
|
||||||
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "invalidateGetPlan">;
|
|
||||||
queueService: Pick<TQueueServiceFactory, "stopRepeatableJob">;
|
queueService: Pick<TQueueServiceFactory, "stopRepeatableJob">;
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
smtpService: Pick<TSmtpService, "sendMail">;
|
smtpService: Pick<TSmtpService, "sendMail">;
|
||||||
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
||||||
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
|
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
|
||||||
@@ -489,10 +489,6 @@ export const projectServiceFactory = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// no need to invalidate if there was no limit
|
|
||||||
if (plan.workspaceLimit) {
|
|
||||||
await licenseService.invalidateGetPlan(organization.id);
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
...project,
|
...project,
|
||||||
environments: envs,
|
environments: envs,
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
|
import { isAxiosError } from "axios";
|
||||||
|
|
||||||
import { request } from "@app/lib/config/request";
|
import { request } from "@app/lib/config/request";
|
||||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||||
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
|
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
|
||||||
@@ -71,7 +73,7 @@ const putEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, secr
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, secret: TRenderSecret) => {
|
const deleteEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, secret: Pick<TRenderSecret, "key">) => {
|
||||||
const {
|
const {
|
||||||
destinationConfig,
|
destinationConfig,
|
||||||
connection: {
|
connection: {
|
||||||
@@ -79,15 +81,24 @@ const deleteEnvironmentSecret = async (secretSync: TRenderSyncWithCredentials, s
|
|||||||
}
|
}
|
||||||
} = secretSync;
|
} = secretSync;
|
||||||
|
|
||||||
await request.delete(
|
try {
|
||||||
`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars/${secret.key}`,
|
await request.delete(
|
||||||
{
|
`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars/${secret.key}`,
|
||||||
headers: {
|
{
|
||||||
Authorization: `Bearer ${apiKey}`,
|
headers: {
|
||||||
Accept: "application/json"
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
Accept: "application/json"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (isAxiosError(error) && error.response?.status === 404) {
|
||||||
|
// If the secret does not exist, we can ignore this error
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const sleep = async () =>
|
const sleep = async () =>
|
||||||
@@ -99,6 +110,11 @@ export const RenderSyncFns = {
|
|||||||
syncSecrets: async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap) => {
|
syncSecrets: async (secretSync: TRenderSyncWithCredentials, secretMap: TSecretMap) => {
|
||||||
const renderSecrets = await getRenderEnvironmentSecrets(secretSync);
|
const renderSecrets = await getRenderEnvironmentSecrets(secretSync);
|
||||||
for await (const key of Object.keys(secretMap)) {
|
for await (const key of Object.keys(secretMap)) {
|
||||||
|
// If value is empty skip it as render does not allow empty variables
|
||||||
|
if (secretMap[key].value === "") {
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
await putEnvironmentSecret(secretSync, secretMap, key);
|
await putEnvironmentSecret(secretSync, secretMap, key);
|
||||||
await sleep();
|
await sleep();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,11 +119,20 @@ export type TAvailableAppConnectionsResponse = { appConnections: TAvailableAppCo
|
|||||||
|
|
||||||
export type TCreateAppConnectionDTO = Pick<
|
export type TCreateAppConnectionDTO = Pick<
|
||||||
TAppConnection,
|
TAppConnection,
|
||||||
"name" | "credentials" | "method" | "app" | "description" | "isPlatformManagedCredentials"
|
| "name"
|
||||||
|
| "credentials"
|
||||||
|
| "method"
|
||||||
|
| "app"
|
||||||
|
| "description"
|
||||||
|
| "isPlatformManagedCredentials"
|
||||||
|
| "gatewayId"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type TUpdateAppConnectionDTO = Partial<
|
export type TUpdateAppConnectionDTO = Partial<
|
||||||
Pick<TAppConnection, "name" | "credentials" | "description" | "isPlatformManagedCredentials">
|
Pick<
|
||||||
|
TAppConnection,
|
||||||
|
"name" | "credentials" | "description" | "isPlatformManagedCredentials" | "gatewayId"
|
||||||
|
>
|
||||||
> & {
|
> & {
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
app: AppConnection;
|
app: AppConnection;
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ export type TRootAppConnection = {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
isPlatformManagedCredentials?: boolean;
|
isPlatformManagedCredentials?: boolean;
|
||||||
|
gatewayId?: string | null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { CommitHistoryItem, CommitWithChanges, RollbackPreview } from "./types";
|
import { Commit, CommitHistoryItem, CommitWithChanges, RollbackPreview } from "./types";
|
||||||
|
|
||||||
export const commitKeys = {
|
export const commitKeys = {
|
||||||
count: ({
|
count: ({
|
||||||
@@ -242,7 +243,6 @@ export const useGetFolderCommitHistory = ({
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
directory,
|
directory,
|
||||||
offset = 0,
|
|
||||||
limit = 20,
|
limit = 20,
|
||||||
search,
|
search,
|
||||||
sort = "desc"
|
sort = "desc"
|
||||||
@@ -250,22 +250,33 @@ export const useGetFolderCommitHistory = ({
|
|||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
directory: string;
|
directory: string;
|
||||||
offset?: number;
|
|
||||||
limit?: number;
|
limit?: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
sort?: "asc" | "desc";
|
sort?: "asc" | "desc";
|
||||||
}) => {
|
}) => {
|
||||||
return useQuery({
|
return useInfiniteQuery({
|
||||||
queryKey: [
|
initialPageParam: 0,
|
||||||
commitKeys.history({ workspaceId, environment, directory }),
|
queryKey: [commitKeys.history({ workspaceId, environment, directory }), limit, search, sort],
|
||||||
offset,
|
queryFn: ({ pageParam }) =>
|
||||||
limit,
|
fetchFolderCommitHistory(workspaceId, environment, directory, pageParam, limit, search, sort),
|
||||||
search,
|
enabled: Boolean(workspaceId && environment),
|
||||||
sort
|
select: (data) => {
|
||||||
],
|
return (data?.pages ?? [])
|
||||||
queryFn: () =>
|
?.map((page) => page.commits)
|
||||||
fetchFolderCommitHistory(workspaceId, environment, directory, offset, limit, search, sort),
|
.flat()
|
||||||
enabled: Boolean(workspaceId && environment)
|
.reduce(
|
||||||
|
(acc, commit) => {
|
||||||
|
const date = format(new Date(commit.createdAt), "MMM d, yyyy");
|
||||||
|
if (!acc[date]) {
|
||||||
|
acc[date] = [];
|
||||||
|
}
|
||||||
|
acc[date].push(commit);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, Commit[]>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
getNextPageParam: (lastPage, pages) => (lastPage.hasMore ? pages.length * limit : undefined)
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -62,3 +62,16 @@ export type RollbackPreview = {
|
|||||||
folderPath: string;
|
folderPath: string;
|
||||||
changes: RollbackChange[];
|
changes: RollbackChange[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface CommitActorMetadata {
|
||||||
|
email?: string;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Commit {
|
||||||
|
id: string;
|
||||||
|
message: string;
|
||||||
|
createdAt: string;
|
||||||
|
actorType: string;
|
||||||
|
actorMetadata?: CommitActorMetadata;
|
||||||
|
}
|
||||||
|
|||||||
+5
-1
@@ -6,7 +6,11 @@ import { slugSchema } from "@app/lib/schemas";
|
|||||||
|
|
||||||
export const genericAppConnectionFieldsSchema = z.object({
|
export const genericAppConnectionFieldsSchema = z.object({
|
||||||
name: slugSchema({ min: 1, max: 64, field: "Name" }),
|
name: slugSchema({ min: 1, max: 64, field: "Name" }),
|
||||||
description: z.string().trim().max(256, "Description cannot exceed 256 characters").nullish()
|
description: z.string().trim().max(256, "Description cannot exceed 256 characters").nullish(),
|
||||||
|
gatewayId: z
|
||||||
|
.string()
|
||||||
|
.nullish()
|
||||||
|
.transform((v) => (v === "" ? null : v))
|
||||||
});
|
});
|
||||||
|
|
||||||
export const GenericAppConnectionsFields = () => {
|
export const GenericAppConnectionsFields = () => {
|
||||||
|
|||||||
+59
-1
@@ -1,10 +1,17 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { Button, FormControl, ModalClose, Select, SelectItem } from "@app/components/v2";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
|
import { Button, FormControl, ModalClose, Select, SelectItem, Tooltip } from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
OrgGatewayPermissionActions,
|
||||||
|
OrgPermissionSubjects
|
||||||
|
} from "@app/context/OrgPermissionContext/types";
|
||||||
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||||
|
import { gatewaysQueryKeys } from "@app/hooks/api";
|
||||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
import {
|
import {
|
||||||
MsSqlConnectionMethod,
|
MsSqlConnectionMethod,
|
||||||
@@ -51,6 +58,7 @@ export const MsSqlConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
|||||||
defaultValues: appConnection ?? {
|
defaultValues: appConnection ?? {
|
||||||
app: AppConnection.MsSql,
|
app: AppConnection.MsSql,
|
||||||
method: MsSqlConnectionMethod.UsernameAndPassword,
|
method: MsSqlConnectionMethod.UsernameAndPassword,
|
||||||
|
gatewayId: null,
|
||||||
credentials: {
|
credentials: {
|
||||||
host: "",
|
host: "",
|
||||||
port: 1433,
|
port: 1433,
|
||||||
@@ -71,6 +79,7 @@ export const MsSqlConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
|||||||
} = form;
|
} = form;
|
||||||
|
|
||||||
const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false;
|
const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false;
|
||||||
|
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
||||||
|
|
||||||
const confirmSubmit = async (formData: FormData) => {
|
const confirmSubmit = async (formData: FormData) => {
|
||||||
if (formData.isPlatformManagedCredentials) {
|
if (formData.isPlatformManagedCredentials) {
|
||||||
@@ -90,6 +99,55 @@ export const MsSqlConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!isUpdate && <GenericAppConnectionsFields />}
|
{!isUpdate && <GenericAppConnectionsFields />}
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgGatewayPermissionActions.AttachGateways}
|
||||||
|
a={OrgPermissionSubjects.Gateway}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="gatewayId"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
label="Gateway"
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
isDisabled={isAllowed}
|
||||||
|
content="Restricted access. You don't have permission to attach gateways to resources."
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
value={value as string}
|
||||||
|
onValueChange={onChange}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
dropdownContainerClassName="max-w-none"
|
||||||
|
isLoading={isGatewaysLoading}
|
||||||
|
placeholder="Default: Internet Gateway"
|
||||||
|
position="popper"
|
||||||
|
>
|
||||||
|
<SelectItem
|
||||||
|
value={null as unknown as string}
|
||||||
|
onClick={() => onChange(undefined)}
|
||||||
|
>
|
||||||
|
Internet Gateway
|
||||||
|
</SelectItem>
|
||||||
|
{gateways?.map((el) => (
|
||||||
|
<SelectItem value={el.id} key={el.id}>
|
||||||
|
{el.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
<Controller
|
<Controller
|
||||||
name="method"
|
name="method"
|
||||||
control={control}
|
control={control}
|
||||||
|
|||||||
+59
-1
@@ -1,10 +1,17 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { Button, FormControl, ModalClose, Select, SelectItem } from "@app/components/v2";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
|
import { Button, FormControl, ModalClose, Select, SelectItem, Tooltip } from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
OrgGatewayPermissionActions,
|
||||||
|
OrgPermissionSubjects
|
||||||
|
} from "@app/context/OrgPermissionContext/types";
|
||||||
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||||
|
import { gatewaysQueryKeys } from "@app/hooks/api";
|
||||||
import { MySqlConnectionMethod, TMySqlConnection } from "@app/hooks/api/appConnections";
|
import { MySqlConnectionMethod, TMySqlConnection } from "@app/hooks/api/appConnections";
|
||||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
import { PlatformManagedConfirmationModal } from "@app/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/shared/PlatformManagedConfirmationModal";
|
import { PlatformManagedConfirmationModal } from "@app/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/shared/PlatformManagedConfirmationModal";
|
||||||
@@ -48,6 +55,7 @@ export const MySqlConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
|||||||
defaultValues: appConnection ?? {
|
defaultValues: appConnection ?? {
|
||||||
app: AppConnection.MySql,
|
app: AppConnection.MySql,
|
||||||
method: MySqlConnectionMethod.UsernameAndPassword,
|
method: MySqlConnectionMethod.UsernameAndPassword,
|
||||||
|
gatewayId: null,
|
||||||
credentials: {
|
credentials: {
|
||||||
host: "",
|
host: "",
|
||||||
port: 3306,
|
port: 3306,
|
||||||
@@ -68,6 +76,7 @@ export const MySqlConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
|||||||
} = form;
|
} = form;
|
||||||
|
|
||||||
const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false;
|
const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false;
|
||||||
|
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
||||||
|
|
||||||
const confirmSubmit = async (formData: FormData) => {
|
const confirmSubmit = async (formData: FormData) => {
|
||||||
if (formData.isPlatformManagedCredentials) {
|
if (formData.isPlatformManagedCredentials) {
|
||||||
@@ -87,6 +96,55 @@ export const MySqlConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!isUpdate && <GenericAppConnectionsFields />}
|
{!isUpdate && <GenericAppConnectionsFields />}
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgGatewayPermissionActions.AttachGateways}
|
||||||
|
a={OrgPermissionSubjects.Gateway}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="gatewayId"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
label="Gateway"
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
isDisabled={isAllowed}
|
||||||
|
content="Restricted access. You don't have permission to attach gateways to resources."
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
value={value as string}
|
||||||
|
onValueChange={onChange}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
dropdownContainerClassName="max-w-none"
|
||||||
|
isLoading={isGatewaysLoading}
|
||||||
|
placeholder="Default: Internet Gateway"
|
||||||
|
position="popper"
|
||||||
|
>
|
||||||
|
<SelectItem
|
||||||
|
value={null as unknown as string}
|
||||||
|
onClick={() => onChange(undefined)}
|
||||||
|
>
|
||||||
|
Internet Gateway
|
||||||
|
</SelectItem>
|
||||||
|
{gateways?.map((el) => (
|
||||||
|
<SelectItem value={el.id} key={el.id}>
|
||||||
|
{el.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
<Controller
|
<Controller
|
||||||
name="method"
|
name="method"
|
||||||
control={control}
|
control={control}
|
||||||
|
|||||||
+59
-1
@@ -1,10 +1,17 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { Button, FormControl, ModalClose, Select, SelectItem } from "@app/components/v2";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
|
import { Button, FormControl, ModalClose, Select, SelectItem, Tooltip } from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
OrgGatewayPermissionActions,
|
||||||
|
OrgPermissionSubjects
|
||||||
|
} from "@app/context/OrgPermissionContext/types";
|
||||||
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||||
|
import { gatewaysQueryKeys } from "@app/hooks/api";
|
||||||
import { OracleDBConnectionMethod, TOracleDBConnection } from "@app/hooks/api/appConnections";
|
import { OracleDBConnectionMethod, TOracleDBConnection } from "@app/hooks/api/appConnections";
|
||||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
import { PlatformManagedConfirmationModal } from "@app/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/shared/PlatformManagedConfirmationModal";
|
import { PlatformManagedConfirmationModal } from "@app/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/shared/PlatformManagedConfirmationModal";
|
||||||
@@ -48,6 +55,7 @@ export const OracleDBConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
|||||||
defaultValues: appConnection ?? {
|
defaultValues: appConnection ?? {
|
||||||
app: AppConnection.OracleDB,
|
app: AppConnection.OracleDB,
|
||||||
method: OracleDBConnectionMethod.UsernameAndPassword,
|
method: OracleDBConnectionMethod.UsernameAndPassword,
|
||||||
|
gatewayId: null,
|
||||||
credentials: {
|
credentials: {
|
||||||
host: "",
|
host: "",
|
||||||
port: 1521,
|
port: 1521,
|
||||||
@@ -68,6 +76,7 @@ export const OracleDBConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
|||||||
} = form;
|
} = form;
|
||||||
|
|
||||||
const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false;
|
const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false;
|
||||||
|
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
||||||
|
|
||||||
const confirmSubmit = async (formData: FormData) => {
|
const confirmSubmit = async (formData: FormData) => {
|
||||||
if (formData.isPlatformManagedCredentials) {
|
if (formData.isPlatformManagedCredentials) {
|
||||||
@@ -87,6 +96,55 @@ export const OracleDBConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!isUpdate && <GenericAppConnectionsFields />}
|
{!isUpdate && <GenericAppConnectionsFields />}
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgGatewayPermissionActions.AttachGateways}
|
||||||
|
a={OrgPermissionSubjects.Gateway}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="gatewayId"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
label="Gateway"
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
isDisabled={isAllowed}
|
||||||
|
content="Restricted access. You don't have permission to attach gateways to resources."
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
value={value as string}
|
||||||
|
onValueChange={onChange}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
dropdownContainerClassName="max-w-none"
|
||||||
|
isLoading={isGatewaysLoading}
|
||||||
|
placeholder="Default: Internet Gateway"
|
||||||
|
position="popper"
|
||||||
|
>
|
||||||
|
<SelectItem
|
||||||
|
value={null as unknown as string}
|
||||||
|
onClick={() => onChange(undefined)}
|
||||||
|
>
|
||||||
|
Internet Gateway
|
||||||
|
</SelectItem>
|
||||||
|
{gateways?.map((el) => (
|
||||||
|
<SelectItem value={el.id} key={el.id}>
|
||||||
|
{el.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
<Controller
|
<Controller
|
||||||
name="method"
|
name="method"
|
||||||
control={control}
|
control={control}
|
||||||
|
|||||||
+59
-1
@@ -1,10 +1,17 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { Button, FormControl, ModalClose, Select, SelectItem } from "@app/components/v2";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
|
import { Button, FormControl, ModalClose, Select, SelectItem, Tooltip } from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
OrgGatewayPermissionActions,
|
||||||
|
OrgPermissionSubjects
|
||||||
|
} from "@app/context/OrgPermissionContext/types";
|
||||||
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||||
|
import { gatewaysQueryKeys } from "@app/hooks/api";
|
||||||
import { PostgresConnectionMethod, TPostgresConnection } from "@app/hooks/api/appConnections";
|
import { PostgresConnectionMethod, TPostgresConnection } from "@app/hooks/api/appConnections";
|
||||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
import { PlatformManagedConfirmationModal } from "@app/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/shared/PlatformManagedConfirmationModal";
|
import { PlatformManagedConfirmationModal } from "@app/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/shared/PlatformManagedConfirmationModal";
|
||||||
@@ -48,6 +55,7 @@ export const PostgresConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
|||||||
defaultValues: appConnection ?? {
|
defaultValues: appConnection ?? {
|
||||||
app: AppConnection.Postgres,
|
app: AppConnection.Postgres,
|
||||||
method: PostgresConnectionMethod.UsernameAndPassword,
|
method: PostgresConnectionMethod.UsernameAndPassword,
|
||||||
|
gatewayId: null,
|
||||||
credentials: {
|
credentials: {
|
||||||
host: "",
|
host: "",
|
||||||
port: 5432,
|
port: 5432,
|
||||||
@@ -68,6 +76,7 @@ export const PostgresConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
|||||||
} = form;
|
} = form;
|
||||||
|
|
||||||
const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false;
|
const isPlatformManagedCredentials = appConnection?.isPlatformManagedCredentials ?? false;
|
||||||
|
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
||||||
|
|
||||||
const confirmSubmit = async (formData: FormData) => {
|
const confirmSubmit = async (formData: FormData) => {
|
||||||
if (formData.isPlatformManagedCredentials) {
|
if (formData.isPlatformManagedCredentials) {
|
||||||
@@ -87,6 +96,55 @@ export const PostgresConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!isUpdate && <GenericAppConnectionsFields />}
|
{!isUpdate && <GenericAppConnectionsFields />}
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgGatewayPermissionActions.AttachGateways}
|
||||||
|
a={OrgPermissionSubjects.Gateway}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="gatewayId"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
label="Gateway"
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
isDisabled={isAllowed}
|
||||||
|
content="Restricted access. You don't have permission to attach gateways to resources."
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
value={value as string}
|
||||||
|
onValueChange={onChange}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
dropdownContainerClassName="max-w-none"
|
||||||
|
isLoading={isGatewaysLoading}
|
||||||
|
placeholder="Default: Internet Gateway"
|
||||||
|
position="popper"
|
||||||
|
>
|
||||||
|
<SelectItem
|
||||||
|
value={null as unknown as string}
|
||||||
|
onClick={() => onChange(undefined)}
|
||||||
|
>
|
||||||
|
Internet Gateway
|
||||||
|
</SelectItem>
|
||||||
|
{gateways?.map((el) => (
|
||||||
|
<SelectItem value={el.id} key={el.id}>
|
||||||
|
{el.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
<Controller
|
<Controller
|
||||||
name="method"
|
name="method"
|
||||||
control={control}
|
control={control}
|
||||||
|
|||||||
+125
-127
@@ -1,5 +1,10 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { faAngleDown } from "@fortawesome/free-solid-svg-icons";
|
import {
|
||||||
|
faAngleDown,
|
||||||
|
faChevronLeft,
|
||||||
|
faCodeCommit,
|
||||||
|
faWarning
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { DropdownMenuItem } from "@radix-ui/react-dropdown-menu";
|
import { DropdownMenuItem } from "@radix-ui/react-dropdown-menu";
|
||||||
import { useSearch } from "@tanstack/react-router";
|
import { useSearch } from "@tanstack/react-router";
|
||||||
@@ -7,12 +12,14 @@ import { useSearch } from "@tanstack/react-router";
|
|||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
|
Button,
|
||||||
|
ContentLoader,
|
||||||
DeleteActionModal,
|
DeleteActionModal,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
IconButton,
|
EmptyState,
|
||||||
Spinner
|
PageHeader
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { ROUTE_PATHS } from "@app/const/routes";
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
import {
|
import {
|
||||||
@@ -108,25 +115,25 @@ export const CommitDetailsTab = ({
|
|||||||
// If no commit is selected or data is loading, show appropriate message
|
// If no commit is selected or data is loading, show appropriate message
|
||||||
if (!selectedCommitId) {
|
if (!selectedCommitId) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-64 items-center justify-center">
|
<EmptyState className="mt-40" title="Select a commit to view details." icon={faCodeCommit}>
|
||||||
<p className="text-gray-400">Select a commit to view details</p>
|
<Button className="mt-4" colorSchema="secondary" onClick={() => goBackToHistory()}>
|
||||||
</div>
|
Back to Commits
|
||||||
|
</Button>
|
||||||
|
</EmptyState>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return <ContentLoader />;
|
||||||
<div className="flex h-64 items-center justify-center">
|
|
||||||
<Spinner size="lg" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!commitDetails) {
|
if (!commitDetails) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-64 items-center justify-center">
|
<EmptyState className="mt-40" title="No details found for this commit." icon={faCodeCommit}>
|
||||||
<p className="text-gray-400">No details found for this commit</p>
|
<Button className="mt-4" colorSchema="secondary" onClick={() => goBackToHistory()}>
|
||||||
</div>
|
Back to Commits
|
||||||
|
</Button>
|
||||||
|
</EmptyState>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,9 +145,11 @@ export const CommitDetailsTab = ({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to parse commit details:", error);
|
console.error("Failed to parse commit details:", error);
|
||||||
return (
|
return (
|
||||||
<div className="flex h-64 items-center justify-center">
|
<EmptyState className="mt-40" title="Error parsing commit details." icon={faWarning}>
|
||||||
<p className="text-gray-400">Error parsing commit details</p>
|
<Button className="mt-4" colorSchema="secondary" onClick={() => goBackToHistory()}>
|
||||||
</div>
|
Back to Commits
|
||||||
|
</Button>
|
||||||
|
</EmptyState>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,13 +232,12 @@ export const CommitDetailsTab = ({
|
|||||||
// Render an item from the merged list
|
// Render an item from the merged list
|
||||||
const renderMergedItem = (item: MergedItem): JSX.Element => {
|
const renderMergedItem = (item: MergedItem): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div key={item.id} className="mb-2">
|
<SecretVersionDiffView
|
||||||
<SecretVersionDiffView
|
key={item.id}
|
||||||
item={item}
|
item={item}
|
||||||
isCollapsed={collapsedItems[item.id]}
|
isCollapsed={collapsedItems[item.id]}
|
||||||
onToggleCollapse={(id) => toggleItemCollapsed(id)}
|
onToggleCollapse={(id) => toggleItemCollapsed(id)}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -240,114 +248,104 @@ export const CommitDetailsTab = ({
|
|||||||
"Unknown";
|
"Unknown";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<>
|
||||||
<div>
|
<Button
|
||||||
<div className="flex justify-between pb-2">
|
variant="link"
|
||||||
<div className="w-5/6">
|
type="submit"
|
||||||
<div>
|
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
|
||||||
<div className="flex items-center">
|
onClick={() => {
|
||||||
<h1 className="mr-4 truncate text-3xl font-semibold text-white">
|
goBackToHistory();
|
||||||
{parsedCommitDetails.changes?.message || "No message"}
|
}}
|
||||||
</h1>
|
>
|
||||||
</div>
|
Commit History
|
||||||
</div>
|
</Button>
|
||||||
<div className="font-small mb-4 mt-2 flex items-center text-sm">
|
<PageHeader
|
||||||
<p>
|
title={`${parsedCommitDetails.changes?.message}` || "No message"}
|
||||||
<span> Commited by </span>
|
description={
|
||||||
<b>{actorDisplay}</b>
|
<>
|
||||||
<span> on </span>
|
Commited by {actorDisplay} on{" "}
|
||||||
<b>
|
{formatDisplayDate(parsedCommitDetails.changes?.createdAt || new Date().toISOString())}
|
||||||
{formatDisplayDate(
|
{parsedCommitDetails.changes?.isLatest && (
|
||||||
parsedCommitDetails.changes?.createdAt || new Date().toISOString()
|
<span className="ml-1 text-mineshaft-400">(Latest)</span>
|
||||||
)}
|
)}
|
||||||
</b>
|
</>
|
||||||
{parsedCommitDetails.changes?.isLatest && (
|
}
|
||||||
<span className="ml-1 italic text-gray-400">(Latest)</span>
|
>
|
||||||
)}
|
<ProjectPermissionCan
|
||||||
</p>
|
I={ProjectPermissionCommitsActions.PerformRollback}
|
||||||
</div>
|
a={ProjectPermissionSub.Commits}
|
||||||
</div>
|
>
|
||||||
<div className="flex items-center justify-start">
|
{(isAllowed) => (
|
||||||
<ProjectPermissionCan
|
<DropdownMenu>
|
||||||
I={ProjectPermissionCommitsActions.PerformRollback}
|
<DropdownMenuTrigger
|
||||||
a={ProjectPermissionSub.Commits}
|
asChild
|
||||||
>
|
disabled={!isAllowed}
|
||||||
{(isAllowed) => (
|
className={`${!isAllowed ? "cursor-not-allowed" : ""}`}
|
||||||
<DropdownMenu>
|
>
|
||||||
<DropdownMenuTrigger
|
<Button
|
||||||
asChild
|
rightIcon={<FontAwesomeIcon className="ml-2" icon={faAngleDown} />}
|
||||||
disabled={!isAllowed}
|
variant="solid"
|
||||||
className={`${!isAllowed ? "cursor-not-allowed" : ""}`}
|
className="h-min"
|
||||||
|
colorSchema="secondary"
|
||||||
|
>
|
||||||
|
Restore Options
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="max-w-sm bg-bunker-500" sideOffset={2}>
|
||||||
|
{!parsedCommitDetails.changes.isLatest && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="group cursor-pointer border-b border-mineshaft-600 px-3 py-3 transition-colors hover:bg-mineshaft-700"
|
||||||
|
onClick={() => goToRollbackPreview()}
|
||||||
>
|
>
|
||||||
<IconButton
|
<div className="flex items-center space-x-3">
|
||||||
ariaLabel="commit-options"
|
<div className="flex flex-col">
|
||||||
variant="outline_bg"
|
<span className="text-sm font-medium text-white">
|
||||||
className="h-10 rounded border border-mineshaft-600 bg-mineshaft-800 px-4 py-2 text-sm font-medium"
|
Roll back to this commit
|
||||||
>
|
</span>
|
||||||
<p className="mr-2">Restore Options</p>
|
<span className="whitespace-normal break-words text-xs leading-snug text-gray-400">
|
||||||
<FontAwesomeIcon icon={faAngleDown} />
|
Return this folder to its exact state at the time of this commit,
|
||||||
</IconButton>
|
discarding all other changes made after it
|
||||||
</DropdownMenuTrigger>
|
</span>
|
||||||
<DropdownMenuContent
|
|
||||||
align="end"
|
|
||||||
sideOffset={2}
|
|
||||||
className="animate-in fade-in-50 zoom-in-95 min-w-[240px] rounded-md bg-mineshaft-800 p-1 shadow-lg"
|
|
||||||
style={{ marginTop: "0" }}
|
|
||||||
>
|
|
||||||
{!parsedCommitDetails.changes.isLatest && (
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="group cursor-pointer rounded-md px-3 py-3 transition-colors hover:bg-mineshaft-700"
|
|
||||||
onClick={() => goToRollbackPreview()}
|
|
||||||
>
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-sm font-medium text-white">
|
|
||||||
Roll back to this commit
|
|
||||||
</span>
|
|
||||||
<span className="max-w-[180px] whitespace-normal break-words text-xs leading-snug text-gray-400">
|
|
||||||
Return this folder to its exact state at the time of this commit,
|
|
||||||
discarding all other changes made after it
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="group cursor-pointer rounded-md px-3 py-3 transition-colors hover:bg-mineshaft-700"
|
|
||||||
onClick={() => handlePopUpOpen("revertChanges")}
|
|
||||||
>
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-sm font-medium text-white">Revert changes</span>
|
|
||||||
<span className="max-w-[180px] whitespace-normal break-words text-xs leading-snug text-gray-400">
|
|
||||||
Will restore to the previous version of affected resources
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</div>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuItem>
|
||||||
</DropdownMenu>
|
)}
|
||||||
)}
|
<DropdownMenuItem
|
||||||
</ProjectPermissionCan>
|
className="group cursor-pointer px-3 py-3 transition-colors hover:bg-mineshaft-700"
|
||||||
</div>
|
onClick={() => handlePopUpOpen("revertChanges")}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-sm font-medium text-white">Revert changes</span>
|
||||||
|
<span className="whitespace-normal break-words text-xs leading-snug text-gray-400">
|
||||||
|
Will restore to the previous version of affected resources
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</PageHeader>
|
||||||
|
<div className="flex w-full flex-col rounded-lg border border-mineshaft-600 bg-mineshaft-900 pt-4">
|
||||||
|
<div className="mx-4 flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||||
|
<h3 className="text-lg font-semibold text-mineshaft-100">Commit Changes</h3>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-col overflow-hidden px-4">
|
||||||
<div className="py-2">
|
<div className="thin-scrollbar overflow-y-auto py-4">
|
||||||
<div className="overflow-hidden">
|
{sortedChangedItems.length > 0 ? (
|
||||||
<div className="space-y-2">
|
sortedChangedItems.map((item) => renderMergedItem(item))
|
||||||
{sortedChangedItems.length > 0 ? (
|
) : (
|
||||||
sortedChangedItems.map((item) => renderMergedItem(item))
|
<EmptyState
|
||||||
) : (
|
title="No changes found."
|
||||||
<div className="flex h-32 items-center justify-center rounded-lg border border-mineshaft-600 bg-mineshaft-800">
|
className="h-full pb-0 pt-28"
|
||||||
<p className="text-gray-400">No changed items found</p>
|
icon={faCodeCommit}
|
||||||
</div>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.revertChanges.isOpen}
|
isOpen={popUp.revertChanges.isOpen}
|
||||||
deleteKey="revert"
|
deleteKey="revert"
|
||||||
@@ -357,6 +355,6 @@ export const CommitDetailsTab = ({
|
|||||||
onDeleteApproved={handleRevertChanges}
|
onDeleteApproved={handleRevertChanges}
|
||||||
buttonText="Yes, revert changes"
|
buttonText="Yes, revert changes"
|
||||||
/>
|
/>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+38
-46
@@ -2,6 +2,7 @@
|
|||||||
import { useCallback, useRef, useState } from "react";
|
import { useCallback, useRef, useState } from "react";
|
||||||
import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons";
|
import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export interface Version {
|
export interface Version {
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -225,15 +226,12 @@ const renderJsonWithDiffs = (
|
|||||||
|
|
||||||
const getLineClass = (different: boolean) => {
|
const getLineClass = (different: boolean) => {
|
||||||
if (!different) return "flex";
|
if (!different) return "flex";
|
||||||
return isOldVersion ? "flex bg-red-950 text-red-300" : "flex bg-green-950 text-green-300";
|
return isOldVersion
|
||||||
|
? "flex bg-red-500/50 rounded-sm text-red-300"
|
||||||
|
: "flex bg-green-500/50 rounded-sm text-green-300";
|
||||||
};
|
};
|
||||||
|
|
||||||
const getHighlightClass = (different: boolean) => {
|
const prefix = isDifferent ? (isOldVersion ? " -" : " +") : " ";
|
||||||
if (!different) return "";
|
|
||||||
return isOldVersion ? "bg-red-900 rounded px-1" : "bg-green-900 rounded px-1";
|
|
||||||
};
|
|
||||||
|
|
||||||
const prefix = isDifferent ? (isOldVersion ? "-" : "+") : " ";
|
|
||||||
const keyDisplay = keyName ? `"${keyName}": ` : "";
|
const keyDisplay = keyName ? `"${keyName}": ` : "";
|
||||||
const comma = !isLastItem ? "," : "";
|
const comma = !isLastItem ? "," : "";
|
||||||
|
|
||||||
@@ -255,8 +253,8 @@ const renderJsonWithDiffs = (
|
|||||||
<div className="w-4 flex-shrink-0">{prefix}</div>
|
<div className="w-4 flex-shrink-0">{prefix}</div>
|
||||||
<div>
|
<div>
|
||||||
{indent}
|
{indent}
|
||||||
{keyName && <span className={getHighlightClass(isDifferent)}>{keyDisplay}</span>}
|
{keyName && <span>{keyDisplay}</span>}
|
||||||
<span className={getHighlightClass(isDifferent)}>{valueDisplay}</span>
|
<span>{valueDisplay}</span>
|
||||||
{comma}
|
{comma}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -269,8 +267,8 @@ const renderJsonWithDiffs = (
|
|||||||
<div className="w-4 flex-shrink-0">{prefix}</div>
|
<div className="w-4 flex-shrink-0">{prefix}</div>
|
||||||
<div>
|
<div>
|
||||||
{indent}
|
{indent}
|
||||||
{keyName && <span className={getHighlightClass(isDifferent)}>{keyDisplay}</span>}
|
{keyName && <span>{keyDisplay}</span>}
|
||||||
<span className={getHighlightClass(isDifferent)}>[]</span>
|
<span>[]</span>
|
||||||
{comma}
|
{comma}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -283,8 +281,8 @@ const renderJsonWithDiffs = (
|
|||||||
<div className="w-4 flex-shrink-0">{prefix}</div>
|
<div className="w-4 flex-shrink-0">{prefix}</div>
|
||||||
<div>
|
<div>
|
||||||
{indent}
|
{indent}
|
||||||
{keyName && <span className={getHighlightClass(isDifferent)}>{keyDisplay}</span>}
|
{keyName && <span>{keyDisplay}</span>}
|
||||||
<span className={getHighlightClass(isDifferent)}>{"{}"}</span>
|
<span>{"{}"}</span>
|
||||||
{comma}
|
{comma}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -320,16 +318,12 @@ const renderJsonWithDiffs = (
|
|||||||
<div key={reactKey}>
|
<div key={reactKey}>
|
||||||
<div className={getLineClass(isContainerAddedOrRemoved)}>
|
<div className={getLineClass(isContainerAddedOrRemoved)}>
|
||||||
<div className="w-4 flex-shrink-0">
|
<div className="w-4 flex-shrink-0">
|
||||||
{isContainerAddedOrRemoved ? (isOldVersion ? "-" : "+") : " "}
|
{isContainerAddedOrRemoved ? (isOldVersion ? " -" : " +") : " "}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{indent}
|
{indent}
|
||||||
{keyName && (
|
{keyName && <span>{keyDisplay}</span>}
|
||||||
<span className={isContainerAddedOrRemoved ? getHighlightClass(true) : ""}>
|
<span>[</span>
|
||||||
{keyDisplay}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span className={isContainerAddedOrRemoved ? getHighlightClass(true) : ""}>[</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -357,11 +351,11 @@ const renderJsonWithDiffs = (
|
|||||||
|
|
||||||
<div className={getLineClass(isContainerAddedOrRemoved)}>
|
<div className={getLineClass(isContainerAddedOrRemoved)}>
|
||||||
<div className="w-4 flex-shrink-0">
|
<div className="w-4 flex-shrink-0">
|
||||||
{isContainerAddedOrRemoved ? (isOldVersion ? "-" : "+") : " "}
|
{isContainerAddedOrRemoved ? (isOldVersion ? " -" : " +") : " "}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{indent}
|
{indent}
|
||||||
<span className={isContainerAddedOrRemoved ? getHighlightClass(true) : ""}>]</span>
|
<span>]</span>
|
||||||
{comma}
|
{comma}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -376,16 +370,12 @@ const renderJsonWithDiffs = (
|
|||||||
<div key={reactKey}>
|
<div key={reactKey}>
|
||||||
<div className={getLineClass(isContainerAddedOrRemoved)}>
|
<div className={getLineClass(isContainerAddedOrRemoved)}>
|
||||||
<div className="w-4 flex-shrink-0">
|
<div className="w-4 flex-shrink-0">
|
||||||
{isContainerAddedOrRemoved ? (isOldVersion ? "-" : "+") : " "}
|
{isContainerAddedOrRemoved ? (isOldVersion ? " -" : " +") : " "}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{indent}
|
{indent}
|
||||||
{keyName && (
|
{keyName && <span>{keyDisplay}</span>}
|
||||||
<span className={isContainerAddedOrRemoved ? getHighlightClass(true) : ""}>
|
<span>{"{"}</span>
|
||||||
{keyDisplay}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span className={isContainerAddedOrRemoved ? getHighlightClass(true) : ""}>{"{"}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -414,11 +404,11 @@ const renderJsonWithDiffs = (
|
|||||||
|
|
||||||
<div className={getLineClass(isContainerAddedOrRemoved)}>
|
<div className={getLineClass(isContainerAddedOrRemoved)}>
|
||||||
<div className="w-4 flex-shrink-0">
|
<div className="w-4 flex-shrink-0">
|
||||||
{isContainerAddedOrRemoved ? (isOldVersion ? "-" : "+") : " "}
|
{isContainerAddedOrRemoved ? (isOldVersion ? " -" : " +") : " "}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{indent}
|
{indent}
|
||||||
<span className={isContainerAddedOrRemoved ? getHighlightClass(true) : ""}>{"}"}</span>
|
<span>{"}"}</span>
|
||||||
{comma}
|
{comma}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -527,7 +517,7 @@ export const SecretVersionDiffView = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
oldVersionContent = (
|
oldVersionContent = (
|
||||||
<div className="font-mono text-sm">
|
<div className="w-fit min-w-[100%] font-mono text-sm">
|
||||||
{renderJsonWithDiffs(
|
{renderJsonWithDiffs(
|
||||||
cleanOldVersion,
|
cleanOldVersion,
|
||||||
diffPaths,
|
diffPaths,
|
||||||
@@ -543,7 +533,7 @@ export const SecretVersionDiffView = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
newVersionContent = (
|
newVersionContent = (
|
||||||
<div className="font-mono text-sm">
|
<div className="w-fit min-w-[100%] font-mono text-sm">
|
||||||
{renderJsonWithDiffs(
|
{renderJsonWithDiffs(
|
||||||
cleanNewVersion,
|
cleanNewVersion,
|
||||||
diffPaths,
|
diffPaths,
|
||||||
@@ -583,19 +573,19 @@ export const SecretVersionDiffView = ({
|
|||||||
if (item.isDeleted) {
|
if (item.isDeleted) {
|
||||||
textStyle = "line-through text-red-300";
|
textStyle = "line-through text-red-300";
|
||||||
changeBadge = (
|
changeBadge = (
|
||||||
<span className="ml-2 rounded-md bg-mineshaft-600 px-2 py-0.5 text-xs font-medium">
|
<span className="ml-2 whitespace-nowrap rounded-md bg-mineshaft-600 px-2 py-0.5 text-xs font-medium">
|
||||||
{isSecret ? "Secret" : "Folder"} Deleted
|
{isSecret ? "Secret" : "Folder"} Deleted
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
} else if (item.isAdded) {
|
} else if (item.isAdded) {
|
||||||
changeBadge = (
|
changeBadge = (
|
||||||
<span className="ml-2 rounded-md bg-mineshaft-600 px-2 py-0.5 text-xs font-medium">
|
<span className="ml-2 whitespace-nowrap rounded-md bg-mineshaft-600 px-2 py-0.5 text-xs font-medium">
|
||||||
{isSecret ? "Secret" : "Folder"} Added
|
{isSecret ? "Secret" : "Folder"} Added
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
} else if (item.isUpdated) {
|
} else if (item.isUpdated) {
|
||||||
changeBadge = (
|
changeBadge = (
|
||||||
<span className="ml-2 rounded-md bg-mineshaft-600 px-2 py-0.5 text-xs font-medium">
|
<span className="ml-2 whitespace-nowrap rounded-md bg-mineshaft-600 px-2 py-0.5 text-xs font-medium">
|
||||||
{isSecret ? "Secret" : "Folder"} Updated
|
{isSecret ? "Secret" : "Folder"} Updated
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@@ -615,32 +605,34 @@ export const SecretVersionDiffView = ({
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
aria-expanded={!collapsed}
|
aria-expanded={!collapsed}
|
||||||
>
|
>
|
||||||
<div className="flex items-center">
|
<div className="flex min-w-0 flex-1 items-center">
|
||||||
<span className={textStyle}>{key}</span>
|
<p className={twMerge(textStyle, "truncate")}>{key}</p>
|
||||||
{changeBadge}
|
{changeBadge}
|
||||||
</div>
|
</div>
|
||||||
<FontAwesomeIcon icon={collapsed ? faChevronDown : faChevronUp} className="text-gray-400" />
|
<FontAwesomeIcon
|
||||||
|
icon={collapsed ? faChevronDown : faChevronUp}
|
||||||
|
className="ml-2 text-gray-400"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-hidden rounded-lg border border-mineshaft-600 bg-mineshaft-800">
|
<div className="overflow-hidden border border-b-0 border-mineshaft-600 bg-mineshaft-800 first:rounded-t last:rounded-b last:border-b">
|
||||||
{showHeader && renderHeader()}
|
{showHeader && renderHeader()}
|
||||||
|
|
||||||
{!collapsed && (
|
{!collapsed && (
|
||||||
<div className="border-t border-mineshaft-700 bg-mineshaft-900 px-6 py-4">
|
<div className="border-t border-mineshaft-700 bg-mineshaft-900 p-3 text-mineshaft-100">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="flex gap-3">
|
||||||
<div
|
<div
|
||||||
ref={oldContainerRef}
|
ref={oldContainerRef}
|
||||||
className="thin-scrollbar max-h-96 overflow-auto whitespace-pre rounded border border-mineshaft-600 bg-mineshaft-900 p-4"
|
className="thin-scrollbar max-h-96 flex-1 overflow-auto whitespace-pre"
|
||||||
>
|
>
|
||||||
{oldVersionContent}
|
{oldVersionContent}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="max-h-96 w-[0.05rem] self-stretch bg-mineshaft-600" />
|
||||||
<div
|
<div
|
||||||
ref={newContainerRef}
|
ref={newContainerRef}
|
||||||
className="thin-scrollbar max-h-96 overflow-auto whitespace-pre rounded border border-mineshaft-600 bg-mineshaft-900 p-4"
|
className="thin-scrollbar max-h-96 flex-1 overflow-auto whitespace-pre"
|
||||||
>
|
>
|
||||||
{newVersionContent}
|
{newVersionContent}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const CommitsPage = () => {
|
|||||||
title="Commits"
|
title="Commits"
|
||||||
description="Track, inspect, and restore your secrets and folders with confidence. View the complete history of changes made to your environment, examine specific modifications at each commit point, and preview the exact impact before rolling back to previous states."
|
description="Track, inspect, and restore your secrets and folders with confidence. View the complete history of changes made to your environment, examine specific modifications at each commit point, and preview the exact impact before rolling back to previous states."
|
||||||
/>
|
/>
|
||||||
<NoticeBannerV2 title="" className="mb-2">
|
<NoticeBannerV2 title="Secret Snapshots Update" className="mb-2">
|
||||||
<p className="my-1 text-sm text-mineshaft-300">
|
<p className="my-1 text-sm text-mineshaft-300">
|
||||||
Secret Snapshots have been officially renamed to Commits. Going forward, all secret
|
Secret Snapshots have been officially renamed to Commits. Going forward, all secret
|
||||||
changes will be tracked as Commits. If you made changes before this update, you can
|
changes will be tracked as Commits. If you made changes before this update, you can
|
||||||
|
|||||||
+68
-168
@@ -1,29 +1,17 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
faArrowDownWideShort,
|
faArrowDownWideShort,
|
||||||
faArrowUpWideShort,
|
faArrowUpWideShort,
|
||||||
|
faCodeCommit,
|
||||||
faCopy,
|
faCopy,
|
||||||
faSearch
|
faSearch
|
||||||
} 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 { format, formatDistanceToNow } from "date-fns";
|
import { formatDistanceToNow } from "date-fns";
|
||||||
|
|
||||||
import { Button, Input, Spinner } from "@app/components/v2";
|
import { Button, ContentLoader, EmptyState, IconButton, Input } from "@app/components/v2";
|
||||||
import { CopyButton } from "@app/components/v2/CopyButton";
|
import { CopyButton } from "@app/components/v2/CopyButton";
|
||||||
import { useGetFolderCommitHistory } from "@app/hooks/api/folderCommits";
|
import { Commit, useGetFolderCommitHistory } from "@app/hooks/api/folderCommits";
|
||||||
|
|
||||||
interface CommitActorMetadata {
|
|
||||||
email?: string;
|
|
||||||
name?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Commit {
|
|
||||||
id: string;
|
|
||||||
message: string;
|
|
||||||
createdAt: string;
|
|
||||||
actorType: string;
|
|
||||||
actorMetadata?: CommitActorMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatTimeAgo = (timestamp: string): string => {
|
const formatTimeAgo = (timestamp: string): string => {
|
||||||
return formatDistanceToNow(new Date(timestamp), { addSuffix: true });
|
return formatDistanceToNow(new Date(timestamp), { addSuffix: true });
|
||||||
@@ -40,58 +28,40 @@ const CommitItem = ({
|
|||||||
onSelectCommit: (commitId: string, tab: string) => void;
|
onSelectCommit: (commitId: string, tab: string) => void;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="border-b border-zinc-800 last:border-b-0">
|
<button
|
||||||
<div className="px-4 py-4 transition-colors duration-200 hover:bg-zinc-800">
|
type="button"
|
||||||
<div className="flex flex-col sm:flex-row sm:justify-between">
|
onClick={(e) => {
|
||||||
<div className="w-5/6 flex-1">
|
e.stopPropagation();
|
||||||
<div className="flex items-center">
|
onSelectCommit(commit.id, "tab-commit-details");
|
||||||
<Button
|
}}
|
||||||
variant="link"
|
className="w-full border border-b-0 border-mineshaft-600 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md last:border-b"
|
||||||
className="truncate text-left text-white hover:underline"
|
>
|
||||||
isFullWidth
|
<div className="flex gap-2 px-4 py-3 transition-colors duration-200 hover:bg-zinc-800">
|
||||||
onClick={(e) => {
|
<div className="flex min-w-0 flex-1 flex-col items-start">
|
||||||
e.stopPropagation();
|
<p className="block w-full truncate text-left text-sm text-mineshaft-100">
|
||||||
onSelectCommit(commit.id, "tab-commit-details");
|
{commit.message}
|
||||||
}}
|
</p>
|
||||||
>
|
<p className="text-left text-xs text-mineshaft-300">
|
||||||
{commit.message}
|
{commit.actorMetadata?.email || commit.actorMetadata?.name || commit.actorType}{" "}
|
||||||
</Button>
|
committed <time dateTime={commit.createdAt}>{formatTimeAgo(commit.createdAt)}</time>
|
||||||
</div>
|
</p>
|
||||||
<p className="text-white-400 mt-2 flex flex-wrap items-center gap-4 text-sm">
|
</div>
|
||||||
<span className="flex items-center text-mineshaft-300">
|
|
||||||
{commit.actorMetadata?.email || commit.actorMetadata?.name || commit.actorType}
|
<div className="flex items-center space-x-2">
|
||||||
<p className="ml-1 mr-1">committed</p>
|
<code className="mt-0.5 font-mono text-xs text-mineshaft-400">
|
||||||
<time dateTime={commit.createdAt}>{formatTimeAgo(commit.createdAt)}</time>
|
{commit.id?.substring(0, 11)}
|
||||||
</span>
|
</code>
|
||||||
</p>
|
<CopyButton
|
||||||
</div>
|
value={commit.id}
|
||||||
<div className="mt-2 flex w-1/6 items-center justify-end sm:mt-0">
|
name={commit.id}
|
||||||
<div className="flex items-center space-x-1">
|
size="xs"
|
||||||
<Button
|
variant="plain"
|
||||||
variant="link"
|
color="text-mineshaft-400"
|
||||||
className="text-white hover:underline"
|
icon={faCopy}
|
||||||
onClick={(e) => {
|
/>
|
||||||
e.stopPropagation();
|
|
||||||
onSelectCommit(commit.id, "tab-commit-details");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<code className="text-white-400 px-3 py-1 font-mono text-sm">
|
|
||||||
{commit.id?.substring(0, 11)}
|
|
||||||
</code>
|
|
||||||
</Button>
|
|
||||||
<CopyButton
|
|
||||||
value={commit.id}
|
|
||||||
name={commit.id}
|
|
||||||
size="sm"
|
|
||||||
variant="plain"
|
|
||||||
color="text-mineshaft-400"
|
|
||||||
icon={faCopy}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -108,24 +78,16 @@ const DateGroup = ({
|
|||||||
onSelectCommit: (commitId: string, tab: string) => void;
|
onSelectCommit: (commitId: string, tab: string) => void;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="mb-8 last:mb-0 last:pb-2">
|
<div className="mt-4 first:mt-0">
|
||||||
<div className="mb-4 flex items-center">
|
<div className="mb-4 ml-[0.15rem] flex items-center">
|
||||||
<div className="relative mr-3 flex h-6 w-6 items-center justify-center">
|
<FontAwesomeIcon icon={faCodeCommit} className="text-mineshaft-400" />
|
||||||
<div className="z-10 h-3 w-3 rounded-full border-2 border-mineshaft-600 bg-bunker-800" />
|
<h2 className="ml-4 text-sm text-mineshaft-400">Commits on {date}</h2>
|
||||||
<div className="absolute left-0 right-0 top-1/2 h-0.5 -translate-y-1/2 bg-mineshaft-600" />
|
|
||||||
</div>
|
|
||||||
<h2 className="text-sm text-white">Commits on {date}</h2>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute bottom-0 left-3 top-0 w-0.5 bg-mineshaft-600" />
|
<div className="absolute bottom-0 left-3 top-0 w-[0.1rem] bg-mineshaft-500" />
|
||||||
<div className="ml-10">
|
<div className="ml-10">
|
||||||
{commits.map((commit) => (
|
{commits.map((commit) => (
|
||||||
<div key={commit.id} className="relative mb-3 pb-1">
|
<CommitItem key={commit.id} commit={commit} onSelectCommit={onSelectCommit} />
|
||||||
<div className="overflow-hidden rounded-md border border-solid border-mineshaft-600">
|
|
||||||
<CommitItem commit={commit} onSelectCommit={onSelectCommit} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -147,10 +109,8 @@ export const CommitHistoryTab = ({
|
|||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
|
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
|
||||||
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
|
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
|
||||||
const [offset, setOffset] = useState(0);
|
|
||||||
const [allCommits, setAllCommits] = useState<Commit[]>([]);
|
|
||||||
const debounceTimeoutRef = useRef<NodeJS.Timeout>();
|
const debounceTimeoutRef = useRef<NodeJS.Timeout>();
|
||||||
const limit = 5;
|
const limit = 10;
|
||||||
|
|
||||||
// Debounce search term
|
// Debounce search term
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -170,55 +130,20 @@ export const CommitHistoryTab = ({
|
|||||||
}, [searchTerm]);
|
}, [searchTerm]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: response,
|
data: groupedCommits,
|
||||||
isLoading,
|
isLoading,
|
||||||
isFetching
|
fetchNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
hasNextPage
|
||||||
} = useGetFolderCommitHistory({
|
} = useGetFolderCommitHistory({
|
||||||
workspaceId: projectId,
|
workspaceId: projectId,
|
||||||
environment,
|
environment,
|
||||||
directory: secretPath,
|
directory: secretPath,
|
||||||
offset,
|
|
||||||
limit,
|
limit,
|
||||||
search: debouncedSearchTerm,
|
search: debouncedSearchTerm,
|
||||||
sort: sortDirection
|
sort: sortDirection
|
||||||
});
|
});
|
||||||
|
|
||||||
const commits = response?.commits || [];
|
|
||||||
const hasMore = response?.hasMore || false;
|
|
||||||
|
|
||||||
// Reset accumulated commits when search or sort changes
|
|
||||||
useEffect(() => {
|
|
||||||
setAllCommits([]);
|
|
||||||
setOffset(0);
|
|
||||||
}, [debouncedSearchTerm, sortDirection]);
|
|
||||||
|
|
||||||
// Accumulate commits instead of replacing them
|
|
||||||
useEffect(() => {
|
|
||||||
if (commits.length > 0) {
|
|
||||||
if (offset === 0) {
|
|
||||||
// First load or after search/sort change - replace all commits
|
|
||||||
setAllCommits(commits);
|
|
||||||
} else {
|
|
||||||
// Subsequent loads - append new commits
|
|
||||||
setAllCommits((prev) => [...prev, ...commits]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [commits, offset]);
|
|
||||||
|
|
||||||
const groupedCommits = useMemo(() => {
|
|
||||||
return allCommits.reduce(
|
|
||||||
(acc, commit) => {
|
|
||||||
const date = format(new Date(commit.createdAt), "MMM d, yyyy");
|
|
||||||
if (!acc[date]) {
|
|
||||||
acc[date] = [];
|
|
||||||
}
|
|
||||||
acc[date].push(commit);
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as Record<string, Commit[]>
|
|
||||||
);
|
|
||||||
}, [allCommits]);
|
|
||||||
|
|
||||||
const handleSort = useCallback(() => {
|
const handleSort = useCallback(() => {
|
||||||
setSortDirection((prev) => (prev === "desc" ? "asc" : "desc"));
|
setSortDirection((prev) => (prev === "desc" ? "asc" : "desc"));
|
||||||
}, []);
|
}, []);
|
||||||
@@ -227,50 +152,39 @@ export const CommitHistoryTab = ({
|
|||||||
setSearchTerm(value);
|
setSearchTerm(value);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadMoreCommits = useCallback(() => {
|
|
||||||
if (hasMore && !isFetching) {
|
|
||||||
setOffset((prev) => prev + limit);
|
|
||||||
}
|
|
||||||
}, [hasMore, isFetching, limit]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="mt-4 w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<p className="mb-4 text-xl font-semibold text-mineshaft-100">Commit History</p>
|
||||||
<div className="mb-4 flex flex-col sm:flex-row sm:justify-end">
|
<div className="mb-4 flex flex-col sm:flex-row sm:justify-end">
|
||||||
<div className="flex w-full flex-wrap items-center gap-2">
|
<div className="flex w-full flex-wrap items-center gap-2">
|
||||||
<div className="relative flex-grow">
|
<div className="relative flex-grow">
|
||||||
<Input
|
<Input
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faSearch} aria-hidden="true" />}
|
||||||
placeholder="Search commits..."
|
placeholder="Search commits..."
|
||||||
className="h-10 w-full rounded-md border-transparent bg-zinc-800 pl-9 pr-3 text-sm text-white placeholder-gray-400 focus:border-gray-600 focus:ring-primary-500/20"
|
|
||||||
onChange={(e) => handleSearch(e.target.value)}
|
onChange={(e) => handleSearch(e.target.value)}
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
aria-label="Search commits"
|
aria-label="Search commits"
|
||||||
/>
|
/>
|
||||||
<div className="absolute left-3 top-1/2 -translate-y-1/2 transform text-gray-400">
|
|
||||||
<FontAwesomeIcon icon={faSearch} aria-hidden="true" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<IconButton
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
size="md"
|
size="sm"
|
||||||
className="flex h-10 items-center justify-center gap-2 rounded-md bg-zinc-800 px-4 py-2 text-sm text-white transition-colors duration-200 hover:bg-zinc-700 focus:outline-none focus:ring-2 focus:ring-primary-500"
|
className="flex h-[2.4rem] items-center justify-center gap-2 rounded-md"
|
||||||
onClick={handleSort}
|
onClick={handleSort}
|
||||||
aria-label={`Sort by date ${sortDirection === "desc" ? "ascending" : "descending"}`}
|
ariaLabel={`Sort by date ${sortDirection === "desc" ? "ascending" : "descending"}`}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={sortDirection === "desc" ? faArrowDownWideShort : faArrowUpWideShort}
|
icon={sortDirection === "desc" ? faArrowDownWideShort : faArrowUpWideShort}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</Button>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{isLoading ? (
|
||||||
{isLoading && offset === 0 ? (
|
<ContentLoader className="h-80" />
|
||||||
<div className="flex h-64 items-center justify-center">
|
|
||||||
<Spinner size="lg" aria-label="Loading commits" />
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-8">
|
<div>
|
||||||
{Object.keys(groupedCommits).length > 0 ? (
|
{groupedCommits && Object.keys(groupedCommits).length > 0 ? (
|
||||||
<>
|
<>
|
||||||
{Object.entries(groupedCommits).map(([date, dateCommits]) => (
|
{Object.entries(groupedCommits).map(([date, dateCommits]) => (
|
||||||
<DateGroup
|
<DateGroup
|
||||||
@@ -282,34 +196,20 @@ export const CommitHistoryTab = ({
|
|||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-white-400 flex min-h-40 flex-col items-center justify-center rounded-lg bg-zinc-900 py-8 text-center">
|
<EmptyState title="No commits found." icon={faCodeCommit} />
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faSearch}
|
|
||||||
className="text-white-500 mb-3 text-3xl"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<p>No matching commits found. Try a different search term.</p>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
{hasNextPage && (
|
||||||
{hasMore && (
|
|
||||||
<div className="flex justify-center pb-2">
|
<div className="flex justify-center pb-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
size="md"
|
size="sm"
|
||||||
className="rounded-md bg-zinc-900 px-6 py-2 text-sm font-medium text-white transition-colors duration-200 hover:bg-zinc-800 focus:outline-none focus:ring-2 focus:ring-primary-500"
|
className="ml-10 mt-4 w-full"
|
||||||
onClick={loadMoreCommits}
|
onClick={() => fetchNextPage()}
|
||||||
disabled={isFetching}
|
disabled={isFetchingNextPage}
|
||||||
|
isLoading={isFetchingNextPage}
|
||||||
aria-label="Load more commits"
|
aria-label="Load more commits"
|
||||||
>
|
>
|
||||||
{isFetching ? (
|
Load More Commits
|
||||||
<>
|
|
||||||
<Spinner size="sm" className="mr-2" />
|
|
||||||
Loading...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
"Load more commits"
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user