diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 42123f896a..e6e473616a 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -192,6 +192,12 @@ export const PROJECTS = { }, LIST_GROUPS_IN_PROJECT: { projectSlug: "The slug of the project to list groups for." + }, + LIST_INTEGRATION: { + workspaceId: "The ID of the project to list integrations for." + }, + LIST_INTEGRATION_AUTHORIZATION: { + workspaceId: "The ID of the project to list integration auths for." } } as const; @@ -553,11 +559,8 @@ export const INTEGRATION_AUTH = { url: "", namespace: "", refreshToken: "The refresh token for integration authorization." - }, - LIST_AUTHORIZATION: { - workspaceId: "The ID of the project to list integration auths for." } -}; +} as const; export const INTEGRATION = { CREATE: { diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index a4df0416ee..489e953229 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -7,7 +7,7 @@ import { UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; -import { INTEGRATION_AUTH, PROJECTS } from "@app/lib/api-docs"; +import { PROJECTS } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -326,8 +326,14 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { rateLimit: readLimit }, schema: { + description: "List integrations for a project.", + security: [ + { + bearerAuth: [] + } + ], params: z.object({ - workspaceId: z.string().trim() + workspaceId: z.string().trim().describe(PROJECTS.LIST_INTEGRATION.workspaceId) }), response: { 200: z.object({ @@ -370,7 +376,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { } ], params: z.object({ - workspaceId: z.string().trim().describe(INTEGRATION_AUTH.LIST_AUTHORIZATION.workspaceId) + workspaceId: z.string().trim().describe(PROJECTS.LIST_INTEGRATION_AUTHORIZATION.workspaceId) }), response: { 200: z.object({ diff --git a/backend/src/services/project/project-dal.ts b/backend/src/services/project/project-dal.ts index 99e9cadd8c..42cc54393b 100644 --- a/backend/src/services/project/project-dal.ts +++ b/backend/src/services/project/project-dal.ts @@ -151,13 +151,11 @@ export const projectDALFactory = (db: TDbClient) => { const findProjectById = async (id: string) => { try { - const workspaces = await db(TableName.ProjectMembership) + const workspaces = await db(TableName.Project) .where(`${TableName.Project}.id`, id) - .join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`) - .join(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`) + .leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`) .select( selectAllTableCols(TableName.Project), - db.ref("id").withSchema(TableName.Project).as("_id"), db.ref("id").withSchema(TableName.Environment).as("envId"), db.ref("slug").withSchema(TableName.Environment).as("envSlug"), db.ref("name").withSchema(TableName.Environment).as("envName") @@ -166,10 +164,11 @@ export const projectDALFactory = (db: TDbClient) => { { column: `${TableName.Project}.name`, order: "asc" }, { column: `${TableName.Environment}.position`, order: "asc" } ]); + const project = sqlNestRelationships({ data: workspaces, key: "id", - parentMapper: ({ _id, ...el }) => ({ _id, ...ProjectsSchema.parse(el) }), + parentMapper: ({ ...el }) => ({ _id: el.id, ...ProjectsSchema.parse(el) }), childrenMapper: [ { key: "envId", @@ -199,14 +198,12 @@ export const projectDALFactory = (db: TDbClient) => { throw new BadRequestError({ message: "Organization ID is required when querying with slugs" }); } - const projects = await db(TableName.ProjectMembership) + const projects = await db(TableName.Project) .where(`${TableName.Project}.slug`, slug) .where(`${TableName.Project}.orgId`, orgId) - .join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`) - .join(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`) + .leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`) .select( selectAllTableCols(TableName.Project), - db.ref("id").withSchema(TableName.Project).as("_id"), db.ref("id").withSchema(TableName.Environment).as("envId"), db.ref("slug").withSchema(TableName.Environment).as("envSlug"), db.ref("name").withSchema(TableName.Environment).as("envName") @@ -219,7 +216,7 @@ export const projectDALFactory = (db: TDbClient) => { const project = sqlNestRelationships({ data: projects, key: "id", - parentMapper: ({ _id, ...el }) => ({ _id, ...ProjectsSchema.parse(el) }), + parentMapper: ({ ...el }) => ({ _id: el.id, ...ProjectsSchema.parse(el) }), childrenMapper: [ { key: "envId", diff --git a/docs/api-reference/endpoints/integrations/list-project-integrations.mdx b/docs/api-reference/endpoints/integrations/list-project-integrations.mdx new file mode 100644 index 0000000000..ce62ba25de --- /dev/null +++ b/docs/api-reference/endpoints/integrations/list-project-integrations.mdx @@ -0,0 +1,4 @@ +--- +title: "List project integrations " +openapi: "GET /api/v1/workspace/{workspaceId}/integrations" +--- diff --git a/docs/images/self-hosting/configuration/email/ses-create-identity.png b/docs/images/self-hosting/configuration/email/ses-create-identity.png new file mode 100644 index 0000000000..58b2b2e244 Binary files /dev/null and b/docs/images/self-hosting/configuration/email/ses-create-identity.png differ diff --git a/docs/mint.json b/docs/mint.json index 864d115ff8..8335c8bd6b 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -520,7 +520,8 @@ "api-reference/endpoints/integrations/delete-auth-by-id", "api-reference/endpoints/integrations/create", "api-reference/endpoints/integrations/update", - "api-reference/endpoints/integrations/delete" + "api-reference/endpoints/integrations/delete", + "api-reference/endpoints/integrations/list-project-integrations" ] }, { diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index db80341995..125f69421d 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -121,24 +121,35 @@ Without email configuration, Infisical's core functions like sign-up/login and s - 1. Create an account and [configure AWS SES](https://aws.amazon.com/premiumsupport/knowledge-center/ses-set-up-connect-smtp/) to send emails in the Amazon SES console. - 2. Create an IAM user for SMTP authentication and obtain SMTP credentials in SMTP settings > Create SMTP credentials + + + This will be used to verify the email you are sending from. + ![Create SES identity](../../images/self-hosting/configuration/email/ses-create-identity.png) + + If you AWS SES is under sandbox mode, you will only be able to send emails to verified identies. + + + + Create an IAM user for SMTP authentication and obtain SMTP credentials in SMTP settings > Create SMTP credentials - ![opening AWS SES console](../../images/self-hosting/configuration/email/email-aws-ses-console.png) + ![opening AWS SES console](../../images/self-hosting/configuration/email/email-aws-ses-console.png) - ![creating AWS IAM SES user](../../images/self-hosting/configuration/email/email-aws-ses-user.png) + ![creating AWS IAM SES user](../../images/self-hosting/configuration/email/email-aws-ses-user.png) + + + With your AWS SES SMTP credentials, you can now set up your SMTP environment variables for your Infisical instance. - 3. With your AWS SES SMTP credentials, you can now set up your SMTP environment variables: - - ``` - SMTP_HOST=email-smtp.ap-northeast-1.amazonaws.com # SMTP endpoint obtained from SMTP settings - SMTP_USERNAME=xxx # your SMTP username - SMTP_PASSWORD=xxx # your SMTP password - SMTP_PORT=587 - SMTP_SECURE=true - SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails - SMTP_FROM_NAME=Infisical - ``` + ``` + SMTP_HOST=email-smtp.ap-northeast-1.amazonaws.com # SMTP endpoint obtained from SMTP settings + SMTP_USERNAME=xxx # your SMTP username + SMTP_PASSWORD=xxx # your SMTP password + SMTP_PORT=587 + SMTP_SECURE=false + SMTP_FROM_ADDRESS=hey@example.com # your email address being used to send out emails + SMTP_FROM_NAME=Infisical + ``` + + Remember that you will need to restart Infisical for this to work properly. diff --git a/frontend/src/components/integrations/NoEnvironmentsBanner.tsx b/frontend/src/components/integrations/NoEnvironmentsBanner.tsx new file mode 100644 index 0000000000..fa900ff6dd --- /dev/null +++ b/frontend/src/components/integrations/NoEnvironmentsBanner.tsx @@ -0,0 +1,29 @@ +import { useRouter } from "next/router"; + +import { Button } from "../v2"; + +interface IProps { + projectId: string; +} + +export const NoEnvironmentsBanner = ({ projectId }: IProps) => { + const router = useRouter(); + + return ( +
+
+ + No environments in your project was found + +

+ In order to use integrations, you need to create at least one environment in your project. +

+
+
+ +
+
+ ); +}; diff --git a/frontend/src/views/IntegrationsPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx b/frontend/src/views/IntegrationsPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx index 5a0d85851b..f81f8befe0 100644 --- a/frontend/src/views/IntegrationsPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx +++ b/frontend/src/views/IntegrationsPage/components/CloudIntegrationSection/CloudIntegrationSection.tsx @@ -1,10 +1,17 @@ +import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { faCheck, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { NoEnvironmentsBanner } from "@app/components/integrations/NoEnvironmentsBanner"; import { createNotification } from "@app/components/notifications"; import { DeleteActionModal, Skeleton, Tooltip } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useProjectPermission, + useWorkspace +} from "@app/context"; import { usePopUp } from "@app/hooks"; import { IntegrationAuth, TCloudIntegration } from "@app/hooks/api/types"; @@ -31,18 +38,32 @@ export const CloudIntegrationSection = ({ "deleteConfirmation" ] as const); const { permission } = useProjectPermission(); - + const { currentWorkspace } = useWorkspace(); const isEmpty = !isLoading && !cloudIntegrations?.length; - const sortedCloudIntegrations = cloudIntegrations.sort((a, b) => a.name.localeCompare(b.name)); + const sortedCloudIntegrations = useMemo(() => { + const sortedIntegrations = cloudIntegrations.sort((a, b) => a.name.localeCompare(b.name)); + + if (currentWorkspace?.environments.length === 0) { + return sortedIntegrations.map((integration) => ({ ...integration, isAvailable: false })); + } + + return sortedIntegrations; + }, [cloudIntegrations, currentWorkspace?.environments]); return (
+
+ {currentWorkspace?.environments.length === 0 && ( + + )} +

{t("integrations.cloud-integrations")}

{t("integrations.click-to-start")}

+
{isLoading && Array.from({ length: 12 }).map((_, index) => ( diff --git a/frontend/src/views/Login/components/InitialStep/InitialStep.tsx b/frontend/src/views/Login/components/InitialStep/InitialStep.tsx index abf26d6917..cf81228f91 100644 --- a/frontend/src/views/Login/components/InitialStep/InitialStep.tsx +++ b/frontend/src/views/Login/components/InitialStep/InitialStep.tsx @@ -251,7 +251,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
- Don't have an acount yet? {t("login.create-account")} + Don't have an account yet? {t("login.create-account")}