Fix merge conflicts

This commit is contained in:
Tuan Dang
2023-06-29 17:53:33 +07:00
38 changed files with 5718 additions and 4180 deletions
@@ -53,7 +53,7 @@ export const getServiceTokenData = async (req: Request, res: Response) => {
req.authData.authPayload._id req.authData.authPayload._id
) )
.select("+encryptedKey +iv +tag") .select("+encryptedKey +iv +tag")
.populate("user"); .populate("user").lean();
return res.status(200).json(serviceTokenData); return res.status(200).json(serviceTokenData);
}; };
+1 -1
View File
@@ -188,7 +188,7 @@ type GetServiceTokenDetailsResponse struct {
Tag string `json:"tag"` Tag string `json:"tag"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"` UpdatedAt time.Time `json:"updatedAt"`
V int `json:"__v"` SecretPath string `json:"secretPath"`
} }
type GetAccessibleEnvironmentsRequest struct { type GetAccessibleEnvironmentsRequest struct {
+1
View File
@@ -37,6 +37,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string) ([]models.Singl
encryptedSecrets, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{ encryptedSecrets, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{
WorkspaceId: serviceTokenDetails.Workspace, WorkspaceId: serviceTokenDetails.Workspace,
Environment: serviceTokenDetails.Environment, Environment: serviceTokenDetails.Environment,
SecretPath: serviceTokenDetails.SecretPath,
}) })
if err != nil { if err != nil {
+8 -1
View File
@@ -6,7 +6,14 @@ module.exports = {
'@storybook/addon-essentials', '@storybook/addon-essentials',
'@storybook/addon-interactions', '@storybook/addon-interactions',
'storybook-dark-mode', 'storybook-dark-mode',
'@storybook/addon-postcss' {
name: '@storybook/addon-styling',
options: {
postCss: {
implementation: require('postcss')
}
}
}
], ],
framework: { framework: {
name: '@storybook/nextjs', name: '@storybook/nextjs',
+4493 -3451
View File
File diff suppressed because it is too large Load Diff
+13 -13
View File
@@ -7,7 +7,7 @@
"start": "next start", "start": "next start",
"start:docker": "next build && next start", "start:docker": "next build && next start",
"lint": "eslint --ext js,ts,tsx ./src", "lint": "eslint --ext js,ts,tsx ./src",
"lint-and-fix": "eslint --fix --ext js,ts,tsx ./src", "lint:fix": "eslint --fix --ext js,ts,tsx ./src",
"type-check": "tsc --project tsconfig.json", "type-check": "tsc --project tsconfig.json",
"storybook": "storybook dev -p 6006 -s ./public", "storybook": "storybook dev -p 6006 -s ./public",
"build-storybook": "storybook build" "build-storybook": "storybook build"
@@ -62,8 +62,8 @@
"infisical-node": "^1.0.37", "infisical-node": "^1.0.37",
"jspdf": "^2.5.1", "jspdf": "^2.5.1",
"jsrp": "^0.2.4", "jsrp": "^0.2.4",
"lottie-react": "^2.4.0",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"lottie-react": "^2.4.0",
"markdown-it": "^13.0.1", "markdown-it": "^13.0.1",
"next": "^12.3.4", "next": "^12.3.4",
"posthog-js": "^1.58.0", "posthog-js": "^1.58.0",
@@ -91,14 +91,14 @@
"yup": "^0.32.11" "yup": "^0.32.11"
}, },
"devDependencies": { "devDependencies": {
"@storybook/addon-essentials": "^7.0.0-beta.30", "@storybook/addon-essentials": "^7.0.23",
"@storybook/addon-interactions": "^7.0.0-beta.30", "@storybook/addon-interactions": "^7.0.23",
"@storybook/addon-links": "^7.0.0-beta.30", "@storybook/addon-links": "^7.0.23",
"@storybook/addon-postcss": "^2.0.0", "@storybook/addon-styling": "^1.3.0",
"@storybook/blocks": "^7.0.0-beta.30", "@storybook/blocks": "^7.0.23",
"@storybook/nextjs": "^7.0.0-beta.30", "@storybook/nextjs": "^7.0.23",
"@storybook/react": "^7.0.0-beta.30", "@storybook/react": "^7.0.23",
"@storybook/testing-library": "^0.0.13", "@storybook/testing-library": "^0.2.0",
"@tailwindcss/typography": "^0.5.4", "@tailwindcss/typography": "^0.5.4",
"@types/jsrp": "^0.2.4", "@types/jsrp": "^0.2.4",
"@types/node": "18.11.9", "@types/node": "18.11.9",
@@ -118,12 +118,12 @@
"eslint-plugin-react": "^7.32.0", "eslint-plugin-react": "^7.32.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-simple-import-sort": "^8.0.0", "eslint-plugin-simple-import-sort": "^8.0.0",
"eslint-plugin-storybook": "^0.6.10", "eslint-plugin-storybook": "^0.6.12",
"postcss": "^8.4.14", "postcss": "^8.4.14",
"prettier": "^2.8.3", "prettier": "^2.8.3",
"prettier-plugin-tailwindcss": "^0.2.2", "prettier-plugin-tailwindcss": "^0.2.2",
"storybook": "^7.0.0-beta.30", "storybook": "^7.0.23",
"storybook-dark-mode": "^2.0.5", "storybook-dark-mode": "^3.0.0",
"tailwindcss": "3.2", "tailwindcss": "3.2",
"typescript": "^4.9.3" "typescript": "^4.9.3"
} }
@@ -13,6 +13,7 @@ type Props = {
onChange?: (isOpen: boolean) => void; onChange?: (isOpen: boolean) => void;
deleteKey: string; deleteKey: string;
title: string; title: string;
subTitle?: string;
onDeleteApproved: () => Promise<void>; onDeleteApproved: () => Promise<void>;
}; };
@@ -22,7 +23,8 @@ export const DeleteActionModal = ({
onChange, onChange,
deleteKey, deleteKey,
onDeleteApproved, onDeleteApproved,
title title,
subTitle = "This action is irreversible!"
}: Props): JSX.Element => { }: Props): JSX.Element => {
const [inputData, setInputData] = useState(""); const [inputData, setInputData] = useState("");
const [isLoading, setIsLoading] = useToggle(); const [isLoading, setIsLoading] = useToggle();
@@ -52,7 +54,7 @@ export const DeleteActionModal = ({
> >
<ModalContent <ModalContent
title={title} title={title}
subTitle="This action is irreversible!" subTitle={subTitle}
footerContent={ footerContent={
<div className="flex items-center"> <div className="flex items-center">
<Button <Button
@@ -11,7 +11,7 @@ export type FormLabelProps = {
}; };
export const FormLabel = ({ id, label, isRequired }: FormLabelProps) => ( export const FormLabel = ({ id, label, isRequired }: FormLabelProps) => (
<Label.Root className="mb-1 ml-0.5 block text-sm font-medium text-mineshaft-300" htmlFor={id}> <Label.Root className="mb-0.5 ml-1 block text-sm font-normal text-mineshaft-400" htmlFor={id}>
{label} {label}
{isRequired && <span className="ml-1 text-red">*</span>} {isRequired && <span className="ml-1 text-red">*</span>}
</Label.Root> </Label.Root>
+5 -4
View File
@@ -1,5 +1,6 @@
export { export {
useGetAuthToken, useGetAuthToken,
useGetCommonPasswords, useGetCommonPasswords,
useSendMfaToken, useSendMfaToken,
useVerifyMfaToken} from "./queries" useVerifyMfaToken
} from "./queries"
+1
View File
@@ -0,0 +1 @@
export { useGetWorkspaceBot, useUpdateBotActiveStatus } from "./queries";
+38
View File
@@ -0,0 +1,38 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TBot, TSetBotActiveStatusDto } from "./types";
const queryKeys = {
getBot: (workspaceId: string) => [{ workspaceId }, "bot"] as const
};
const fetchWorkspaceBot = async (workspaceId: string) => {
const { data } = await apiRequest.get<{ bot: TBot }>(`/api/v1/bot/${workspaceId}`);
return data.bot;
};
export const useGetWorkspaceBot = (workspaceId: string) =>
useQuery({
queryKey: queryKeys.getBot(workspaceId),
queryFn: () => fetchWorkspaceBot(workspaceId),
enabled: Boolean(workspaceId)
});
// mutation
export const useUpdateBotActiveStatus = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TSetBotActiveStatusDto>({
mutationFn: ({ botId, isActive, botKey }) =>
apiRequest.patch(`/api/v1/bot/${botId}/active`, {
isActive,
botKey
}),
onSuccess: (_, { workspaceId }) => {
queryClient.invalidateQueries(queryKeys.getBot(workspaceId));
}
});
};
+20
View File
@@ -0,0 +1,20 @@
export type TBot = {
_id: string;
name: string;
workspace: string;
isActive: boolean;
publicKey: string;
createdAt: string;
updatedAt: string;
__v: number;
};
export type TSetBotActiveStatusDto = {
workspaceId: string;
botId: string;
isActive: boolean;
botKey?: {
encryptedKey: string;
nonce: string;
};
};
+3
View File
@@ -1,5 +1,8 @@
export * from "./auth"; export * from "./auth";
export * from "./bots";
export * from "./incidentContacts"; export * from "./incidentContacts";
export * from "./integrationAuth";
export * from "./integrations";
export * from "./keys"; export * from "./keys";
export * from "./organization"; export * from "./organization";
export * from "./secretFolders"; export * from "./secretFolders";
@@ -1,7 +1,9 @@
export { export {
useGetIntegrationAuthApps, useDeleteIntegrationAuth,
useGetIntegrationAuthById, useGetIntegrationAuthApps,
useGetIntegrationAuthRailwayEnvironments, useGetIntegrationAuthById,
useGetIntegrationAuthRailwayServices, useGetIntegrationAuthRailwayEnvironments,
useGetIntegrationAuthTeams, useGetIntegrationAuthRailwayServices,
useGetIntegrationAuthVercelBranches} from "./queries"; useGetIntegrationAuthTeams,
useGetIntegrationAuthVercelBranches
} from "./queries";
+196 -163
View File
@@ -1,204 +1,237 @@
import { useQuery } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { import { workspaceKeys } from "../workspace/queries";
App, import { App, Environment, IntegrationAuth, Service, Team } from "./types";
Environment,
IntegrationAuth,
Service,
Team
} from "./types";
const integrationAuthKeys = { const integrationAuthKeys = {
getIntegrationAuthById: (integrationAuthId: string) => [{ integrationAuthId }, "integrationAuth"] as const, getIntegrationAuthById: (integrationAuthId: string) =>
getIntegrationAuthApps: (integrationAuthId: string, teamId?: string) => [{ integrationAuthId, teamId }, "integrationAuthApps"] as const, [{ integrationAuthId }, "integrationAuth"] as const,
getIntegrationAuthTeams: (integrationAuthId: string) => [{ integrationAuthId }, "integrationAuthTeams"] as const, getIntegrationAuthApps: (integrationAuthId: string, teamId?: string) =>
getIntegrationAuthVercelBranches: ({ [{ integrationAuthId, teamId }, "integrationAuthApps"] as const,
integrationAuthId, getIntegrationAuthTeams: (integrationAuthId: string) =>
appId, [{ integrationAuthId }, "integrationAuthTeams"] as const,
}: { getIntegrationAuthVercelBranches: ({
integrationAuthId: string;
appId: string;
}) => [{ integrationAuthId, appId }, "integrationAuthVercelBranches"] as const,
getIntegrationAuthRailwayEnvironments: ({
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
}) => [{ integrationAuthId, appId }, "integrationAuthRailwayEnvironments"] as const,
getIntegrationAuthRailwayServices: ({
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
}) => [{ integrationAuthId, appId }, "integrationAuthRailwayServices"] as const
}
const fetchIntegrationAuthById = async (integrationAuthId: string) => {
const { data } = await apiRequest.get<{ integrationAuth: IntegrationAuth }>(`/api/v1/integration-auth/${integrationAuthId}`);
return data.integrationAuth;
}
const fetchIntegrationAuthApps = async ({
integrationAuthId,
teamId
}: {
integrationAuthId: string;
teamId?: string;
}) => {
const searchParams = new URLSearchParams(teamId ? { teamId } : undefined);
const { data } = await apiRequest.get<{ apps: App[] }>(
`/api/v1/integration-auth/${integrationAuthId}/apps`,
{ params: searchParams }
);
return data.apps;
}
const fetchIntegrationAuthTeams = async (integrationAuthId: string) => {
const { data } = await apiRequest.get<{ teams: Team[] }>(`/api/v1/integration-auth/${integrationAuthId}/teams`);
return data.teams;
}
const fetchIntegrationAuthVercelBranches = async ({
integrationAuthId, integrationAuthId,
appId appId
}: { }: {
integrationAuthId: string; integrationAuthId: string;
appId: string; appId: string;
}) => { }) => [{ integrationAuthId, appId }, "integrationAuthVercelBranches"] as const,
const { data: { branches } } = await apiRequest.get<{ branches: string[] }>(`/api/v1/integration-auth/${integrationAuthId}/vercel/branches`, { getIntegrationAuthRailwayEnvironments: ({
params: { integrationAuthId,
appId appId
} }: {
}); integrationAuthId: string;
appId: string;
}) => [{ integrationAuthId, appId }, "integrationAuthRailwayEnvironments"] as const,
getIntegrationAuthRailwayServices: ({
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
}) => [{ integrationAuthId, appId }, "integrationAuthRailwayServices"] as const
};
return branches; const fetchIntegrationAuthById = async (integrationAuthId: string) => {
const { data } = await apiRequest.get<{ integrationAuth: IntegrationAuth }>(
`/api/v1/integration-auth/${integrationAuthId}`
);
return data.integrationAuth;
};
const fetchIntegrationAuthApps = async ({
integrationAuthId,
teamId
}: {
integrationAuthId: string;
teamId?: string;
}) => {
const searchParams = new URLSearchParams(teamId ? { teamId } : undefined);
const { data } = await apiRequest.get<{ apps: App[] }>(
`/api/v1/integration-auth/${integrationAuthId}/apps`,
{ params: searchParams }
);
return data.apps;
};
const fetchIntegrationAuthTeams = async (integrationAuthId: string) => {
const { data } = await apiRequest.get<{ teams: Team[] }>(
`/api/v1/integration-auth/${integrationAuthId}/teams`
);
return data.teams;
};
const fetchIntegrationAuthVercelBranches = async ({
integrationAuthId,
appId
}: {
integrationAuthId: string;
appId: string;
}) => {
const {
data: { branches }
} = await apiRequest.get<{ branches: string[] }>(
`/api/v1/integration-auth/${integrationAuthId}/vercel/branches`,
{
params: {
appId
}
}
);
return branches;
}; };
const fetchIntegrationAuthRailwayEnvironments = async ({ const fetchIntegrationAuthRailwayEnvironments = async ({
integrationAuthId, integrationAuthId,
appId appId
}: { }: {
integrationAuthId: string; integrationAuthId: string;
appId: string; appId: string;
}) => { }) => {
const { data: { environments } } = await apiRequest.get<{ environments: Environment[] }>(`/api/v1/integration-auth/${integrationAuthId}/railway/environments`, { const {
params: { data: { environments }
appId } = await apiRequest.get<{ environments: Environment[] }>(
} `/api/v1/integration-auth/${integrationAuthId}/railway/environments`,
}); {
params: {
appId
}
}
);
return environments; return environments;
} };
const fetchIntegrationAuthRailwayServices = async ({ const fetchIntegrationAuthRailwayServices = async ({
integrationAuthId, integrationAuthId,
appId appId
}: { }: {
integrationAuthId: string; integrationAuthId: string;
appId: string; appId: string;
}) => { }) => {
const { data: { services } } = await apiRequest.get<{ services: Service[] }>(`/api/v1/integration-auth/${integrationAuthId}/railway/services`, { const {
params: { data: { services }
appId } = await apiRequest.get<{ services: Service[] }>(
} `/api/v1/integration-auth/${integrationAuthId}/railway/services`,
}); {
params: {
appId
}
}
);
return services; return services;
} };
export const useGetIntegrationAuthById = (integrationAuthId: string) => { export const useGetIntegrationAuthById = (integrationAuthId: string) => {
return useQuery({ return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId), queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
queryFn: () => fetchIntegrationAuthById(integrationAuthId), queryFn: () => fetchIntegrationAuthById(integrationAuthId),
enabled: true enabled: true
}); });
} };
export const useGetIntegrationAuthApps = ({ export const useGetIntegrationAuthApps = ({
integrationAuthId, integrationAuthId,
teamId teamId
}: { }: {
integrationAuthId: string; integrationAuthId: string;
teamId?: string; teamId?: string;
}) => { }) => {
return useQuery({ return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId, teamId), queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId, teamId),
queryFn: () => fetchIntegrationAuthApps({ queryFn: () =>
integrationAuthId, fetchIntegrationAuthApps({
teamId integrationAuthId,
}), teamId
enabled: true }),
}); enabled: true
} });
};
export const useGetIntegrationAuthTeams = (integrationAuthId: string) => { export const useGetIntegrationAuthTeams = (integrationAuthId: string) => {
return useQuery({ return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthTeams(integrationAuthId), queryKey: integrationAuthKeys.getIntegrationAuthTeams(integrationAuthId),
queryFn: () => fetchIntegrationAuthTeams(integrationAuthId), queryFn: () => fetchIntegrationAuthTeams(integrationAuthId),
enabled: true enabled: true
}); });
} };
export const useGetIntegrationAuthVercelBranches = ({ export const useGetIntegrationAuthVercelBranches = ({
integrationAuthId, integrationAuthId,
appId, appId
}: { }: {
integrationAuthId: string; integrationAuthId: string;
appId: string; appId: string;
}) => { }) => {
return useQuery({ return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthVercelBranches({ queryKey: integrationAuthKeys.getIntegrationAuthVercelBranches({
integrationAuthId, integrationAuthId,
appId, appId
}), }),
queryFn: () => fetchIntegrationAuthVercelBranches({ queryFn: () =>
integrationAuthId, fetchIntegrationAuthVercelBranches({
appId, integrationAuthId,
}), appId
enabled: true }),
}); enabled: true
} });
};
export const useGetIntegrationAuthRailwayEnvironments = ({ export const useGetIntegrationAuthRailwayEnvironments = ({
integrationAuthId, integrationAuthId,
appId appId
}: { }: {
integrationAuthId: string; integrationAuthId: string;
appId: string; appId: string;
}) => { }) => {
return useQuery({ return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthRailwayEnvironments({ queryKey: integrationAuthKeys.getIntegrationAuthRailwayEnvironments({
integrationAuthId, integrationAuthId,
appId, appId
}), }),
queryFn: () => fetchIntegrationAuthRailwayEnvironments({ queryFn: () =>
integrationAuthId, fetchIntegrationAuthRailwayEnvironments({
appId, integrationAuthId,
}), appId
enabled: true }),
}); enabled: true
} });
};
export const useGetIntegrationAuthRailwayServices = ({ export const useGetIntegrationAuthRailwayServices = ({
integrationAuthId, integrationAuthId,
appId appId
}: { }: {
integrationAuthId: string; integrationAuthId: string;
appId: string; appId: string;
}) => { }) => {
return useQuery({ return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthRailwayServices({ queryKey: integrationAuthKeys.getIntegrationAuthRailwayServices({
integrationAuthId, integrationAuthId,
appId, appId
}), }),
queryFn: () => fetchIntegrationAuthRailwayServices({ queryFn: () =>
integrationAuthId, fetchIntegrationAuthRailwayServices({
appId, integrationAuthId,
}), appId
enabled: true }),
}); enabled: true
} });
};
export const useDeleteIntegrationAuth = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, { id: string; workspaceId: string }>({
mutationFn: ({ id }) => apiRequest.delete(`/api/v1/integration-auth/${id}`),
onSuccess: (_, { workspaceId }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceAuthorization(workspaceId));
queryClient.invalidateQueries(workspaceKeys.getWorkspaceIntegrations(workspaceId));
}
});
};
+22 -19
View File
@@ -1,28 +1,31 @@
export type IntegrationAuth = { export type IntegrationAuth = {
_id: string; _id: string;
workspace: string; integration: string;
integration: string; workspace: string;
teamId?: string; __v: number;
accountId?: string; createdAt: string;
} updatedAt: string;
algorithm: string;
keyEncoding: string;
};
export type App = { export type App = {
name: string; name: string;
appId?: string; appId?: string;
owner?: string; owner?: string;
} };
export type Team = { export type Team = {
name: string; name: string;
teamId: string; teamId: string;
} };
export type Environment = { export type Environment = {
name: string; name: string;
environmentId: string; environmentId: string;
} };
export type Service = { export type Service = {
name: string; name: string;
serviceId: string; serviceId: string;
} };
@@ -0,0 +1 @@
export { useDeleteIntegration,useGetCloudIntegrations } from "./queries";
@@ -0,0 +1,35 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace/queries";
import { TCloudIntegration } from "./types";
export const integrationQueryKeys = {
getIntegrations: () => ["integrations"] as const
};
const fetchIntegrations = async () => {
const { data } = await apiRequest.get<{ integrationOptions: TCloudIntegration[] }>(
"/api/v1/integration-auth/integration-options"
);
return data.integrationOptions;
};
export const useGetCloudIntegrations = () =>
useQuery({
queryKey: integrationQueryKeys.getIntegrations(),
queryFn: () => fetchIntegrations()
});
export const useDeleteIntegration = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, { id: string; workspaceId: string }>({
mutationFn: ({ id }) => apiRequest.delete(`/api/v1/integration/${id}`),
onSuccess: (_, { workspaceId }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceIntegrations(workspaceId));
}
});
};
@@ -0,0 +1,33 @@
export type TCloudIntegration = {
name: string;
slug: string;
image: string;
isAvailable: boolean;
type: string;
clientId: string;
docsLink: string;
clientSlug: string;
};
export type TIntegration = {
_id: string;
workspace: string;
environment: string;
isActive: boolean;
url: any;
app: string;
appId: string;
targetEnvironment: string;
targetEnvironmentId: string;
targetService: string;
targetServiceId: string;
owner: string;
path: string;
region: string;
integration: string;
integrationAuth: string;
secretPath: string;
createdAt: string;
updatedAt: string;
__v: number;
};
+2
View File
@@ -1,5 +1,7 @@
export type { GetAuthTokenAPI } from "./auth/types"; export type { GetAuthTokenAPI } from "./auth/types";
export type { IncidentContact } from "./incidentContacts/types"; export type { IncidentContact } from "./incidentContacts/types";
export type { IntegrationAuth } from "./integrationAuth/types";
export type { TCloudIntegration, TIntegration } from "./integrations/types";
export type { UserWsKeyPair } from "./keys/types"; export type { UserWsKeyPair } from "./keys/types";
export type { Organization } from "./organization/types"; export type { Organization } from "./organization/types";
export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types"; export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types";
@@ -6,8 +6,10 @@ export {
useGetUserWorkspaceMemberships, useGetUserWorkspaceMemberships,
useGetUserWorkspaces, useGetUserWorkspaces,
useGetUserWsEnvironments, useGetUserWsEnvironments,
useGetWorkspaceAuthorizations,
useGetWorkspaceById, useGetWorkspaceById,
useGetWorkspaceIndexStatus, useGetWorkspaceIndexStatus,
useGetWorkspaceIntegrations,
useGetWorkspaceSecrets, useGetWorkspaceSecrets,
useNameWorkspaceSecrets, useNameWorkspaceSecrets,
useRenameWorkspace, useRenameWorkspace,
+49 -12
View File
@@ -2,9 +2,9 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { import { IntegrationAuth } from "../integrationAuth/types";
EncryptedSecret import { TIntegration } from "../integrations/types";
} from "../secrets/types"; import { EncryptedSecret } from "../secrets/types";
import { import {
CreateEnvironmentDTO, CreateEnvironmentDTO,
CreateWorkspaceDTO, CreateWorkspaceDTO,
@@ -19,11 +19,14 @@ import {
WorkspaceEnv WorkspaceEnv
} from "./types"; } from "./types";
const workspaceKeys = { export const workspaceKeys = {
getWorkspaceById: (workspaceId: string) => [{ workspaceId }, "workspace"] as const, getWorkspaceById: (workspaceId: string) => [{ workspaceId }, "workspace"] as const,
getWorkspaceSecrets: (workspaceId: string) => [{ workspaceId }, "workspace-secrets"] as const, getWorkspaceSecrets: (workspaceId: string) => [{ workspaceId }, "workspace-secrets"] as const,
getWorkspaceIndexStatus: (workspaceId: string) => [{ workspaceId}, "workspace-index-status"] as const, getWorkspaceIndexStatus: (workspaceId: string) =>
[{ workspaceId }, "workspace-index-status"] as const,
getWorkspaceMemberships: (orgId: string) => [{ orgId }, "workspace-memberships"], getWorkspaceMemberships: (orgId: string) => [{ orgId }, "workspace-memberships"],
getWorkspaceAuthorization: (workspaceId: string) => [{ workspaceId }, "workspace-authorizations"],
getWorkspaceIntegrations: (workspaceId: string) => [{ workspaceId }, "workspace-integrations"],
getAllUserWorkspace: ["workspaces"] as const, getAllUserWorkspace: ["workspaces"] as const,
getUserWsEnvironments: (workspaceId: string) => ["workspace-env", { workspaceId }] as const getUserWsEnvironments: (workspaceId: string) => ["workspace-env", { workspaceId }] as const
}; };
@@ -42,15 +45,17 @@ const fetchWorkspaceIndexStatus = async (workspaceId: string) => {
); );
return data; return data;
} };
const fetchWorkspaceSecrets = async (workspaceId: string) => { const fetchWorkspaceSecrets = async (workspaceId: string) => {
const { data: { secrets } } = await apiRequest.get<{ secrets: EncryptedSecret[] }>( const {
data: { secrets }
} = await apiRequest.get<{ secrets: EncryptedSecret[] }>(
`/api/v3/workspaces/${workspaceId}/secrets` `/api/v3/workspaces/${workspaceId}/secrets`
); );
return secrets; return secrets;
} };
const fetchUserWorkspaces = async () => { const fetchUserWorkspaces = async () => {
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace"); const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace");
@@ -63,15 +68,15 @@ export const useGetWorkspaceIndexStatus = (workspaceId: string) => {
queryFn: () => fetchWorkspaceIndexStatus(workspaceId), queryFn: () => fetchWorkspaceIndexStatus(workspaceId),
enabled: true enabled: true
}); });
} };
export const useGetWorkspaceSecrets = (workspaceId: string) => { export const useGetWorkspaceSecrets = (workspaceId: string) => {
return useQuery({ return useQuery({
queryKey: workspaceKeys.getWorkspaceSecrets(workspaceId), queryKey: workspaceKeys.getWorkspaceSecrets(workspaceId),
queryFn: () => fetchWorkspaceSecrets(workspaceId), queryFn: () => fetchWorkspaceSecrets(workspaceId),
enabled: true enabled: true
}) });
} };
export const useGetWorkspaceById = (workspaceId: string) => { export const useGetWorkspaceById = (workspaceId: string) => {
return useQuery({ return useQuery({
@@ -126,7 +131,39 @@ export const useNameWorkspaceSecrets = () => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceIndexStatus(variables.workspaceId)); queryClient.invalidateQueries(workspaceKeys.getWorkspaceIndexStatus(variables.workspaceId));
} }
}); });
} };
const fetchWorkspaceAuthorization = async (workspaceId: string) => {
const { data } = await apiRequest.get<{ authorizations: IntegrationAuth[] }>(
`/api/v1/workspace/${workspaceId}/authorizations`
);
return data.authorizations;
};
export const useGetWorkspaceAuthorizations = <TData = IntegrationAuth[],>(
workspaceId: string,
select?: (data: IntegrationAuth[]) => TData
) =>
useQuery({
queryKey: workspaceKeys.getWorkspaceAuthorization(workspaceId),
queryFn: () => fetchWorkspaceAuthorization(workspaceId),
enabled: Boolean(workspaceId),
select
});
const fetchWorkspaceIntegrations = async (workspaceId: string) => {
const { data } = await apiRequest.get<{ integrations: TIntegration[] }>(
`/api/v1/workspace/${workspaceId}/integrations`
);
return data.integrations;
};
export const useGetWorkspaceIntegrations = (workspaceId: string) =>
useQuery({
queryKey: workspaceKeys.getWorkspaceIntegrations(workspaceId),
queryFn: () => fetchWorkspaceIntegrations(workspaceId),
enabled: Boolean(workspaceId)
});
// mutation // mutation
export const useCreateWorkspace = () => { export const useCreateWorkspace = () => {
+27 -446
View File
@@ -1,409 +1,18 @@
import crypto from "crypto";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Head from "next/head"; import Head from "next/head";
import { useRouter } from "next/router";
import frameworkIntegrationOptions from "public/json/frameworkIntegrations.json"; import frameworkIntegrationOptions from "public/json/frameworkIntegrations.json";
import ActivateBotDialog from "@app/components/basic/dialog/ActivateBotDialog"; import { IntegrationsPage } from "@app/views/IntegrationsPage";
import CloudIntegrationSection from "@app/components/integrations/CloudIntegrationSection";
import FrameworkIntegrationSection from "@app/components/integrations/FrameworkIntegrationSection";
import IntegrationSection from "@app/components/integrations/IntegrationSection";
import NavHeader from "@app/components/navigation/NavHeader";
import { type Props = {
decryptAssymmetric, frameworkIntegrations: typeof frameworkIntegrationOptions;
encryptAssymmetric };
} from "../../components/utilities/cryptography/crypto";
import getBot from "../api/bot/getBot";
import setBotActiveStatus from "../api/bot/setBotActiveStatus";
import deleteIntegration from "../api/integrations/DeleteIntegration";
import getIntegrationOptions from "../api/integrations/GetIntegrationOptions";
import getWorkspaceAuthorizations from "../api/integrations/getWorkspaceAuthorizations";
import getWorkspaceIntegrations from "../api/integrations/getWorkspaceIntegrations";
import getAWorkspace from "../api/workspace/getAWorkspace";
import getLatestFileKey from "../api/workspace/getLatestFileKey";
interface IntegrationAuth {
_id: string;
integration: string;
workspace: string;
createdAt: string;
updatedAt: string;
}
interface Integration {
_id: string;
isActive: boolean;
app: string | null;
appId: string | null;
createdAt: string;
updatedAt: string;
environment: string;
integration: string;
targetEnvironment: string;
workspace: string;
secretPath:string;
integrationAuth: string;
}
interface IntegrationOption {
tenantId?: string;
clientId: string;
clientSlug?: string; // vercel-integration specific
docsLink: string;
image: string;
isAvailable: boolean;
name: string;
slug: string;
type: string;
}
export default function Integrations() {
const [cloudIntegrationOptions, setCloudIntegrationOptions] = useState([]);
const [integrationAuths, setIntegrationAuths] = useState<IntegrationAuth[]>([]);
const [environments, setEnvironments] = useState<
{
name: string;
slug: string;
}[]
>([]);
const [integrations, setIntegrations] = useState<Integration[]>([]);
// TODO: These will have its type when migratiing towards react-query
const [bot, setBot] = useState<any>(null);
const [isActivateBotDialogOpen, setIsActivateBotDialogOpen] = useState(false);
const [selectedIntegrationOption, setSelectedIntegrationOption] =
useState<IntegrationOption | null>(null);
const router = useRouter();
const workspaceId = router.query.id as string;
const Integration = ({ frameworkIntegrations }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => {
(async () => {
try {
const workspace = await getAWorkspace(workspaceId);
setEnvironments(workspace.environments);
// get cloud integration options
setCloudIntegrationOptions(await getIntegrationOptions());
// get project integration authorizations
setIntegrationAuths(
await getWorkspaceAuthorizations({
workspaceId
})
);
// get project integrations
setIntegrations(
await getWorkspaceIntegrations({
workspaceId
})
);
// get project bot
setBot(await getBot({ workspaceId }));
} catch (err) {
console.error(err);
}
})();
}, []);
/**
* Activate bot for project by performing the following steps:
* 1. Get the (encrypted) project key
* 2. Decrypt project key with user's private key
* 3. Encrypt project key with bot's public key
* 4. Send encrypted project key to backend and set bot status to active
*/
const handleBotActivate = async () => {
let botKey;
try {
if (bot) {
// case: there is a bot
const key = await getLatestFileKey({ workspaceId });
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
if (!PRIVATE_KEY) {
throw new Error("Private Key missing");
}
const WORKSPACE_KEY = decryptAssymmetric({
ciphertext: key.latestKey.encryptedKey,
nonce: key.latestKey.nonce,
publicKey: key.latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: WORKSPACE_KEY,
publicKey: bot.publicKey,
privateKey: PRIVATE_KEY
});
botKey = {
encryptedKey: ciphertext,
nonce
};
setBot(
(
await setBotActiveStatus({
botId: bot._id,
isActive: true,
botKey
})
).bot
);
}
} catch (err) {
console.error(err);
}
};
const handleUnauthorizedIntegrationOptionPress = (integrationOption: IntegrationOption) => {
try {
// generate CSRF token for OAuth2 code-token exchange integrations
const state = crypto.randomBytes(16).toString("hex");
localStorage.setItem("latestCSRFToken", state);
let link = "";
switch (integrationOption.slug) {
case "azure-key-vault":
link = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/azure-key-vault/oauth2/callback&response_mode=query&scope=https://vault.azure.net/.default openid offline_access&state=${state}`;
break;
case "aws-parameter-store":
link = `${window.location.origin}/integrations/aws-parameter-store/authorize`;
break;
case "aws-secret-manager":
link = `${window.location.origin}/integrations/aws-secret-manager/authorize`;
break;
case "heroku":
link = `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`;
break;
case "vercel":
link = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`;
break;
case "netlify":
link = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/integrations/netlify/oauth2/callback`;
break;
case "github":
link = `https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/integrations/github/oauth2/callback&state=${state}`;
break;
case "gitlab":
link = `https://gitlab.com/oauth/authorize?client_id=${integrationOption.clientId}&redirect_uri=${window.location.origin}/integrations/gitlab/oauth2/callback&response_type=code&state=${state}`;
break;
case "render":
link = `${window.location.origin}/integrations/render/authorize`;
break;
case "flyio":
link = `${window.location.origin}/integrations/flyio/authorize`;
break;
case "circleci":
link = `${window.location.origin}/integrations/circleci/authorize`;
break;
case "travisci":
link = `${window.location.origin}/integrations/travisci/authorize`;
break;
case "supabase":
link = `${window.location.origin}/integrations/supabase/authorize`;
break;
case "checkly":
link = `${window.location.origin}/integrations/checkly/authorize`;
break;
case "railway":
link = `${window.location.origin}/integrations/railway/authorize`;
break;
case "hashicorp-vault":
link = `${window.location.origin}/integrations/hashicorp-vault/authorize`;
break;
case "cloudflare-pages":
link = `${window.location.origin}/integrations/cloudflare-pages/authorize`;
break;
default:
break;
}
if (link !== "") {
window.location.assign(link);
}
} catch (err) {
console.error(err);
}
};
const handleAuthorizedIntegrationOptionPress = (integrationAuth: IntegrationAuth) => {
try {
let link = "";
switch (integrationAuth.integration) {
case "azure-key-vault":
link = `${window.location.origin}/integrations/azure-key-vault/create?integrationAuthId=${integrationAuth._id}`;
break;
case "aws-parameter-store":
link = `${window.location.origin}/integrations/aws-parameter-store/create?integrationAuthId=${integrationAuth._id}`;
break;
case "aws-secret-manager":
link = `${window.location.origin}/integrations/aws-secret-manager/create?integrationAuthId=${integrationAuth._id}`;
break;
case "heroku":
link = `${window.location.origin}/integrations/heroku/create?integrationAuthId=${integrationAuth._id}`;
break;
case "vercel":
link = `${window.location.origin}/integrations/vercel/create?integrationAuthId=${integrationAuth._id}`;
break;
case "netlify":
link = `${window.location.origin}/integrations/netlify/create?integrationAuthId=${integrationAuth._id}`;
break;
case "github":
link = `${window.location.origin}/integrations/github/create?integrationAuthId=${integrationAuth._id}`;
break;
case "gitlab":
link = `${window.location.origin}/integrations/gitlab/create?integrationAuthId=${integrationAuth._id}`;
break;
case "render":
link = `${window.location.origin}/integrations/render/create?integrationAuthId=${integrationAuth._id}`;
break;
case "flyio":
link = `${window.location.origin}/integrations/flyio/create?integrationAuthId=${integrationAuth._id}`;
break;
case "circleci":
link = `${window.location.origin}/integrations/circleci/create?integrationAuthId=${integrationAuth._id}`;
break;
case "travisci":
link = `${window.location.origin}/integrations/travisci/create?integrationAuthId=${integrationAuth._id}`;
break;
case "supabase":
link = `${window.location.origin}/integrations/supabase/create?integrationAuthId=${integrationAuth._id}`;
break;
case "checkly":
link = `${window.location.origin}/integrations/checkly/create?integrationAuthId=${integrationAuth._id}`;
break;
case "railway":
link = `${window.location.origin}/integrations/railway/create?integrationAuthId=${integrationAuth._id}`;
break;
case "hashicorp-vault":
link = `${window.location.origin}/integrations/hashicorp-vault/create?integrationAuthId=${integrationAuth._id}`;
break;
case "cloudflare-pages":
link = `${window.location.origin}/integrations/cloudflare-pages/create?integrationAuthId=${integrationAuth._id}`;
break;
default:
break;
}
if (link !== "") {
window.location.assign(link);
}
} catch (err) {
console.error(err);
}
};
/**
* Open dialog to activate bot if bot is not active.
* Otherwise, start integration [integrationOption]
* @param {Object} integrationOption - an integration option
* @param {String} integrationOption.name
* @param {String} integrationOption.type
* @param {String} integrationOption.docsLink
* @returns
*/
const integrationOptionPress = async (integrationOption: IntegrationOption) => {
try {
const integrationAuthX = integrationAuths.find(
(integrationAuth) => integrationAuth.integration === integrationOption.slug
);
if (!bot.isActive) {
await handleBotActivate();
}
if (!integrationAuthX) {
// case: integration has not been authorized
handleUnauthorizedIntegrationOptionPress(integrationOption);
return;
}
handleAuthorizedIntegrationOptionPress(integrationAuthX);
} catch (err) {
console.error(err);
}
};
/**
* Handle deleting integration authorization [integrationAuth] and corresponding integrations from state where applicable
* @param {Object} obj
* @param {IntegrationAuth} obj.integrationAuth - integrationAuth to delete
*/
const handleDeleteIntegrationAuth = async ({
integrationAuth: deletedIntegrationAuth
}: {
integrationAuth: IntegrationAuth;
}) => {
try {
const newIntegrations = integrations.filter(
(integration) => integration.integrationAuth !== deletedIntegrationAuth._id
);
setIntegrationAuths(
integrationAuths.filter(
(integrationAuth) => integrationAuth._id !== deletedIntegrationAuth._id
)
);
setIntegrations(newIntegrations);
// handle updating bot
if (newIntegrations.length < 1) {
// case: no integrations left
setBot(
(
await setBotActiveStatus({
botId: bot._id,
isActive: false
})
).bot
);
}
} catch (err) {
console.error(err);
}
};
/**
* Handle deleting integration [integration]
* @param {Object} obj
* @param {Integration} obj.integration - integration to delete
*/
const handleDeleteIntegration = async ({ integration }: { integration: Integration }) => {
try {
const deletedIntegration = await deleteIntegration({
integrationId: integration._id
});
const newIntegrations = integrations.filter((i) => i._id !== deletedIntegration._id);
setIntegrations(newIntegrations);
// handle updating bot
if (newIntegrations.length < 1) {
// case: no integrations left
setBot(
(
await setBotActiveStatus({
botId: bot._id,
isActive: false
})
).bot
);
}
} catch (err) {
console.error(err);
}
};
return ( return (
<div className="flex max-h-full flex-col justify-between bg-bunker-800 text-white"> <>
<Head> <Head>
<title>{t("common.head-title", { title: t("integrations.title") })}</title> <title>{t("common.head-title", { title: t("integrations.title") })}</title>
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
@@ -411,54 +20,26 @@ export default function Integrations() {
<meta property="og:title" content="Manage your .env files in seconds" /> <meta property="og:title" content="Manage your .env files in seconds" />
<meta name="og:description" content={t("integrations.description") as string} /> <meta name="og:description" content={t("integrations.description") as string} />
</Head> </Head>
<div className="no-scrollbar::-webkit-scrollbar h-screen max-h-[calc(100vh-10px)] w-full overflow-y-scroll pb-6 no-scrollbar"> <IntegrationsPage frameworkIntegrations={frameworkIntegrations} />
<div className="ml-6"> </>
<NavHeader pageName={t("integrations.title")} isProjectRelated />
</div>
<ActivateBotDialog
isOpen={isActivateBotDialogOpen}
closeModal={() => setIsActivateBotDialogOpen(false)}
selectedIntegrationOption={selectedIntegrationOption}
integrationOptionPress={integrationOptionPress}
/>
<IntegrationSection
integrations={integrations}
setIntegrations={setIntegrations}
bot={bot}
setBot={setBot}
environments={environments}
handleDeleteIntegration={handleDeleteIntegration}
/>
{cloudIntegrationOptions.length > 0 && bot ? (
<CloudIntegrationSection
cloudIntegrationOptions={cloudIntegrationOptions}
setSelectedIntegrationOption={setSelectedIntegrationOption as any}
integrationOptionPress={(integrationOption: IntegrationOption) => {
if (!bot.isActive) {
// case: bot is not active -> open modal to activate bot
setIsActivateBotDialogOpen(true);
return;
}
integrationOptionPress(integrationOption);
}}
integrationAuths={integrationAuths}
handleDeleteIntegrationAuth={handleDeleteIntegrationAuth}
/>
) : (
<>
<div className="m-4 mt-7 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
<h1 className="text-3xl font-semibold">{t("integrations.cloud-integrations")}</h1>
<p className="text-base text-gray-400">{t("integrations.click-to-start")}</p>
</div>
<div className="mx-6 grid max-w-5xl grid-cols-4 grid-rows-2 gap-4">
{[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16].map(elem => <div key={elem} className="bg-mineshaft-800 border border-mineshaft-600 animate-pulse h-32 rounded-md"/>)}
</div>
</>
)}
<FrameworkIntegrationSection frameworks={frameworkIntegrationOptions as any} />
</div>
</div>
); );
} };
Integrations.requireAuth = true; export const getStaticProps = () => {
return {
props: {
frameworkIntegrations: frameworkIntegrationOptions
}
};
};
export const getStaticPaths = async () => {
return {
paths: [], // indicates that no page needs be created at build time
fallback: "blocking" // indicates the type of fallback
};
};
Integration.requireAuth = true;
export default Integration;
@@ -98,7 +98,7 @@ export default function ChecklyCreateIntegrationPage() {
> >
Checkly Integration Checkly Integration
</CardTitle> </CardTitle>
<FormControl label="Infisical Project Environment" className="mt-2 px-6"> <FormControl label="Infisical Project Environment" className="mt-4 px-6">
<Select <Select
value={selectedSourceEnvironment} value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)} onValueChange={(val) => setSelectedSourceEnvironment(val)}
@@ -114,7 +114,7 @@ export default function ChecklyCreateIntegrationPage() {
))} ))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl label="Secrets Path"> <FormControl label="Secrets Path" className="mt-4 px-6">
<Input <Input
value={secretPath} value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)} onChange={(evt) => setSecretPath(evt.target.value)}
@@ -0,0 +1,105 @@
import crypto from "crypto";
import { TCloudIntegration,UserWsKeyPair } from "@app/hooks/api/types";
import {
decryptAssymmetric,
encryptAssymmetric
} from "../../components/utilities/cryptography/crypto";
export const generateBotKey = (botPublicKey: string, latestKey: UserWsKeyPair) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
if (!PRIVATE_KEY) {
throw new Error("Private Key missing");
}
const WORKSPACE_KEY = decryptAssymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: WORKSPACE_KEY,
publicKey: botPublicKey,
privateKey: PRIVATE_KEY
});
return { encryptedKey: ciphertext, nonce };
};
export const redirectForProviderAuth = (integrationOption: TCloudIntegration) => {
try {
// generate CSRF token for OAuth2 code-token exchange integrations
const state = crypto.randomBytes(16).toString("hex");
localStorage.setItem("latestCSRFToken", state);
let link = "";
switch (integrationOption.slug) {
case "azure-key-vault":
link = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=${integrationOption.clientId}&response_type=code&redirect_uri=${window.location.origin}/integrations/azure-key-vault/oauth2/callback&response_mode=query&scope=https://vault.azure.net/.default openid offline_access&state=${state}`;
break;
case "aws-parameter-store":
link = `${window.location.origin}/integrations/aws-parameter-store/authorize`;
break;
case "aws-secret-manager":
link = `${window.location.origin}/integrations/aws-secret-manager/authorize`;
break;
case "heroku":
link = `https://id.heroku.com/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=write-protected&state=${state}`;
break;
case "vercel":
link = `https://vercel.com/integrations/${integrationOption.clientSlug}/new?state=${state}`;
break;
case "netlify":
link = `https://app.netlify.com/authorize?client_id=${integrationOption.clientId}&response_type=code&state=${state}&redirect_uri=${window.location.origin}/integrations/netlify/oauth2/callback`;
break;
case "github":
link = `https://github.com/login/oauth/authorize?client_id=${integrationOption.clientId}&response_type=code&scope=repo&redirect_uri=${window.location.origin}/integrations/github/oauth2/callback&state=${state}`;
break;
case "gitlab":
link = `https://gitlab.com/oauth/authorize?client_id=${integrationOption.clientId}&redirect_uri=${window.location.origin}/integrations/gitlab/oauth2/callback&response_type=code&state=${state}`;
break;
case "render":
link = `${window.location.origin}/integrations/render/authorize`;
break;
case "flyio":
link = `${window.location.origin}/integrations/flyio/authorize`;
break;
case "circleci":
link = `${window.location.origin}/integrations/circleci/authorize`;
break;
case "travisci":
link = `${window.location.origin}/integrations/travisci/authorize`;
break;
case "supabase":
link = `${window.location.origin}/integrations/supabase/authorize`;
break;
case "checkly":
link = `${window.location.origin}/integrations/checkly/authorize`;
break;
case "railway":
link = `${window.location.origin}/integrations/railway/authorize`;
break;
case "hashicorp-vault":
link = `${window.location.origin}/integrations/hashicorp-vault/authorize`;
break;
case "cloudflare-pages":
link = `${window.location.origin}/integrations/cloudflare-pages/authorize`;
break;
default:
break;
}
if (link !== "") {
window.location.assign(link);
}
} catch (err) {
console.error(err);
}
};
export const redirectToIntegrationAppConfigScreen = (provider: string, integrationAuthId: string) =>
`/integrations/${provider}/create?integrationAuthId=${integrationAuthId}`;
@@ -0,0 +1,224 @@
import { useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useRouter } from "next/router";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import NavHeader from "@app/components/navigation/NavHeader";
import { Button, Modal, ModalContent } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { usePopUp } from "@app/hooks";
import {
useDeleteIntegration,
useDeleteIntegrationAuth,
useGetCloudIntegrations,
useGetUserWsKey,
useGetWorkspaceAuthorizations,
useGetWorkspaceBot,
useGetWorkspaceIntegrations,
useUpdateBotActiveStatus
} from "@app/hooks/api";
import { IntegrationAuth } from "@app/hooks/api/types";
import { CloudIntegrationSection } from "./components/CloudIntegrationSection";
import { FrameworkIntegrationSection } from "./components/FrameworkIntegrationSection";
import { IntegrationsSection } from "./components/IntegrationsSection";
import {
generateBotKey,
redirectForProviderAuth,
redirectToIntegrationAppConfigScreen
} from "./IntegrationPage.utils";
type Props = {
frameworkIntegrations: Array<{ name: string; slug: string; image: string; docsLink: string }>;
};
export const IntegrationsPage = ({ frameworkIntegrations }: Props) => {
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
const router = useRouter();
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?._id || "";
const environments = currentWorkspace?.environments || [];
const { data: latestWsKey } = useGetUserWsKey(workspaceId);
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
"activeBot"
] as const);
const { data: cloudIntegrations, isLoading: isCloudIntegrationsLoading } =
useGetCloudIntegrations();
const { data: integrationAuths, isLoading: isIntegrationAuthLoading } =
useGetWorkspaceAuthorizations(
workspaceId,
useCallback((data: IntegrationAuth[]) => {
const groupBy: Record<string, IntegrationAuth> = {};
data.forEach((el) => {
groupBy[el.integration] = el;
});
return groupBy;
}, [])
);
// mutation
const {
data: integrations,
isLoading: isIntegrationLoading,
isFetching: isIntegrationFetching
} = useGetWorkspaceIntegrations(workspaceId);
const { data: bot } = useGetWorkspaceBot(workspaceId);
// mutation
const { mutateAsync: updateBotActiveStatus, mutate: updateBotActiveStatusSync } =
useUpdateBotActiveStatus();
const { mutateAsync: deleteIntegration } = useDeleteIntegration();
const {
mutateAsync: deleteIntegrationAuth,
isLoading: isDeleteIntegrationAuthSuccess,
reset: resetDeleteIntegrationAuth
} = useDeleteIntegrationAuth();
// summary: this use effect is trigger when all integration auths are removed thus deactivate bot
// details: so onsuccessfully deleting an integration auth, immediately integration list is refeteched
// After the refetch is completed check if its empty. Then set bot active and reset the submit hook
useEffect(() => {
if (isDeleteIntegrationAuthSuccess && !isIntegrationFetching && !integrations?.length) {
if (bot?._id)
updateBotActiveStatusSync({
isActive: false,
botId: bot._id,
workspaceId
});
resetDeleteIntegrationAuth();
}
}, [isIntegrationFetching, isDeleteIntegrationAuthSuccess, integrations?.length]);
const handleProviderIntegration = async (provider: string) => {
const selectedCloudIntegration = cloudIntegrations?.find(({ slug }) => provider === slug);
if (!selectedCloudIntegration) return;
try {
if (bot && !bot.isActive) {
const botKey = generateBotKey(bot.publicKey, latestWsKey!);
await updateBotActiveStatus({
workspaceId,
botKey,
isActive: true,
botId: bot._id
});
}
const integrationAuthForProvider = integrationAuths?.[provider];
if (!integrationAuthForProvider) {
redirectForProviderAuth(selectedCloudIntegration);
return;
}
const url = redirectToIntegrationAppConfigScreen(provider, integrationAuthForProvider._id);
router.push(url);
} catch (error) {
console.error(error);
}
};
// function to strat integration for a provider
// confirmation to user passing the bot key for provider to get secret access
const handleProviderIntegrationStart = (provider: string) => {
if (!bot?.isActive) {
handlePopUpOpen("activeBot", { provider });
return;
}
handleProviderIntegration(provider);
};
const handleUserAcceptBotCondition = () => {
const { provider } = popUp.activeBot?.data as { provider: string };
handleProviderIntegration(provider);
handlePopUpClose("activeBot");
};
const handleIntegrationDelete = async (integrationId: string, cb: () => void) => {
try {
await deleteIntegration({ id: integrationId, workspaceId });
if (cb) cb();
createNotification({
type: "success",
text: "Deleted integration"
});
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "Failed to delete integration"
});
}
};
const handleIntegrationAuthRevoke = async (provider: string, cb?: () => void) => {
const integrationAuthForProvider = integrationAuths?.[provider];
if (!integrationAuthForProvider) return;
try {
await deleteIntegrationAuth({
id: integrationAuthForProvider._id,
workspaceId
});
if (cb) cb();
createNotification({
type: "success",
text: "Revoked provider authentication"
});
} catch (err) {
console.error(err);
createNotification({
type: "error",
text: "Failed to revoke provider authentication"
});
}
};
return (
<div className="container mx-auto max-w-7xl px-8 pb-12 text-white">
<div className="ml-4">
<NavHeader pageName={t("integrations.title")} isProjectRelated />
</div>
<IntegrationsSection
isLoading={isIntegrationLoading}
integrations={integrations}
environments={environments}
onIntegrationDelete={({ _id: id }, cb) => handleIntegrationDelete(id, cb)}
/>
<CloudIntegrationSection
isLoading={isCloudIntegrationsLoading || isIntegrationAuthLoading}
cloudIntegrations={cloudIntegrations}
integrationAuths={integrationAuths}
onIntegrationStart={handleProviderIntegrationStart}
onIntegrationRevoke={handleIntegrationAuthRevoke}
/>
<Modal
isOpen={popUp.activeBot?.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("activeBot", isOpen)}
>
<ModalContent
title={t("integrations.grant-access-to-secrets") as string}
footerContent={
<div className="flex items-center space-x-2">
<Button onClick={() => handleUserAcceptBotCondition()}>
{t("integrations.grant-access-button") as string}
</Button>
<Button
onClick={() => handlePopUpClose("activeBot")}
variant="outline_bg"
colorSchema="secondary"
>
Cancel
</Button>
</div>
}
>
{t("integrations.why-infisical-needs-access")}
</ModalContent>
</Modal>
<FrameworkIntegrationSection frameworks={frameworkIntegrations} />
</div>
);
};
@@ -0,0 +1,140 @@
import { useTranslation } from "react-i18next";
import { faCheck, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { DeleteActionModal,Skeleton, Tooltip } from "@app/components/v2";
import { usePopUp } from "@app/hooks";
import { IntegrationAuth, TCloudIntegration } from "@app/hooks/api/types";
type Props = {
isLoading?: boolean;
integrationAuths?: Record<string, IntegrationAuth>;
cloudIntegrations?: TCloudIntegration[];
onIntegrationStart: (slug: string) => void;
// cb: handle popUpClose child->parent communication pattern
onIntegrationRevoke: (slug: string, cb: () => void) => void;
};
type TRevokeIntegrationPopUp = { provider: string };
export const CloudIntegrationSection = ({
isLoading,
cloudIntegrations = [],
integrationAuths = {},
onIntegrationStart,
onIntegrationRevoke
}: Props) => {
const { t } = useTranslation();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteConfirmation"
] as const);
const isEmpty = !isLoading && !cloudIntegrations?.length;
return (
<div>
<div className="m-4 mt-7 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
<h1 className="text-3xl font-semibold">{t("integrations.cloud-integrations")}</h1>
<p className="text-base text-gray-400">{t("integrations.click-to-start")}</p>
</div>
<div
className="mx-6 grid grid-flow-dense gap-4"
style={{ gridTemplateColumns: "repeat(auto-fill, minmax(256px, 1fr))" }}
>
{isLoading &&
Array.from({ length: 12 }).map((_, index) => (
<Skeleton className="h-32" key={`cloud-integration-skeleton-${index + 1}`} />
))}
{!isLoading &&
cloudIntegrations?.map((cloudIntegration) => (
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
className={`group relative ${
cloudIntegration.isAvailable
? "cursor-pointer duration-200 hover:bg-mineshaft-700"
: "opacity-50"
} flex h-32 flex-row items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4`}
onClick={() => {
if (!cloudIntegration.isAvailable) return;
onIntegrationStart(cloudIntegration.slug);
}}
key={cloudIntegration.slug}
>
<img
src={`/images/integrations/${cloudIntegration.image}`}
height={70}
width={70}
alt="integration logo"
/>
{cloudIntegration.name.split(" ").length > 2 ? (
<div className="ml-4 max-w-xs text-3xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
<div>{cloudIntegration.name.split(" ")[0]}</div>
<div className="text-base">
{cloudIntegration.name.split(" ")[1]} {cloudIntegration.name.split(" ")[2]}
</div>
</div>
) : (
<div className="ml-4 max-w-xs text-xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
{cloudIntegration.name}
</div>
)}
{cloudIntegration.isAvailable &&
Boolean(integrationAuths?.[cloudIntegration.slug]) && (
<div className="absolute top-0 right-0 z-40 h-full">
<div className="relative h-full">
<div className="absolute top-0 right-0 w-24 flex-row items-center overflow-hidden whitespace-nowrap rounded-tr-md rounded-bl-md bg-primary py-0.5 px-2 text-xs text-black opacity-80 transition-all duration-300 group-hover:w-0 group-hover:p-0">
<FontAwesomeIcon icon={faCheck} className="mr-2 text-xs" />
Authorized
</div>
<Tooltip content="Revoke Access">
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={async (event) => {
event.stopPropagation();
handlePopUpOpen("deleteConfirmation", {
provider: cloudIntegration.slug
});
}}
className="absolute top-0 right-0 flex h-0 w-12 cursor-pointer items-center justify-center overflow-hidden rounded-r-md bg-red text-xs opacity-50 transition-all duration-300 hover:opacity-100 group-hover:h-full"
>
<FontAwesomeIcon icon={faXmark} size="xl" />
</div>
</Tooltip>
</div>
</div>
)}
</div>
))}
</div>
{isEmpty && (
<div className="mx-6 grid max-w-5xl grid-cols-4 grid-rows-2 gap-4">
{Array.from({ length: 16 }).map((_, index) => (
<div
key={`dummy-cloud-integration-${index + 1}`}
className="h-32 animate-pulse rounded-md border border-mineshaft-600 bg-mineshaft-800"
/>
))}
</div>
)}
<DeleteActionModal
isOpen={popUp.deleteConfirmation.isOpen}
title={`Are you sure want to revoke access ${
(popUp?.deleteConfirmation.data as TRevokeIntegrationPopUp)?.provider || " "
}?`}
subTitle="This will remove all the secret integration of this provider!!!"
onChange={(isOpen) => handlePopUpToggle("deleteConfirmation", isOpen)}
deleteKey={(popUp?.deleteConfirmation?.data as TRevokeIntegrationPopUp)?.provider || ""}
onDeleteApproved={async () => {
onIntegrationRevoke(
(popUp.deleteConfirmation.data as TRevokeIntegrationPopUp)?.provider,
() => handlePopUpClose("deleteConfirmation")
);
}}
/>
</div>
);
};
@@ -0,0 +1 @@
export { CloudIntegrationSection } from "./CloudIntegrationSection";
@@ -0,0 +1,54 @@
import { useTranslation } from "react-i18next";
type Props = {
frameworks: Array<{
name: string;
image: string;
slug: string;
docsLink: string;
}>;
};
export const FrameworkIntegrationSection = ({ frameworks }: Props) => {
const { t } = useTranslation();
return (
<>
<div className="mx-4 mt-12 mb-4 flex flex-col items-start justify-between px-2 text-xl">
<h1 className="text-3xl font-semibold">{t("integrations.framework-integrations")}</h1>
<p className="text-base text-gray-400">{t("integrations.click-to-setup")}</p>
</div>
<div
className="mx-6 mt-4 grid grid-flow-dense gap-3"
style={{ gridTemplateColumns: "repeat(auto-fill, minmax(120px, 1fr))" }}
>
{frameworks.map((framework) => (
<a
key={`framework-integration-${framework.slug}`}
href={framework.docsLink}
rel="noopener noreferrer"
target="_blank"
className="relative flex h-32 cursor-pointer flex-row items-center justify-center rounded-md p-0.5 duration-200"
>
<div
className={`flex h-full w-full cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 font-semibold text-gray-300 duration-200 hover:bg-mineshaft-700 group-hover:text-gray-200 ${
framework?.name?.split(" ").length > 1 ? "px-1 text-sm" : "px-2 text-xl"
} w-full max-w-xs text-center`}
>
{framework?.image && (
<img
src={`/images/integrations/${framework.image}.png`}
height={framework?.name ? 60 : 90}
width={framework?.name ? 60 : 90}
alt="integration logo"
/>
)}
{framework?.name && framework?.image && <div className="h-2" />}
{framework?.name && framework.name}
</div>
</a>
))}
</div>
</>
);
};
@@ -0,0 +1 @@
export { FrameworkIntegrationSection } from "./FrameworkIntegrationSection";
@@ -0,0 +1,149 @@
import { faArrowRight, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { integrationSlugNameMapping } from "public/data/frequentConstants";
import {
DeleteActionModal,
EmptyState,
FormControl,
FormLabel,
IconButton,
Select,
SelectItem,
Skeleton,
Tooltip
} from "@app/components/v2";
import { usePopUp } from "@app/hooks";
import { TIntegration } from "@app/hooks/api/types";
type Props = {
environments: Array<{ name: string; slug: string }>;
integrations?: TIntegration[];
isLoading?: boolean;
onIntegrationDelete: (integration: TIntegration, cb: () => void) => void;
};
export const IntegrationsSection = ({
integrations = [],
environments = [],
isLoading,
onIntegrationDelete
}: Props) => {
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteConfirmation"
] as const);
return (
<div className="mb-8">
<div className="mx-4 mb-4 mt-6 flex flex-col items-start justify-between px-2 text-xl">
<h1 className="text-3xl font-semibold">Current Integrations</h1>
<p className="text-base text-bunker-300">Manage integrations with third-party services.</p>
</div>
{isLoading && (
<div className="p-6 pt-0">
<Skeleton className="h-28" />
</div>
)}
{!isLoading && !integrations.length && (
<EmptyState
className="mx-6 rounded-md border border-mineshaft-700 pt-8 pb-4"
title="No integrations found. Click on one of the below providers to sync secrets."
/>
)}
{!isLoading && (
<div className="flex flex-col space-y-4 p-6 pt-0">
{integrations?.map((integration) => (
<div
className="max-w-8xl flex justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6 pb-2"
key={`integration-${integration?._id.toString()}`}
>
<div className="flex">
<div>
<FormControl label="Environment">
<Select
value={integration.environment}
isDisabled={integration.isActive}
className="min-w-[8rem] border border-mineshaft-700"
>
{environments.map((environment) => {
return (
<SelectItem
value={environment.slug}
key={`environment-${environment.slug}`}
>
{environment.name}
</SelectItem>
);
})}
</Select>
</FormControl>
</div>
<div className="ml-2 flex flex-col">
<FormLabel label="Secret Path" />
<div className="min-w-[8rem] rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{integration.secretPath}
</div>
</div>
<div className="flex h-full items-center">
<FontAwesomeIcon icon={faArrowRight} className="mx-4 text-gray-400" />
</div>
<div className="ml-4 flex flex-col">
<FormLabel label="Integration" />
<div className="min-w-[8rem] rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{integrationSlugNameMapping[integration.integration]}
</div>
</div>
<div className="ml-2 flex flex-col">
<FormLabel label="App" />
<div className="min-w-[8rem] rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{integration.integration === "hashicorp-vault"
? `${integration.app} - path: ${integration.path}`
: integration.app}
</div>
</div>
{(integration.integration === "vercel" ||
integration.integration === "netlify" ||
integration.integration === "railway" ||
integration.integration === "gitlab") && (
<div className="ml-4 flex flex-col">
<FormLabel label="Target Environment" />
<div className="rounded-md border border-mineshaft-700 bg-mineshaft-900 px-3 py-2 font-inter text-sm text-bunker-200">
{integration.targetEnvironment}
</div>
</div>
)}
</div>
<div className="flex cursor-default items-center">
<div className="ml-2 opacity-80 duration-200 hover:opacity-100">
<Tooltip content="Remove Integration">
<IconButton
onClick={() => handlePopUpOpen("deleteConfirmation", integration)}
ariaLabel="delete"
colorSchema="danger"
variant="star"
>
<FontAwesomeIcon icon={faXmark} className="px-0.5" />
</IconButton>
</Tooltip>
</div>
</div>
</div>
))}
</div>
)}
<DeleteActionModal
isOpen={popUp.deleteConfirmation.isOpen}
title={`Are you sure want to remove ${
(popUp?.deleteConfirmation.data as TIntegration)?.integration || " "
} integration for ${(popUp?.deleteConfirmation.data as TIntegration)?.app || " "}?`}
onChange={(isOpen) => handlePopUpToggle("deleteConfirmation", isOpen)}
deleteKey={(popUp?.deleteConfirmation?.data as TIntegration)?.app || ""}
onDeleteApproved={async () =>
onIntegrationDelete(popUp?.deleteConfirmation.data as TIntegration, () =>
handlePopUpClose("deleteConfirmation")
)
}
/>
</div>
);
};
@@ -0,0 +1 @@
export { IntegrationsSection } from "./IntegrationsSection";
@@ -0,0 +1 @@
export { IntegrationsPage } from "./IntegrationsPage";
+1 -1
View File
@@ -18,4 +18,4 @@ version: 0.1.6
# incremented each time you make changes to the application. Versions are not expected to # incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using. # follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes. # It is recommended to use it with quotes.
appVersion: "0.1.6" appVersion: "0.1.7"
@@ -127,7 +127,7 @@ func (r *InfisicalSecretReconciler) GetInfisicalServiceAccountCredentialsFromKub
return model.ServiceAccountDetails{AccessKey: string(accessKeyFromSecret), PrivateKey: string(privateKeyFromSecret), PublicKey: string(publicKeyFromSecret)}, nil return model.ServiceAccountDetails{AccessKey: string(accessKeyFromSecret), PrivateKey: string(privateKeyFromSecret), PublicKey: string(publicKeyFromSecret)}, nil
} }
func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, secretsFromAPI []model.SingleEnvironmentVariable, encryptedSecretsResponse api.GetEncryptedSecretsV2Response) error { func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, secretsFromAPI []model.SingleEnvironmentVariable, encryptedSecretsResponse api.GetEncryptedSecretsV3Response) error {
plainProcessedSecrets := make(map[string][]byte) plainProcessedSecrets := make(map[string][]byte)
for _, secret := range secretsFromAPI { for _, secret := range secretsFromAPI {
plainProcessedSecrets[secret.Key] = []byte(secret.Value) // plain process plainProcessedSecrets[secret.Key] = []byte(secret.Value) // plain process
@@ -155,7 +155,7 @@ func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context
return nil return nil
} }
func (r *InfisicalSecretReconciler) UpdateInfisicalManagedKubeSecret(ctx context.Context, managedKubeSecret corev1.Secret, secretsFromAPI []model.SingleEnvironmentVariable, encryptedSecretsResponse api.GetEncryptedSecretsV2Response) error { func (r *InfisicalSecretReconciler) UpdateInfisicalManagedKubeSecret(ctx context.Context, managedKubeSecret corev1.Secret, secretsFromAPI []model.SingleEnvironmentVariable, encryptedSecretsResponse api.GetEncryptedSecretsV3Response) error {
plainProcessedSecrets := make(map[string][]byte) plainProcessedSecrets := make(map[string][]byte)
for _, secret := range secretsFromAPI { for _, secret := range secretsFromAPI {
plainProcessedSecrets[secret.Key] = []byte(secret.Value) plainProcessedSecrets[secret.Key] = []byte(secret.Value)
@@ -208,7 +208,7 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
} }
var plainTextSecretsFromApi []model.SingleEnvironmentVariable var plainTextSecretsFromApi []model.SingleEnvironmentVariable
var fullEncryptedSecretsResponse api.GetEncryptedSecretsV2Response var fullEncryptedSecretsResponse api.GetEncryptedSecretsV3Response
if serviceAccountCreds.AccessKey != "" || serviceAccountCreds.PrivateKey != "" || serviceAccountCreds.PublicKey != "" { if serviceAccountCreds.AccessKey != "" || serviceAccountCreds.PrivateKey != "" || serviceAccountCreds.PublicKey != "" {
plainTextSecretsFromApi, fullEncryptedSecretsResponse, err = util.GetPlainTextSecretsViaServiceAccount(serviceAccountCreds, infisicalSecret.Spec.Authentication.ServiceAccount.ProjectId, infisicalSecret.Spec.Authentication.ServiceAccount.EnvironmentName, secretVersionBasedOnETag) plainTextSecretsFromApi, fullEncryptedSecretsResponse, err = util.GetPlainTextSecretsViaServiceAccount(serviceAccountCreds, infisicalSecret.Spec.Authentication.ServiceAccount.ProjectId, infisicalSecret.Spec.Authentication.ServiceAccount.EnvironmentName, secretVersionBasedOnETag)
+18 -14
View File
@@ -47,36 +47,40 @@ func CallGetServiceTokenDetailsV2(httpClient *resty.Client) (GetServiceTokenDeta
return tokenDetailsResponse, nil return tokenDetailsResponse, nil
} }
func CallGetSecretsV2(httpClient *resty.Client, request GetEncryptedSecretsV2Request) (GetEncryptedSecretsV2Response, error) { func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Request) (GetEncryptedSecretsV3Response, error) {
var encryptedSecretsResponse GetEncryptedSecretsV2Response var secretsResponse GetEncryptedSecretsV3Response
createHttpRequest := httpClient.
httpRequest := httpClient.
R(). R().
SetResult(&secretsResponse).
SetHeader("User-Agent", USER_AGENT_NAME).
SetHeader("If-None-Match", request.ETag).
SetQueryParam("environment", request.Environment). SetQueryParam("environment", request.Environment).
SetQueryParam("workspaceId", request.WorkspaceId). SetQueryParam("workspaceId", request.WorkspaceId)
SetResult(&encryptedSecretsResponse).
SetHeader("User-Agent", USER_AGENT_NAME)
createHttpRequest.SetHeader("If-None-Match", request.ETag) if request.SecretPath != "" {
httpRequest.SetQueryParam("secretPath", request.SecretPath)
}
response, err := createHttpRequest.Get(fmt.Sprintf("%v/v2/secrets", API_HOST_URL)) response, err := httpRequest.Get(fmt.Sprintf("%v/v3/secrets", API_HOST_URL))
if err != nil { if err != nil {
return GetEncryptedSecretsV2Response{}, fmt.Errorf("CallGetSecretsV2: Unable to complete api request [err=%s]", err) return GetEncryptedSecretsV3Response{}, fmt.Errorf("CallGetSecretsV3: Unable to complete api request [err=%s]", err)
} }
if response.IsError() { if response.IsError() {
return GetEncryptedSecretsV2Response{}, fmt.Errorf("CallGetSecretsV2: Unsuccessful response: [response=%s]", response) return GetEncryptedSecretsV3Response{}, fmt.Errorf("CallGetSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response)
} }
if response.StatusCode() == 304 { if response.StatusCode() == 304 {
encryptedSecretsResponse.Modified = false secretsResponse.Modified = false
} else { } else {
encryptedSecretsResponse.Modified = true secretsResponse.Modified = true
} }
encryptedSecretsResponse.ETag = response.Header().Get("etag") secretsResponse.ETag = response.Header().Get("etag")
return encryptedSecretsResponse, nil return secretsResponse, nil
} }
func CallGetServiceTokenAccountDetailsV2(httpClient *resty.Client) (ServiceAccountDetailsResponse, error) { func CallGetServiceTokenAccountDetailsV2(httpClient *resty.Client) (ServiceAccountDetailsResponse, error) {
+30 -17
View File
@@ -28,30 +28,42 @@ type GetEncryptedWorkspaceKeyResponse struct {
UpdatedAt time.Time `json:"updatedAt"` UpdatedAt time.Time `json:"updatedAt"`
} }
type GetEncryptedSecretsV2Request struct { type GetEncryptedSecretsV3Request struct {
Environment string `json:"environment"` Environment string `json:"environment"`
WorkspaceId string `json:"workspaceId"` WorkspaceId string `json:"workspaceId"`
SecretPath string `json:"secretPath"`
ETag string `json:"etag,omitempty"` ETag string `json:"etag,omitempty"`
} }
type GetEncryptedSecretsV2Response struct { type GetEncryptedSecretsV3Response struct {
Secrets []struct { Secrets []struct {
ID string `json:"_id"` ID string `json:"_id"`
Version int `json:"version"` Version int `json:"version"`
Workspace string `json:"workspace"` Workspace string `json:"workspace"`
Type string `json:"type"` Type string `json:"type"`
Environment string `json:"environment"` Tags []struct {
SecretKeyCiphertext string `json:"secretKeyCiphertext"` ID string `json:"_id"`
SecretKeyIV string `json:"secretKeyIV"` Name string `json:"name"`
SecretKeyTag string `json:"secretKeyTag"` Slug string `json:"slug"`
SecretValueCiphertext string `json:"secretValueCiphertext"` Workspace string `json:"workspace"`
SecretValueIV string `json:"secretValueIV"` } `json:"tags"`
SecretValueTag string `json:"secretValueTag"` Environment string `json:"environment"`
CreatedAt time.Time `json:"createdAt"` SecretKeyCiphertext string `json:"secretKeyCiphertext"`
UpdatedAt time.Time `json:"updatedAt"` SecretKeyIV string `json:"secretKeyIV"`
User string `json:"user,omitempty"` SecretKeyTag string `json:"secretKeyTag"`
SecretValueCiphertext string `json:"secretValueCiphertext"`
SecretValueIV string `json:"secretValueIV"`
SecretValueTag string `json:"secretValueTag"`
SecretCommentCiphertext string `json:"secretCommentCiphertext"`
SecretCommentIV string `json:"secretCommentIV"`
SecretCommentTag string `json:"secretCommentTag"`
Algorithm string `json:"algorithm"`
KeyEncoding string `json:"keyEncoding"`
Folder string `json:"folder"`
V int `json:"__v"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
} `json:"secrets"` } `json:"secrets"`
Modified bool `json:"modified,omitempty"` Modified bool `json:"modified,omitempty"`
ETag string `json:"ETag,omitempty"` ETag string `json:"ETag,omitempty"`
} }
@@ -64,6 +76,7 @@ type GetServiceTokenDetailsResponse struct {
EncryptedKey string `json:"encryptedKey"` EncryptedKey string `json:"encryptedKey"`
Iv string `json:"iv"` Iv string `json:"iv"`
Tag string `json:"tag"` Tag string `json:"tag"`
SecretPath string `json:"secretPath"`
} }
type ServiceAccountDetailsResponse struct { type ServiceAccountDetailsResponse struct {
+21 -20
View File
@@ -48,10 +48,10 @@ func GetServiceTokenDetails(infisicalToken string) (api.GetServiceTokenDetailsRe
return serviceTokenDetails, nil return serviceTokenDetails, nil
} }
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string) ([]model.SingleEnvironmentVariable, api.GetEncryptedSecretsV2Response, error) { func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string) ([]model.SingleEnvironmentVariable, api.GetEncryptedSecretsV3Response, error) {
serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4) serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
if len(serviceTokenParts) < 4 { if len(serviceTokenParts) < 4 {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again") return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
} }
serviceToken := fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2]) serviceToken := fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2])
@@ -63,32 +63,33 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string) ([
serviceTokenDetails, err := api.CallGetServiceTokenDetailsV2(httpClient) serviceTokenDetails, err := api.CallGetServiceTokenDetailsV2(httpClient)
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("unable to get service token details. [err=%v]", err) return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to get service token details. [err=%v]", err)
} }
encryptedSecretsResponse, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{ encryptedSecretsResponse, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{
WorkspaceId: serviceTokenDetails.Workspace, WorkspaceId: serviceTokenDetails.Workspace,
Environment: serviceTokenDetails.Environment, Environment: serviceTokenDetails.Environment,
ETag: etag, ETag: etag,
SecretPath: serviceTokenDetails.SecretPath,
}) })
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, err return nil, api.GetEncryptedSecretsV3Response{}, err
} }
decodedSymmetricEncryptionDetails, err := GetBase64DecodedSymmetricEncryptionDetails(serviceTokenParts[3], serviceTokenDetails.EncryptedKey, serviceTokenDetails.Iv, serviceTokenDetails.Tag) decodedSymmetricEncryptionDetails, err := GetBase64DecodedSymmetricEncryptionDetails(serviceTokenParts[3], serviceTokenDetails.EncryptedKey, serviceTokenDetails.Iv, serviceTokenDetails.Tag)
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("unable to decode symmetric encryption details [err=%v]", err) return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to decode symmetric encryption details [err=%v]", err)
} }
plainTextWorkspaceKey, err := crypto.DecryptSymmetric([]byte(serviceTokenParts[3]), decodedSymmetricEncryptionDetails.Cipher, decodedSymmetricEncryptionDetails.Tag, decodedSymmetricEncryptionDetails.IV) plainTextWorkspaceKey, err := crypto.DecryptSymmetric([]byte(serviceTokenParts[3]), decodedSymmetricEncryptionDetails.Cipher, decodedSymmetricEncryptionDetails.Tag, decodedSymmetricEncryptionDetails.IV)
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("unable to decrypt the required workspace key") return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to decrypt the required workspace key")
} }
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse) plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse)
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err) return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err)
} }
return plainTextSecrets, encryptedSecretsResponse, nil return plainTextSecrets, encryptedSecretsResponse, nil
@@ -97,19 +98,19 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string) ([
// Fetches plaintext secrets from an API endpoint using a service account. // Fetches plaintext secrets from an API endpoint using a service account.
// The function fetches the service account details and keys, decrypts the workspace key, fetches the encrypted secrets for the specified project and environment, and decrypts the secrets using the decrypted workspace key. // The function fetches the service account details and keys, decrypts the workspace key, fetches the encrypted secrets for the specified project and environment, and decrypts the secrets using the decrypted workspace key.
// Returns the plaintext secrets, encrypted secrets response, and any errors that occurred during the process. // Returns the plaintext secrets, encrypted secrets response, and any errors that occurred during the process.
func GetPlainTextSecretsViaServiceAccount(serviceAccountCreds model.ServiceAccountDetails, projectId string, environmentName string, etag string) ([]model.SingleEnvironmentVariable, api.GetEncryptedSecretsV2Response, error) { func GetPlainTextSecretsViaServiceAccount(serviceAccountCreds model.ServiceAccountDetails, projectId string, environmentName string, etag string) ([]model.SingleEnvironmentVariable, api.GetEncryptedSecretsV3Response, error) {
httpClient := resty.New() httpClient := resty.New()
httpClient.SetAuthToken(serviceAccountCreds.AccessKey). httpClient.SetAuthToken(serviceAccountCreds.AccessKey).
SetHeader("Accept", "application/json") SetHeader("Accept", "application/json")
serviceAccountDetails, err := api.CallGetServiceTokenAccountDetailsV2(httpClient) serviceAccountDetails, err := api.CallGetServiceTokenAccountDetailsV2(httpClient)
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account details. [err=%v]", err) return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account details. [err=%v]", err)
} }
serviceAccountKeys, err := api.CallGetServiceAccountKeysV2(httpClient, api.GetServiceAccountKeysRequest{ServiceAccountId: serviceAccountDetails.ServiceAccount.ID}) serviceAccountKeys, err := api.CallGetServiceAccountKeysV2(httpClient, api.GetServiceAccountKeysRequest{ServiceAccountId: serviceAccountDetails.ServiceAccount.ID})
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account key details. [err=%v]", err) return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account key details. [err=%v]", err)
} }
// find key for requested project // find key for requested project
@@ -121,45 +122,45 @@ func GetPlainTextSecretsViaServiceAccount(serviceAccountCreds model.ServiceAccou
} }
if workspaceServiceAccountKey.ID == "" || workspaceServiceAccountKey.EncryptedKey == "" || workspaceServiceAccountKey.Nonce == "" || serviceAccountCreds.PublicKey == "" || serviceAccountCreds.PrivateKey == "" { if workspaceServiceAccountKey.ID == "" || workspaceServiceAccountKey.EncryptedKey == "" || workspaceServiceAccountKey.Nonce == "" || serviceAccountCreds.PublicKey == "" || serviceAccountCreds.PrivateKey == "" {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("unable to find key for [projectId=%s] [err=%v]. Ensure that the given service account has access to given projectId", projectId, err) return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to find key for [projectId=%s] [err=%v]. Ensure that the given service account has access to given projectId", projectId, err)
} }
cipherText, err := base64.StdEncoding.DecodeString(workspaceServiceAccountKey.EncryptedKey) cipherText, err := base64.StdEncoding.DecodeString(workspaceServiceAccountKey.EncryptedKey)
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode EncryptedKey secrets because [err=%v]", err) return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode EncryptedKey secrets because [err=%v]", err)
} }
nonce, err := base64.StdEncoding.DecodeString(workspaceServiceAccountKey.Nonce) nonce, err := base64.StdEncoding.DecodeString(workspaceServiceAccountKey.Nonce)
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode nonce secrets because [err=%v]", err) return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode nonce secrets because [err=%v]", err)
} }
publickey, err := base64.StdEncoding.DecodeString(serviceAccountCreds.PublicKey) publickey, err := base64.StdEncoding.DecodeString(serviceAccountCreds.PublicKey)
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode PublicKey secrets because [err=%v]", err) return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode PublicKey secrets because [err=%v]", err)
} }
privateKey, err := base64.StdEncoding.DecodeString(serviceAccountCreds.PrivateKey) privateKey, err := base64.StdEncoding.DecodeString(serviceAccountCreds.PrivateKey)
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode PrivateKey secrets because [err=%v]", err) return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to decode PrivateKey secrets because [err=%v]", err)
} }
plainTextWorkspaceKey := crypto.DecryptAsymmetric(cipherText, nonce, publickey, privateKey) plainTextWorkspaceKey := crypto.DecryptAsymmetric(cipherText, nonce, publickey, privateKey)
encryptedSecretsResponse, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{ encryptedSecretsResponse, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{
WorkspaceId: projectId, WorkspaceId: projectId,
Environment: environmentName, Environment: environmentName,
ETag: etag, ETag: etag,
}) })
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("unable to fetch secrets because [err=%v]", err) return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("unable to fetch secrets because [err=%v]", err)
} }
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse) plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse)
if err != nil { if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get plain text secrets because [err=%v]", err) return nil, api.GetEncryptedSecretsV3Response{}, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get plain text secrets because [err=%v]", err)
} }
return plainTextSecrets, encryptedSecretsResponse, nil return plainTextSecrets, encryptedSecretsResponse, nil
@@ -194,7 +195,7 @@ func GetBase64DecodedSymmetricEncryptionDetails(key string, cipher string, IV st
}, nil }, nil
} }
func GetPlainTextSecrets(key []byte, encryptedSecretsResponse api.GetEncryptedSecretsV2Response) ([]model.SingleEnvironmentVariable, error) { func GetPlainTextSecrets(key []byte, encryptedSecretsResponse api.GetEncryptedSecretsV3Response) ([]model.SingleEnvironmentVariable, error) {
plainTextSecrets := []model.SingleEnvironmentVariable{} plainTextSecrets := []model.SingleEnvironmentVariable{}
for _, secret := range encryptedSecretsResponse.Secrets { for _, secret := range encryptedSecretsResponse.Secrets {
// Decrypt key // Decrypt key