mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Fix merge conflicts
This commit is contained in:
@@ -53,7 +53,7 @@ export const getServiceTokenData = async (req: Request, res: Response) => {
|
||||
req.authData.authPayload._id
|
||||
)
|
||||
.select("+encryptedKey +iv +tag")
|
||||
.populate("user");
|
||||
.populate("user").lean();
|
||||
|
||||
return res.status(200).json(serviceTokenData);
|
||||
};
|
||||
|
||||
@@ -188,7 +188,7 @@ type GetServiceTokenDetailsResponse struct {
|
||||
Tag string `json:"tag"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
V int `json:"__v"`
|
||||
SecretPath string `json:"secretPath"`
|
||||
}
|
||||
|
||||
type GetAccessibleEnvironmentsRequest struct {
|
||||
|
||||
@@ -37,6 +37,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string) ([]models.Singl
|
||||
encryptedSecrets, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{
|
||||
WorkspaceId: serviceTokenDetails.Workspace,
|
||||
Environment: serviceTokenDetails.Environment,
|
||||
SecretPath: serviceTokenDetails.SecretPath,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -6,7 +6,14 @@ module.exports = {
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions',
|
||||
'storybook-dark-mode',
|
||||
'@storybook/addon-postcss'
|
||||
{
|
||||
name: '@storybook/addon-styling',
|
||||
options: {
|
||||
postCss: {
|
||||
implementation: require('postcss')
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/nextjs',
|
||||
|
||||
7944
frontend/package-lock.json
generated
7944
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@
|
||||
"start": "next start",
|
||||
"start:docker": "next build && next start",
|
||||
"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",
|
||||
"storybook": "storybook dev -p 6006 -s ./public",
|
||||
"build-storybook": "storybook build"
|
||||
@@ -62,8 +62,8 @@
|
||||
"infisical-node": "^1.0.37",
|
||||
"jspdf": "^2.5.1",
|
||||
"jsrp": "^0.2.4",
|
||||
"lottie-react": "^2.4.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"lottie-react": "^2.4.0",
|
||||
"markdown-it": "^13.0.1",
|
||||
"next": "^12.3.4",
|
||||
"posthog-js": "^1.58.0",
|
||||
@@ -91,14 +91,14 @@
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-essentials": "^7.0.0-beta.30",
|
||||
"@storybook/addon-interactions": "^7.0.0-beta.30",
|
||||
"@storybook/addon-links": "^7.0.0-beta.30",
|
||||
"@storybook/addon-postcss": "^2.0.0",
|
||||
"@storybook/blocks": "^7.0.0-beta.30",
|
||||
"@storybook/nextjs": "^7.0.0-beta.30",
|
||||
"@storybook/react": "^7.0.0-beta.30",
|
||||
"@storybook/testing-library": "^0.0.13",
|
||||
"@storybook/addon-essentials": "^7.0.23",
|
||||
"@storybook/addon-interactions": "^7.0.23",
|
||||
"@storybook/addon-links": "^7.0.23",
|
||||
"@storybook/addon-styling": "^1.3.0",
|
||||
"@storybook/blocks": "^7.0.23",
|
||||
"@storybook/nextjs": "^7.0.23",
|
||||
"@storybook/react": "^7.0.23",
|
||||
"@storybook/testing-library": "^0.2.0",
|
||||
"@tailwindcss/typography": "^0.5.4",
|
||||
"@types/jsrp": "^0.2.4",
|
||||
"@types/node": "18.11.9",
|
||||
@@ -118,12 +118,12 @@
|
||||
"eslint-plugin-react": "^7.32.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.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",
|
||||
"prettier": "^2.8.3",
|
||||
"prettier-plugin-tailwindcss": "^0.2.2",
|
||||
"storybook": "^7.0.0-beta.30",
|
||||
"storybook-dark-mode": "^2.0.5",
|
||||
"storybook": "^7.0.23",
|
||||
"storybook-dark-mode": "^3.0.0",
|
||||
"tailwindcss": "3.2",
|
||||
"typescript": "^4.9.3"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ type Props = {
|
||||
onChange?: (isOpen: boolean) => void;
|
||||
deleteKey: string;
|
||||
title: string;
|
||||
subTitle?: string;
|
||||
onDeleteApproved: () => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -22,7 +23,8 @@ export const DeleteActionModal = ({
|
||||
onChange,
|
||||
deleteKey,
|
||||
onDeleteApproved,
|
||||
title
|
||||
title,
|
||||
subTitle = "This action is irreversible!"
|
||||
}: Props): JSX.Element => {
|
||||
const [inputData, setInputData] = useState("");
|
||||
const [isLoading, setIsLoading] = useToggle();
|
||||
@@ -52,7 +54,7 @@ export const DeleteActionModal = ({
|
||||
>
|
||||
<ModalContent
|
||||
title={title}
|
||||
subTitle="This action is irreversible!"
|
||||
subTitle={subTitle}
|
||||
footerContent={
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
|
||||
@@ -11,7 +11,7 @@ export type 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}
|
||||
{isRequired && <span className="ml-1 text-red">*</span>}
|
||||
</Label.Root>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export {
|
||||
useGetAuthToken,
|
||||
useGetCommonPasswords,
|
||||
useSendMfaToken,
|
||||
useVerifyMfaToken} from "./queries"
|
||||
useGetAuthToken,
|
||||
useGetCommonPasswords,
|
||||
useSendMfaToken,
|
||||
useVerifyMfaToken
|
||||
} from "./queries"
|
||||
|
||||
1
frontend/src/hooks/api/bots/index.tsx
Normal file
1
frontend/src/hooks/api/bots/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { useGetWorkspaceBot, useUpdateBotActiveStatus } from "./queries";
|
||||
38
frontend/src/hooks/api/bots/queries.tsx
Normal file
38
frontend/src/hooks/api/bots/queries.tsx
Normal 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
frontend/src/hooks/api/bots/types.ts
Normal file
20
frontend/src/hooks/api/bots/types.ts
Normal 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;
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,8 @@
|
||||
export * from "./auth";
|
||||
export * from "./bots";
|
||||
export * from "./incidentContacts";
|
||||
export * from "./integrationAuth";
|
||||
export * from "./integrations";
|
||||
export * from "./keys";
|
||||
export * from "./organization";
|
||||
export * from "./secretFolders";
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
export {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById,
|
||||
useGetIntegrationAuthRailwayEnvironments,
|
||||
useGetIntegrationAuthRailwayServices,
|
||||
useGetIntegrationAuthTeams,
|
||||
useGetIntegrationAuthVercelBranches} from "./queries";
|
||||
useDeleteIntegrationAuth,
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById,
|
||||
useGetIntegrationAuthRailwayEnvironments,
|
||||
useGetIntegrationAuthRailwayServices,
|
||||
useGetIntegrationAuthTeams,
|
||||
useGetIntegrationAuthVercelBranches
|
||||
} from "./queries";
|
||||
|
||||
@@ -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 {
|
||||
App,
|
||||
Environment,
|
||||
IntegrationAuth,
|
||||
Service,
|
||||
Team
|
||||
} from "./types";
|
||||
import { workspaceKeys } from "../workspace/queries";
|
||||
import { App, Environment, IntegrationAuth, Service, Team } from "./types";
|
||||
|
||||
const integrationAuthKeys = {
|
||||
getIntegrationAuthById: (integrationAuthId: string) => [{ integrationAuthId }, "integrationAuth"] as const,
|
||||
getIntegrationAuthApps: (integrationAuthId: string, teamId?: string) => [{ integrationAuthId, teamId }, "integrationAuthApps"] as const,
|
||||
getIntegrationAuthTeams: (integrationAuthId: string) => [{ integrationAuthId }, "integrationAuthTeams"] as const,
|
||||
getIntegrationAuthVercelBranches: ({
|
||||
integrationAuthId,
|
||||
appId,
|
||||
}: {
|
||||
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 ({
|
||||
getIntegrationAuthById: (integrationAuthId: string) =>
|
||||
[{ integrationAuthId }, "integrationAuth"] as const,
|
||||
getIntegrationAuthApps: (integrationAuthId: string, teamId?: string) =>
|
||||
[{ integrationAuthId, teamId }, "integrationAuthApps"] as const,
|
||||
getIntegrationAuthTeams: (integrationAuthId: string) =>
|
||||
[{ integrationAuthId }, "integrationAuthTeams"] as const,
|
||||
getIntegrationAuthVercelBranches: ({
|
||||
integrationAuthId,
|
||||
appId
|
||||
}: {
|
||||
}: {
|
||||
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 { data: { branches } } = await apiRequest.get<{ branches: string[] }>(`/api/v1/integration-auth/${integrationAuthId}/vercel/branches`, {
|
||||
params: {
|
||||
appId
|
||||
}
|
||||
});
|
||||
|
||||
return branches;
|
||||
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 ({
|
||||
integrationAuthId,
|
||||
appId
|
||||
integrationAuthId,
|
||||
appId
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
}) => {
|
||||
const { data: { environments } } = await apiRequest.get<{ environments: Environment[] }>(`/api/v1/integration-auth/${integrationAuthId}/railway/environments`, {
|
||||
params: {
|
||||
appId
|
||||
}
|
||||
});
|
||||
|
||||
return environments;
|
||||
}
|
||||
const {
|
||||
data: { environments }
|
||||
} = await apiRequest.get<{ environments: Environment[] }>(
|
||||
`/api/v1/integration-auth/${integrationAuthId}/railway/environments`,
|
||||
{
|
||||
params: {
|
||||
appId
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return environments;
|
||||
};
|
||||
|
||||
const fetchIntegrationAuthRailwayServices = async ({
|
||||
integrationAuthId,
|
||||
appId
|
||||
integrationAuthId,
|
||||
appId
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
}) => {
|
||||
const { data: { services } } = await apiRequest.get<{ services: Service[] }>(`/api/v1/integration-auth/${integrationAuthId}/railway/services`, {
|
||||
params: {
|
||||
appId
|
||||
}
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
const {
|
||||
data: { services }
|
||||
} = await apiRequest.get<{ services: Service[] }>(
|
||||
`/api/v1/integration-auth/${integrationAuthId}/railway/services`,
|
||||
{
|
||||
params: {
|
||||
appId
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return services;
|
||||
};
|
||||
|
||||
export const useGetIntegrationAuthById = (integrationAuthId: string) => {
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
|
||||
queryFn: () => fetchIntegrationAuthById(integrationAuthId),
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
|
||||
queryFn: () => fetchIntegrationAuthById(integrationAuthId),
|
||||
enabled: true
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIntegrationAuthApps = ({
|
||||
integrationAuthId,
|
||||
teamId
|
||||
integrationAuthId,
|
||||
teamId
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
teamId?: string;
|
||||
integrationAuthId: string;
|
||||
teamId?: string;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId, teamId),
|
||||
queryFn: () => fetchIntegrationAuthApps({
|
||||
integrationAuthId,
|
||||
teamId
|
||||
}),
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId, teamId),
|
||||
queryFn: () =>
|
||||
fetchIntegrationAuthApps({
|
||||
integrationAuthId,
|
||||
teamId
|
||||
}),
|
||||
enabled: true
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIntegrationAuthTeams = (integrationAuthId: string) => {
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthTeams(integrationAuthId),
|
||||
queryFn: () => fetchIntegrationAuthTeams(integrationAuthId),
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthTeams(integrationAuthId),
|
||||
queryFn: () => fetchIntegrationAuthTeams(integrationAuthId),
|
||||
enabled: true
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIntegrationAuthVercelBranches = ({
|
||||
integrationAuthId,
|
||||
appId,
|
||||
integrationAuthId,
|
||||
appId
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthVercelBranches({
|
||||
integrationAuthId,
|
||||
appId,
|
||||
}),
|
||||
queryFn: () => fetchIntegrationAuthVercelBranches({
|
||||
integrationAuthId,
|
||||
appId,
|
||||
}),
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthVercelBranches({
|
||||
integrationAuthId,
|
||||
appId
|
||||
}),
|
||||
queryFn: () =>
|
||||
fetchIntegrationAuthVercelBranches({
|
||||
integrationAuthId,
|
||||
appId
|
||||
}),
|
||||
enabled: true
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIntegrationAuthRailwayEnvironments = ({
|
||||
integrationAuthId,
|
||||
appId
|
||||
integrationAuthId,
|
||||
appId
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthRailwayEnvironments({
|
||||
integrationAuthId,
|
||||
appId,
|
||||
}),
|
||||
queryFn: () => fetchIntegrationAuthRailwayEnvironments({
|
||||
integrationAuthId,
|
||||
appId,
|
||||
}),
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthRailwayEnvironments({
|
||||
integrationAuthId,
|
||||
appId
|
||||
}),
|
||||
queryFn: () =>
|
||||
fetchIntegrationAuthRailwayEnvironments({
|
||||
integrationAuthId,
|
||||
appId
|
||||
}),
|
||||
enabled: true
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIntegrationAuthRailwayServices = ({
|
||||
integrationAuthId,
|
||||
appId
|
||||
integrationAuthId,
|
||||
appId
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthRailwayServices({
|
||||
integrationAuthId,
|
||||
appId,
|
||||
}),
|
||||
queryFn: () => fetchIntegrationAuthRailwayServices({
|
||||
integrationAuthId,
|
||||
appId,
|
||||
}),
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthRailwayServices({
|
||||
integrationAuthId,
|
||||
appId
|
||||
}),
|
||||
queryFn: () =>
|
||||
fetchIntegrationAuthRailwayServices({
|
||||
integrationAuthId,
|
||||
appId
|
||||
}),
|
||||
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));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
export type IntegrationAuth = {
|
||||
_id: string;
|
||||
workspace: string;
|
||||
integration: string;
|
||||
teamId?: string;
|
||||
accountId?: string;
|
||||
}
|
||||
_id: string;
|
||||
integration: string;
|
||||
workspace: string;
|
||||
__v: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
algorithm: string;
|
||||
keyEncoding: string;
|
||||
};
|
||||
|
||||
export type App = {
|
||||
name: string;
|
||||
appId?: string;
|
||||
owner?: string;
|
||||
}
|
||||
name: string;
|
||||
appId?: string;
|
||||
owner?: string;
|
||||
};
|
||||
|
||||
export type Team = {
|
||||
name: string;
|
||||
teamId: string;
|
||||
}
|
||||
name: string;
|
||||
teamId: string;
|
||||
};
|
||||
|
||||
export type Environment = {
|
||||
name: string;
|
||||
environmentId: string;
|
||||
}
|
||||
name: string;
|
||||
environmentId: string;
|
||||
};
|
||||
|
||||
export type Service = {
|
||||
name: string;
|
||||
serviceId: string;
|
||||
}
|
||||
name: string;
|
||||
serviceId: string;
|
||||
};
|
||||
|
||||
1
frontend/src/hooks/api/integrations/index.tsx
Normal file
1
frontend/src/hooks/api/integrations/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { useDeleteIntegration,useGetCloudIntegrations } from "./queries";
|
||||
35
frontend/src/hooks/api/integrations/queries.tsx
Normal file
35
frontend/src/hooks/api/integrations/queries.tsx
Normal file
@@ -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));
|
||||
}
|
||||
});
|
||||
};
|
||||
33
frontend/src/hooks/api/integrations/types.ts
Normal file
33
frontend/src/hooks/api/integrations/types.ts
Normal file
@@ -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;
|
||||
};
|
||||
@@ -1,5 +1,7 @@
|
||||
export type { GetAuthTokenAPI } from "./auth/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 { Organization } from "./organization/types";
|
||||
export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types";
|
||||
|
||||
@@ -6,8 +6,10 @@ export {
|
||||
useGetUserWorkspaceMemberships,
|
||||
useGetUserWorkspaces,
|
||||
useGetUserWsEnvironments,
|
||||
useGetWorkspaceAuthorizations,
|
||||
useGetWorkspaceById,
|
||||
useGetWorkspaceIndexStatus,
|
||||
useGetWorkspaceIntegrations,
|
||||
useGetWorkspaceSecrets,
|
||||
useNameWorkspaceSecrets,
|
||||
useRenameWorkspace,
|
||||
|
||||
@@ -2,9 +2,9 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import {
|
||||
EncryptedSecret
|
||||
} from "../secrets/types";
|
||||
import { IntegrationAuth } from "../integrationAuth/types";
|
||||
import { TIntegration } from "../integrations/types";
|
||||
import { EncryptedSecret } from "../secrets/types";
|
||||
import {
|
||||
CreateEnvironmentDTO,
|
||||
CreateWorkspaceDTO,
|
||||
@@ -19,11 +19,14 @@ import {
|
||||
WorkspaceEnv
|
||||
} from "./types";
|
||||
|
||||
const workspaceKeys = {
|
||||
export const workspaceKeys = {
|
||||
getWorkspaceById: (workspaceId: string) => [{ workspaceId }, "workspace"] 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"],
|
||||
getWorkspaceAuthorization: (workspaceId: string) => [{ workspaceId }, "workspace-authorizations"],
|
||||
getWorkspaceIntegrations: (workspaceId: string) => [{ workspaceId }, "workspace-integrations"],
|
||||
getAllUserWorkspace: ["workspaces"] as const,
|
||||
getUserWsEnvironments: (workspaceId: string) => ["workspace-env", { workspaceId }] as const
|
||||
};
|
||||
@@ -42,15 +45,17 @@ const fetchWorkspaceIndexStatus = async (workspaceId: string) => {
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
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`
|
||||
);
|
||||
|
||||
|
||||
return secrets;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchUserWorkspaces = async () => {
|
||||
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace");
|
||||
@@ -63,15 +68,15 @@ export const useGetWorkspaceIndexStatus = (workspaceId: string) => {
|
||||
queryFn: () => fetchWorkspaceIndexStatus(workspaceId),
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const useGetWorkspaceSecrets = (workspaceId: string) => {
|
||||
return useQuery({
|
||||
queryKey: workspaceKeys.getWorkspaceSecrets(workspaceId),
|
||||
queryFn: () => fetchWorkspaceSecrets(workspaceId),
|
||||
enabled: true
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetWorkspaceById = (workspaceId: string) => {
|
||||
return useQuery({
|
||||
@@ -126,7 +131,39 @@ export const useNameWorkspaceSecrets = () => {
|
||||
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
|
||||
export const useCreateWorkspace = () => {
|
||||
|
||||
@@ -1,409 +1,18 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import frameworkIntegrationOptions from "public/json/frameworkIntegrations.json";
|
||||
|
||||
import ActivateBotDialog from "@app/components/basic/dialog/ActivateBotDialog";
|
||||
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 { IntegrationsPage } from "@app/views/IntegrationsPage";
|
||||
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
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;
|
||||
type Props = {
|
||||
frameworkIntegrations: typeof frameworkIntegrationOptions;
|
||||
};
|
||||
|
||||
const Integration = ({ frameworkIntegrations }: Props) => {
|
||||
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 (
|
||||
<div className="flex max-h-full flex-col justify-between bg-bunker-800 text-white">
|
||||
<>
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("integrations.title") })}</title>
|
||||
<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 name="og:description" content={t("integrations.description") as string} />
|
||||
</Head>
|
||||
<div className="no-scrollbar::-webkit-scrollbar h-screen max-h-[calc(100vh-10px)] w-full overflow-y-scroll pb-6 no-scrollbar">
|
||||
<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>
|
||||
<IntegrationsPage frameworkIntegrations={frameworkIntegrations} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
</CardTitle>
|
||||
<FormControl label="Infisical Project Environment" className="mt-2 px-6">
|
||||
<FormControl label="Infisical Project Environment" className="mt-4 px-6">
|
||||
<Select
|
||||
value={selectedSourceEnvironment}
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
@@ -114,7 +114,7 @@ export default function ChecklyCreateIntegrationPage() {
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Secrets Path">
|
||||
<FormControl label="Secrets Path" className="mt-4 px-6">
|
||||
<Input
|
||||
value={secretPath}
|
||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||
|
||||
105
frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx
Normal file
105
frontend/src/views/IntegrationsPage/IntegrationPage.utils.tsx
Normal file
@@ -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}`;
|
||||
224
frontend/src/views/IntegrationsPage/IntegrationsPage.tsx
Normal file
224
frontend/src/views/IntegrationsPage/IntegrationsPage.tsx
Normal file
@@ -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";
|
||||
1
frontend/src/views/IntegrationsPage/index.tsx
Normal file
1
frontend/src/views/IntegrationsPage/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { IntegrationsPage } from "./IntegrationsPage";
|
||||
@@ -18,4 +18,4 @@ version: 0.1.6
|
||||
# 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.
|
||||
# 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
|
||||
}
|
||||
|
||||
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)
|
||||
for _, secret := range secretsFromAPI {
|
||||
plainProcessedSecrets[secret.Key] = []byte(secret.Value) // plain process
|
||||
@@ -155,7 +155,7 @@ func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context
|
||||
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)
|
||||
for _, secret := range secretsFromAPI {
|
||||
plainProcessedSecrets[secret.Key] = []byte(secret.Value)
|
||||
@@ -208,7 +208,7 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
|
||||
}
|
||||
|
||||
var plainTextSecretsFromApi []model.SingleEnvironmentVariable
|
||||
var fullEncryptedSecretsResponse api.GetEncryptedSecretsV2Response
|
||||
var fullEncryptedSecretsResponse api.GetEncryptedSecretsV3Response
|
||||
|
||||
if serviceAccountCreds.AccessKey != "" || serviceAccountCreds.PrivateKey != "" || serviceAccountCreds.PublicKey != "" {
|
||||
plainTextSecretsFromApi, fullEncryptedSecretsResponse, err = util.GetPlainTextSecretsViaServiceAccount(serviceAccountCreds, infisicalSecret.Spec.Authentication.ServiceAccount.ProjectId, infisicalSecret.Spec.Authentication.ServiceAccount.EnvironmentName, secretVersionBasedOnETag)
|
||||
|
||||
@@ -47,36 +47,40 @@ func CallGetServiceTokenDetailsV2(httpClient *resty.Client) (GetServiceTokenDeta
|
||||
return tokenDetailsResponse, nil
|
||||
}
|
||||
|
||||
func CallGetSecretsV2(httpClient *resty.Client, request GetEncryptedSecretsV2Request) (GetEncryptedSecretsV2Response, error) {
|
||||
var encryptedSecretsResponse GetEncryptedSecretsV2Response
|
||||
createHttpRequest := httpClient.
|
||||
func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Request) (GetEncryptedSecretsV3Response, error) {
|
||||
var secretsResponse GetEncryptedSecretsV3Response
|
||||
|
||||
httpRequest := httpClient.
|
||||
R().
|
||||
SetResult(&secretsResponse).
|
||||
SetHeader("User-Agent", USER_AGENT_NAME).
|
||||
SetHeader("If-None-Match", request.ETag).
|
||||
SetQueryParam("environment", request.Environment).
|
||||
SetQueryParam("workspaceId", request.WorkspaceId).
|
||||
SetResult(&encryptedSecretsResponse).
|
||||
SetHeader("User-Agent", USER_AGENT_NAME)
|
||||
SetQueryParam("workspaceId", request.WorkspaceId)
|
||||
|
||||
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 {
|
||||
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() {
|
||||
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 {
|
||||
encryptedSecretsResponse.Modified = false
|
||||
secretsResponse.Modified = false
|
||||
} 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) {
|
||||
|
||||
@@ -28,30 +28,42 @@ type GetEncryptedWorkspaceKeyResponse struct {
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type GetEncryptedSecretsV2Request struct {
|
||||
type GetEncryptedSecretsV3Request struct {
|
||||
Environment string `json:"environment"`
|
||||
WorkspaceId string `json:"workspaceId"`
|
||||
SecretPath string `json:"secretPath"`
|
||||
ETag string `json:"etag,omitempty"`
|
||||
}
|
||||
|
||||
type GetEncryptedSecretsV2Response struct {
|
||||
type GetEncryptedSecretsV3Response struct {
|
||||
Secrets []struct {
|
||||
ID string `json:"_id"`
|
||||
Version int `json:"version"`
|
||||
Workspace string `json:"workspace"`
|
||||
Type string `json:"type"`
|
||||
Environment string `json:"environment"`
|
||||
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
|
||||
SecretKeyIV string `json:"secretKeyIV"`
|
||||
SecretKeyTag string `json:"secretKeyTag"`
|
||||
SecretValueCiphertext string `json:"secretValueCiphertext"`
|
||||
SecretValueIV string `json:"secretValueIV"`
|
||||
SecretValueTag string `json:"secretValueTag"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
User string `json:"user,omitempty"`
|
||||
ID string `json:"_id"`
|
||||
Version int `json:"version"`
|
||||
Workspace string `json:"workspace"`
|
||||
Type string `json:"type"`
|
||||
Tags []struct {
|
||||
ID string `json:"_id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Workspace string `json:"workspace"`
|
||||
} `json:"tags"`
|
||||
Environment string `json:"environment"`
|
||||
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
|
||||
SecretKeyIV string `json:"secretKeyIV"`
|
||||
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"`
|
||||
|
||||
Modified bool `json:"modified,omitempty"`
|
||||
ETag string `json:"ETag,omitempty"`
|
||||
}
|
||||
@@ -64,6 +76,7 @@ type GetServiceTokenDetailsResponse struct {
|
||||
EncryptedKey string `json:"encryptedKey"`
|
||||
Iv string `json:"iv"`
|
||||
Tag string `json:"tag"`
|
||||
SecretPath string `json:"secretPath"`
|
||||
}
|
||||
|
||||
type ServiceAccountDetailsResponse struct {
|
||||
|
||||
@@ -48,10 +48,10 @@ func GetServiceTokenDetails(infisicalToken string) (api.GetServiceTokenDetailsRe
|
||||
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)
|
||||
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])
|
||||
@@ -63,32 +63,33 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string) ([
|
||||
|
||||
serviceTokenDetails, err := api.CallGetServiceTokenDetailsV2(httpClient)
|
||||
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,
|
||||
Environment: serviceTokenDetails.Environment,
|
||||
ETag: etag,
|
||||
SecretPath: serviceTokenDetails.SecretPath,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, api.GetEncryptedSecretsV2Response{}, err
|
||||
return nil, api.GetEncryptedSecretsV3Response{}, err
|
||||
}
|
||||
|
||||
decodedSymmetricEncryptionDetails, err := GetBase64DecodedSymmetricEncryptionDetails(serviceTokenParts[3], serviceTokenDetails.EncryptedKey, serviceTokenDetails.Iv, serviceTokenDetails.Tag)
|
||||
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)
|
||||
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)
|
||||
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
|
||||
@@ -97,19 +98,19 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string) ([
|
||||
// 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.
|
||||
// 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.SetAuthToken(serviceAccountCreds.AccessKey).
|
||||
SetHeader("Accept", "application/json")
|
||||
|
||||
serviceAccountDetails, err := api.CallGetServiceTokenAccountDetailsV2(httpClient)
|
||||
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})
|
||||
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
|
||||
@@ -121,45 +122,45 @@ func GetPlainTextSecretsViaServiceAccount(serviceAccountCreds model.ServiceAccou
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
encryptedSecretsResponse, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{
|
||||
encryptedSecretsResponse, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{
|
||||
WorkspaceId: projectId,
|
||||
Environment: environmentName,
|
||||
ETag: etag,
|
||||
})
|
||||
|
||||
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)
|
||||
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
|
||||
@@ -194,7 +195,7 @@ func GetBase64DecodedSymmetricEncryptionDetails(key string, cipher string, IV st
|
||||
}, nil
|
||||
}
|
||||
|
||||
func GetPlainTextSecrets(key []byte, encryptedSecretsResponse api.GetEncryptedSecretsV2Response) ([]model.SingleEnvironmentVariable, error) {
|
||||
func GetPlainTextSecrets(key []byte, encryptedSecretsResponse api.GetEncryptedSecretsV3Response) ([]model.SingleEnvironmentVariable, error) {
|
||||
plainTextSecrets := []model.SingleEnvironmentVariable{}
|
||||
for _, secret := range encryptedSecretsResponse.Secrets {
|
||||
// Decrypt key
|
||||
|
||||
Reference in New Issue
Block a user