mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
improvements: address feedback
This commit is contained in:
@@ -8,6 +8,7 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
await knex.schema.createTable(TableName.AppConnection, (t) => {
|
await knex.schema.createTable(TableName.AppConnection, (t) => {
|
||||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
t.string("name", 32).notNullable();
|
t.string("name", 32).notNullable();
|
||||||
|
t.string("description");
|
||||||
t.string("app").notNullable();
|
t.string("app").notNullable();
|
||||||
t.string("method").notNullable();
|
t.string("method").notNullable();
|
||||||
t.binary("encryptedCredentials").notNullable();
|
t.binary("encryptedCredentials").notNullable();
|
||||||
@@ -16,9 +17,9 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||||
t.timestamps(true, true, true);
|
t.timestamps(true, true, true);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
await createOnUpdateTrigger(knex, TableName.AppConnection);
|
await createOnUpdateTrigger(knex, TableName.AppConnection);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(knex: Knex): Promise<void> {
|
export async function down(knex: Knex): Promise<void> {
|
||||||
@@ -12,6 +12,7 @@ import { TImmutableDBKeys } from "./models";
|
|||||||
export const AppConnectionsSchema = z.object({
|
export const AppConnectionsSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
description: z.string().nullable().optional(),
|
||||||
app: z.string(),
|
app: z.string(),
|
||||||
method: z.string(),
|
method: z.string(),
|
||||||
encryptedCredentials: zodBuffer,
|
encryptedCredentials: zodBuffer,
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import {
|
|||||||
} from "@app/ee/services/project-template/project-template-types";
|
} from "@app/ee/services/project-template/project-template-types";
|
||||||
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
||||||
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
|
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
|
||||||
import { AppConnection, TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/lib/app-connections";
|
|
||||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/services/app-connection/app-connection-types";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||||
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
|
||||||
@@ -1875,8 +1876,10 @@ interface ApplyProjectTemplateEvent {
|
|||||||
|
|
||||||
interface GetAppConnectionsEvent {
|
interface GetAppConnectionsEvent {
|
||||||
type: EventType.GET_APP_CONNECTIONS;
|
type: EventType.GET_APP_CONNECTIONS;
|
||||||
metadata?: {
|
metadata: {
|
||||||
app: AppConnection;
|
app?: AppConnection;
|
||||||
|
count: number;
|
||||||
|
connectionIds: string[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
pkiEst: false,
|
pkiEst: false,
|
||||||
enforceMfa: false,
|
enforceMfa: false,
|
||||||
projectTemplates: false,
|
projectTemplates: false,
|
||||||
appConnections: false
|
appConnections: true
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AppConnection } from "@app/lib/app-connections";
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
import { APP_CONNECTION_NAME_MAP } from "@app/lib/app-connections/maps";
|
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
|
||||||
|
|
||||||
export const GROUPS = {
|
export const GROUPS = {
|
||||||
CREATE: {
|
CREATE: {
|
||||||
@@ -1620,6 +1620,7 @@ export const AppConnections = {
|
|||||||
const appName = APP_CONNECTION_NAME_MAP[app];
|
const appName = APP_CONNECTION_NAME_MAP[app];
|
||||||
return {
|
return {
|
||||||
name: `The name of the ${appName} Connection to create. Must be slug-friendly.`,
|
name: `The name of the ${appName} Connection to create. Must be slug-friendly.`,
|
||||||
|
description: `An optional description for the ${appName} Connection.`,
|
||||||
credentials: `The credentials used to connect with ${appName}.`,
|
credentials: `The credentials used to connect with ${appName}.`,
|
||||||
method: `The method used to authenticate with ${appName}.`
|
method: `The method used to authenticate with ${appName}.`
|
||||||
};
|
};
|
||||||
@@ -1629,11 +1630,12 @@ export const AppConnections = {
|
|||||||
return {
|
return {
|
||||||
connectionId: `The ID of the ${appName} Connection to be updated.`,
|
connectionId: `The ID of the ${appName} Connection to be updated.`,
|
||||||
name: `The updated name of the ${appName} Connection. Must be slug-friendly.`,
|
name: `The updated name of the ${appName} Connection. Must be slug-friendly.`,
|
||||||
|
description: `The updated description of the ${appName} Connection.`,
|
||||||
credentials: `The credentials used to connect with ${appName}.`,
|
credentials: `The credentials used to connect with ${appName}.`,
|
||||||
method: `The method used to authenticate with ${appName}.`
|
method: `The method used to authenticate with ${appName}.`
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
DELETE: (app: AppConnection) => ({
|
DELETE: (app: AppConnection) => ({
|
||||||
connectionId: `The ID of the ${app} connection to be deleted.`
|
connectionId: `The ID of the ${APP_CONNECTION_NAME_MAP[app]} connection to be deleted.`
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
import { TAwsConnection } from "@app/lib/app-connections/aws/aws-connection-types";
|
|
||||||
import { TGitHubConnection, TGitHubConnectionInput } from "@app/lib/app-connections/github";
|
|
||||||
import { DiscriminativePick } from "@app/lib/types";
|
|
||||||
|
|
||||||
import { AppConnection } from "./app-connection-enums";
|
|
||||||
|
|
||||||
export type AppConnectionListItem = {
|
|
||||||
app: AppConnection;
|
|
||||||
name: string;
|
|
||||||
methods: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TAppConnection = { id: string } & (TAwsConnection | TGitHubConnection);
|
|
||||||
|
|
||||||
export type TAppConnectionInput = { id: string } & (TAwsConnection | TGitHubConnectionInput);
|
|
||||||
|
|
||||||
export type TCreateAppConnectionDTO = Pick<TAppConnectionInput, "credentials" | "method" | "name" | "app">;
|
|
||||||
|
|
||||||
export type TUpdateAppConnectionDTO = Partial<Omit<TCreateAppConnectionDTO, "method" | "app">> & {
|
|
||||||
connectionId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TAppConnectionConfig = { orgId: string } & DiscriminativePick<
|
|
||||||
TAppConnectionInput,
|
|
||||||
"app" | "method" | "credentials"
|
|
||||||
>;
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { AppConnections } from "@app/lib/api-docs";
|
|
||||||
import { slugSchema } from "@app/server/lib/schemas";
|
|
||||||
import { BaseAppConnectionSchema } from "@app/services/app-connection/app-connection-schemas";
|
|
||||||
|
|
||||||
import { AppConnection } from "../app-connection-enums";
|
|
||||||
import { AwsConnectionMethod } from "./aws-connection-enums";
|
|
||||||
|
|
||||||
export const AwsConnectionAssumeRoleCredentialsSchema = z.object({
|
|
||||||
roleArn: z.string().min(1, "Role ARN required")
|
|
||||||
});
|
|
||||||
|
|
||||||
export const AwsConnectionAccessTokenCredentialsSchema = z.object({
|
|
||||||
accessKeyId: z.string().min(1, "Access Key ID required"),
|
|
||||||
secretAccessKey: z.string().min(1, "Secret Access Key required")
|
|
||||||
});
|
|
||||||
|
|
||||||
const BaseAwsConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.AWS) });
|
|
||||||
|
|
||||||
export const AwsConnectionSchema = z.intersection(
|
|
||||||
BaseAwsConnectionSchema,
|
|
||||||
z.discriminatedUnion("method", [
|
|
||||||
z.object({
|
|
||||||
method: z.literal(AwsConnectionMethod.AssumeRole),
|
|
||||||
credentials: AwsConnectionAssumeRoleCredentialsSchema
|
|
||||||
}),
|
|
||||||
z.object({
|
|
||||||
method: z.literal(AwsConnectionMethod.AccessKey),
|
|
||||||
credentials: AwsConnectionAccessTokenCredentialsSchema
|
|
||||||
})
|
|
||||||
])
|
|
||||||
);
|
|
||||||
|
|
||||||
export const SanitizedAwsConnectionSchema = z.discriminatedUnion("method", [
|
|
||||||
BaseAwsConnectionSchema.extend({
|
|
||||||
method: z.literal(AwsConnectionMethod.AssumeRole),
|
|
||||||
credentials: AwsConnectionAssumeRoleCredentialsSchema.omit({ roleArn: true })
|
|
||||||
}),
|
|
||||||
BaseAwsConnectionSchema.extend({
|
|
||||||
method: z.literal(AwsConnectionMethod.AccessKey),
|
|
||||||
credentials: AwsConnectionAccessTokenCredentialsSchema.omit({ secretAccessKey: true })
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const CreateAwsConnectionSchema = z
|
|
||||||
.discriminatedUnion("method", [
|
|
||||||
z.object({
|
|
||||||
method: z.literal(AwsConnectionMethod.AssumeRole).describe(AppConnections.CREATE(AppConnection.AWS).method),
|
|
||||||
credentials: AwsConnectionAssumeRoleCredentialsSchema.describe(
|
|
||||||
AppConnections.CREATE(AppConnection.AWS).credentials
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
z.object({
|
|
||||||
method: z.literal(AwsConnectionMethod.AccessKey).describe(AppConnections.CREATE(AppConnection.AWS).method),
|
|
||||||
credentials: AwsConnectionAccessTokenCredentialsSchema.describe(
|
|
||||||
AppConnections.CREATE(AppConnection.AWS).credentials
|
|
||||||
)
|
|
||||||
})
|
|
||||||
])
|
|
||||||
.and(z.object({ name: slugSchema({ field: "name" }).describe(AppConnections.CREATE(AppConnection.AWS).name) }));
|
|
||||||
|
|
||||||
export const UpdateAwsConnectionSchema = z.object({
|
|
||||||
name: slugSchema({ field: "name" }).optional().describe(AppConnections.UPDATE(AppConnection.AWS).name),
|
|
||||||
credentials: z
|
|
||||||
.union([AwsConnectionAccessTokenCredentialsSchema, AwsConnectionAssumeRoleCredentialsSchema])
|
|
||||||
.optional()
|
|
||||||
.describe(AppConnections.UPDATE(AppConnection.AWS).credentials)
|
|
||||||
});
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { DiscriminativePick } from "@app/lib/types";
|
|
||||||
|
|
||||||
import { AwsConnectionSchema } from "./aws-connection-schemas";
|
|
||||||
|
|
||||||
export type TAwsConnection = z.infer<typeof AwsConnectionSchema>;
|
|
||||||
|
|
||||||
export type TAwsConnectionConfig = DiscriminativePick<TAwsConnection, "orgId" | "method" | "app" | "credentials">;
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { AppConnections } from "@app/lib/api-docs";
|
|
||||||
import { AppConnection } from "@app/lib/app-connections";
|
|
||||||
import { slugSchema } from "@app/server/lib/schemas";
|
|
||||||
import { BaseAppConnectionSchema } from "@app/services/app-connection/app-connection-schemas";
|
|
||||||
|
|
||||||
import { GitHubConnectionMethod } from "./github-connection-enums";
|
|
||||||
|
|
||||||
export const GitHubConnectionOAuthInputCredentialsSchema = z.object({
|
|
||||||
code: z.string().min(1, "OAuth code required")
|
|
||||||
});
|
|
||||||
|
|
||||||
export const GitHubConnectionAppInputCredentialsSchema = z.object({
|
|
||||||
code: z.string().min(1, "GitHub App code required"),
|
|
||||||
installationId: z.string().min(1, "GitHub App Installation ID required")
|
|
||||||
});
|
|
||||||
|
|
||||||
export const GitHubConnectionOAuthOutputCredentialsSchema = z.object({
|
|
||||||
accessToken: z.string()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const GitHubConnectionAppOutputCredentialsSchema = z.object({
|
|
||||||
installationId: z.string()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const CreateGitHubConnectionSchema = z
|
|
||||||
.discriminatedUnion("method", [
|
|
||||||
z.object({
|
|
||||||
method: z.literal(GitHubConnectionMethod.App).describe(AppConnections.CREATE(AppConnection.GitHub).method),
|
|
||||||
credentials: GitHubConnectionAppInputCredentialsSchema.describe(
|
|
||||||
AppConnections.CREATE(AppConnection.GitHub).credentials
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
z.object({
|
|
||||||
method: z.literal(GitHubConnectionMethod.OAuth).describe(AppConnections.CREATE(AppConnection.GitHub).method),
|
|
||||||
credentials: GitHubConnectionOAuthInputCredentialsSchema.describe(
|
|
||||||
AppConnections.CREATE(AppConnection.GitHub).credentials
|
|
||||||
)
|
|
||||||
})
|
|
||||||
])
|
|
||||||
.and(z.object({ name: slugSchema({ field: "name" }).describe(AppConnections.CREATE(AppConnection.GitHub).name) }));
|
|
||||||
|
|
||||||
export const UpdateGitHubConnectionSchema = z.object({
|
|
||||||
name: slugSchema({ field: "name" }).optional().describe(AppConnections.UPDATE(AppConnection.GitHub).name),
|
|
||||||
credentials: z
|
|
||||||
.union([GitHubConnectionAppInputCredentialsSchema, GitHubConnectionOAuthInputCredentialsSchema])
|
|
||||||
.optional()
|
|
||||||
.describe(AppConnections.UPDATE(AppConnection.GitHub).credentials)
|
|
||||||
});
|
|
||||||
|
|
||||||
const BaseGitHubConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.GitHub) });
|
|
||||||
|
|
||||||
export const GitHubAppConnectionSchema = z.intersection(
|
|
||||||
BaseGitHubConnectionSchema,
|
|
||||||
z.discriminatedUnion("method", [
|
|
||||||
z.object({
|
|
||||||
method: z.literal(GitHubConnectionMethod.App),
|
|
||||||
credentials: GitHubConnectionAppOutputCredentialsSchema
|
|
||||||
}),
|
|
||||||
z.object({
|
|
||||||
method: z.literal(GitHubConnectionMethod.OAuth),
|
|
||||||
credentials: GitHubConnectionOAuthOutputCredentialsSchema
|
|
||||||
})
|
|
||||||
])
|
|
||||||
);
|
|
||||||
|
|
||||||
export const SanitizedGitHubConnectionSchema = z.discriminatedUnion("method", [
|
|
||||||
BaseGitHubConnectionSchema.extend({
|
|
||||||
method: z.literal(GitHubConnectionMethod.App),
|
|
||||||
credentials: GitHubConnectionAppOutputCredentialsSchema.omit({ installationId: true })
|
|
||||||
}),
|
|
||||||
BaseGitHubConnectionSchema.extend({
|
|
||||||
method: z.literal(GitHubConnectionMethod.OAuth),
|
|
||||||
credentials: GitHubConnectionOAuthOutputCredentialsSchema.omit({ accessToken: true })
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from "./app-connection-enums";
|
|
||||||
export * from "./app-connection-types";
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { TAppConnection } from "@app/lib/app-connections/app-connection-types";
|
|
||||||
|
|
||||||
import { AppConnection } from "./app-connection-enums";
|
|
||||||
import { AwsConnectionMethod } from "./aws/aws-connection-enums";
|
|
||||||
import { GitHubConnectionMethod } from "./github/github-connection-enums";
|
|
||||||
|
|
||||||
export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
|
||||||
[AppConnection.AWS]: "AWS",
|
|
||||||
[AppConnection.GitHub]: "GitHub"
|
|
||||||
};
|
|
||||||
|
|
||||||
export const APP_CONNECTION_METHOD_NAME_MAP: Record<TAppConnection["method"], string> = {
|
|
||||||
[AwsConnectionMethod.AssumeRole]: "Assume Role",
|
|
||||||
[AwsConnectionMethod.AccessKey]: "Access Key",
|
|
||||||
[GitHubConnectionMethod.App]: "Github App",
|
|
||||||
[GitHubConnectionMethod.OAuth]: "OAuth"
|
|
||||||
};
|
|
||||||
@@ -14,3 +14,5 @@ export const prefixWithSlash = (str: string) => {
|
|||||||
if (str.startsWith("/")) return str;
|
if (str.startsWith("/")) return str;
|
||||||
return `/${str}`;
|
return `/${str}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const startsWithVowel = (str: string) => /^[aeiou]/i.test(str);
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
|
import { AwsConnectionListItemSchema, SanitizedAwsConnectionSchema } from "src/services/app-connection/aws";
|
||||||
|
import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "src/services/app-connection/github";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { AppConnection } from "@app/lib/app-connections";
|
|
||||||
import { SanitizedAwsConnectionSchema } from "@app/lib/app-connections/aws";
|
|
||||||
import { SanitizedGitHubConnectionSchema } from "@app/lib/app-connections/github";
|
|
||||||
import { readLimit } from "@app/server/config/rateLimiter";
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
// can't use discriminated due to multiple schemas for certain apps
|
// can't use discriminated due to multiple schemas for certain apps
|
||||||
export const SanitizedAppConnectionSchema = z.union([
|
const SanitizedAppConnectionSchema = z.union([
|
||||||
...SanitizedAwsConnectionSchema.options,
|
...SanitizedAwsConnectionSchema.options,
|
||||||
...SanitizedGitHubConnectionSchema.options
|
...SanitizedGitHubConnectionSchema.options
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||||
|
AwsConnectionListItemSchema,
|
||||||
|
GitHubConnectionListItemSchema
|
||||||
|
]);
|
||||||
|
|
||||||
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -25,18 +29,11 @@ export const registerAppConnectionRouter = async (server: FastifyZodProvider) =>
|
|||||||
description: "List the available App Connection Options.",
|
description: "List the available App Connection Options.",
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
appConnectionOptions: z
|
appConnectionOptions: AppConnectionOptionsSchema.array()
|
||||||
.object({
|
|
||||||
name: z.string(),
|
|
||||||
app: z.nativeEnum(AppConnection),
|
|
||||||
methods: z.string().array()
|
|
||||||
})
|
|
||||||
.passthrough()
|
|
||||||
.array()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: () => {
|
handler: () => {
|
||||||
const appConnectionOptions = server.services.appConnection.listAppConnectionOptions();
|
const appConnectionOptions = server.services.appConnection.listAppConnectionOptions();
|
||||||
return { appConnectionOptions };
|
return { appConnectionOptions };
|
||||||
@@ -55,7 +52,7 @@ export const registerAppConnectionRouter = async (server: FastifyZodProvider) =>
|
|||||||
200: z.object({ appConnections: SanitizedAppConnectionSchema.array() })
|
200: z.object({ appConnections: SanitizedAppConnectionSchema.array() })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const appConnections = await server.services.appConnection.listAppConnectionsByOrg(req.permission);
|
const appConnections = await server.services.appConnection.listAppConnectionsByOrg(req.permission);
|
||||||
|
|
||||||
@@ -63,7 +60,11 @@ export const registerAppConnectionRouter = async (server: FastifyZodProvider) =>
|
|||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
orgId: req.permission.orgId,
|
orgId: req.permission.orgId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.GET_APP_CONNECTIONS
|
type: EventType.GET_APP_CONNECTIONS,
|
||||||
|
metadata: {
|
||||||
|
count: appConnections.length,
|
||||||
|
connectionIds: appConnections.map((connection) => connection.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { AppConnections } from "@app/lib/api-docs";
|
import { AppConnections } from "@app/lib/api-docs";
|
||||||
import { AppConnection, TAppConnection, TAppConnectionInput } from "@app/lib/app-connections";
|
import { startsWithVowel } from "@app/lib/fn";
|
||||||
import { APP_CONNECTION_NAME_MAP } from "@app/lib/app-connections/maps";
|
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
|
||||||
|
import { TAppConnection, TAppConnectionInput } from "@app/services/app-connection/app-connection-types";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
export const registerAppConnectionEndpoints = <T extends TAppConnection, I extends TAppConnectionInput>({
|
export const registerAppConnectionEndpoints = <T extends TAppConnection, I extends TAppConnectionInput>({
|
||||||
@@ -17,8 +19,13 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
|
|||||||
}: {
|
}: {
|
||||||
app: AppConnection;
|
app: AppConnection;
|
||||||
server: FastifyZodProvider;
|
server: FastifyZodProvider;
|
||||||
createSchema: z.ZodType<{ name: string; method: I["method"]; credentials: I["credentials"] }>;
|
createSchema: z.ZodType<{
|
||||||
updateSchema: z.ZodType<{ name?: string; credentials?: I["credentials"] }>;
|
name: string;
|
||||||
|
method: I["method"];
|
||||||
|
credentials: I["credentials"];
|
||||||
|
description?: string | null;
|
||||||
|
}>;
|
||||||
|
updateSchema: z.ZodType<{ name?: string; credentials?: I["credentials"]; description?: string | null }>;
|
||||||
responseSchema: z.ZodTypeAny;
|
responseSchema: z.ZodTypeAny;
|
||||||
}) => {
|
}) => {
|
||||||
const appName = APP_CONNECTION_NAME_MAP[app];
|
const appName = APP_CONNECTION_NAME_MAP[app];
|
||||||
@@ -35,7 +42,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
|
|||||||
200: z.object({ appConnections: responseSchema.array() })
|
200: z.object({ appConnections: responseSchema.array() })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const appConnections = (await server.services.appConnection.listAppConnectionsByOrg(req.permission, app)) as T[];
|
const appConnections = (await server.services.appConnection.listAppConnectionsByOrg(req.permission, app)) as T[];
|
||||||
|
|
||||||
@@ -45,7 +52,9 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
|
|||||||
event: {
|
event: {
|
||||||
type: EventType.GET_APP_CONNECTIONS,
|
type: EventType.GET_APP_CONNECTIONS,
|
||||||
metadata: {
|
metadata: {
|
||||||
app
|
app,
|
||||||
|
count: appConnections.length,
|
||||||
|
connectionIds: appConnections.map((connection) => connection.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -69,7 +78,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
|
|||||||
200: z.object({ appConnection: responseSchema })
|
200: z.object({ appConnection: responseSchema })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { connectionId } = req.params;
|
const { connectionId } = req.params;
|
||||||
|
|
||||||
@@ -112,7 +121,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
|
|||||||
200: z.object({ appConnection: responseSchema })
|
200: z.object({ appConnection: responseSchema })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { connectionName } = req.params;
|
const { connectionName } = req.params;
|
||||||
|
|
||||||
@@ -144,18 +153,20 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
|
|||||||
rateLimit: writeLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
description: `Create an ${appName} Connection for the current organization.`,
|
description: `Create ${
|
||||||
|
startsWithVowel(appName) ? "an" : "a"
|
||||||
|
} ${appName} Connection for the current organization.`,
|
||||||
body: createSchema,
|
body: createSchema,
|
||||||
response: {
|
response: {
|
||||||
200: z.object({ appConnection: responseSchema })
|
200: z.object({ appConnection: responseSchema })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { name, method, credentials } = req.body;
|
const { name, method, credentials, description } = req.body;
|
||||||
|
|
||||||
const appConnection = (await server.services.appConnection.createAppConnection(
|
const appConnection = (await server.services.appConnection.createAppConnection(
|
||||||
{ name, method, app, credentials },
|
{ name, method, app, credentials, description },
|
||||||
req.permission
|
req.permission
|
||||||
)) as TAppConnection;
|
)) as TAppConnection;
|
||||||
|
|
||||||
@@ -193,13 +204,13 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
|
|||||||
200: z.object({ appConnection: responseSchema })
|
200: z.object({ appConnection: responseSchema })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { name, credentials } = req.body;
|
const { name, credentials, description } = 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 },
|
{ name, credentials, connectionId, description },
|
||||||
req.permission
|
req.permission
|
||||||
)) as T;
|
)) as T;
|
||||||
|
|
||||||
@@ -210,6 +221,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
|
|||||||
type: EventType.UPDATE_APP_CONNECTION,
|
type: EventType.UPDATE_APP_CONNECTION,
|
||||||
metadata: {
|
metadata: {
|
||||||
name,
|
name,
|
||||||
|
description,
|
||||||
credentialsUpdated: Boolean(credentials),
|
credentialsUpdated: Boolean(credentials),
|
||||||
connectionId
|
connectionId
|
||||||
}
|
}
|
||||||
@@ -235,7 +247,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
|
|||||||
200: z.object({ appConnection: responseSchema })
|
200: z.object({ appConnection: responseSchema })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { connectionId } = req.params;
|
const { connectionId } = req.params;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { AppConnection } from "@app/lib/app-connections";
|
|
||||||
import {
|
import {
|
||||||
CreateAwsConnectionSchema,
|
CreateAwsConnectionSchema,
|
||||||
SanitizedAwsConnectionSchema,
|
SanitizedAwsConnectionSchema,
|
||||||
UpdateAwsConnectionSchema
|
UpdateAwsConnectionSchema
|
||||||
} from "@app/lib/app-connections/aws";
|
} from "src/services/app-connection/aws";
|
||||||
|
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { AppConnection } from "@app/lib/app-connections";
|
|
||||||
import {
|
import {
|
||||||
CreateGitHubConnectionSchema,
|
CreateGitHubConnectionSchema,
|
||||||
GitHubAppConnectionSchema,
|
SanitizedGitHubConnectionSchema,
|
||||||
UpdateGitHubConnectionSchema
|
UpdateGitHubConnectionSchema
|
||||||
} from "@app/lib/app-connections/github";
|
} from "src/services/app-connection/github";
|
||||||
|
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ export const registerGitHubConnectionRouter = async (server: FastifyZodProvider)
|
|||||||
registerAppConnectionEndpoints({
|
registerAppConnectionEndpoints({
|
||||||
app: AppConnection.GitHub,
|
app: AppConnection.GitHub,
|
||||||
server,
|
server,
|
||||||
responseSchema: GitHubAppConnectionSchema,
|
responseSchema: SanitizedGitHubConnectionSchema,
|
||||||
createSchema: CreateGitHubConnectionSchema,
|
createSchema: CreateGitHubConnectionSchema,
|
||||||
updateSchema: UpdateGitHubConnectionSchema
|
updateSchema: UpdateGitHubConnectionSchema
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { AppConnection } from "@app/lib/app-connections";
|
|
||||||
import { registerAwsConnectionRouter } from "@app/server/routes/v1/app-connection-routers/apps/aws-connection-router";
|
import { registerAwsConnectionRouter } from "@app/server/routes/v1/app-connection-routers/apps/aws-connection-router";
|
||||||
import { registerGitHubConnectionRouter } from "@app/server/routes/v1/app-connection-routers/apps/github-connection-router";
|
import { registerGitHubConnectionRouter } from "@app/server/routes/v1/app-connection-routers/apps/github-connection-router";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
export const APP_CONNECTION_REGISTER_MAP: Record<AppConnection, (server: FastifyZodProvider) => Promise<void>> = {
|
export const APP_CONNECTION_REGISTER_MAP: Record<AppConnection, (server: FastifyZodProvider) => Promise<void>> = {
|
||||||
[AppConnection.AWS]: registerAwsConnectionRouter,
|
[AppConnection.AWS]: registerAwsConnectionRouter,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { ormify } from "@app/lib/knex";
|
|||||||
export type TAppConnectionDALFactory = ReturnType<typeof appConnectionDALFactory>;
|
export type TAppConnectionDALFactory = ReturnType<typeof appConnectionDALFactory>;
|
||||||
|
|
||||||
export const appConnectionDALFactory = (db: TDbClient) => {
|
export const appConnectionDALFactory = (db: TDbClient) => {
|
||||||
const appConnection = ormify(db, TableName.AppConnection);
|
const appConnectionOrm = ormify(db, TableName.AppConnection);
|
||||||
|
|
||||||
return { ...appConnection };
|
return { ...appConnectionOrm };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
import { AppConnection, AppConnectionListItem, TAppConnection, TAppConnectionConfig } from "@app/lib/app-connections";
|
import {
|
||||||
import { getAwsAppConnectionListItem, validateAwsConnectionCredentials } from "@app/lib/app-connections/aws";
|
AwsConnectionMethod,
|
||||||
import { getGitHubConnectionListItem, validateGitHubConnectionCredentials } from "@app/lib/app-connections/github";
|
getAwsAppConnectionListItem,
|
||||||
|
validateAwsConnectionCredentials
|
||||||
|
} from "src/services/app-connection/aws";
|
||||||
|
import {
|
||||||
|
getGitHubConnectionListItem,
|
||||||
|
GitHubConnectionMethod,
|
||||||
|
validateGitHubConnectionCredentials
|
||||||
|
} from "src/services/app-connection/github";
|
||||||
|
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
import { TAppConnectionServiceFactoryDep } from "@app/services/app-connection/app-connection-service";
|
import { TAppConnectionServiceFactoryDep } from "@app/services/app-connection/app-connection-service";
|
||||||
|
import { TAppConnection, TAppConnectionConfig } from "@app/services/app-connection/app-connection-types";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
|
|
||||||
export const listAppConnectionOptions = (): (AppConnectionListItem & Record<string, unknown>)[] => {
|
export const listAppConnectionOptions = () => {
|
||||||
return [getAwsAppConnectionListItem(), getGitHubConnectionListItem()].sort((a, b) => a.name.localeCompare(b.name));
|
return [getAwsAppConnectionListItem(), getGitHubConnectionListItem()].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -65,3 +75,19 @@ export const validateAppConnectionCredentials = async (
|
|||||||
throw new Error(`Unhandled App Connection ${app}`);
|
throw new Error(`Unhandled App Connection ${app}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getAppConnectionMethodName = (method: TAppConnection["method"]) => {
|
||||||
|
switch (method) {
|
||||||
|
case GitHubConnectionMethod.App:
|
||||||
|
return "GitHub App";
|
||||||
|
case GitHubConnectionMethod.OAuth:
|
||||||
|
return "OAuth";
|
||||||
|
case AwsConnectionMethod.AccessKey:
|
||||||
|
return "Access Key";
|
||||||
|
case AwsConnectionMethod.AssumeRole:
|
||||||
|
return "Assume Role";
|
||||||
|
default:
|
||||||
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
|
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { AppConnection } from "./app-connection-enums";
|
||||||
|
|
||||||
|
export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||||
|
[AppConnection.AWS]: "AWS",
|
||||||
|
[AppConnection.GitHub]: "GitHub"
|
||||||
|
};
|
||||||
@@ -1,7 +1,35 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { AppConnectionsSchema } from "@app/db/schemas/app-connections";
|
import { AppConnectionsSchema } from "@app/db/schemas/app-connections";
|
||||||
|
import { AppConnections } from "@app/lib/api-docs";
|
||||||
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
|
|
||||||
|
import { AppConnection } from "./app-connection-enums";
|
||||||
|
|
||||||
export const BaseAppConnectionSchema = AppConnectionsSchema.omit({
|
export const BaseAppConnectionSchema = AppConnectionsSchema.omit({
|
||||||
encryptedCredentials: true,
|
encryptedCredentials: true,
|
||||||
app: true,
|
app: true,
|
||||||
method: true
|
method: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const GenericCreateAppConnectionFieldsSchema = (app: AppConnection) =>
|
||||||
|
z.object({
|
||||||
|
name: slugSchema({ field: "name" }).describe(AppConnections.CREATE(app).name),
|
||||||
|
description: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.max(256, "Description cannot exceed 256 characters")
|
||||||
|
.nullish()
|
||||||
|
.describe(AppConnections.CREATE(app).description)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const GenericUpdateAppConnectionFieldsSchema = (app: AppConnection) =>
|
||||||
|
z.object({
|
||||||
|
name: slugSchema({ field: "name" }).describe(AppConnections.UPDATE(app).name).optional(),
|
||||||
|
description: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.max(256, "Description cannot exceed 256 characters")
|
||||||
|
.nullish()
|
||||||
|
.describe(AppConnections.UPDATE(app).description)
|
||||||
|
});
|
||||||
|
|||||||
@@ -3,21 +3,26 @@ import { ForbiddenError } from "@casl/ability";
|
|||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import {
|
|
||||||
AppConnection,
|
|
||||||
TAppConnection,
|
|
||||||
TAppConnectionConfig,
|
|
||||||
TCreateAppConnectionDTO,
|
|
||||||
TUpdateAppConnectionDTO
|
|
||||||
} from "@app/lib/app-connections";
|
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { OrgServiceActor } from "@app/lib/types";
|
import { DiscriminativePick, OrgServiceActor } from "@app/lib/types";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
import {
|
import {
|
||||||
decryptAppConnectionCredentials,
|
decryptAppConnectionCredentials,
|
||||||
encryptAppConnectionCredentials,
|
encryptAppConnectionCredentials,
|
||||||
|
getAppConnectionMethodName,
|
||||||
listAppConnectionOptions,
|
listAppConnectionOptions,
|
||||||
validateAppConnectionCredentials
|
validateAppConnectionCredentials
|
||||||
} from "@app/services/app-connection/app-connection-fns";
|
} from "@app/services/app-connection/app-connection-fns";
|
||||||
|
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
|
||||||
|
import {
|
||||||
|
TAppConnection,
|
||||||
|
TAppConnectionConfig,
|
||||||
|
TCreateAppConnectionDTO,
|
||||||
|
TUpdateAppConnectionDTO,
|
||||||
|
TValidateAppConnectionCredentials
|
||||||
|
} from "@app/services/app-connection/app-connection-types";
|
||||||
|
import { ValidateAwsConnectionCredentialsSchema } from "@app/services/app-connection/aws";
|
||||||
|
import { ValidateGitHubConnectionCredentialsSchema } from "@app/services/app-connection/github";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
|
||||||
import { TAppConnectionDALFactory } from "./app-connection-dal";
|
import { TAppConnectionDALFactory } from "./app-connection-dal";
|
||||||
@@ -31,6 +36,11 @@ export type TAppConnectionServiceFactoryDep = {
|
|||||||
|
|
||||||
export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServiceFactory>;
|
export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServiceFactory>;
|
||||||
|
|
||||||
|
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAppConnectionCredentials> = {
|
||||||
|
[AppConnection.AWS]: ValidateAwsConnectionCredentialsSchema,
|
||||||
|
[AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema
|
||||||
|
};
|
||||||
|
|
||||||
export const appConnectionServiceFactory = ({
|
export const appConnectionServiceFactory = ({
|
||||||
appConnectionDAL,
|
appConnectionDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
@@ -160,40 +170,53 @@ export const appConnectionServiceFactory = ({
|
|||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.AppConnections);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.AppConnections);
|
||||||
|
|
||||||
const isConflictingName = Boolean(
|
const appConnection = await appConnectionDAL.transaction(async (tx) => {
|
||||||
await appConnectionDAL.findOne({
|
const isConflictingName = Boolean(
|
||||||
name: params.name,
|
await appConnectionDAL.findOne(
|
||||||
orgId: actor.orgId
|
{
|
||||||
})
|
name: params.name,
|
||||||
);
|
orgId: actor.orgId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
if (isConflictingName)
|
if (isConflictingName)
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: `An App Connection with the name "${params.name}" already exists`
|
message: `An App Connection with the name "${params.name}" already exists`
|
||||||
|
});
|
||||||
|
|
||||||
|
const validatedCredentials = await validateAppConnectionCredentials({
|
||||||
|
app,
|
||||||
|
credentials,
|
||||||
|
method,
|
||||||
|
orgId: actor.orgId
|
||||||
|
} as TAppConnectionConfig);
|
||||||
|
|
||||||
|
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||||
|
credentials: validatedCredentials,
|
||||||
|
orgId: actor.orgId,
|
||||||
|
kmsService
|
||||||
});
|
});
|
||||||
|
|
||||||
const validatedCredentials = await validateAppConnectionCredentials({
|
const connection = await appConnectionDAL.create(
|
||||||
app,
|
{
|
||||||
credentials,
|
orgId: actor.orgId,
|
||||||
method,
|
encryptedCredentials,
|
||||||
orgId: actor.orgId
|
method,
|
||||||
} as TAppConnectionConfig);
|
app,
|
||||||
|
...params
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
const encryptedCredentials = await encryptAppConnectionCredentials({
|
return {
|
||||||
credentials: validatedCredentials,
|
...connection,
|
||||||
orgId: actor.orgId,
|
credentials: validatedCredentials
|
||||||
kmsService
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const appConnection = await appConnectionDAL.create({
|
return appConnection;
|
||||||
orgId: actor.orgId,
|
|
||||||
encryptedCredentials,
|
|
||||||
method,
|
|
||||||
app,
|
|
||||||
...params
|
|
||||||
});
|
|
||||||
|
|
||||||
return { ...appConnection, credentials: validatedCredentials };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateAppConnection = async (
|
const updateAppConnection = async (
|
||||||
@@ -216,44 +239,69 @@ export const appConnectionServiceFactory = ({
|
|||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.AppConnections);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.AppConnections);
|
||||||
|
|
||||||
if (params.name && appConnection.name !== params.name) {
|
const updatedAppConnection = await appConnectionDAL.transaction(async (tx) => {
|
||||||
const isConflictingName = Boolean(
|
if (params.name && appConnection.name !== params.name) {
|
||||||
await appConnectionDAL.findOne({
|
const isConflictingName = Boolean(
|
||||||
name: params.name,
|
await appConnectionDAL.findOne(
|
||||||
orgId: appConnection.orgId
|
{
|
||||||
})
|
name: params.name,
|
||||||
|
orgId: appConnection.orgId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isConflictingName)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `An App Connection with the name "${params.name}" already exists`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let encryptedCredentials: undefined | Buffer;
|
||||||
|
|
||||||
|
if (credentials) {
|
||||||
|
const { app, method } = appConnection as DiscriminativePick<TAppConnectionConfig, "app" | "method">;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[app].safeParse({
|
||||||
|
method,
|
||||||
|
credentials
|
||||||
|
}).success
|
||||||
|
)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Invalid credential format for ${
|
||||||
|
APP_CONNECTION_NAME_MAP[app]
|
||||||
|
} Connection with method ${getAppConnectionMethodName(method)}`
|
||||||
|
});
|
||||||
|
|
||||||
|
const validatedCredentials = await validateAppConnectionCredentials({
|
||||||
|
app,
|
||||||
|
orgId: actor.orgId,
|
||||||
|
credentials,
|
||||||
|
method
|
||||||
|
} as TAppConnectionConfig);
|
||||||
|
|
||||||
|
if (!validatedCredentials)
|
||||||
|
throw new BadRequestError({ message: "Unable to validate connection - check credentials" });
|
||||||
|
|
||||||
|
encryptedCredentials = await encryptAppConnectionCredentials({
|
||||||
|
credentials: validatedCredentials,
|
||||||
|
orgId: actor.orgId,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedConnection = await appConnectionDAL.updateById(
|
||||||
|
connectionId,
|
||||||
|
{
|
||||||
|
orgId: actor.orgId,
|
||||||
|
encryptedCredentials,
|
||||||
|
...params
|
||||||
|
},
|
||||||
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isConflictingName)
|
return updatedConnection;
|
||||||
throw new BadRequestError({
|
|
||||||
message: `An App Connection with the name "${params.name}" already exists`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let encryptedCredentials: undefined | Buffer;
|
|
||||||
|
|
||||||
if (credentials) {
|
|
||||||
const validatedCredentials = await validateAppConnectionCredentials({
|
|
||||||
app: appConnection.app,
|
|
||||||
credentials,
|
|
||||||
method: appConnection.method,
|
|
||||||
orgId: actor.orgId
|
|
||||||
} as TAppConnectionConfig);
|
|
||||||
|
|
||||||
if (!validatedCredentials)
|
|
||||||
throw new BadRequestError({ message: "Unable to validate connection - check credentials" });
|
|
||||||
|
|
||||||
encryptedCredentials = await encryptAppConnectionCredentials({
|
|
||||||
credentials: validatedCredentials,
|
|
||||||
orgId: actor.orgId,
|
|
||||||
kmsService
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedAppConnection = await appConnectionDAL.updateById(connectionId, {
|
|
||||||
orgId: actor.orgId,
|
|
||||||
encryptedCredentials,
|
|
||||||
...params
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
31
backend/src/services/app-connection/app-connection-types.ts
Normal file
31
backend/src/services/app-connection/app-connection-types.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import {
|
||||||
|
TAwsConnection,
|
||||||
|
TAwsConnectionConfig,
|
||||||
|
TAwsConnectionInput,
|
||||||
|
TValidateAwsConnectionCredentials
|
||||||
|
} from "@app/services/app-connection/aws";
|
||||||
|
import {
|
||||||
|
TGitHubConnection,
|
||||||
|
TGitHubConnectionConfig,
|
||||||
|
TGitHubConnectionInput,
|
||||||
|
TValidateGitHubConnectionCredentials
|
||||||
|
} from "@app/services/app-connection/github";
|
||||||
|
|
||||||
|
export type TAppConnection = { id: string } & (TAwsConnection | TGitHubConnection);
|
||||||
|
|
||||||
|
export type TAppConnectionInput = { id: string } & (TAwsConnectionInput | TGitHubConnectionInput);
|
||||||
|
|
||||||
|
export type TCreateAppConnectionDTO = Pick<
|
||||||
|
TAppConnectionInput,
|
||||||
|
"credentials" | "method" | "name" | "app" | "description"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type TUpdateAppConnectionDTO = Partial<Omit<TCreateAppConnectionDTO, "method" | "app">> & {
|
||||||
|
connectionId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAppConnectionConfig = TAwsConnectionConfig | TGitHubConnectionConfig;
|
||||||
|
|
||||||
|
export type TValidateAppConnectionCredentials =
|
||||||
|
| TValidateAwsConnectionCredentials
|
||||||
|
| TValidateGitHubConnectionCredentials;
|
||||||
@@ -2,20 +2,20 @@ import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
|
|||||||
import AWS from "aws-sdk";
|
import AWS from "aws-sdk";
|
||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
|
|
||||||
import { AppConnection } from "@app/lib/app-connections/app-connection-enums";
|
|
||||||
import { TAwsConnectionConfig } from "@app/lib/app-connections/aws/aws-connection-types";
|
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
import { AwsConnectionMethod } from "./aws-connection-enums";
|
import { AwsConnectionMethod } from "./aws-connection-enums";
|
||||||
|
import { TAwsConnectionConfig } from "./aws-connection-types";
|
||||||
|
|
||||||
export const getAwsAppConnectionListItem = () => {
|
export const getAwsAppConnectionListItem = () => {
|
||||||
const { INF_APP_CONNECTION_AWS_ACCESS_KEY_ID } = getConfig();
|
const { INF_APP_CONNECTION_AWS_ACCESS_KEY_ID } = getConfig();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: "AWS",
|
name: "AWS" as const,
|
||||||
app: AppConnection.AWS,
|
app: AppConnection.AWS as const,
|
||||||
methods: Object.values(AwsConnectionMethod),
|
methods: Object.values(AwsConnectionMethod) as [AwsConnectionMethod.AssumeRole, AwsConnectionMethod.AccessKey],
|
||||||
accessKeyId: INF_APP_CONNECTION_AWS_ACCESS_KEY_ID
|
accessKeyId: INF_APP_CONNECTION_AWS_ACCESS_KEY_ID
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { AppConnections } from "@app/lib/api-docs";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import {
|
||||||
|
BaseAppConnectionSchema,
|
||||||
|
GenericCreateAppConnectionFieldsSchema,
|
||||||
|
GenericUpdateAppConnectionFieldsSchema
|
||||||
|
} from "@app/services/app-connection/app-connection-schemas";
|
||||||
|
|
||||||
|
import { AwsConnectionMethod } from "./aws-connection-enums";
|
||||||
|
|
||||||
|
export const AwsConnectionAssumeRoleCredentialsSchema = z.object({
|
||||||
|
roleArn: z.string().trim().min(1, "Role ARN required")
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AwsConnectionAccessTokenCredentialsSchema = z.object({
|
||||||
|
accessKeyId: z.string().trim().min(1, "Access Key ID required"),
|
||||||
|
secretAccessKey: z.string().trim().min(1, "Secret Access Key required")
|
||||||
|
});
|
||||||
|
|
||||||
|
const BaseAwsConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.AWS) });
|
||||||
|
|
||||||
|
export const AwsConnectionSchema = z.intersection(
|
||||||
|
BaseAwsConnectionSchema,
|
||||||
|
z.discriminatedUnion("method", [
|
||||||
|
z.object({
|
||||||
|
method: z.literal(AwsConnectionMethod.AssumeRole),
|
||||||
|
credentials: AwsConnectionAssumeRoleCredentialsSchema
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
method: z.literal(AwsConnectionMethod.AccessKey),
|
||||||
|
credentials: AwsConnectionAccessTokenCredentialsSchema
|
||||||
|
})
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
export const SanitizedAwsConnectionSchema = z.discriminatedUnion("method", [
|
||||||
|
BaseAwsConnectionSchema.extend({
|
||||||
|
method: z.literal(AwsConnectionMethod.AssumeRole),
|
||||||
|
credentials: AwsConnectionAssumeRoleCredentialsSchema.omit({ roleArn: true })
|
||||||
|
}),
|
||||||
|
BaseAwsConnectionSchema.extend({
|
||||||
|
method: z.literal(AwsConnectionMethod.AccessKey),
|
||||||
|
credentials: AwsConnectionAccessTokenCredentialsSchema.omit({ secretAccessKey: true })
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const ValidateAwsConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||||
|
z.object({
|
||||||
|
method: z.literal(AwsConnectionMethod.AssumeRole).describe(AppConnections?.CREATE(AppConnection.AWS).method),
|
||||||
|
credentials: AwsConnectionAssumeRoleCredentialsSchema.describe(AppConnections.CREATE(AppConnection.AWS).credentials)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
method: z.literal(AwsConnectionMethod.AccessKey).describe(AppConnections?.CREATE(AppConnection.AWS).method),
|
||||||
|
credentials: AwsConnectionAccessTokenCredentialsSchema.describe(
|
||||||
|
AppConnections.CREATE(AppConnection.AWS).credentials
|
||||||
|
)
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const CreateAwsConnectionSchema = ValidateAwsConnectionCredentialsSchema.and(
|
||||||
|
GenericCreateAppConnectionFieldsSchema(AppConnection.AWS)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const UpdateAwsConnectionSchema = z
|
||||||
|
.object({
|
||||||
|
credentials: z
|
||||||
|
.union([AwsConnectionAccessTokenCredentialsSchema, AwsConnectionAssumeRoleCredentialsSchema])
|
||||||
|
.optional()
|
||||||
|
.describe(AppConnections.UPDATE(AppConnection.AWS).credentials)
|
||||||
|
})
|
||||||
|
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AWS));
|
||||||
|
|
||||||
|
export const AwsConnectionListItemSchema = z.object({
|
||||||
|
name: z.literal("AWS"),
|
||||||
|
app: z.literal(AppConnection.AWS),
|
||||||
|
// the below is preferable but currently breaks mintlify
|
||||||
|
// methods: z.tuple([z.literal(AwsConnectionMethod.AssumeRole), z.literal(AwsConnectionMethod.AccessKey)]),
|
||||||
|
methods: z.nativeEnum(AwsConnectionMethod).array(),
|
||||||
|
accessKeyId: z.string().optional()
|
||||||
|
});
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { DiscriminativePick } from "@app/lib/types";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AwsConnectionSchema,
|
||||||
|
CreateAwsConnectionSchema,
|
||||||
|
ValidateAwsConnectionCredentialsSchema
|
||||||
|
} from "./aws-connection-schemas";
|
||||||
|
|
||||||
|
export type TAwsConnection = z.infer<typeof AwsConnectionSchema>;
|
||||||
|
|
||||||
|
export type TAwsConnectionInput = z.infer<typeof CreateAwsConnectionSchema> & {
|
||||||
|
app: AppConnection.AWS;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TValidateAwsConnectionCredentials = typeof ValidateAwsConnectionCredentialsSchema;
|
||||||
|
|
||||||
|
export type TAwsConnectionConfig = DiscriminativePick<TAwsConnectionInput, "method" | "app" | "credentials"> & {
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
@@ -3,10 +3,10 @@ import { AxiosResponse } from "axios";
|
|||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { request } from "@app/lib/config/request";
|
import { request } from "@app/lib/config/request";
|
||||||
import { BadRequestError, ForbiddenRequestError, InternalServerError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, InternalServerError } from "@app/lib/errors";
|
||||||
|
import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns";
|
||||||
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 { APP_CONNECTION_METHOD_NAME_MAP } from "../maps";
|
|
||||||
import { GitHubConnectionMethod } from "./github-connection-enums";
|
import { GitHubConnectionMethod } from "./github-connection-enums";
|
||||||
import { TGitHubConnectionConfig } from "./github-connection-types";
|
import { TGitHubConnectionConfig } from "./github-connection-types";
|
||||||
|
|
||||||
@@ -14,9 +14,9 @@ export const getGitHubConnectionListItem = () => {
|
|||||||
const { INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID, INF_APP_CONNECTION_GITHUB_APP_SLUG } = getConfig();
|
const { INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID, INF_APP_CONNECTION_GITHUB_APP_SLUG } = getConfig();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: "GitHub",
|
name: "GitHub" as const,
|
||||||
app: AppConnection.GitHub,
|
app: AppConnection.GitHub as const,
|
||||||
methods: Object.values(GitHubConnectionMethod),
|
methods: Object.values(GitHubConnectionMethod) as [GitHubConnectionMethod.App, GitHubConnectionMethod.OAuth],
|
||||||
oauthClientId: INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
|
oauthClientId: INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
|
||||||
appClientSlug: INF_APP_CONNECTION_GITHUB_APP_SLUG
|
appClientSlug: INF_APP_CONNECTION_GITHUB_APP_SLUG
|
||||||
};
|
};
|
||||||
@@ -53,7 +53,7 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
|
|||||||
|
|
||||||
if (!clientId || !clientSecret) {
|
if (!clientId || !clientSecret) {
|
||||||
throw new InternalServerError({
|
throw new InternalServerError({
|
||||||
message: `GitHub ${APP_CONNECTION_METHOD_NAME_MAP[method]} environment variables have not been configured`
|
message: `GitHub ${getAppConnectionMethodName(method)} environment variables have not been configured`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { AppConnections } from "@app/lib/api-docs";
|
||||||
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
import {
|
||||||
|
BaseAppConnectionSchema,
|
||||||
|
GenericCreateAppConnectionFieldsSchema,
|
||||||
|
GenericUpdateAppConnectionFieldsSchema
|
||||||
|
} from "@app/services/app-connection/app-connection-schemas";
|
||||||
|
|
||||||
|
import { GitHubConnectionMethod } from "./github-connection-enums";
|
||||||
|
|
||||||
|
export const GitHubConnectionOAuthInputCredentialsSchema = z.object({
|
||||||
|
code: z.string().trim().min(1, "OAuth code required")
|
||||||
|
});
|
||||||
|
|
||||||
|
export const GitHubConnectionAppInputCredentialsSchema = z.object({
|
||||||
|
code: z.string().trim().min(1, "GitHub App code required"),
|
||||||
|
installationId: z.string().min(1, "GitHub App Installation ID required")
|
||||||
|
});
|
||||||
|
|
||||||
|
export const GitHubConnectionOAuthOutputCredentialsSchema = z.object({
|
||||||
|
accessToken: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const GitHubConnectionAppOutputCredentialsSchema = z.object({
|
||||||
|
installationId: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ValidateGitHubConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||||
|
z.object({
|
||||||
|
method: z.literal(GitHubConnectionMethod.App).describe(AppConnections.CREATE(AppConnection.GitHub).method),
|
||||||
|
credentials: GitHubConnectionAppInputCredentialsSchema.describe(
|
||||||
|
AppConnections.CREATE(AppConnection.GitHub).credentials
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
method: z.literal(GitHubConnectionMethod.OAuth).describe(AppConnections.CREATE(AppConnection.GitHub).method),
|
||||||
|
credentials: GitHubConnectionOAuthInputCredentialsSchema.describe(
|
||||||
|
AppConnections.CREATE(AppConnection.GitHub).credentials
|
||||||
|
)
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const CreateGitHubConnectionSchema = ValidateGitHubConnectionCredentialsSchema.and(
|
||||||
|
GenericCreateAppConnectionFieldsSchema(AppConnection.GitHub)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const UpdateGitHubConnectionSchema = z
|
||||||
|
.object({
|
||||||
|
credentials: z
|
||||||
|
.union([GitHubConnectionAppInputCredentialsSchema, GitHubConnectionOAuthInputCredentialsSchema])
|
||||||
|
.optional()
|
||||||
|
.describe(AppConnections.UPDATE(AppConnection.GitHub).credentials)
|
||||||
|
})
|
||||||
|
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.GitHub));
|
||||||
|
|
||||||
|
const BaseGitHubConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.GitHub) });
|
||||||
|
|
||||||
|
export const GitHubAppConnectionSchema = z.intersection(
|
||||||
|
BaseGitHubConnectionSchema,
|
||||||
|
z.discriminatedUnion("method", [
|
||||||
|
z.object({
|
||||||
|
method: z.literal(GitHubConnectionMethod.App),
|
||||||
|
credentials: GitHubConnectionAppOutputCredentialsSchema
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
method: z.literal(GitHubConnectionMethod.OAuth),
|
||||||
|
credentials: GitHubConnectionOAuthOutputCredentialsSchema
|
||||||
|
})
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
export const SanitizedGitHubConnectionSchema = z.discriminatedUnion("method", [
|
||||||
|
BaseGitHubConnectionSchema.extend({
|
||||||
|
method: z.literal(GitHubConnectionMethod.App),
|
||||||
|
credentials: GitHubConnectionAppOutputCredentialsSchema.omit({ installationId: true })
|
||||||
|
}),
|
||||||
|
BaseGitHubConnectionSchema.extend({
|
||||||
|
method: z.literal(GitHubConnectionMethod.OAuth),
|
||||||
|
credentials: GitHubConnectionOAuthOutputCredentialsSchema.omit({ accessToken: true })
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const GitHubConnectionListItemSchema = z.object({
|
||||||
|
name: z.literal("GitHub"),
|
||||||
|
app: z.literal(AppConnection.GitHub),
|
||||||
|
// the below is preferable but currently breaks mintlify
|
||||||
|
// methods: z.tuple([z.literal(GitHubConnectionMethod.GitHubApp), z.literal(GitHubConnectionMethod.OAuth)]),
|
||||||
|
methods: z.nativeEnum(GitHubConnectionMethod).array(),
|
||||||
|
oauthClientId: z.string().optional(),
|
||||||
|
appClientSlug: z.string().optional()
|
||||||
|
});
|
||||||
@@ -3,12 +3,18 @@ import { z } from "zod";
|
|||||||
import { DiscriminativePick } from "@app/lib/types";
|
import { DiscriminativePick } from "@app/lib/types";
|
||||||
|
|
||||||
import { AppConnection } from "../app-connection-enums";
|
import { AppConnection } from "../app-connection-enums";
|
||||||
import { CreateGitHubConnectionSchema, GitHubAppConnectionSchema } from "./github-connection-schemas";
|
import {
|
||||||
|
CreateGitHubConnectionSchema,
|
||||||
export type TGitHubConnectionConfig = DiscriminativePick<TGitHubConnectionInput, "method" | "app" | "credentials">;
|
GitHubAppConnectionSchema,
|
||||||
|
ValidateGitHubConnectionCredentialsSchema
|
||||||
|
} from "./github-connection-schemas";
|
||||||
|
|
||||||
export type TGitHubConnection = z.infer<typeof GitHubAppConnectionSchema>;
|
export type TGitHubConnection = z.infer<typeof GitHubAppConnectionSchema>;
|
||||||
|
|
||||||
export type TGitHubConnectionInput = z.infer<typeof CreateGitHubConnectionSchema> & {
|
export type TGitHubConnectionInput = z.infer<typeof CreateGitHubConnectionSchema> & {
|
||||||
app: AppConnection.GitHub;
|
app: AppConnection.GitHub;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TValidateGitHubConnectionCredentials = typeof ValidateGitHubConnectionCredentialsSchema;
|
||||||
|
|
||||||
|
export type TGitHubConnectionConfig = DiscriminativePick<TGitHubConnectionInput, "method" | "app" | "credentials">;
|
||||||
BIN
docs/images/app-connections/aws/parameter-store-permissions.png
Normal file
BIN
docs/images/app-connections/aws/parameter-store-permissions.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 306 KiB |
BIN
docs/images/app-connections/aws/secrets-manager-permissions.png
Normal file
BIN
docs/images/app-connections/aws/secrets-manager-permissions.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 311 KiB |
@@ -68,37 +68,69 @@ Infisical supports two methods for connecting to AWS.
|
|||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab title="Secrets Sync">
|
<Tab title="Secrets Sync">
|
||||||
Add the **SecretsManagerReadWrite** policy to your IAM Role.
|
<AccordionGroup>
|
||||||
|
<Accordion title="AWS Secrets Manager">
|
||||||
|
Use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Secrets Manager:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Alternatively, use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Parameter Store:
|
```json
|
||||||
|
{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Sid": "AllowSecretsManagerAccess",
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": [
|
||||||
|
"secretsmanager:GetSecretValue",
|
||||||
|
"secretsmanager:CreateSecret",
|
||||||
|
"secretsmanager:UpdateSecret",
|
||||||
|
"secretsmanager:DescribeSecret",
|
||||||
|
"secretsmanager:TagResource",
|
||||||
|
"secretsmanager:UntagResource",
|
||||||
|
"kms:ListKeys",
|
||||||
|
"kms:ListAliases",
|
||||||
|
"kms:Encrypt",
|
||||||
|
"kms:Decrypt"
|
||||||
|
],
|
||||||
|
"Resource": "*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</Accordion>
|
||||||
|
<Accordion title="AWS Paramter Store">
|
||||||
|
Use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Parameter Store:
|
||||||
|
|
||||||
```json
|

|
||||||
{
|
|
||||||
"Version": "2012-10-17",
|
```json
|
||||||
"Statement": [
|
{
|
||||||
{
|
"Version": "2012-10-17",
|
||||||
"Sid": "AllowSSMAccess",
|
"Statement": [
|
||||||
"Effect": "Allow",
|
{
|
||||||
"Action": [
|
"Sid": "AllowSSMAccess",
|
||||||
"ssm:PutParameter",
|
"Effect": "Allow",
|
||||||
"ssm:DeleteParameter",
|
"Action": [
|
||||||
"ssm:GetParameters",
|
"ssm:PutParameter",
|
||||||
"ssm:GetParametersByPath",
|
"ssm:DeleteParameter",
|
||||||
"ssm:DescribeParameters",
|
"ssm:GetParameters",
|
||||||
"ssm:DeleteParameters",
|
"ssm:GetParametersByPath",
|
||||||
"ssm:AddTagsToResource", // if you need to add tags to secrets
|
"ssm:DescribeParameters",
|
||||||
"kms:ListKeys", // if you need to specify the KMS key
|
"ssm:DeleteParameters",
|
||||||
"kms:ListAliases", // if you need to specify the KMS key
|
"ssm:AddTagsToResource", // if you need to add tags to secrets
|
||||||
"kms:Encrypt", // if you need to specify the KMS key
|
"kms:ListKeys", // if you need to specify the KMS key
|
||||||
"kms:Decrypt" // if you need to specify the KMS key
|
"kms:ListAliases", // if you need to specify the KMS key
|
||||||
],
|
"kms:Encrypt", // if you need to specify the KMS key
|
||||||
"Resource": "*"
|
"kms:Decrypt" // if you need to specify the KMS key
|
||||||
}
|
],
|
||||||
]
|
"Resource": "*"
|
||||||
}
|
}
|
||||||
```
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</Accordion>
|
||||||
|
</AccordionGroup>
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Step>
|
</Step>
|
||||||
@@ -186,36 +218,69 @@ Infisical supports two methods for connecting to AWS.
|
|||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab title="Secrets Sync">
|
<Tab title="Secrets Sync">
|
||||||
Add the **SecretsManagerReadWrite** policy to your IAM Role.
|
<AccordionGroup>
|
||||||
|
<Accordion title="AWS Secrets Manager">
|
||||||
|
Use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Secrets Manager:
|
||||||
|
|
||||||

|

|
||||||
Alternatively, use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Parameter Store:
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"Version": "2012-10-17",
|
"Version": "2012-10-17",
|
||||||
"Statement": [
|
"Statement": [
|
||||||
{
|
{
|
||||||
"Sid": "AllowSSMAccess",
|
"Sid": "AllowSecretsManagerAccess",
|
||||||
"Effect": "Allow",
|
"Effect": "Allow",
|
||||||
"Action": [
|
"Action": [
|
||||||
"ssm:PutParameter",
|
"secretsmanager:GetSecretValue",
|
||||||
"ssm:DeleteParameter",
|
"secretsmanager:CreateSecret",
|
||||||
"ssm:GetParameters",
|
"secretsmanager:UpdateSecret",
|
||||||
"ssm:GetParametersByPath",
|
"secretsmanager:DescribeSecret",
|
||||||
"ssm:DescribeParameters",
|
"secretsmanager:TagResource",
|
||||||
"ssm:DeleteParameters",
|
"secretsmanager:UntagResource",
|
||||||
"ssm:AddTagsToResource", // if you need to add tags to secrets
|
"kms:ListKeys",
|
||||||
"kms:ListKeys", // if you need to specify the KMS key
|
"kms:ListAliases",
|
||||||
"kms:ListAliases", // if you need to specify the KMS key
|
"kms:Encrypt",
|
||||||
"kms:Encrypt", // if you need to specify the KMS key
|
"kms:Decrypt"
|
||||||
"kms:Decrypt" // if you need to specify the KMS key
|
],
|
||||||
],
|
"Resource": "*"
|
||||||
"Resource": "*"
|
}
|
||||||
}
|
]
|
||||||
]
|
}
|
||||||
}
|
```
|
||||||
```
|
</Accordion>
|
||||||
|
<Accordion title="AWS Paramter Store">
|
||||||
|
Use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Parameter Store:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Sid": "AllowSSMAccess",
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": [
|
||||||
|
"ssm:PutParameter",
|
||||||
|
"ssm:DeleteParameter",
|
||||||
|
"ssm:GetParameters",
|
||||||
|
"ssm:GetParametersByPath",
|
||||||
|
"ssm:DescribeParameters",
|
||||||
|
"ssm:DeleteParameters",
|
||||||
|
"ssm:AddTagsToResource", // if you need to add tags to secrets
|
||||||
|
"kms:ListKeys", // if you need to specify the KMS key
|
||||||
|
"kms:ListAliases", // if you need to specify the KMS key
|
||||||
|
"kms:Encrypt", // if you need to specify the KMS key
|
||||||
|
"kms:Decrypt" // if you need to specify the KMS key
|
||||||
|
],
|
||||||
|
"Resource": "*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</Accordion>
|
||||||
|
</AccordionGroup>
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Step>
|
</Step>
|
||||||
|
|||||||
@@ -104,6 +104,42 @@ Infisical supports two methods for connecting to GitHub.
|
|||||||
|
|
||||||
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
|
||||||
|
|
||||||
|
<Accordion title="Self-Hosted Instance">
|
||||||
|
Using the GitHub integration on a self-hosted instance of Infisical requires configuring an OAuth application in GitHub
|
||||||
|
and registering your instance with it.
|
||||||
|
<Steps>
|
||||||
|
<Step title="Create an OAuth application in GitHub">
|
||||||
|
Navigate to your user Settings > Developer settings > OAuth Apps to create a new GitHub OAuth application.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
Create the OAuth application. As part of the form, set the **Homepage URL** to your self-hosted domain `https://your-domain.com`
|
||||||
|
and the **Authorization callback URL** to `https://your-domain.com/app-connections/github/oauth/callback`.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Note>
|
||||||
|
If you have a GitHub organization, you can create an OAuth application under it
|
||||||
|
in your organization Settings > Developer settings > OAuth Apps > New Org OAuth App.
|
||||||
|
</Note>
|
||||||
|
</Step>
|
||||||
|
<Step title="Add your OAuth application credentials to Infisical">
|
||||||
|
Obtain the **Client ID** and generate a new **Client Secret** for your GitHub OAuth application.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Back in your Infisical instance, add two new environment variables for the credentials of your GitHub OAuth application:
|
||||||
|
|
||||||
|
- `INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID`: The **Client ID** of your GitHub OAuth application.
|
||||||
|
- `INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_SECRET`: The **Client Secret** of your GitHub OAuth application.
|
||||||
|
|
||||||
|
Once added, restart your Infisical instance and use the GitHub integration.
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
## Setup GitHub Connection in Infisical
|
## Setup GitHub Connection in Infisical
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
|
|||||||
@@ -17,17 +17,25 @@ that can be used across Infisical projects. Example use cases include syncing se
|
|||||||
```mermaid
|
```mermaid
|
||||||
%%{init: {'flowchart': {'curve': 'linear'} } }%%
|
%%{init: {'flowchart': {'curve': 'linear'} } }%%
|
||||||
graph TD
|
graph TD
|
||||||
A[AWS Connection]
|
A[AWS]
|
||||||
A --> B[Project 1 Secret Sync]
|
B[AWS Connection]
|
||||||
A --> C[Project 2 Secret Sync]
|
C[Project 1 Secret Sync]
|
||||||
A --> D[Project 3 Generate Dynamic Secret]
|
D[Project 2 Secret Sync]
|
||||||
|
E[Project 3 Generate Dynamic Secret]
|
||||||
|
|
||||||
|
B --> A
|
||||||
|
C --> B
|
||||||
|
D --> B
|
||||||
|
E --> B
|
||||||
|
|
||||||
classDef default fill:#ffffff,stroke:#666,stroke-width:2px,rx:10px,color:black
|
classDef default fill:#ffffff,stroke:#666,stroke-width:2px,rx:10px,color:black
|
||||||
classDef aws fill:#FFF2B2,stroke:#E6C34A,stroke-width:2px,color:black,rx:15px
|
classDef aws fill:#FFF2B2,stroke:#E6C34A,stroke-width:2px,color:black,rx:15px
|
||||||
classDef project fill:#E6F4FF,stroke:#0096D6,stroke-width:2px,color:black,rx:15px
|
classDef project fill:#E6F4FF,stroke:#0096D6,stroke-width:2px,color:black,rx:15px
|
||||||
|
classDef connection fill:#F4FFE6,stroke:#96D600,stroke-width:2px,color:black,rx:15px
|
||||||
|
|
||||||
class A aws
|
class A aws
|
||||||
class B,C,D project
|
class B connection
|
||||||
|
class C,D,E project
|
||||||
```
|
```
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { faCheck } from "@fortawesome/free-solid-svg-icons";
|
|||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { Modal, ModalContent, ModalTrigger, Select, SelectItem } from "@app/components/v2";
|
import { Modal, ModalContent, ModalTrigger, Select, SelectItem } from "@app/components/v2";
|
||||||
|
import { isInfisicalCloud } from "@app/helpers/platform";
|
||||||
|
|
||||||
enum Region {
|
enum Region {
|
||||||
US = "us",
|
US = "us",
|
||||||
@@ -79,10 +80,7 @@ export const RegionSelect = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const shouldDisplay =
|
const shouldDisplay =
|
||||||
window.location.origin.includes("https://app.infisical.com") ||
|
isInfisicalCloud() || window.location.origin.includes("http://localhost:8080");
|
||||||
window.location.origin.includes("https://us.infisical.com") ||
|
|
||||||
window.location.origin.includes("https://eu.infisical.com") ||
|
|
||||||
window.location.origin.includes("http://localhost:8080");
|
|
||||||
|
|
||||||
// only display region select for cloud
|
// only display region select for cloud
|
||||||
if (!shouldDisplay) return null;
|
if (!shouldDisplay) return null;
|
||||||
|
|||||||
@@ -1,22 +1,29 @@
|
|||||||
import { faGithub, IconDefinition } from "@fortawesome/free-brands-svg-icons";
|
import { faGithub } from "@fortawesome/free-brands-svg-icons";
|
||||||
import { faKey, faPassport, faUser } from "@fortawesome/free-solid-svg-icons";
|
import { faKey, faPassport, faUser } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
import { TAppConnection } from "@app/hooks/api/appConnections/types";
|
import {
|
||||||
import { AwsConnectionMethod } from "@app/hooks/api/appConnections/types/aws-connection";
|
AwsConnectionMethod,
|
||||||
import { GitHubConnectionMethod } from "@app/hooks/api/appConnections/types/github-connection";
|
GitHubConnectionMethod,
|
||||||
|
TAppConnection
|
||||||
|
} from "@app/hooks/api/appConnections/types";
|
||||||
|
|
||||||
export const APP_CONNECTION_MAP: Record<AppConnection, { name: string; image: string }> = {
|
export const APP_CONNECTION_MAP: Record<AppConnection, { name: string; image: string }> = {
|
||||||
[AppConnection.AWS]: { name: "AWS", image: "Amazon Web Services.png" },
|
[AppConnection.AWS]: { name: "AWS", image: "Amazon Web Services.png" },
|
||||||
[AppConnection.GitHub]: { name: "GitHub", image: "GitHub.png" }
|
[AppConnection.GitHub]: { name: "GitHub", image: "GitHub.png" }
|
||||||
};
|
};
|
||||||
|
|
||||||
export const APP_CONNECTION_METHOD_MAP: Record<
|
export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => {
|
||||||
TAppConnection["method"],
|
switch (method) {
|
||||||
{ name: string; icon: IconDefinition }
|
case GitHubConnectionMethod.App:
|
||||||
> = {
|
return { name: "GitHub App", icon: faGithub };
|
||||||
[AwsConnectionMethod.AssumeRole]: { name: "Assume Role", icon: faUser },
|
case GitHubConnectionMethod.OAuth:
|
||||||
[AwsConnectionMethod.AccessKey]: { name: "Access Key", icon: faKey },
|
return { name: "OAuth", icon: faPassport };
|
||||||
[GitHubConnectionMethod.App]: { name: "GitHub App", icon: faGithub },
|
case AwsConnectionMethod.AccessKey:
|
||||||
[GitHubConnectionMethod.OAuth]: { name: "OAuth", icon: faPassport }
|
return { name: "Access Key", icon: faKey };
|
||||||
|
case AwsConnectionMethod.AssumeRole:
|
||||||
|
return { name: "Assume Role", icon: faUser };
|
||||||
|
default:
|
||||||
|
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
4
frontend/src/helpers/platform.ts
Normal file
4
frontend/src/helpers/platform.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export const isInfisicalCloud = () =>
|
||||||
|
window.location.origin.includes("https://app.infisical.com") ||
|
||||||
|
window.location.origin.includes("https://us.infisical.com") ||
|
||||||
|
window.location.origin.includes("https://eu.infisical.com");
|
||||||
@@ -15,10 +15,12 @@ export type TAppConnectionResponse = { appConnection: TAppConnection };
|
|||||||
|
|
||||||
export type TCreateAppConnectionDTO = Pick<
|
export type TCreateAppConnectionDTO = Pick<
|
||||||
TAppConnection,
|
TAppConnection,
|
||||||
"name" | "credentials" | "method" | "app"
|
"name" | "credentials" | "method" | "app" | "description"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type TUpdateAppConnectionDTO = Partial<Pick<TAppConnection, "name" | "credentials">> & {
|
export type TUpdateAppConnectionDTO = Partial<
|
||||||
|
Pick<TAppConnection, "name" | "credentials" | "description">
|
||||||
|
> & {
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
app: AppConnection;
|
app: AppConnection;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export type TRootAppConnection = {
|
export type TRootAppConnection = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
description?: string | null;
|
||||||
version: number;
|
version: number;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
} from "@app/hooks/api/appConnections";
|
} from "@app/hooks/api/appConnections";
|
||||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
|
|
||||||
type FormData = Pick<TGitHubConnection, "name" | "method"> & {
|
type FormData = Pick<TGitHubConnection, "name" | "method" | "description"> & {
|
||||||
returnUrl?: string;
|
returnUrl?: string;
|
||||||
connectionId?: string;
|
connectionId?: string;
|
||||||
};
|
};
|
||||||
@@ -58,7 +58,7 @@ export default function GitHubOAuthCallbackPage() {
|
|||||||
localStorage.removeItem("githubConnectionFormData");
|
localStorage.removeItem("githubConnectionFormData");
|
||||||
localStorage.removeItem("latestCSRFToken");
|
localStorage.removeItem("latestCSRFToken");
|
||||||
|
|
||||||
const { connectionId, name, returnUrl } = formData;
|
const { connectionId, name, description, returnUrl } = formData;
|
||||||
|
|
||||||
let appConnection: TAppConnection;
|
let appConnection: TAppConnection;
|
||||||
|
|
||||||
@@ -85,6 +85,7 @@ export default function GitHubOAuthCallbackPage() {
|
|||||||
appConnection = await createAppConnection.mutateAsync({
|
appConnection = await createAppConnection.mutateAsync({
|
||||||
app: AppConnection.GitHub,
|
app: AppConnection.GitHub,
|
||||||
name,
|
name,
|
||||||
|
description,
|
||||||
...(installationId
|
...(installationId
|
||||||
? {
|
? {
|
||||||
method: GitHubConnectionMethod.App,
|
method: GitHubConnectionMethod.App,
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export const AppConnectionsTab = withPermission(
|
|||||||
</div>
|
</div>
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgPermissionActions.Create}
|
I={OrgPermissionActions.Create}
|
||||||
a={OrgPermissionSubjects.ProjectTemplates}
|
a={OrgPermissionSubjects.AppConnections}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Controller, 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 { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -11,18 +11,21 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
SelectItem
|
SelectItem
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { APP_CONNECTION_MAP, APP_CONNECTION_METHOD_MAP } from "@app/helpers/appConnections";
|
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||||
import { AwsConnectionMethod, TAwsConnection } from "@app/hooks/api/appConnections";
|
import { AwsConnectionMethod, TAwsConnection } from "@app/hooks/api/appConnections";
|
||||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
import { slugSchema } from "@app/lib/schemas";
|
|
||||||
|
import {
|
||||||
|
genericAppConnectionFieldsSchema,
|
||||||
|
GenericAppConnectionsFields
|
||||||
|
} from "./GenericAppConnectionFields";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
appConnection?: TAwsConnection;
|
appConnection?: TAwsConnection;
|
||||||
onSubmit: (formData: FormData) => void;
|
onSubmit: (formData: FormData) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const rootSchema = z.object({
|
const rootSchema = genericAppConnectionFieldsSchema.extend({
|
||||||
name: slugSchema({ min: 1, max: 32, field: "Name" }),
|
|
||||||
app: z.literal(AppConnection.AWS)
|
app: z.literal(AppConnection.AWS)
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -30,14 +33,14 @@ const formSchema = z.discriminatedUnion("method", [
|
|||||||
rootSchema.extend({
|
rootSchema.extend({
|
||||||
method: z.literal(AwsConnectionMethod.AssumeRole),
|
method: z.literal(AwsConnectionMethod.AssumeRole),
|
||||||
credentials: z.object({
|
credentials: z.object({
|
||||||
roleArn: z.string().min(1, "Role ARN required")
|
roleArn: z.string().trim().min(1, "Role ARN required")
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
rootSchema.extend({
|
rootSchema.extend({
|
||||||
method: z.literal(AwsConnectionMethod.AccessKey),
|
method: z.literal(AwsConnectionMethod.AccessKey),
|
||||||
credentials: z.object({
|
credentials: z.object({
|
||||||
accessKeyId: z.string().min(1, "Access Key ID required"),
|
accessKeyId: z.string().trim().min(1, "Access Key ID required"),
|
||||||
secretAccessKey: z.string().min(1, "Secret Access Key required")
|
secretAccessKey: z.string().trim().min(1, "Secret Access Key required")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
@@ -47,13 +50,7 @@ type FormData = z.infer<typeof formSchema>;
|
|||||||
export const AwsConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
export const AwsConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
||||||
const isUpdate = Boolean(appConnection);
|
const isUpdate = Boolean(appConnection);
|
||||||
|
|
||||||
const {
|
const form = useForm<FormData>({
|
||||||
handleSubmit,
|
|
||||||
register,
|
|
||||||
control,
|
|
||||||
watch,
|
|
||||||
formState: { isSubmitting, errors, isDirty }
|
|
||||||
} = useForm<FormData>({
|
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: appConnection ?? {
|
defaultValues: appConnection ?? {
|
||||||
app: AppConnection.AWS,
|
app: AppConnection.AWS,
|
||||||
@@ -61,105 +58,61 @@ export const AwsConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
watch,
|
||||||
|
formState: { isSubmitting, isDirty }
|
||||||
|
} = form;
|
||||||
|
|
||||||
const selectedMethod = watch("method");
|
const selectedMethod = watch("method");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<FormProvider {...form}>
|
||||||
{!isUpdate && (
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<FormControl
|
{!isUpdate && <GenericAppConnectionsFields />}
|
||||||
helperText="Name must be slug-friendly"
|
|
||||||
errorText={errors.name?.message}
|
|
||||||
isError={Boolean(errors.name?.message)}
|
|
||||||
label="Name"
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
autoFocus
|
|
||||||
placeholder={`my-${AppConnection.AWS}-connection`}
|
|
||||||
{...register("name")}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
<Controller
|
|
||||||
name="method"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
tooltipText={`The method you would like to use to connect with ${
|
|
||||||
APP_CONNECTION_MAP[AppConnection.AWS].name
|
|
||||||
}. This field cannot be changed after creation.`}
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error?.message)}
|
|
||||||
label="Method"
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
isDisabled={isUpdate}
|
|
||||||
value={value}
|
|
||||||
onValueChange={(val) => onChange(val)}
|
|
||||||
className="w-full border border-mineshaft-500"
|
|
||||||
position="popper"
|
|
||||||
dropdownContainerClassName="max-w-none"
|
|
||||||
>
|
|
||||||
{Object.values(AwsConnectionMethod).map((method) => {
|
|
||||||
return (
|
|
||||||
<SelectItem value={method} key={method}>
|
|
||||||
{APP_CONNECTION_METHOD_MAP[method].name}{" "}
|
|
||||||
{method === AwsConnectionMethod.AssumeRole ? " (Recommended)" : ""}
|
|
||||||
</SelectItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{selectedMethod === AwsConnectionMethod.AssumeRole ? (
|
|
||||||
<Controller
|
<Controller
|
||||||
name="credentials.roleArn"
|
name="method"
|
||||||
control={control}
|
control={control}
|
||||||
shouldUnregister
|
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
|
tooltipText={`The method you would like to use to connect with ${
|
||||||
|
APP_CONNECTION_MAP[AppConnection.AWS].name
|
||||||
|
}. This field cannot be changed after creation.`}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
isError={Boolean(error?.message)}
|
isError={Boolean(error?.message)}
|
||||||
label="Role ARN"
|
label="Method"
|
||||||
className="group"
|
|
||||||
>
|
>
|
||||||
<SecretInput
|
<Select
|
||||||
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
|
isDisabled={isUpdate}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onValueChange={(val) => onChange(val)}
|
||||||
/>
|
className="w-full border border-mineshaft-500"
|
||||||
|
position="popper"
|
||||||
|
dropdownContainerClassName="max-w-none"
|
||||||
|
>
|
||||||
|
{Object.values(AwsConnectionMethod).map((method) => {
|
||||||
|
return (
|
||||||
|
<SelectItem value={method} key={method}>
|
||||||
|
{getAppConnectionMethodDetails(method).name}{" "}
|
||||||
|
{method === AwsConnectionMethod.AssumeRole ? " (Recommended)" : ""}
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
) : (
|
{selectedMethod === AwsConnectionMethod.AssumeRole ? (
|
||||||
<>
|
|
||||||
<Controller
|
<Controller
|
||||||
name="credentials.accessKeyId"
|
name="credentials.roleArn"
|
||||||
control={control}
|
control={control}
|
||||||
shouldUnregister
|
shouldUnregister
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
isError={Boolean(error?.message)}
|
isError={Boolean(error?.message)}
|
||||||
label="Access Key ID"
|
label="Role ARN"
|
||||||
>
|
|
||||||
<Input
|
|
||||||
placeholder={"*".repeat(20)}
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => onChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
name="credentials.secretAccessKey"
|
|
||||||
control={control}
|
|
||||||
shouldUnregister
|
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error?.message)}
|
|
||||||
label="Secret Access Key"
|
|
||||||
className="group"
|
className="group"
|
||||||
>
|
>
|
||||||
<SecretInput
|
<SecretInput
|
||||||
@@ -170,25 +123,65 @@ export const AwsConnectionForm = ({ appConnection, onSubmit }: Props) => {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</>
|
) : (
|
||||||
)}
|
<>
|
||||||
<div className="mt-8 flex items-center">
|
<Controller
|
||||||
<Button
|
name="credentials.accessKeyId"
|
||||||
className="mr-4"
|
control={control}
|
||||||
size="sm"
|
shouldUnregister
|
||||||
type="submit"
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
colorSchema="secondary"
|
<FormControl
|
||||||
isLoading={isSubmitting}
|
errorText={error?.message}
|
||||||
isDisabled={isSubmitting || !isDirty}
|
isError={Boolean(error?.message)}
|
||||||
>
|
label="Access Key ID"
|
||||||
{isUpdate ? "Update Credentials" : "Connect to AWS"}
|
>
|
||||||
</Button>
|
<Input
|
||||||
<ModalClose asChild>
|
placeholder={"*".repeat(20)}
|
||||||
<Button colorSchema="secondary" variant="plain">
|
value={value}
|
||||||
Cancel
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name="credentials.secretAccessKey"
|
||||||
|
control={control}
|
||||||
|
shouldUnregister
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
label="Secret Access Key"
|
||||||
|
className="group"
|
||||||
|
>
|
||||||
|
<SecretInput
|
||||||
|
containerClassName="text-gray-400 group-focus-within:!border-primary-400/50 border border-mineshaft-500 bg-mineshaft-900 px-2.5 py-1.5"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div className="mt-8 flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
colorSchema="secondary"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting || !isDirty}
|
||||||
|
>
|
||||||
|
{isUpdate ? "Update Credentials" : "Connect to AWS"}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalClose>
|
<ModalClose asChild>
|
||||||
</div>
|
<Button colorSchema="secondary" variant="plain">
|
||||||
</form>
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { useFormContext } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { FormControl, Input, TextArea } from "@app/components/v2";
|
||||||
|
import { slugSchema } from "@app/lib/schemas";
|
||||||
|
|
||||||
|
export const genericAppConnectionFieldsSchema = z.object({
|
||||||
|
name: slugSchema({ min: 1, max: 32, field: "Name" }),
|
||||||
|
description: z.string().trim().max(256, "Description cannot exceed 256 characters").nullish()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const GenericAppConnectionsFields = () => {
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
formState: { errors }
|
||||||
|
} = useFormContext<{ name: string; description?: string | null }>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormControl
|
||||||
|
helperText="Name must be slug-friendly"
|
||||||
|
errorText={errors.name?.message}
|
||||||
|
isError={Boolean(errors.name?.message)}
|
||||||
|
label="Name"
|
||||||
|
>
|
||||||
|
<Input autoFocus placeholder="my-app-connection" {...register("name")} />
|
||||||
|
</FormControl>
|
||||||
|
<FormControl
|
||||||
|
errorText={errors.description?.message}
|
||||||
|
isError={Boolean(errors.description?.message)}
|
||||||
|
label="Description"
|
||||||
|
isOptional
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
className="h-20 !resize-none"
|
||||||
|
placeholder="Connection description..."
|
||||||
|
{...register("description")}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,38 +1,34 @@
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, 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 { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2";
|
import { Button, FormControl, ModalClose, Select, SelectItem } from "@app/components/v2";
|
||||||
import { APP_CONNECTION_MAP, APP_CONNECTION_METHOD_MAP } from "@app/helpers/appConnections";
|
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||||
|
import { isInfisicalCloud } from "@app/helpers/platform";
|
||||||
import {
|
import {
|
||||||
GitHubConnectionMethod,
|
GitHubConnectionMethod,
|
||||||
TGitHubConnection,
|
TGitHubConnection,
|
||||||
useGetAppConnectionOption
|
useGetAppConnectionOption
|
||||||
} from "@app/hooks/api/appConnections";
|
} from "@app/hooks/api/appConnections";
|
||||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
import { slugSchema } from "@app/lib/schemas";
|
|
||||||
|
import {
|
||||||
|
genericAppConnectionFieldsSchema,
|
||||||
|
GenericAppConnectionsFields
|
||||||
|
} from "./GenericAppConnectionFields";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
appConnection?: TGitHubConnection;
|
appConnection?: TGitHubConnection;
|
||||||
};
|
};
|
||||||
|
|
||||||
const rootSchema = z.object({
|
const formSchema = genericAppConnectionFieldsSchema.extend({
|
||||||
name: slugSchema({ min: 1, max: 32, field: "Name" }),
|
app: z.literal(AppConnection.GitHub),
|
||||||
app: z.literal(AppConnection.GitHub)
|
method: z.nativeEnum(GitHubConnectionMethod)
|
||||||
});
|
});
|
||||||
|
|
||||||
const formSchema = z.discriminatedUnion("method", [
|
|
||||||
rootSchema.extend({
|
|
||||||
method: z.literal(GitHubConnectionMethod.App)
|
|
||||||
}),
|
|
||||||
rootSchema.extend({
|
|
||||||
method: z.literal(GitHubConnectionMethod.OAuth)
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
|
|
||||||
type FormData = z.infer<typeof formSchema>;
|
type FormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
export const GitHubConnectionForm = ({ appConnection }: Props) => {
|
export const GitHubConnectionForm = ({ appConnection }: Props) => {
|
||||||
@@ -44,13 +40,7 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => {
|
|||||||
isLoading
|
isLoading
|
||||||
} = useGetAppConnectionOption(AppConnection.GitHub);
|
} = useGetAppConnectionOption(AppConnection.GitHub);
|
||||||
|
|
||||||
const {
|
const form = useForm<FormData>({
|
||||||
handleSubmit,
|
|
||||||
register,
|
|
||||||
control,
|
|
||||||
watch,
|
|
||||||
formState: { isSubmitting, errors, isDirty }
|
|
||||||
} = useForm<FormData>({
|
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: appConnection ?? {
|
defaultValues: appConnection ?? {
|
||||||
app: AppConnection.GitHub,
|
app: AppConnection.GitHub,
|
||||||
@@ -58,6 +48,13 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
watch,
|
||||||
|
formState: { isSubmitting, isDirty }
|
||||||
|
} = form;
|
||||||
|
|
||||||
const selectedMethod = watch("method");
|
const selectedMethod = watch("method");
|
||||||
|
|
||||||
const onSubmit = (formData: FormData) => {
|
const onSubmit = (formData: FormData) => {
|
||||||
@@ -98,75 +95,70 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => {
|
|||||||
throw new Error(`Unhandled GitHub Connection method: ${selectedMethod}`);
|
throw new Error(`Unhandled GitHub Connection method: ${selectedMethod}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const methodDetails = getAppConnectionMethodDetails(selectedMethod);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<FormProvider {...form}>
|
||||||
{!isUpdate && (
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<FormControl
|
{!isUpdate && <GenericAppConnectionsFields />}
|
||||||
helperText="Name must be slug-friendly"
|
<Controller
|
||||||
errorText={errors.name?.message}
|
name="method"
|
||||||
isError={Boolean(errors.name?.message)}
|
control={control}
|
||||||
label="Name"
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
>
|
<FormControl
|
||||||
<Input
|
tooltipText={`The method you would like to use to connect with ${
|
||||||
autoFocus
|
APP_CONNECTION_MAP[AppConnection.GitHub].name
|
||||||
placeholder={`my-${AppConnection.GitHub}-connection`}
|
}. This field cannot be changed after creation.`}
|
||||||
{...register("name")}
|
errorText={
|
||||||
/>
|
!isLoading && isMissingConfig
|
||||||
</FormControl>
|
? `Environment variables have not been configured. ${
|
||||||
)}
|
isInfisicalCloud()
|
||||||
<Controller
|
? "Please contact Infisical."
|
||||||
name="method"
|
: `See Docs to configure GitHub ${methodDetails.name} Connections.`
|
||||||
control={control}
|
}`
|
||||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
: error?.message
|
||||||
<FormControl
|
}
|
||||||
tooltipText={`The method you would like to use to connect with ${
|
isError={Boolean(error?.message) || isMissingConfig}
|
||||||
APP_CONNECTION_MAP[AppConnection.GitHub].name
|
label="Method"
|
||||||
}. This field cannot be changed after creation.`}
|
|
||||||
errorText={
|
|
||||||
!isLoading && isMissingConfig
|
|
||||||
? `Environment variables have not been configured. See Docs to configure GitHub ${APP_CONNECTION_METHOD_MAP[selectedMethod].name} Connections.`
|
|
||||||
: error?.message
|
|
||||||
}
|
|
||||||
isError={Boolean(error?.message) || isMissingConfig}
|
|
||||||
label="Method"
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
isDisabled={isUpdate}
|
|
||||||
value={value}
|
|
||||||
onValueChange={(val) => onChange(val)}
|
|
||||||
className="w-full border border-mineshaft-500"
|
|
||||||
position="popper"
|
|
||||||
dropdownContainerClassName="max-w-none"
|
|
||||||
>
|
>
|
||||||
{Object.values(GitHubConnectionMethod).map((method) => {
|
<Select
|
||||||
return (
|
isDisabled={isUpdate}
|
||||||
<SelectItem value={method} key={method}>
|
value={value}
|
||||||
{APP_CONNECTION_METHOD_MAP[method].name}{" "}
|
onValueChange={(val) => onChange(val)}
|
||||||
{method === GitHubConnectionMethod.App ? " (Recommended)" : ""}
|
className="w-full border border-mineshaft-500"
|
||||||
</SelectItem>
|
position="popper"
|
||||||
);
|
dropdownContainerClassName="max-w-none"
|
||||||
})}
|
>
|
||||||
</Select>
|
{Object.values(GitHubConnectionMethod).map((method) => {
|
||||||
</FormControl>
|
return (
|
||||||
)}
|
<SelectItem value={method} key={method}>
|
||||||
/>
|
{methodDetails.name}{" "}
|
||||||
<div className="mt-8 flex items-center">
|
{method === GitHubConnectionMethod.App ? " (Recommended)" : ""}
|
||||||
<Button
|
</SelectItem>
|
||||||
className="mr-4"
|
);
|
||||||
size="sm"
|
})}
|
||||||
type="submit"
|
</Select>
|
||||||
colorSchema="secondary"
|
</FormControl>
|
||||||
isLoading={isSubmitting || isRedirecting}
|
)}
|
||||||
isDisabled={isSubmitting || (!isUpdate && !isDirty) || isMissingConfig || isRedirecting}
|
/>
|
||||||
>
|
<div className="mt-8 flex items-center">
|
||||||
{isUpdate ? "Reconnect to GitHub" : "Connect to GitHub"}
|
<Button
|
||||||
</Button>
|
className="mr-4"
|
||||||
<ModalClose asChild>
|
size="sm"
|
||||||
<Button colorSchema="secondary" variant="plain">
|
type="submit"
|
||||||
Cancel
|
colorSchema="secondary"
|
||||||
|
isLoading={isSubmitting || isRedirecting}
|
||||||
|
isDisabled={isSubmitting || (!isUpdate && !isDirty) || isMissingConfig || isRedirecting}
|
||||||
|
>
|
||||||
|
{isUpdate ? "Reconnect to GitHub" : "Connect to GitHub"}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalClose>
|
<ModalClose asChild>
|
||||||
</div>
|
<Button colorSchema="secondary" variant="plain">
|
||||||
</form>
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export * from "./AppConnectionForm";
|
export * from "./AppConnectionForm";
|
||||||
|
export * from "./GenericAppConnectionFields";
|
||||||
|
|||||||
@@ -24,10 +24,7 @@ export const AppConnectionHeader = ({ app, isConnected, onBack }: Props) => {
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex items-center text-mineshaft-300">
|
<div className="flex items-center text-mineshaft-300">
|
||||||
{appDetails.name}
|
{appDetails.name}
|
||||||
<Link
|
<Link href={`https://infisical.com/docs/integrations/app-connections/${app}`} passHref>
|
||||||
href={`https://infisical.com/docs/documentation/platform/app-connections/${app}`}
|
|
||||||
passHref
|
|
||||||
>
|
|
||||||
<a target="_blank" className="ml-1 mb-1" rel="noopener noreferrer">
|
<a target="_blank" className="ml-1 mb-1" rel="noopener noreferrer">
|
||||||
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
<div className="inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
|
||||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1 mb-[0.03rem] text-[12px]" />
|
<FontAwesomeIcon icon={faBookOpen} className="mr-1 mb-[0.03rem] text-[12px]" />
|
||||||
|
|||||||
@@ -45,8 +45,34 @@ export const AppConnectionsSelect = ({ onSelect }: Props) => {
|
|||||||
))}
|
))}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
side="bottom"
|
side="bottom"
|
||||||
className="text-center"
|
className="max-w-sm py-4"
|
||||||
content="Infisical is busy adding support for more connections. Check back soon if you don't see the one you're looking for."
|
content={
|
||||||
|
<>
|
||||||
|
<p className="mb-2">Infisical is constantly adding support for more connections.</p>
|
||||||
|
<p>
|
||||||
|
{`If you don't see the third-party
|
||||||
|
app you're looking for,`}{" "}
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
className="underline hover:text-mineshaft-300"
|
||||||
|
href="https://infisical.com/slack"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
let us know on Slack
|
||||||
|
</a>{" "}
|
||||||
|
or{" "}
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
className="underline hover:text-mineshaft-300"
|
||||||
|
href="https://github.com/Infisical/infisical/discussions"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
make a request on GitHub
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className="group relative flex h-28 flex-col items-center justify-center rounded-md border border-dashed border-mineshaft-600 bg-mineshaft-800 p-4">
|
<div className="group relative flex h-28 flex-col items-center justify-center rounded-md border border-dashed border-mineshaft-600 bg-mineshaft-800 p-4">
|
||||||
<FontAwesomeIcon className="mt-auto text-xl" icon={faWrench} />
|
<FontAwesomeIcon className="mt-auto text-xl" icon={faWrench} />
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
faCopy,
|
faCopy,
|
||||||
faEdit,
|
faEdit,
|
||||||
faEllipsisV,
|
faEllipsisV,
|
||||||
|
faInfoCircle,
|
||||||
faTrash
|
faTrash
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
@@ -23,7 +24,7 @@ import {
|
|||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||||
import { APP_CONNECTION_MAP, APP_CONNECTION_METHOD_MAP } from "@app/helpers/appConnections";
|
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||||
import { useToggle } from "@app/hooks";
|
import { useToggle } from "@app/hooks";
|
||||||
import { TAppConnection } from "@app/hooks/api/appConnections";
|
import { TAppConnection } from "@app/hooks/api/appConnections";
|
||||||
|
|
||||||
@@ -31,16 +32,16 @@ type Props = {
|
|||||||
appConnection: TAppConnection;
|
appConnection: TAppConnection;
|
||||||
onDelete: (appConnection: TAppConnection) => void;
|
onDelete: (appConnection: TAppConnection) => void;
|
||||||
onEditCredentials: (appConnection: TAppConnection) => void;
|
onEditCredentials: (appConnection: TAppConnection) => void;
|
||||||
onEditName: (appConnection: TAppConnection) => void;
|
onEditDetails: (appConnection: TAppConnection) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AppConnectionRow = ({
|
export const AppConnectionRow = ({
|
||||||
appConnection,
|
appConnection,
|
||||||
onDelete,
|
onDelete,
|
||||||
onEditCredentials,
|
onEditCredentials,
|
||||||
onEditName
|
onEditDetails
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { id, name, method, app } = appConnection;
|
const { id, name, method, app, description } = appConnection;
|
||||||
|
|
||||||
const [isIdCopied, setIsIdCopied] = useToggle(false);
|
const [isIdCopied, setIsIdCopied] = useToggle(false);
|
||||||
|
|
||||||
@@ -59,6 +60,8 @@ export const AppConnectionRow = ({
|
|||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [isIdCopied]);
|
}, [isIdCopied]);
|
||||||
|
|
||||||
|
const methodDetails = getAppConnectionMethodDetails(method);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tr
|
<Tr
|
||||||
className={twMerge("group h-12 transition-colors duration-100 hover:bg-mineshaft-700")}
|
className={twMerge("group h-12 transition-colors duration-100 hover:bg-mineshaft-700")}
|
||||||
@@ -75,16 +78,23 @@ export const AppConnectionRow = ({
|
|||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
<Td className="!min-w-[8rem] max-w-0">
|
<Td className="!min-w-[8rem] max-w-0">
|
||||||
<p className="truncate">{name}</p>
|
<div className="flex w-full items-center">
|
||||||
|
<p className="truncate">{name}</p>
|
||||||
|
{description && (
|
||||||
|
<Tooltip content={description}>
|
||||||
|
<FontAwesomeIcon icon={faInfoCircle} className="ml-1 text-mineshaft-400" />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
<Td className="!min-w-[8rem] max-w-0">
|
<Td className="!min-w-[8rem] max-w-0">
|
||||||
<p className="truncate">
|
<p className="truncate">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
size="sm"
|
size="sm"
|
||||||
className="mr-1.5 text-mineshaft-300/75"
|
className="mr-1.5 text-mineshaft-300/75"
|
||||||
icon={APP_CONNECTION_METHOD_MAP[method].icon}
|
icon={methodDetails.icon}
|
||||||
/>
|
/>
|
||||||
{APP_CONNECTION_METHOD_MAP[method].name}
|
{methodDetails.name}
|
||||||
</p>
|
</p>
|
||||||
</Td>
|
</Td>
|
||||||
|
|
||||||
@@ -116,9 +126,9 @@ export const AppConnectionRow = ({
|
|||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
icon={<FontAwesomeIcon icon={faEdit} />}
|
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||||
onClick={() => onEditName(appConnection)}
|
onClick={() => onEditDetails(appConnection)}
|
||||||
>
|
>
|
||||||
Edit Name
|
Edit Details
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
</OrgPermissionCan>
|
</OrgPermissionCan>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useSubscription } from "@app/context";
|
import { useSubscription } from "@app/context";
|
||||||
import { APP_CONNECTION_MAP, APP_CONNECTION_METHOD_MAP } from "@app/helpers/appConnections";
|
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
|
||||||
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||||
import { TAppConnection, useListAppConnections } from "@app/hooks/api/appConnections";
|
import { TAppConnection, useListAppConnections } from "@app/hooks/api/appConnections";
|
||||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
@@ -39,7 +39,7 @@ import { OrderByDirection } from "@app/hooks/api/generic/types";
|
|||||||
import { AppConnectionRow } from "./AppConnectionRow";
|
import { AppConnectionRow } from "./AppConnectionRow";
|
||||||
import { DeleteAppConnectionModal } from "./DeleteAppConnectionModal";
|
import { DeleteAppConnectionModal } from "./DeleteAppConnectionModal";
|
||||||
import { EditAppConnectionCredentialsModal } from "./EditAppConnectionCredentialsModal";
|
import { EditAppConnectionCredentialsModal } from "./EditAppConnectionCredentialsModal";
|
||||||
import { EditAppConnectionNameModal } from "./EditAppConnectionNameModal";
|
import { EditAppConnectionDetailsModal } from "./EditAppConnectionDetailsModal";
|
||||||
|
|
||||||
enum AppConnectionsOrderBy {
|
enum AppConnectionsOrderBy {
|
||||||
App = "app",
|
App = "app",
|
||||||
@@ -61,7 +61,7 @@ export const AppConnectionsTable = () => {
|
|||||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||||
"deleteConnection",
|
"deleteConnection",
|
||||||
"editCredentials",
|
"editCredentials",
|
||||||
"editName"
|
"editDetails"
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
const [filters, setFilters] = useState<AppConnectionFilters>({
|
const [filters, setFilters] = useState<AppConnectionFilters>({
|
||||||
@@ -95,7 +95,7 @@ export const AppConnectionsTable = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
APP_CONNECTION_MAP[app].name.toLowerCase().includes(searchValue) ||
|
APP_CONNECTION_MAP[app].name.toLowerCase().includes(searchValue) ||
|
||||||
APP_CONNECTION_METHOD_MAP[method].name.toLowerCase().includes(searchValue) ||
|
getAppConnectionMethodDetails(method).name.toLowerCase().includes(searchValue) ||
|
||||||
name.toLowerCase().includes(searchValue)
|
name.toLowerCase().includes(searchValue)
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@@ -109,9 +109,11 @@ export const AppConnectionsTable = () => {
|
|||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.localeCompare(connectionTwo.name.toLowerCase());
|
.localeCompare(connectionTwo.name.toLowerCase());
|
||||||
case AppConnectionsOrderBy.Method:
|
case AppConnectionsOrderBy.Method:
|
||||||
return APP_CONNECTION_METHOD_MAP[connectionOne.method].name
|
return getAppConnectionMethodDetails(connectionOne.method)
|
||||||
.toLowerCase()
|
.name.toLowerCase()
|
||||||
.localeCompare(APP_CONNECTION_METHOD_MAP[connectionTwo.method].name.toLowerCase());
|
.localeCompare(
|
||||||
|
getAppConnectionMethodDetails(connectionTwo.method).name.toLowerCase()
|
||||||
|
);
|
||||||
case AppConnectionsOrderBy.App:
|
case AppConnectionsOrderBy.App:
|
||||||
default:
|
default:
|
||||||
return APP_CONNECTION_MAP[connectionOne.app].name
|
return APP_CONNECTION_MAP[connectionOne.app].name
|
||||||
@@ -152,8 +154,8 @@ export const AppConnectionsTable = () => {
|
|||||||
const handleEditCredentials = (appConnection: TAppConnection) =>
|
const handleEditCredentials = (appConnection: TAppConnection) =>
|
||||||
handlePopUpOpen("editCredentials", appConnection);
|
handlePopUpOpen("editCredentials", appConnection);
|
||||||
|
|
||||||
const handleEditName = (appConnection: TAppConnection) =>
|
const handleEditDetails = (appConnection: TAppConnection) =>
|
||||||
handlePopUpOpen("editName", appConnection);
|
handlePopUpOpen("editDetails", appConnection);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -274,7 +276,7 @@ export const AppConnectionsTable = () => {
|
|||||||
key={connection.id}
|
key={connection.id}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
onEditCredentials={handleEditCredentials}
|
onEditCredentials={handleEditCredentials}
|
||||||
onEditName={handleEditName}
|
onEditDetails={handleEditDetails}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</TBody>
|
</TBody>
|
||||||
@@ -309,10 +311,10 @@ export const AppConnectionsTable = () => {
|
|||||||
onOpenChange={(isOpen) => handlePopUpToggle("editCredentials", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("editCredentials", isOpen)}
|
||||||
appConnection={popUp.editCredentials.data}
|
appConnection={popUp.editCredentials.data}
|
||||||
/>
|
/>
|
||||||
<EditAppConnectionNameModal
|
<EditAppConnectionDetailsModal
|
||||||
isOpen={popUp.editName.isOpen}
|
isOpen={popUp.editDetails.isOpen}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("editName", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("editDetails", isOpen)}
|
||||||
appConnection={popUp.editName.data}
|
appConnection={popUp.editDetails.data}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
import { useForm } from "react-hook-form";
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, FormControl, Input, Modal, ModalClose, ModalContent } from "@app/components/v2";
|
import { Button, Modal, ModalClose, ModalContent } from "@app/components/v2";
|
||||||
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
|
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
|
||||||
import { TAppConnection, useUpdateAppConnection } from "@app/hooks/api/appConnections";
|
import { TAppConnection, useUpdateAppConnection } from "@app/hooks/api/appConnections";
|
||||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||||
import { slugSchema } from "@app/lib/schemas";
|
|
||||||
import { DiscriminativePick } from "@app/lib/types";
|
import { DiscriminativePick } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { genericAppConnectionFieldsSchema, GenericAppConnectionsFields } from "./AppConnectionForm";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onOpenChange: (isOpen: boolean) => void;
|
onOpenChange: (isOpen: boolean) => void;
|
||||||
appConnection?: TAppConnection;
|
appConnection?: TAppConnection;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = genericAppConnectionFieldsSchema.extend({
|
||||||
name: slugSchema({ min: 1, max: 32, field: "Name" }),
|
|
||||||
app: z.nativeEnum(AppConnection)
|
app: z.nativeEnum(AppConnection)
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -29,14 +29,19 @@ const Content = ({ appConnection, onComplete }: ContentProps) => {
|
|||||||
const updateAppConnection = useUpdateAppConnection();
|
const updateAppConnection = useUpdateAppConnection();
|
||||||
const { name: appName } = APP_CONNECTION_MAP[appConnection.app];
|
const { name: appName } = APP_CONNECTION_MAP[appConnection.app];
|
||||||
|
|
||||||
|
const form = useForm<FormData>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: appConnection.name,
|
||||||
|
app: appConnection.app,
|
||||||
|
description: appConnection.description
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
register,
|
formState: { isSubmitting, isDirty }
|
||||||
formState: { isSubmitting, errors, isDirty }
|
} = form;
|
||||||
} = useForm<FormData>({
|
|
||||||
resolver: zodResolver(formSchema),
|
|
||||||
defaultValues: { name: appConnection.name, app: appConnection.app }
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = async (formData: DiscriminativePick<TAppConnection, "name" | "app">) => {
|
const onSubmit = async (formData: DiscriminativePick<TAppConnection, "name" | "app">) => {
|
||||||
try {
|
try {
|
||||||
@@ -60,38 +65,32 @@ const Content = ({ appConnection, onComplete }: ContentProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<FormProvider {...form}>
|
||||||
<FormControl
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
helperText="Name must be slug-friendly"
|
<GenericAppConnectionsFields />
|
||||||
errorText={errors.name?.message}
|
<div className="mt-8 flex items-center">
|
||||||
isError={Boolean(errors.name?.message)}
|
<Button
|
||||||
label="Name"
|
className="mr-4"
|
||||||
>
|
size="sm"
|
||||||
<Input autoFocus placeholder={`my-${AppConnection.AWS}-connection`} {...register("name")} />
|
type="submit"
|
||||||
</FormControl>
|
colorSchema="secondary"
|
||||||
|
isLoading={isSubmitting}
|
||||||
<div className="mt-8 flex items-center">
|
isDisabled={isSubmitting || !isDirty}
|
||||||
<Button
|
>
|
||||||
className="mr-4"
|
Update Details
|
||||||
size="sm"
|
|
||||||
type="submit"
|
|
||||||
colorSchema="secondary"
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
isDisabled={isSubmitting || !isDirty}
|
|
||||||
>
|
|
||||||
Update Name
|
|
||||||
</Button>
|
|
||||||
<ModalClose asChild>
|
|
||||||
<Button colorSchema="secondary" variant="plain">
|
|
||||||
Cancel
|
|
||||||
</Button>
|
</Button>
|
||||||
</ModalClose>
|
<ModalClose asChild>
|
||||||
</div>
|
<Button colorSchema="secondary" variant="plain">
|
||||||
</form>
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EditAppConnectionNameModal = ({ isOpen, onOpenChange, appConnection }: Props) => {
|
export const EditAppConnectionDetailsModal = ({ isOpen, onOpenChange, appConnection }: Props) => {
|
||||||
if (!appConnection) return null;
|
if (!appConnection) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
Reference in New Issue
Block a user