Merge branch 'Infisical:main' into feat/create-multiple-orgs-under-same-account

This commit is contained in:
Ronit Panda
2023-10-24 05:42:31 +05:30
committed by GitHub
126 changed files with 2887 additions and 1714 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ export const initPostHog = () => {
try {
if (typeof window !== "undefined") {
// @ts-ignore
if (ENV === "production" && TELEMETRY_CAPTURING_ENABLED) {
if (ENV === "production" && TELEMETRY_CAPTURING_ENABLED === "true") {
posthog.init(POSTHOG_API_KEY, {
api_host: POSTHOG_HOST
});
@@ -13,7 +13,7 @@ class Capturer {
}
capture(item: string) {
if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED) {
if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED === "true") {
try {
this.api.capture(item);
} catch (error) {
@@ -23,7 +23,7 @@ class Capturer {
}
identify(id: string, email?: string) {
if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED) {
if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED === "true") {
try {
this.api.identify(id, {
email: email
@@ -0,0 +1,61 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import type { Meta, StoryObj } from "@storybook/react";
import { Alert, AlertDescription } from "./Alert";
const meta: Meta<typeof Alert> = {
title: "Components/Alert",
component: Alert,
tags: ["v2"]
};
export default meta;
type Story = StoryObj<typeof Alert>;
const ExampleComponent = () => <AlertDescription>this is a description</AlertDescription>;
export const Default: Story = {
args: {
children: <ExampleComponent />
}
};
export const Warning: Story = {
args: {
children: <ExampleComponent />,
variant: "warning"
}
};
export const Danger: Story = {
args: {
children: <ExampleComponent />,
variant: "danger"
}
};
export const WithCustomIcon: Story = {
args: {
children: <ExampleComponent />,
variant: "warning",
icon: <FontAwesomeIcon icon={faPlus} />
}
};
export const WithOutIcon: Story = {
args: {
children: <ExampleComponent />,
variant: "warning",
icon: null
}
};
export const WithOutTitle: Story = {
args: {
children: <ExampleComponent />,
variant: "warning",
hideTitle: true
}
};
@@ -0,0 +1,85 @@
import { forwardRef } from "react";
import {
faExclamationCircle,
faExclamationTriangle,
faInfoCircle
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { type VariantProps, cva } from "cva";
import { twMerge } from "tailwind-merge";
const alertVariants = cva(
"w-full bg-mineshaft-800 rounded-lg border px-4 py-3 text-sm flex items-center gap-x-4",
{
variants: {
variant: {
default: "",
danger: "text-red border-red",
warning: "text-yellow border-yellow"
}
},
defaultVariants: {
variant: "default"
}
}
);
type AlertProps = {
title?: string;
hideTitle?: boolean;
icon?: React.ReactNode;
};
const variantTitleMap = {
default: "Info",
danger: "Danger",
warning: "Warning"
};
const variantIconMap = {
default: faInfoCircle,
danger: faExclamationCircle,
warning: faExclamationTriangle
};
const Alert = forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants> & AlertProps
>(({ className, variant, title, icon, hideTitle = false, children, ...props }, ref) => {
const defaultTitle = title ?? variantTitleMap[variant ?? "default"];
return (
<div
ref={ref}
role="alert"
className={twMerge(alertVariants({ variant }), className)}
{...props}
>
<div>
{typeof icon !== "undefined" ? (
<>{icon} </>
) : (
<FontAwesomeIcon className="text-lg" icon={variantIconMap[variant ?? "default"]} />
)}
</div>
<div className="flex flex-col gap-y-1">
{hideTitle ? null : (
<h5 className="font-medium leading-none tracking-tight" {...props}>
{defaultTitle}
</h5>
)}
{children}
</div>
</div>
);
});
Alert.displayName = "Alert";
const AlertDescription = forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={twMerge("text-sm [&_p]:leading-relaxed", className)} {...props} />
));
AlertDescription.displayName = "AlertDescription";
export { Alert, AlertDescription };
@@ -0,0 +1 @@
export { Alert, AlertDescription } from "./Alert";
+1
View File
@@ -1,4 +1,5 @@
export * from "./Accordion";
export * from "./Alert";
export * from "./Button";
export * from "./Card";
export * from "./Checkbox";
+4
View File
@@ -0,0 +1,4 @@
export {
useCreateAPIKeyV2,
useDeleteAPIKeyV2,
useUpdateAPIKeyV2} from "./queries";
@@ -0,0 +1,62 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { userKeys } from "../users/queries";
import {
APIKeyDataV2,
CreateAPIKeyDataV2DTO,
CreateServiceTokenDataV3Res,
DeleteAPIKeyDataV2DTO,
UpdateAPIKeyDataV2DTO} from "./types";
export const useCreateAPIKeyV2 = () => {
const queryClient = useQueryClient();
return useMutation<CreateServiceTokenDataV3Res, {}, CreateAPIKeyDataV2DTO>({
mutationFn: async ({
name
}) => {
const { data } = await apiRequest.post("/api/v3/api-key", {
name
});
return data;
},
onSuccess: () => {
queryClient.invalidateQueries(userKeys.myAPIKeysV2);
}
});
};
export const useUpdateAPIKeyV2 = () => {
const queryClient = useQueryClient();
return useMutation<APIKeyDataV2, {}, UpdateAPIKeyDataV2DTO>({
mutationFn: async ({
apiKeyDataId,
name
}) => {
const { data: { apiKeyData } } = await apiRequest.patch(`/api/v3/api-key/${apiKeyDataId}`, {
name
});
return apiKeyData;
},
onSuccess: () => {
queryClient.invalidateQueries(userKeys.myAPIKeysV2);
}
});
};
export const useDeleteAPIKeyV2 = () => {
const queryClient = useQueryClient();
return useMutation<APIKeyDataV2, {}, DeleteAPIKeyDataV2DTO>({
mutationFn: async ({
apiKeyDataId
}) => {
const { data: { apiKeyData } } = await apiRequest.delete(`/api/v3/api-key/${apiKeyDataId}`);
return apiKeyData;
},
onSuccess: () => {
queryClient.invalidateQueries(userKeys.myAPIKeysV2);
}
});
};
+27
View File
@@ -0,0 +1,27 @@
export type APIKeyDataV2 = {
_id: string;
name: string;
user: string;
lastUsed?: string;
usageCount: number;
createdAt: string;
updatedAt: string;
};
export type CreateAPIKeyDataV2DTO = {
name: string;
}
export type CreateServiceTokenDataV3Res = {
apiKeyData: APIKeyDataV2;
apiKey: string;
}
export type UpdateAPIKeyDataV2DTO = {
apiKeyDataId: string;
name: string;
}
export type DeleteAPIKeyDataV2DTO = {
apiKeyDataId: string;
}
+1
View File
@@ -1,3 +1,4 @@
export * from "./apiKeys";
export * from "./auditLogs";
export * from "./auth";
export * from "./bots";
@@ -131,6 +131,7 @@ export const useUpdateSecretV3 = ({
mutationFn: async ({
secretPath = "/",
type,
secretId,
environment,
workspaceId,
secretName,
@@ -157,6 +158,7 @@ export const useUpdateSecretV3 = ({
environment,
type,
secretPath,
secretId,
...encryptSecret(randomBytes, newSecretName ?? secretName, secretValue, secretComment),
tags,
skipMultilineEncoding,
+1
View File
@@ -109,6 +109,7 @@ export type TUpdateSecretsV3DTO = {
skipMultilineEncoding?: boolean;
newSecretName?: string;
secretName: string;
secretId?: string;
secretValue: string;
secretComment?: string;
tags?: string[];
@@ -4,4 +4,5 @@ export {
useDeleteServiceToken,
useDeleteServiceTokenV3,
useGetUserWsServiceTokens,
useUpdateServiceTokenV3} from "./queries";
useUpdateServiceTokenV3
} from "./queries";
+1
View File
@@ -7,6 +7,7 @@ export {
useDeleteOrgMembership,
useDeleteUser,
useGetMyAPIKeys,
useGetMyAPIKeysV2,
useGetMyIp,
useGetMyOrganizationProjects,
useGetMySessions,
+17 -4
View File
@@ -7,6 +7,7 @@ import {
import { apiRequest } from "@app/config/request";
import { setAuthToken } from "@app/reactQuery";
import { APIKeyDataV2 } from "../apiKeys/types";
import { useUploadWsKey } from "../keys/queries";
import { workspaceKeys } from "../workspace/queries";
import {
@@ -24,12 +25,13 @@ import {
User
} from "./types";
const userKeys = {
export const userKeys = {
getUser: ["user"] as const,
userAction: ["user-action"] as const,
getOrgUsers: (orgId: string) => [{ orgId }, "user"],
myIp: ["ip"] as const,
myAPIKeys: ["api-keys"] as const,
myAPIKeysV2: ["api-keys-v2"] as const,
mySessions: ["sessions"] as const,
myOrganizationProjects: (orgId: string) => [{ orgId }, "organization-projects"] as const
};
@@ -270,7 +272,7 @@ export const useGetMyIp = () => {
});
};
export const useGetMyAPIKeys = () => {
export const useGetMyAPIKeys = () => { // TODO: deprecate (moving to API Key V2)
return useQuery({
queryKey: userKeys.myAPIKeys,
queryFn: async () => {
@@ -281,7 +283,18 @@ export const useGetMyAPIKeys = () => {
});
};
export const useCreateAPIKey = () => {
export const useGetMyAPIKeysV2 = () => {
return useQuery({
queryKey: userKeys.myAPIKeysV2,
queryFn: async () => {
const { data: { apiKeyData } } = await apiRequest.get<{ apiKeyData: APIKeyDataV2[] }>("/api/v3/users/me/api-keys");
return apiKeyData;
},
enabled: true
});
};
export const useCreateAPIKey = () => { // TODO: deprecate (moving to API Key V2)
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ name, expiresIn }: { name: string; expiresIn: number }) => {
@@ -298,7 +311,7 @@ export const useCreateAPIKey = () => {
});
};
export const useDeleteAPIKey = () => {
export const useDeleteAPIKey = () => { // TODO: deprecate (moving to API Key V2)
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (apiKeyDataId: string) => {
+1 -1
View File
@@ -726,7 +726,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
{infisicalPlatformVersion && (
<div className="mb-2 w-full pl-5 duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faInfo} className="mr-4 px-[0.1rem]" />
Platform Version: {infisicalPlatformVersion}
Version: {infisicalPlatformVersion}
</div>
)}
</div>
@@ -41,7 +41,6 @@ export default function FlyioAuthorizeIntegrationPage() {
const onFormSubmit = async ({
accessToken
}: FormData) => {
console.log("onFormSubmit accessToken: ", accessToken);
try {
setIsLoading(true);
@@ -0,0 +1,116 @@
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
const schema = yup.object({
accessToken: yup.string().trim().required("Hasura Cloud Access Token is required")
});
type FormData = yup.InferType<typeof schema>;
const APP_NAME = "Hasura Cloud";
export default function HasuraCloudAuthorizeIntegrationPage() {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const { mutateAsync } = useSaveIntegrationAccessToken();
const { control, handleSubmit } = useForm<FormData>({
resolver: yupResolver(schema),
defaultValues: {
accessToken: ""
}
});
const onFormSubmit = async ({ accessToken }: FormData) => {
try {
setIsLoading(true);
const integrationAuth = await mutateAsync({
workspaceId: localStorage.getItem("projectData.id"),
integration: "hasura-cloud",
accessToken
});
setIsLoading(false);
router.push(`/integrations/hasura-cloud/create?integrationAuthId=${integrationAuth._id}`);
} catch (err) {
setIsLoading(false);
console.error(err);
}
};
return (
<div className="flex h-full w-full items-center justify-center">
<Head>
<title>Authorize {APP_NAME} Integration</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<Card className="mb-12 max-w-lg rounded-md border border-mineshaft-600">
<CardTitle
className="px-6 text-left text-xl"
subTitle="After adding your access token, you will be prompted to set up an integration for a particular Infisical project and environment."
>
<div className="flex flex-row items-center">
<div className="inline flex items-center pb-0.5">
<Image
src="/images/integrations/Hasura.svg"
height={30}
width={30}
alt={`${APP_NAME} logo`}
/>
</div>
<span className="ml-2.5">{APP_NAME} Integration </span>
<Link href="https://infisical.com/docs/integrations/cloud/hasura-cloud" passHref>
<a target="_blank" rel="noopener noreferrer">
<div className="ml-2 mb-1 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="ml-1.5 mb-[0.07rem] text-xxs"
/>
</div>
</a>
</Link>
</div>
</CardTitle>
<form onSubmit={handleSubmit(onFormSubmit)} className="px-6 pb-8 text-right">
<Controller
control={control}
name="accessToken"
render={({ field, fieldState: { error } }) => (
<FormControl
label={`${APP_NAME} Access Token`}
errorText={error?.message}
isError={Boolean(error)}
>
<Input {...field} placeholder="" />
</FormControl>
)}
/>
<Button
colorSchema="primary"
variant="outline_bg"
className="mt-2 w-min"
size="sm"
type="submit"
isLoading={isLoading}
>
Connect to {APP_NAME}
</Button>
</form>
</Card>
</div>
);
}
HasuraCloudAuthorizeIntegrationPage.requireAuth = true;
@@ -0,0 +1,228 @@
import { Controller, useForm } from "react-hook-form";
import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router";
import { faArrowUpRightFromSquare, faBookOpen, faBugs } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup";
import queryString from "query-string";
import * as yup from "yup";
import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from "@app/components/v2";
import { useCreateIntegration } from "@app/hooks/api";
import {
useGetIntegrationAuthApps,
useGetIntegrationAuthById
} from "@app/hooks/api/integrationAuth";
import { useGetWorkspaceById } from "@app/hooks/api/workspace";
const schema = yup.object({
secretPath: yup.string().trim().required("Secret path is required"),
sourceEnvironment: yup.string().trim().required("Project environment is required"),
appId: yup.string().trim().required("Hasura Cloud project is required")
});
type FormData = yup.InferType<typeof schema>;
const APP_NAME = "Hasura Cloud";
export default function HasuraCloudCreateIntegrationPage() {
const {
control,
handleSubmit,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: yupResolver(schema)
});
const router = useRouter();
const { mutateAsync } = useCreateIntegration();
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
const { data: integrationAuth, isLoading: isIntegrationAuthLoading } = useGetIntegrationAuthById(
(integrationAuthId as string) ?? ""
);
const { data: integrationAuthApps, isLoading: isIntegrationAuthAppsLoading } =
useGetIntegrationAuthApps({
integrationAuthId: (integrationAuthId as string) ?? ""
});
const onFormSubmit = async ({ secretPath, sourceEnvironment, appId }: FormData) => {
try {
if (!integrationAuth?._id) return;
const app = integrationAuthApps?.find((data) => data.appId === appId);
await mutateAsync({
integrationAuthId: integrationAuth?._id,
isActive: true,
sourceEnvironment,
secretPath,
appId,
app: app?.name
});
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
} catch (err) {
console.error(err);
}
};
return integrationAuth && workspace && integrationAuthApps ? (
<div className="flex h-full w-full flex-col items-center justify-center">
<Head>
<title>Set Up {APP_NAME} Integration</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<Card className="max-w-lg rounded-md border border-mineshaft-600">
<CardTitle
className="px-6 text-left text-xl"
subTitle={`Choose which environment or folder in Infisical you want to sync to ${APP_NAME} environment variables.`}
>
<div className="flex flex-row items-center">
<div className="inline flex items-center pb-0.5">
<Image
src="/images/integrations/Hasura.svg"
height={30}
width={30}
alt={`${APP_NAME} logo`}
/>
</div>
<span className="ml-2.5">{APP_NAME} Integration </span>
<Link href="https://infisical.com/docs/integrations/cloud/flyio" passHref>
<a target="_blank" rel="noopener noreferrer">
<div className="ml-2 mb-1 inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
Docs
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="ml-1.5 mb-[0.07rem] text-xxs"
/>
</div>
</a>
</Link>
</div>
</CardTitle>
<form onSubmit={handleSubmit(onFormSubmit)} className="flex w-full flex-col px-6">
<Controller
control={control}
name="sourceEnvironment"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Project Environment"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
className="w-full border border-mineshaft-500"
value={field.value}
onValueChange={(val) => {
field.onChange(val);
}}
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="secretPath"
render={({ field, fieldState: { error } }) => (
<FormControl label="Secrets Path" errorText={error?.message} isError={Boolean(error)}>
<Input {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="appId"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Hasura Cloud Project"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
className="w-full border border-mineshaft-500"
value={field.value}
isDisabled={integrationAuthApps?.length === 0}
onValueChange={(val) => {
field.onChange(val);
}}
>
{integrationAuthApps?.map((project) => (
<SelectItem value={project.appId ?? ""} key={`project-id-${project.appId}`}>
{project.name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Button
type="submit"
color="mineshaft"
variant="outline_bg"
className="mb-6 mt-2 ml-auto"
isLoading={isSubmitting}
>
Create Integration
</Button>
</form>
</Card>
</div>
) : (
<div className="flex h-full w-full items-center justify-center">
<Head>
<title>Set Up {APP_NAME} Integration</title>
<link rel="icon" href="/infisical.ico" />
</Head>
{isIntegrationAuthLoading || isIntegrationAuthAppsLoading ? (
<img
src="/images/loading/loading.gif"
height={70}
width={120}
alt="infisical loading indicator"
/>
) : (
<div className="flex h-max max-w-md flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6 text-center text-mineshaft-200">
<FontAwesomeIcon icon={faBugs} className="inlineli my-2 text-6xl" />
<p>
Something went wrong. Please contact{" "}
<a
className="inline cursor-pointer text-mineshaft-100 underline decoration-primary-500 underline-offset-4 opacity-80 duration-200 hover:opacity-100"
target="_blank"
rel="noopener noreferrer"
href="mailto:[email protected]"
>
support@infisical.com
</a>{" "}
if the issue persists.
</p>
</div>
)}
</div>
);
}
HasuraCloudCreateIntegrationPage.requireAuth = true;
@@ -1,6 +1,6 @@
import crypto from "crypto";
import { TCloudIntegration,UserWsKeyPair } from "@app/hooks/api/types";
import { TCloudIntegration, UserWsKeyPair } from "@app/hooks/api/types";
import {
decryptAssymmetric,
@@ -32,11 +32,10 @@ export const generateBotKey = (botPublicKey: string, latestKey: UserWsKeyPair) =
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 "gcp-secret-manager":
@@ -123,6 +122,9 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
case "teamcity":
link = `${window.location.origin}/integrations/teamcity/authorize`;
break;
case "hasura-cloud":
link = `${window.location.origin}/integrations/hasura-cloud/authorize`;
break;
default:
break;
}
@@ -130,7 +132,6 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
if (link !== "") {
window.location.assign(link);
}
} catch (err) {
console.error(err);
}
@@ -202,6 +202,8 @@ export const IntegrationsPage = withProjectPermission(
integrations={integrations}
environments={environments}
onIntegrationDelete={({ _id: id }, cb) => handleIntegrationDelete(id, cb)}
isBotActive={bot?.isActive}
workspaceId={workspaceId}
/>
<CloudIntegrationSection
isLoading={isCloudIntegrationsLoading || isIntegrationAuthLoading}
@@ -1,9 +1,12 @@
import Link from "next/link";
import { faArrowRight, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { integrationSlugNameMapping } from "public/data/frequentConstants";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Alert,
AlertDescription,
DeleteActionModal,
EmptyState,
FormControl,
@@ -23,17 +26,22 @@ type Props = {
integrations?: TIntegration[];
isLoading?: boolean;
onIntegrationDelete: (integration: TIntegration, cb: () => void) => void;
isBotActive: boolean | undefined;
workspaceId: string;
};
export const IntegrationsSection = ({
integrations = [],
environments = [],
isLoading,
onIntegrationDelete
onIntegrationDelete,
isBotActive,
workspaceId
}: 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">
@@ -45,7 +53,22 @@ export const IntegrationsSection = ({
<Skeleton className="h-28" />
</div>
)}
{!isLoading && !integrations.length && (
{!isBotActive && (
<div className="px-6 py-4">
<Alert hideTitle variant="warning">
<AlertDescription>
All the active integrations will be disabled. Disable End-to-End Encryption in{" "}
<Link href={`/project/${workspaceId}/settings`} passHref>
<a className="underline underline-offset-2">project settings </a>
</Link>
to re-enable it .
</AlertDescription>
</Alert>
</div>
)}
{!isLoading && !integrations.length && isBotActive && (
<div className="mx-6">
<EmptyState
className="rounded-md border border-mineshaft-700 pt-8 pb-4"
@@ -53,7 +76,7 @@ export const IntegrationsSection = ({
/>
</div>
)}
{!isLoading && (
{!isLoading && isBotActive && (
<div className="flex flex-col space-y-4 p-6 pt-0">
{integrations?.map((integration) => (
<div
@@ -153,6 +153,7 @@ export const SecretListView = ({
workspaceId,
secretPath,
secretName: key,
secretId,
secretValue: value || "",
type,
latestFileKey: decryptFileKey,
@@ -201,11 +202,14 @@ export const SecretListView = ({
try {
// personal secret change
if (overrideAction === "deleted") {
await handleSecretOperation("delete", "personal", oldKey);
await handleSecretOperation("delete", "personal", oldKey, {
secretId: orgSecret.idOverride
});
} else if (overrideAction && idOverride) {
await handleSecretOperation("update", "personal", oldKey, {
value: valueOverride,
newKey: hasKeyChanged ? key : undefined,
secretId: orgSecret.idOverride,
skipMultilineEncoding: modSecret.skipMultilineEncoding
});
} else if (overrideAction) {
@@ -218,6 +222,7 @@ export const SecretListView = ({
value,
tags: tagIds,
comment,
secretId: orgSecret._id,
newKey: hasKeyChanged ? key : undefined,
skipMultilineEncoding: modSecret.skipMultilineEncoding
});
@@ -308,7 +313,6 @@ export const SecretListView = ({
>
{namespace}
</div>
{filteredSecrets.map((secret) => (
<SecretItem
environment={environment}
@@ -146,12 +146,13 @@ export const SecretOverviewPage = () => {
}
};
const handleSecretUpdate = async (env: string, key: string, value: string) => {
const handleSecretUpdate = async (env: string, key: string, value: string, secretId?: string) => {
try {
await updateSecretV3({
environment: env,
workspaceId,
secretPath,
secretId,
secretName: key,
secretValue: value,
type: "shared",
@@ -242,7 +243,6 @@ export const SecretOverviewPage = () => {
);
const canViewOverviewPage = Boolean(userAvailableEnvs.length);
const filteredSecretNames = secKeys
?.filter((name) => name.toUpperCase().includes(searchFilter.toUpperCase()))
.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a)));
@@ -18,7 +18,7 @@ type Props = {
environment: string;
secretPath: string;
onSecretCreate: (env: string, key: string, value: string) => Promise<void>;
onSecretUpdate: (env: string, key: string, value: string) => Promise<void>;
onSecretUpdate: (env: string, key: string, value: string, secretId?: string) => Promise<void>;
onSecretDelete: (env: string, key: string, secretId?: string) => Promise<void>;
};
@@ -42,7 +42,7 @@ export const SecretEditRow = ({
formState: { isDirty, isSubmitting }
} = useForm({
values: {
value: defaultValue
value: defaultValue || null
}
});
const [isDeleting, setIsDeleting] = useToggle();
@@ -70,7 +70,7 @@ export const SecretEditRow = ({
if (isCreatable) {
await onSecretCreate(environment, secretName, value);
} else {
await onSecretUpdate(environment, secretName, value);
await onSecretUpdate(environment, secretName, value, secretId);
}
}
reset({ value });
@@ -80,7 +80,7 @@ export const SecretEditRow = ({
setIsDeleting.on();
try {
await onSecretDelete(environment, secretName, secretId);
reset({ value: undefined });
reset({ value: null });
} finally {
setIsDeleting.off();
}
@@ -23,7 +23,7 @@ type Props = {
expandableColWidth: number;
getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined;
onSecretCreate: (env: string, key: string, value: string) => Promise<void>;
onSecretUpdate: (env: string, key: string, value: string) => Promise<void>;
onSecretUpdate: (env: string, key: string, value: string, secretId?: string) => Promise<void>;
onSecretDelete: (env: string, key: string, secretId?: string) => Promise<void>;
};
@@ -143,8 +143,8 @@ export const SSOModal = ({
entityId: "Identifier (Entity ID)",
entryPoint: "Login URL",
entryPointPlaceholder: "https://login.microsoftonline.com/xxx/saml2",
issuer: "Azure AD Identifier",
issuerPlaceholder: "https://sts.windows.net/xxx/"
issuer: "Azure Application ID",
issuerPlaceholder: "abc-def-ghi-jkl-mno"
});
case AuthProvider.JUMPCLOUD_SAML:
return ({
@@ -0,0 +1,199 @@
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
FormControl,
IconButton,
Input,
Modal,
ModalContent} from "@app/components/v2";
import { useToggle } from "@app/hooks";
import {
useCreateAPIKeyV2,
useUpdateAPIKeyV2
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = yup.object({
name: yup.string().required("API Key V2 name is required")
}).required();
export type FormData = yup.InferType<typeof schema>;
type Props = {
popUp: UsePopUpState<["apiKeyV2"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["apiKeyV2"]>, state?: boolean) => void;
};
export const APIKeyV2Modal = ({
popUp,
handlePopUpToggle
}: Props) => {
const [newAPIKey, setNewAPIKey] = useState("");
const [isAPIKeyCopied, setIsAPIKeyCopied] = useToggle(false);
const { createNotification } = useNotificationContext();
const { mutateAsync: createMutateAsync } = useCreateAPIKeyV2();
const { mutateAsync: updateMutateAsync } = useUpdateAPIKeyV2();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: yupResolver(schema),
defaultValues: {
name: ""
}
});
useEffect(() => {
let timer: NodeJS.Timeout;
if (isAPIKeyCopied) {
timer = setTimeout(() => setIsAPIKeyCopied.off(), 2000);
}
return () => clearTimeout(timer);
}, [setIsAPIKeyCopied]);
useEffect(() => {
const apiKeyData = popUp?.apiKeyV2?.data as {
apiKeyDataId: string;
name: string;
};
if (apiKeyData) {
reset({
name: apiKeyData.name
});
} else {
reset({
name: ""
});
}
}, [popUp?.apiKeyV2?.data]);
const copyTokenToClipboard = () => {
navigator.clipboard.writeText(newAPIKey);
setIsAPIKeyCopied.on();
};
const onFormSubmit = async ({
name
}: FormData) => {
try {
const apiKeyData = popUp?.apiKeyV2?.data as {
apiKeyDataId: string;
name: string;
};
if (apiKeyData) {
// update
await updateMutateAsync({
apiKeyDataId: apiKeyData.apiKeyDataId,
name
});
handlePopUpToggle("apiKeyV2", false);
} else {
// create
const { apiKey } = await createMutateAsync({
name
});
setNewAPIKey(apiKey);
}
createNotification({
text: `Successfully ${popUp?.apiKeyV2?.data ? "updated" : "created"} API Key`,
type: "success"
});
reset();
} catch (err) {
console.error(err);
createNotification({
text: `Failed to ${popUp?.apiKeyV2?.data ? "updated" : "created"} API Key`,
type: "error"
});
}
}
const hasAPIKey = Boolean(newAPIKey);
return (
<Modal
isOpen={popUp?.apiKeyV2?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("apiKeyV2", isOpen);
reset();
setNewAPIKey("");
}}
>
<ModalContent title={`${popUp?.apiKeyV2?.data ? "Update" : "Create"} API Key V2`}>
{!hasAPIKey ? (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
defaultValue=""
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
placeholder="My API Key"
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{popUp?.apiKeyV2?.data ? "Update" : "Create"}
</Button>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</div>
</form>
) : (
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{newAPIKey}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={copyTokenToClipboard}
>
<FontAwesomeIcon icon={isAPIKeyCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
Click to copy
</span>
</IconButton>
</div>
)}
</ModalContent>
</Modal>
);
}
@@ -0,0 +1,81 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
DeleteActionModal
} from "@app/components/v2";
import { useDeleteAPIKeyV2 } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { APIKeyV2Modal } from "./APIKeyV2Modal";
import { APIKeyV2Table } from "./APIKeyV2Table";
export const APIKeyV2Section = () => {
const { createNotification } = useNotificationContext();
const { mutateAsync: deleteMutateAsync } = useDeleteAPIKeyV2();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"apiKeyV2",
"deleteAPIKeyV2"
] as const);
const onDeleteAPIKeyDataSubmit = async (apiKeyDataId: string) => {
try {
await deleteMutateAsync({
apiKeyDataId
});
createNotification({
text: "Successfully deleted API Key V2",
type: "success"
});
handlePopUpClose("deleteAPIKeyV2");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete API Key V2",
type: "error"
});
}
}
return (
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<div className="flex justify-between mb-8">
<p className="text-xl font-semibold text-mineshaft-100">
API Keys V2 (Beta)
</p>
<Button
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("apiKeyV2")}
>
Add API Key
</Button>
</div>
<APIKeyV2Table
handlePopUpOpen={handlePopUpOpen}
/>
<APIKeyV2Modal
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
/>
<DeleteActionModal
isOpen={popUp.deleteAPIKeyV2.isOpen}
title={`Are you sure want to delete ${
(popUp?.deleteAPIKeyV2?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteAPIKeyV2", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onDeleteAPIKeyDataSubmit(
(popUp?.deleteAPIKeyV2?.data as { apiKeyDataId: string })?.apiKeyDataId
)
}
/>
</div>
);
}
@@ -0,0 +1,107 @@
import { faKey, faPencil, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import {
EmptyState,
IconButton,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import {
useGetMyAPIKeysV2
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["deleteAPIKeyV2", "apiKeyV2"]>,
data?: {
apiKeyDataId?: string;
name?: string;
}
) => void;
};
export const APIKeyV2Table = ({
handlePopUpOpen
}: Props) => {
const { data, isLoading } = useGetMyAPIKeysV2();
return (
<TableContainer>
<Table>
<THead>
<Tr>
<Th className="">Name</Th>
<Th className="">Last Used</Th>
<Th className="">Created At</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} innerKey="api-keys-v2" />}
{!isLoading &&
data &&
data.length > 0 &&
data.map(({
_id,
name,
lastUsed,
createdAt
}) => {
return (
<Tr className="h-10" key={`api-key-v2-${_id}`}>
<Td>{name}</Td>
<Td>{lastUsed ? format(new Date(lastUsed), "yyyy-MM-dd") : "-"}</Td>
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
<Td className="flex justify-end">
<IconButton
onClick={async () => {
handlePopUpOpen("apiKeyV2", {
apiKeyDataId: _id,
name
});
}}
size="lg"
colorSchema="primary"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
<IconButton
onClick={() => {
handlePopUpOpen("deleteAPIKeyV2", {
apiKeyDataId: _id
});
}}
size="lg"
colorSchema="danger"
variant="plain"
ariaLabel="update"
className="ml-4"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</Td>
</Tr>
);
})}
{!isLoading && data && data?.length === 0 && (
<Tr>
<Td colSpan={4}>
<EmptyState title="No API key v2 on file" icon={faKey} />
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
);
}
@@ -0,0 +1 @@
export { APIKeyV2Section } from "./APIKeyV2Section";
@@ -1,7 +1,11 @@
// import { APIKeyV2Section } from "../APIKeyV2Section";
import { APIKeySection } from "../APIKeySection";
export const PersonalAPIKeyTab = () => {
return (
<APIKeySection />
<>
{/* <APIKeyV2Section /> */}
<APIKeySection />
</>
);
}
@@ -3,7 +3,7 @@ import {
decryptAssymmetric,
encryptAssymmetric
} from "@app/components/utilities/cryptography/crypto";
import { Checkbox } from "@app/components/v2";
import { Alert, AlertDescription, Checkbox } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useGetUserWsKey, useGetWorkspaceBot, useUpdateBotActiveStatus } from "@app/hooks/api";
@@ -76,30 +76,39 @@ export const E2EESection = () => {
};
return bot ? (
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<p className="mb-3 text-xl font-semibold">End-to-End Encryption</p>
<p className="text-gray-400 mb-8">
<p className="mb-8 text-gray-400">
Disabling, end-to-end encryption (E2EE) unlocks capabilities like native integrations to
cloud providers as well as HTTP calls to get secrets back raw but enables the server to
read/decrypt your secret values.
</p>
<p className="text-gray-400 mb-8">
<p className="mb-8 text-gray-400">
Note that, even with E2EE disabled, your secrets are always encrypted at rest.
</p>
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Settings}>
{(isAllowed) => (
<div className="w-max">
<Checkbox
className="data-[state=checked]:bg-primary"
id="autoCapitalization"
isChecked={!bot.isActive}
isDisabled={!isAllowed}
onCheckedChange={async () => {
await toggleBotActivate();
}}
>
End-to-end encryption enabled
</Checkbox>
<div className="flex w-full flex-col gap-y-3">
<div className="w-max">
<Checkbox
className="data-[state=checked]:bg-primary"
id="autoCapitalization"
isChecked={!bot.isActive}
isDisabled={!isAllowed}
onCheckedChange={async () => {
await toggleBotActivate();
}}
>
End-to-end encryption enabled
</Checkbox>
</div>
<div>
<Alert variant="warning">
<AlertDescription>
Enabling End-to-end encryption disables all the integrations
</AlertDescription>
</Alert>
</div>
</div>
)}
</ProjectPermissionCan>