improvements: address feedback

This commit is contained in:
Scott Wilson
2024-12-18 21:18:38 -08:00
parent 62968c5e43
commit 2d60f389c2
55 changed files with 1040 additions and 690 deletions

View File

@@ -8,6 +8,7 @@ export async function up(knex: Knex): Promise<void> {
await knex.schema.createTable(TableName.AppConnection, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("name", 32).notNullable();
t.string("description");
t.string("app").notNullable();
t.string("method").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.timestamps(true, true, true);
});
}
await createOnUpdateTrigger(knex, TableName.AppConnection);
}
}
export async function down(knex: Knex): Promise<void> {

View File

@@ -12,6 +12,7 @@ import { TImmutableDBKeys } from "./models";
export const AppConnectionsSchema = z.object({
id: z.string().uuid(),
name: z.string(),
description: z.string().nullable().optional(),
app: z.string(),
method: z.string(),
encryptedCredentials: zodBuffer,

View File

@@ -4,9 +4,10 @@ import {
} from "@app/ee/services/project-template/project-template-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 { AppConnection, TCreateAppConnectionDTO, TUpdateAppConnectionDTO } from "@app/lib/app-connections";
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
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 { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
import { CaStatus } from "@app/services/certificate-authority/certificate-authority-types";
@@ -1875,8 +1876,10 @@ interface ApplyProjectTemplateEvent {
interface GetAppConnectionsEvent {
type: EventType.GET_APP_CONNECTIONS;
metadata?: {
app: AppConnection;
metadata: {
app?: AppConnection;
count: number;
connectionIds: string[];
};
}

View File

@@ -50,7 +50,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
pkiEst: false,
enforceMfa: false,
projectTemplates: false,
appConnections: false
appConnections: true
});
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {

View File

@@ -1,5 +1,5 @@
import { AppConnection } from "@app/lib/app-connections";
import { APP_CONNECTION_NAME_MAP } from "@app/lib/app-connections/maps";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
export const GROUPS = {
CREATE: {
@@ -1620,6 +1620,7 @@ export const AppConnections = {
const appName = APP_CONNECTION_NAME_MAP[app];
return {
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}.`,
method: `The method used to authenticate with ${appName}.`
};
@@ -1629,11 +1630,12 @@ export const AppConnections = {
return {
connectionId: `The ID of the ${appName} Connection to be updated.`,
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}.`,
method: `The method used to authenticate with ${appName}.`
};
},
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.`
})
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,2 +0,0 @@
export * from "./app-connection-enums";
export * from "./app-connection-types";

View File

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

View File

@@ -14,3 +14,5 @@ export const prefixWithSlash = (str: string) => {
if (str.startsWith("/")) return str;
return `/${str}`;
};
export const startsWithVowel = (str: string) => /^[aeiou]/i.test(str);

View File

@@ -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 { 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 { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
// can't use discriminated due to multiple schemas for certain apps
export const SanitizedAppConnectionSchema = z.union([
const SanitizedAppConnectionSchema = z.union([
...SanitizedAwsConnectionSchema.options,
...SanitizedGitHubConnectionSchema.options
]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
AwsConnectionListItemSchema,
GitHubConnectionListItemSchema
]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
server.route({
method: "GET",
@@ -25,18 +29,11 @@ export const registerAppConnectionRouter = async (server: FastifyZodProvider) =>
description: "List the available App Connection Options.",
response: {
200: z.object({
appConnectionOptions: z
.object({
name: z.string(),
app: z.nativeEnum(AppConnection),
methods: z.string().array()
})
.passthrough()
.array()
appConnectionOptions: AppConnectionOptionsSchema.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: () => {
const appConnectionOptions = server.services.appConnection.listAppConnectionOptions();
return { appConnectionOptions };
@@ -55,7 +52,7 @@ export const registerAppConnectionRouter = async (server: FastifyZodProvider) =>
200: z.object({ appConnections: SanitizedAppConnectionSchema.array() })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const appConnections = await server.services.appConnection.listAppConnectionsByOrg(req.permission);
@@ -63,7 +60,11 @@ export const registerAppConnectionRouter = async (server: FastifyZodProvider) =>
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.GET_APP_CONNECTIONS
type: EventType.GET_APP_CONNECTIONS,
metadata: {
count: appConnections.length,
connectionIds: appConnections.map((connection) => connection.id)
}
}
});

View File

@@ -2,10 +2,12 @@ import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection, TAppConnection, TAppConnectionInput } from "@app/lib/app-connections";
import { APP_CONNECTION_NAME_MAP } from "@app/lib/app-connections/maps";
import { startsWithVowel } from "@app/lib/fn";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { 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";
export const registerAppConnectionEndpoints = <T extends TAppConnection, I extends TAppConnectionInput>({
@@ -17,8 +19,13 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
}: {
app: AppConnection;
server: FastifyZodProvider;
createSchema: z.ZodType<{ name: string; method: I["method"]; credentials: I["credentials"] }>;
updateSchema: z.ZodType<{ name?: string; credentials?: I["credentials"] }>;
createSchema: z.ZodType<{
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;
}) => {
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() })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
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: {
type: EventType.GET_APP_CONNECTIONS,
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 })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { connectionId } = req.params;
@@ -112,7 +121,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
200: z.object({ appConnection: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { connectionName } = req.params;
@@ -144,18 +153,20 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
rateLimit: writeLimit
},
schema: {
description: `Create an ${appName} Connection for the current organization.`,
description: `Create ${
startsWithVowel(appName) ? "an" : "a"
} ${appName} Connection for the current organization.`,
body: createSchema,
response: {
200: z.object({ appConnection: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { name, method, credentials } = req.body;
const { name, method, credentials, description } = req.body;
const appConnection = (await server.services.appConnection.createAppConnection(
{ name, method, app, credentials },
{ name, method, app, credentials, description },
req.permission
)) as TAppConnection;
@@ -193,13 +204,13 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
200: z.object({ appConnection: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { name, credentials } = req.body;
const { name, credentials, description } = req.body;
const { connectionId } = req.params;
const appConnection = (await server.services.appConnection.updateAppConnection(
{ name, credentials, connectionId },
{ name, credentials, connectionId, description },
req.permission
)) as T;
@@ -210,6 +221,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
type: EventType.UPDATE_APP_CONNECTION,
metadata: {
name,
description,
credentialsUpdated: Boolean(credentials),
connectionId
}
@@ -235,7 +247,7 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
200: z.object({ appConnection: responseSchema })
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.SERVICE_TOKEN]),
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { connectionId } = req.params;

View File

@@ -1,9 +1,10 @@
import { AppConnection } from "@app/lib/app-connections";
import {
CreateAwsConnectionSchema,
SanitizedAwsConnectionSchema,
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";

View File

@@ -1,9 +1,10 @@
import { AppConnection } from "@app/lib/app-connections";
import {
CreateGitHubConnectionSchema,
GitHubAppConnectionSchema,
SanitizedGitHubConnectionSchema,
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";
@@ -11,7 +12,7 @@ export const registerGitHubConnectionRouter = async (server: FastifyZodProvider)
registerAppConnectionEndpoints({
app: AppConnection.GitHub,
server,
responseSchema: GitHubAppConnectionSchema,
responseSchema: SanitizedGitHubConnectionSchema,
createSchema: CreateGitHubConnectionSchema,
updateSchema: UpdateGitHubConnectionSchema
});

View File

@@ -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 { 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>> = {
[AppConnection.AWS]: registerAwsConnectionRouter,

View File

@@ -5,7 +5,7 @@ import { ormify } from "@app/lib/knex";
export type TAppConnectionDALFactory = ReturnType<typeof appConnectionDALFactory>;
export const appConnectionDALFactory = (db: TDbClient) => {
const appConnection = ormify(db, TableName.AppConnection);
const appConnectionOrm = ormify(db, TableName.AppConnection);
return { ...appConnection };
return { ...appConnectionOrm };
};

View File

@@ -1,10 +1,20 @@
import { AppConnection, AppConnectionListItem, TAppConnection, TAppConnectionConfig } from "@app/lib/app-connections";
import { getAwsAppConnectionListItem, validateAwsConnectionCredentials } from "@app/lib/app-connections/aws";
import { getGitHubConnectionListItem, validateGitHubConnectionCredentials } from "@app/lib/app-connections/github";
import {
AwsConnectionMethod,
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 { TAppConnection, TAppConnectionConfig } from "@app/services/app-connection/app-connection-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));
};
@@ -65,3 +75,19 @@ export const validateAppConnectionCredentials = async (
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}`);
}
};

View File

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

View File

@@ -1,7 +1,35 @@
import { z } from "zod";
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({
encryptedCredentials: true,
app: 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)
});

View File

@@ -3,21 +3,26 @@ import { ForbiddenError } from "@casl/ability";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
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 { OrgServiceActor } from "@app/lib/types";
import { DiscriminativePick, OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
decryptAppConnectionCredentials,
encryptAppConnectionCredentials,
getAppConnectionMethodName,
listAppConnectionOptions,
validateAppConnectionCredentials
} 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 { TAppConnectionDALFactory } from "./app-connection-dal";
@@ -31,6 +36,11 @@ export type TAppConnectionServiceFactoryDep = {
export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServiceFactory>;
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAppConnectionCredentials> = {
[AppConnection.AWS]: ValidateAwsConnectionCredentialsSchema,
[AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema
};
export const appConnectionServiceFactory = ({
appConnectionDAL,
permissionService,
@@ -160,11 +170,15 @@ export const appConnectionServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.AppConnections);
const appConnection = await appConnectionDAL.transaction(async (tx) => {
const isConflictingName = Boolean(
await appConnectionDAL.findOne({
await appConnectionDAL.findOne(
{
name: params.name,
orgId: actor.orgId
})
},
tx
)
);
if (isConflictingName)
@@ -185,15 +199,24 @@ export const appConnectionServiceFactory = ({
kmsService
});
const appConnection = await appConnectionDAL.create({
const connection = await appConnectionDAL.create(
{
orgId: actor.orgId,
encryptedCredentials,
method,
app,
...params
},
tx
);
return {
...connection,
credentials: validatedCredentials
};
});
return { ...appConnection, credentials: validatedCredentials };
return appConnection;
};
const updateAppConnection = async (
@@ -216,12 +239,16 @@ export const appConnectionServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.AppConnections);
const updatedAppConnection = await appConnectionDAL.transaction(async (tx) => {
if (params.name && appConnection.name !== params.name) {
const isConflictingName = Boolean(
await appConnectionDAL.findOne({
await appConnectionDAL.findOne(
{
name: params.name,
orgId: appConnection.orgId
})
},
tx
)
);
if (isConflictingName)
@@ -233,11 +260,25 @@ export const appConnectionServiceFactory = ({
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: appConnection.app,
app,
orgId: actor.orgId,
credentials,
method: appConnection.method,
orgId: actor.orgId
method
} as TAppConnectionConfig);
if (!validatedCredentials)
@@ -250,10 +291,17 @@ export const appConnectionServiceFactory = ({
});
}
const updatedAppConnection = await appConnectionDAL.updateById(connectionId, {
const updatedConnection = await appConnectionDAL.updateById(
connectionId,
{
orgId: actor.orgId,
encryptedCredentials,
...params
},
tx
);
return updatedConnection;
});
return {

View 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;

View File

@@ -2,20 +2,20 @@ import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
import AWS from "aws-sdk";
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 { BadRequestError, InternalServerError } from "@app/lib/errors";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { AwsConnectionMethod } from "./aws-connection-enums";
import { TAwsConnectionConfig } from "./aws-connection-types";
export const getAwsAppConnectionListItem = () => {
const { INF_APP_CONNECTION_AWS_ACCESS_KEY_ID } = getConfig();
return {
name: "AWS",
app: AppConnection.AWS,
methods: Object.values(AwsConnectionMethod),
name: "AWS" as const,
app: AppConnection.AWS as const,
methods: Object.values(AwsConnectionMethod) as [AwsConnectionMethod.AssumeRole, AwsConnectionMethod.AccessKey],
accessKeyId: INF_APP_CONNECTION_AWS_ACCESS_KEY_ID
};
};

View File

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

View File

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

View File

@@ -3,10 +3,10 @@ import { AxiosResponse } from "axios";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
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 { AppConnection } from "../app-connection-enums";
import { APP_CONNECTION_METHOD_NAME_MAP } from "../maps";
import { GitHubConnectionMethod } from "./github-connection-enums";
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();
return {
name: "GitHub",
app: AppConnection.GitHub,
methods: Object.values(GitHubConnectionMethod),
name: "GitHub" as const,
app: AppConnection.GitHub as const,
methods: Object.values(GitHubConnectionMethod) as [GitHubConnectionMethod.App, GitHubConnectionMethod.OAuth],
oauthClientId: INF_APP_CONNECTION_GITHUB_OAUTH_CLIENT_ID,
appClientSlug: INF_APP_CONNECTION_GITHUB_APP_SLUG
};
@@ -53,7 +53,7 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
if (!clientId || !clientSecret) {
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`
});
}

View File

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

View File

@@ -3,12 +3,18 @@ import { z } from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "../app-connection-enums";
import { CreateGitHubConnectionSchema, GitHubAppConnectionSchema } from "./github-connection-schemas";
export type TGitHubConnectionConfig = DiscriminativePick<TGitHubConnectionInput, "method" | "app" | "credentials">;
import {
CreateGitHubConnectionSchema,
GitHubAppConnectionSchema,
ValidateGitHubConnectionCredentialsSchema
} from "./github-connection-schemas";
export type TGitHubConnection = z.infer<typeof GitHubAppConnectionSchema>;
export type TGitHubConnectionInput = z.infer<typeof CreateGitHubConnectionSchema> & {
app: AppConnection.GitHub;
};
export type TValidateGitHubConnectionCredentials = typeof ValidateGitHubConnectionCredentialsSchema;
export type TGitHubConnectionConfig = DiscriminativePick<TGitHubConnectionInput, "method" | "app" | "credentials">;

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

View File

@@ -68,11 +68,41 @@ Infisical supports two methods for connecting to AWS.
<Tabs>
<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:
![IAM Role Permissions](/images/integrations/aws/integration-aws-iam-assume-permission.png)
![IAM Role Secrets Manager Permissions](/images/app-connections/aws/secrets-manager-permissions.png)
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:
![IAM Role Secrets Manager Permissions](/images/app-connections/aws/parameter-store-permissions.png)
```json
{
@@ -99,6 +129,8 @@ Infisical supports two methods for connecting to AWS.
]
}
```
</Accordion>
</AccordionGroup>
</Tab>
</Tabs>
</Step>
@@ -186,10 +218,41 @@ Infisical supports two methods for connecting to AWS.
<Tabs>
<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:
![IAM Role Permissions](/images/integrations/aws/integration-aws-iam-assume-permission.png)
Alternatively, use the following custom policy to grant the minimum permissions required by Infisical to sync secrets to AWS Parameter Store:
![IAM Role Secrets Manager Permissions](/images/app-connections/aws/secrets-manager-permissions.png)
```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:
![IAM Role Secrets Manager Permissions](/images/app-connections/aws/parameter-store-permissions.png)
```json
{
@@ -216,6 +279,8 @@ Infisical supports two methods for connecting to AWS.
]
}
```
</Accordion>
</AccordionGroup>
</Tab>
</Tabs>
</Step>

View File

@@ -104,6 +104,42 @@ Infisical supports two methods for connecting to GitHub.
- 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.
![integrations github config](../../images/integrations/github/integrations-github-config-settings.png)
![integrations github config](../../images/integrations/github/integrations-github-config-dev-settings.png)
![integrations github config](../../images/integrations/github/integrations-github-config-new-app.png)
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`.
![integrations github config](../../images/integrations/github/integrations-github-config-new-app-form.png)
<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.
![integrations github config](../../images/integrations/github/integrations-github-config-credentials.png)
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
<Steps>

View File

@@ -17,17 +17,25 @@ that can be used across Infisical projects. Example use cases include syncing se
```mermaid
%%{init: {'flowchart': {'curve': 'linear'} } }%%
graph TD
A[AWS Connection]
A --> B[Project 1 Secret Sync]
A --> C[Project 2 Secret Sync]
A --> D[Project 3 Generate Dynamic Secret]
A[AWS]
B[AWS Connection]
C[Project 1 Secret Sync]
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 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 connection fill:#F4FFE6,stroke:#96D600,stroke-width:2px,color:black,rx:15px
class A aws
class B,C,D project
class B connection
class C,D,E project
```
</div>

View File

@@ -3,6 +3,7 @@ import { faCheck } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Modal, ModalContent, ModalTrigger, Select, SelectItem } from "@app/components/v2";
import { isInfisicalCloud } from "@app/helpers/platform";
enum Region {
US = "us",
@@ -79,10 +80,7 @@ export const RegionSelect = () => {
};
const shouldDisplay =
window.location.origin.includes("https://app.infisical.com") ||
window.location.origin.includes("https://us.infisical.com") ||
window.location.origin.includes("https://eu.infisical.com") ||
window.location.origin.includes("http://localhost:8080");
isInfisicalCloud() || window.location.origin.includes("http://localhost:8080");
// only display region select for cloud
if (!shouldDisplay) return null;

View File

@@ -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 { AppConnection } from "@app/hooks/api/appConnections/enums";
import { TAppConnection } from "@app/hooks/api/appConnections/types";
import { AwsConnectionMethod } from "@app/hooks/api/appConnections/types/aws-connection";
import { GitHubConnectionMethod } from "@app/hooks/api/appConnections/types/github-connection";
import {
AwsConnectionMethod,
GitHubConnectionMethod,
TAppConnection
} from "@app/hooks/api/appConnections/types";
export const APP_CONNECTION_MAP: Record<AppConnection, { name: string; image: string }> = {
[AppConnection.AWS]: { name: "AWS", image: "Amazon Web Services.png" },
[AppConnection.GitHub]: { name: "GitHub", image: "GitHub.png" }
};
export const APP_CONNECTION_METHOD_MAP: Record<
TAppConnection["method"],
{ name: string; icon: IconDefinition }
> = {
[AwsConnectionMethod.AssumeRole]: { name: "Assume Role", icon: faUser },
[AwsConnectionMethod.AccessKey]: { name: "Access Key", icon: faKey },
[GitHubConnectionMethod.App]: { name: "GitHub App", icon: faGithub },
[GitHubConnectionMethod.OAuth]: { name: "OAuth", icon: faPassport }
export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) => {
switch (method) {
case GitHubConnectionMethod.App:
return { name: "GitHub App", icon: faGithub };
case GitHubConnectionMethod.OAuth:
return { name: "OAuth", icon: faPassport };
case AwsConnectionMethod.AccessKey:
return { name: "Access Key", icon: faKey };
case AwsConnectionMethod.AssumeRole:
return { name: "Assume Role", icon: faUser };
default:
throw new Error(`Unhandled App Connection Method: ${method}`);
}
};

View 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");

View File

@@ -15,10 +15,12 @@ export type TAppConnectionResponse = { appConnection: TAppConnection };
export type TCreateAppConnectionDTO = Pick<
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;
app: AppConnection;
};

View File

@@ -1,6 +1,7 @@
export type TRootAppConnection = {
id: string;
name: string;
description?: string | null;
version: number;
orgId: string;
createdAt: string;

View File

@@ -13,7 +13,7 @@ import {
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
type FormData = Pick<TGitHubConnection, "name" | "method"> & {
type FormData = Pick<TGitHubConnection, "name" | "method" | "description"> & {
returnUrl?: string;
connectionId?: string;
};
@@ -58,7 +58,7 @@ export default function GitHubOAuthCallbackPage() {
localStorage.removeItem("githubConnectionFormData");
localStorage.removeItem("latestCSRFToken");
const { connectionId, name, returnUrl } = formData;
const { connectionId, name, description, returnUrl } = formData;
let appConnection: TAppConnection;
@@ -85,6 +85,7 @@ export default function GitHubOAuthCallbackPage() {
appConnection = await createAppConnection.mutateAsync({
app: AppConnection.GitHub,
name,
description,
...(installationId
? {
method: GitHubConnectionMethod.App,

View File

@@ -65,7 +65,7 @@ export const AppConnectionsTab = withPermission(
</div>
<OrgPermissionCan
I={OrgPermissionActions.Create}
a={OrgPermissionSubjects.ProjectTemplates}
a={OrgPermissionSubjects.AppConnections}
>
{(isAllowed) => (
<Button

View File

@@ -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 { z } from "zod";
@@ -11,18 +11,21 @@ import {
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 { AwsConnectionMethod, TAwsConnection } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { slugSchema } from "@app/lib/schemas";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type Props = {
appConnection?: TAwsConnection;
onSubmit: (formData: FormData) => void;
};
const rootSchema = z.object({
name: slugSchema({ min: 1, max: 32, field: "Name" }),
const rootSchema = genericAppConnectionFieldsSchema.extend({
app: z.literal(AppConnection.AWS)
});
@@ -30,14 +33,14 @@ const formSchema = z.discriminatedUnion("method", [
rootSchema.extend({
method: z.literal(AwsConnectionMethod.AssumeRole),
credentials: z.object({
roleArn: z.string().min(1, "Role ARN required")
roleArn: z.string().trim().min(1, "Role ARN required")
})
}),
rootSchema.extend({
method: z.literal(AwsConnectionMethod.AccessKey),
credentials: z.object({
accessKeyId: z.string().min(1, "Access Key ID required"),
secretAccessKey: z.string().min(1, "Secret Access Key required")
accessKeyId: z.string().trim().min(1, "Access Key ID 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) => {
const isUpdate = Boolean(appConnection);
const {
handleSubmit,
register,
control,
watch,
formState: { isSubmitting, errors, isDirty }
} = useForm<FormData>({
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
app: AppConnection.AWS,
@@ -61,24 +58,19 @@ export const AwsConnectionForm = ({ appConnection, onSubmit }: Props) => {
}
});
const {
handleSubmit,
control,
watch,
formState: { isSubmitting, isDirty }
} = form;
const selectedMethod = watch("method");
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && (
<FormControl
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>
)}
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="method"
control={control}
@@ -102,7 +94,7 @@ export const AwsConnectionForm = ({ appConnection, onSubmit }: Props) => {
{Object.values(AwsConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{APP_CONNECTION_METHOD_MAP[method].name}{" "}
{getAppConnectionMethodDetails(method).name}{" "}
{method === AwsConnectionMethod.AssumeRole ? " (Recommended)" : ""}
</SelectItem>
);
@@ -190,5 +182,6 @@ export const AwsConnectionForm = ({ appConnection, onSubmit }: Props) => {
</ModalClose>
</div>
</form>
</FormProvider>
);
};

View File

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

View File

@@ -1,38 +1,34 @@
import crypto from "crypto";
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 { z } from "zod";
import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2";
import { APP_CONNECTION_MAP, APP_CONNECTION_METHOD_MAP } from "@app/helpers/appConnections";
import { Button, FormControl, ModalClose, Select, SelectItem } from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { isInfisicalCloud } from "@app/helpers/platform";
import {
GitHubConnectionMethod,
TGitHubConnection,
useGetAppConnectionOption
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { slugSchema } from "@app/lib/schemas";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
} from "./GenericAppConnectionFields";
type Props = {
appConnection?: TGitHubConnection;
};
const rootSchema = z.object({
name: slugSchema({ min: 1, max: 32, field: "Name" }),
app: z.literal(AppConnection.GitHub)
const formSchema = genericAppConnectionFieldsSchema.extend({
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>;
export const GitHubConnectionForm = ({ appConnection }: Props) => {
@@ -44,13 +40,7 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => {
isLoading
} = useGetAppConnectionOption(AppConnection.GitHub);
const {
handleSubmit,
register,
control,
watch,
formState: { isSubmitting, errors, isDirty }
} = useForm<FormData>({
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: appConnection ?? {
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 onSubmit = (formData: FormData) => {
@@ -98,22 +95,12 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => {
throw new Error(`Unhandled GitHub Connection method: ${selectedMethod}`);
}
const methodDetails = getAppConnectionMethodDetails(selectedMethod);
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
{!isUpdate && (
<FormControl
helperText="Name must be slug-friendly"
errorText={errors.name?.message}
isError={Boolean(errors.name?.message)}
label="Name"
>
<Input
autoFocus
placeholder={`my-${AppConnection.GitHub}-connection`}
{...register("name")}
/>
</FormControl>
)}
{!isUpdate && <GenericAppConnectionsFields />}
<Controller
name="method"
control={control}
@@ -124,7 +111,11 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => {
}. 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.`
? `Environment variables have not been configured. ${
isInfisicalCloud()
? "Please contact Infisical."
: `See Docs to configure GitHub ${methodDetails.name} Connections.`
}`
: error?.message
}
isError={Boolean(error?.message) || isMissingConfig}
@@ -141,7 +132,7 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => {
{Object.values(GitHubConnectionMethod).map((method) => {
return (
<SelectItem value={method} key={method}>
{APP_CONNECTION_METHOD_MAP[method].name}{" "}
{methodDetails.name}{" "}
{method === GitHubConnectionMethod.App ? " (Recommended)" : ""}
</SelectItem>
);
@@ -168,5 +159,6 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => {
</ModalClose>
</div>
</form>
</FormProvider>
);
};

View File

@@ -1 +1,2 @@
export * from "./AppConnectionForm";
export * from "./GenericAppConnectionFields";

View File

@@ -24,10 +24,7 @@ export const AppConnectionHeader = ({ app, isConnected, onBack }: Props) => {
<div>
<div className="flex items-center text-mineshaft-300">
{appDetails.name}
<Link
href={`https://infisical.com/docs/documentation/platform/app-connections/${app}`}
passHref
>
<Link href={`https://infisical.com/docs/integrations/app-connections/${app}`} passHref>
<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">
<FontAwesomeIcon icon={faBookOpen} className="mr-1 mb-[0.03rem] text-[12px]" />

View File

@@ -45,8 +45,34 @@ export const AppConnectionsSelect = ({ onSelect }: Props) => {
))}
<Tooltip
side="bottom"
className="text-center"
content="Infisical is busy adding support for more connections. Check back soon if you don't see the one you're looking for."
className="max-w-sm py-4"
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">
<FontAwesomeIcon className="mt-auto text-xl" icon={faWrench} />

View File

@@ -5,6 +5,7 @@ import {
faCopy,
faEdit,
faEllipsisV,
faInfoCircle,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -23,7 +24,7 @@ import {
Tr
} from "@app/components/v2";
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 { TAppConnection } from "@app/hooks/api/appConnections";
@@ -31,16 +32,16 @@ type Props = {
appConnection: TAppConnection;
onDelete: (appConnection: TAppConnection) => void;
onEditCredentials: (appConnection: TAppConnection) => void;
onEditName: (appConnection: TAppConnection) => void;
onEditDetails: (appConnection: TAppConnection) => void;
};
export const AppConnectionRow = ({
appConnection,
onDelete,
onEditCredentials,
onEditName
onEditDetails
}: Props) => {
const { id, name, method, app } = appConnection;
const { id, name, method, app, description } = appConnection;
const [isIdCopied, setIsIdCopied] = useToggle(false);
@@ -59,6 +60,8 @@ export const AppConnectionRow = ({
return () => clearTimeout(timer);
}, [isIdCopied]);
const methodDetails = getAppConnectionMethodDetails(method);
return (
<Tr
className={twMerge("group h-12 transition-colors duration-100 hover:bg-mineshaft-700")}
@@ -75,16 +78,23 @@ export const AppConnectionRow = ({
</div>
</Td>
<Td className="!min-w-[8rem] max-w-0">
<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 className="!min-w-[8rem] max-w-0">
<p className="truncate">
<FontAwesomeIcon
size="sm"
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>
</Td>
@@ -116,9 +126,9 @@ export const AppConnectionRow = ({
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faEdit} />}
onClick={() => onEditName(appConnection)}
onClick={() => onEditDetails(appConnection)}
>
Edit Name
Edit Details
</DropdownMenuItem>
)}
</OrgPermissionCan>

View File

@@ -30,7 +30,7 @@ import {
Tr
} from "@app/components/v2";
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 { TAppConnection, useListAppConnections } from "@app/hooks/api/appConnections";
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 { DeleteAppConnectionModal } from "./DeleteAppConnectionModal";
import { EditAppConnectionCredentialsModal } from "./EditAppConnectionCredentialsModal";
import { EditAppConnectionNameModal } from "./EditAppConnectionNameModal";
import { EditAppConnectionDetailsModal } from "./EditAppConnectionDetailsModal";
enum AppConnectionsOrderBy {
App = "app",
@@ -61,7 +61,7 @@ export const AppConnectionsTable = () => {
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"deleteConnection",
"editCredentials",
"editName"
"editDetails"
] as const);
const [filters, setFilters] = useState<AppConnectionFilters>({
@@ -95,7 +95,7 @@ export const AppConnectionsTable = () => {
return (
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)
);
})
@@ -109,9 +109,11 @@ export const AppConnectionsTable = () => {
.toLowerCase()
.localeCompare(connectionTwo.name.toLowerCase());
case AppConnectionsOrderBy.Method:
return APP_CONNECTION_METHOD_MAP[connectionOne.method].name
.toLowerCase()
.localeCompare(APP_CONNECTION_METHOD_MAP[connectionTwo.method].name.toLowerCase());
return getAppConnectionMethodDetails(connectionOne.method)
.name.toLowerCase()
.localeCompare(
getAppConnectionMethodDetails(connectionTwo.method).name.toLowerCase()
);
case AppConnectionsOrderBy.App:
default:
return APP_CONNECTION_MAP[connectionOne.app].name
@@ -152,8 +154,8 @@ export const AppConnectionsTable = () => {
const handleEditCredentials = (appConnection: TAppConnection) =>
handlePopUpOpen("editCredentials", appConnection);
const handleEditName = (appConnection: TAppConnection) =>
handlePopUpOpen("editName", appConnection);
const handleEditDetails = (appConnection: TAppConnection) =>
handlePopUpOpen("editDetails", appConnection);
return (
<div>
@@ -274,7 +276,7 @@ export const AppConnectionsTable = () => {
key={connection.id}
onDelete={handleDelete}
onEditCredentials={handleEditCredentials}
onEditName={handleEditName}
onEditDetails={handleEditDetails}
/>
))}
</TBody>
@@ -309,10 +311,10 @@ export const AppConnectionsTable = () => {
onOpenChange={(isOpen) => handlePopUpToggle("editCredentials", isOpen)}
appConnection={popUp.editCredentials.data}
/>
<EditAppConnectionNameModal
isOpen={popUp.editName.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("editName", isOpen)}
appConnection={popUp.editName.data}
<EditAppConnectionDetailsModal
isOpen={popUp.editDetails.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("editDetails", isOpen)}
appConnection={popUp.editDetails.data}
/>
</div>
);

View File

@@ -1,23 +1,23 @@
import { useForm } from "react-hook-form";
import { FormProvider, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
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 { TAppConnection, useUpdateAppConnection } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { slugSchema } from "@app/lib/schemas";
import { DiscriminativePick } from "@app/lib/types";
import { genericAppConnectionFieldsSchema, GenericAppConnectionsFields } from "./AppConnectionForm";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
appConnection?: TAppConnection;
};
const formSchema = z.object({
name: slugSchema({ min: 1, max: 32, field: "Name" }),
const formSchema = genericAppConnectionFieldsSchema.extend({
app: z.nativeEnum(AppConnection)
});
@@ -29,14 +29,19 @@ const Content = ({ appConnection, onComplete }: ContentProps) => {
const updateAppConnection = useUpdateAppConnection();
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 {
handleSubmit,
register,
formState: { isSubmitting, errors, isDirty }
} = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: { name: appConnection.name, app: appConnection.app }
});
formState: { isSubmitting, isDirty }
} = form;
const onSubmit = async (formData: DiscriminativePick<TAppConnection, "name" | "app">) => {
try {
@@ -60,16 +65,9 @@ const Content = ({ appConnection, onComplete }: ContentProps) => {
};
return (
<FormProvider {...form}>
<form onSubmit={handleSubmit(onSubmit)}>
<FormControl
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>
<GenericAppConnectionsFields />
<div className="mt-8 flex items-center">
<Button
className="mr-4"
@@ -79,7 +77,7 @@ const Content = ({ appConnection, onComplete }: ContentProps) => {
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
Update Name
Update Details
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
@@ -88,10 +86,11 @@ const Content = ({ appConnection, onComplete }: ContentProps) => {
</ModalClose>
</div>
</form>
</FormProvider>
);
};
export const EditAppConnectionNameModal = ({ isOpen, onOpenChange, appConnection }: Props) => {
export const EditAppConnectionDetailsModal = ({ isOpen, onOpenChange, appConnection }: Props) => {
if (!appConnection) return null;
return (