mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 07:26:45 +00:00
Feature: add ability to paste .env, .yml or .json secrets for upload and also fix upload when keys conflict but are not on current page
This commit is contained in:
@@ -840,4 +840,91 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/secrets-by-keys",
|
||||||
|
config: {
|
||||||
|
rateLimit: secretsLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
querystring: z.object({
|
||||||
|
projectId: z.string().trim(),
|
||||||
|
environment: z.string().trim(),
|
||||||
|
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
|
||||||
|
keys: z.string().trim().transform(decodeURIComponent)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
secrets: secretRawSchema
|
||||||
|
.extend({
|
||||||
|
secretPath: z.string().optional(),
|
||||||
|
tags: SecretTagsSchema.pick({
|
||||||
|
id: true,
|
||||||
|
slug: true,
|
||||||
|
color: true
|
||||||
|
})
|
||||||
|
.extend({ name: z.string() })
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { secretPath, projectId, environment } = req.query;
|
||||||
|
|
||||||
|
const keys = req.query.keys?.split(",").filter((key) => Boolean(key.trim())) ?? [];
|
||||||
|
if (!keys.length) throw new BadRequestError({ message: "One or more keys required" });
|
||||||
|
|
||||||
|
const { secrets } = await server.services.secret.getSecretsRaw({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
environment,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
projectId,
|
||||||
|
path: secretPath,
|
||||||
|
keys
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
projectId,
|
||||||
|
...req.auditLogInfo,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_SECRETS,
|
||||||
|
metadata: {
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
numberOfSecrets: secrets.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
|
||||||
|
await server.services.telemetry.sendPostHogEvents({
|
||||||
|
event: PostHogEventTypes.SecretPulled,
|
||||||
|
distinctId: getTelemetryDistinctId(req),
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: secrets.length,
|
||||||
|
workspaceId: projectId,
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
channel: getUserAgentType(req.headers["user-agent"]),
|
||||||
|
...req.auditLogInfo
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { secrets };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -361,6 +361,10 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
void bd.whereILike(`${TableName.SecretV2}.key`, `%${filters?.search}%`);
|
void bd.whereILike(`${TableName.SecretV2}.key`, `%${filters?.search}%`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filters?.keys) {
|
||||||
|
void bd.whereIn(`${TableName.SecretV2}.key`, filters.keys);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.where((bd) => {
|
.where((bd) => {
|
||||||
void bd.whereNull(`${TableName.SecretV2}.userId`).orWhere({ userId: userId || null });
|
void bd.whereNull(`${TableName.SecretV2}.userId`).orWhere({ userId: userId || null });
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export type TGetSecretsDTO = {
|
|||||||
offset?: number;
|
offset?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
|
keys?: string[];
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TGetASecretDTO = {
|
export type TGetASecretDTO = {
|
||||||
@@ -294,6 +295,7 @@ export type TFindSecretsByFolderIdsFilter = {
|
|||||||
search?: string;
|
search?: string;
|
||||||
tagSlugs?: string[];
|
tagSlugs?: string[];
|
||||||
includeTagsInSearch?: boolean;
|
includeTagsInSearch?: boolean;
|
||||||
|
keys?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGetSecretsRawByFolderMappingsDTO = {
|
export type TGetSecretsRawByFolderMappingsDTO = {
|
||||||
|
|||||||
@@ -185,6 +185,7 @@ export type TGetSecretsRawDTO = {
|
|||||||
offset?: number;
|
offset?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
search?: string;
|
search?: string;
|
||||||
|
keys?: string[];
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TGetASecretRawDTO = {
|
export type TGetASecretRawDTO = {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const LINE =
|
|||||||
* @param {ArrayBuffer} src - source buffer
|
* @param {ArrayBuffer} src - source buffer
|
||||||
* @returns {String} text - text of buffer
|
* @returns {String} text - text of buffer
|
||||||
*/
|
*/
|
||||||
export function parseDotEnv(src: ArrayBuffer) {
|
export function parseDotEnv(src: ArrayBuffer | string) {
|
||||||
const object: {
|
const object: {
|
||||||
[key: string]: { value: string; comments: string[] };
|
[key: string]: { value: string; comments: string[] };
|
||||||
} = {};
|
} = {};
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export const parseJson = (src: ArrayBuffer | string) => {
|
||||||
|
const file = src.toString();
|
||||||
|
const formatedData: Record<string, string> = JSON.parse(file);
|
||||||
|
const env: Record<string, { value: string; comments: string[] }> = {};
|
||||||
|
Object.keys(formatedData).forEach((key) => {
|
||||||
|
if (typeof formatedData[key] === "string") {
|
||||||
|
env[key] = { value: formatedData[key], comments: [] };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return env;
|
||||||
|
};
|
||||||
@@ -5,6 +5,7 @@ import axios from "axios";
|
|||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
import {
|
import {
|
||||||
|
DashboardProjectSecretsByKeys,
|
||||||
DashboardProjectSecretsDetails,
|
DashboardProjectSecretsDetails,
|
||||||
DashboardProjectSecretsDetailsResponse,
|
DashboardProjectSecretsDetailsResponse,
|
||||||
DashboardProjectSecretsOverview,
|
DashboardProjectSecretsOverview,
|
||||||
@@ -12,6 +13,7 @@ import {
|
|||||||
DashboardSecretsOrderBy,
|
DashboardSecretsOrderBy,
|
||||||
TDashboardProjectSecretsQuickSearch,
|
TDashboardProjectSecretsQuickSearch,
|
||||||
TDashboardProjectSecretsQuickSearchResponse,
|
TDashboardProjectSecretsQuickSearchResponse,
|
||||||
|
TGetDashboardProjectSecretsByKeys,
|
||||||
TGetDashboardProjectSecretsDetailsDTO,
|
TGetDashboardProjectSecretsDetailsDTO,
|
||||||
TGetDashboardProjectSecretsOverviewDTO,
|
TGetDashboardProjectSecretsOverviewDTO,
|
||||||
TGetDashboardProjectSecretsQuickSearchDTO
|
TGetDashboardProjectSecretsQuickSearchDTO
|
||||||
@@ -101,6 +103,23 @@ export const fetchProjectSecretsDetails = async ({
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const fetchDashboardProjectSecretsByKeys = async ({
|
||||||
|
keys,
|
||||||
|
...params
|
||||||
|
}: TGetDashboardProjectSecretsByKeys) => {
|
||||||
|
const { data } = await apiRequest.get<DashboardProjectSecretsByKeys>(
|
||||||
|
"/api/v1/dashboard/secrets-by-keys",
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
...params,
|
||||||
|
keys: encodeURIComponent(keys.join(","))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
export const useGetProjectSecretsOverview = (
|
export const useGetProjectSecretsOverview = (
|
||||||
{
|
{
|
||||||
projectId,
|
projectId,
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ export type DashboardProjectSecretsDetailsResponse = {
|
|||||||
totalCount: number;
|
totalCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DashboardProjectSecretsByKeys = {
|
||||||
|
secrets: SecretV3Raw[];
|
||||||
|
};
|
||||||
|
|
||||||
export type DashboardProjectSecretsOverview = Omit<
|
export type DashboardProjectSecretsOverview = Omit<
|
||||||
DashboardProjectSecretsOverviewResponse,
|
DashboardProjectSecretsOverviewResponse,
|
||||||
"secrets"
|
"secrets"
|
||||||
@@ -89,3 +93,10 @@ export type TGetDashboardProjectSecretsQuickSearchDTO = {
|
|||||||
search: string;
|
search: string;
|
||||||
environments: string[];
|
environments: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TGetDashboardProjectSecretsByKeys = {
|
||||||
|
projectId: string;
|
||||||
|
secretPath: string;
|
||||||
|
environment: string;
|
||||||
|
keys: string[];
|
||||||
|
};
|
||||||
|
|||||||
@@ -552,7 +552,6 @@ const SecretMainPageContent = () => {
|
|||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
<SecretDropzone
|
<SecretDropzone
|
||||||
secrets={secrets}
|
|
||||||
environment={environment}
|
environment={environment}
|
||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
secretPath={secretPath}
|
secretPath={secretPath}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Controller, useForm } from "react-hook-form";
|
|||||||
import { subject } from "@casl/ability";
|
import { subject } from "@casl/ability";
|
||||||
import {
|
import {
|
||||||
faClone,
|
faClone,
|
||||||
|
faFileImport,
|
||||||
faKey,
|
faKey,
|
||||||
faSearch,
|
faSearch,
|
||||||
faSquareCheck,
|
faSquareCheck,
|
||||||
@@ -151,6 +152,7 @@ export const CopySecretsFromBoard = ({
|
|||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faFileImport} />}
|
||||||
onClick={() => onToggle(true)}
|
onClick={() => onToggle(true)}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
variant="star"
|
variant="star"
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { subject } from "@casl/ability";
|
||||||
|
import { faPaste } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import { parseDotEnv } from "@app/components/utilities/parseDotEnv";
|
||||||
|
import { parseJson } from "@app/components/utilities/parseJson";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
ModalTrigger,
|
||||||
|
TextArea
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen?: boolean;
|
||||||
|
isSmaller?: boolean;
|
||||||
|
onToggle: (isOpen: boolean) => void;
|
||||||
|
onParsedEnv: (env: Record<string, { value: string; comments: string[] }>) => void;
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
value: z.string().trim()
|
||||||
|
});
|
||||||
|
|
||||||
|
type TForm = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
const PasteEnvForm = ({ onParsedEnv }: Pick<Props, "onParsedEnv">) => {
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
register,
|
||||||
|
formState: { isDirty, errors },
|
||||||
|
setError,
|
||||||
|
setFocus
|
||||||
|
} = useForm<TForm>({ defaultValues: { value: "" }, resolver: zodResolver(formSchema) });
|
||||||
|
|
||||||
|
const onSubmit = ({ value }: TForm) => {
|
||||||
|
let env: Record<string, { value: string; comments: string[] }>;
|
||||||
|
try {
|
||||||
|
env = parseJson(value);
|
||||||
|
} catch (e) {
|
||||||
|
// not json, parse as env
|
||||||
|
env = parseDotEnv(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Object.keys(env).length) {
|
||||||
|
setError("value", { message: "No secrets found." });
|
||||||
|
setFocus("value");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onParsedEnv(env);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<FormControl
|
||||||
|
label="Secret Values"
|
||||||
|
isError={Boolean(errors.value)}
|
||||||
|
errorText={errors.value?.message}
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
{...register("value")}
|
||||||
|
placeholder="Paste secrets in .json, .yml or .env format..."
|
||||||
|
className="h-[60vh] !resize-none"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Button isDisabled={!isDirty} type="submit">
|
||||||
|
Import Secrets
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PasteSecretEnvModal = ({
|
||||||
|
isSmaller,
|
||||||
|
isOpen,
|
||||||
|
onParsedEnv,
|
||||||
|
onToggle,
|
||||||
|
environment,
|
||||||
|
secretPath
|
||||||
|
}: Props) => {
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={onToggle}>
|
||||||
|
<ModalTrigger asChild>
|
||||||
|
<div>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Create}
|
||||||
|
a={subject(ProjectPermissionSub.Secrets, {
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
secretName: "*",
|
||||||
|
secretTags: ["*"]
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPaste} />}
|
||||||
|
onClick={() => onToggle(true)}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
variant="star"
|
||||||
|
size={isSmaller ? "xs" : "sm"}
|
||||||
|
>
|
||||||
|
Paste Secret Values
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
</ModalTrigger>
|
||||||
|
<ModalContent
|
||||||
|
className="max-w-2xl"
|
||||||
|
title="Past Secret Values"
|
||||||
|
subTitle="Paste values in .env, .json or .yml format"
|
||||||
|
>
|
||||||
|
<PasteEnvForm
|
||||||
|
onParsedEnv={(value) => {
|
||||||
|
onToggle(false);
|
||||||
|
onParsedEnv(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ChangeEvent, DragEvent } from "react";
|
import { ChangeEvent, DragEvent } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { subject } from "@casl/ability";
|
import { subject } from "@casl/ability";
|
||||||
import { faUpload } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus, faUpload } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
@@ -10,29 +10,22 @@ import { createNotification } from "@app/components/notifications";
|
|||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
// TODO:(akhilmhdh) convert all the util functions like this into a lib folder grouped by functionality
|
// TODO:(akhilmhdh) convert all the util functions like this into a lib folder grouped by functionality
|
||||||
import { parseDotEnv } from "@app/components/utilities/parseDotEnv";
|
import { parseDotEnv } from "@app/components/utilities/parseDotEnv";
|
||||||
|
import { parseJson } from "@app/components/utilities/parseJson";
|
||||||
import { Button, Modal, ModalContent } from "@app/components/v2";
|
import { Button, Modal, ModalContent } from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||||
import { usePopUp, useToggle } from "@app/hooks";
|
import { usePopUp, useToggle } from "@app/hooks";
|
||||||
import { useCreateSecretBatch, useUpdateSecretBatch } from "@app/hooks/api";
|
import { useCreateSecretBatch, useUpdateSecretBatch } from "@app/hooks/api";
|
||||||
import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
|
import {
|
||||||
|
dashboardKeys,
|
||||||
|
fetchDashboardProjectSecretsByKeys
|
||||||
|
} from "@app/hooks/api/dashboard/queries";
|
||||||
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
|
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
|
||||||
import { secretKeys } from "@app/hooks/api/secrets/queries";
|
import { secretKeys } from "@app/hooks/api/secrets/queries";
|
||||||
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/types";
|
import { SecretType } from "@app/hooks/api/types";
|
||||||
|
|
||||||
import { PopUpNames, usePopUpAction } from "../../SecretMainPage.store";
|
import { PopUpNames, usePopUpAction } from "../../SecretMainPage.store";
|
||||||
import { CopySecretsFromBoard } from "./CopySecretsFromBoard";
|
import { CopySecretsFromBoard } from "./CopySecretsFromBoard";
|
||||||
|
import { PasteSecretEnvModal } from "./PasteSecretEnvModal";
|
||||||
const parseJson = (src: ArrayBuffer) => {
|
|
||||||
const file = src.toString();
|
|
||||||
const formatedData: Record<string, string> = JSON.parse(file);
|
|
||||||
const env: Record<string, { value: string; comments: string[] }> = {};
|
|
||||||
Object.keys(formatedData).forEach((key) => {
|
|
||||||
if (typeof formatedData[key] === "string") {
|
|
||||||
env[key] = { value: formatedData[key], comments: [] };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return env;
|
|
||||||
};
|
|
||||||
|
|
||||||
type TParsedEnv = Record<string, { value: string; comments: string[] }>;
|
type TParsedEnv = Record<string, { value: string; comments: string[] }>;
|
||||||
type TSecOverwriteOpt = { update: TParsedEnv; create: TParsedEnv };
|
type TSecOverwriteOpt = { update: TParsedEnv; create: TParsedEnv };
|
||||||
@@ -43,7 +36,6 @@ type Props = {
|
|||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
secretPath: string;
|
secretPath: string;
|
||||||
secrets?: SecretV3RawSanitized[];
|
|
||||||
isProtectedBranch?: boolean;
|
isProtectedBranch?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -53,7 +45,6 @@ export const SecretDropzone = ({
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
secretPath,
|
secretPath,
|
||||||
secrets = [],
|
|
||||||
isProtectedBranch = false
|
isProtectedBranch = false
|
||||||
}: Props): JSX.Element => {
|
}: Props): JSX.Element => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -62,7 +53,8 @@ export const SecretDropzone = ({
|
|||||||
|
|
||||||
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
"importSecEnv",
|
"importSecEnv",
|
||||||
"overlapKeyWarning"
|
"confirmUpload",
|
||||||
|
"pasteSecEnv"
|
||||||
] as const);
|
] as const);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { openPopUp } = usePopUpAction();
|
const { openPopUp } = usePopUpAction();
|
||||||
@@ -86,20 +78,10 @@ export const SecretDropzone = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleParsedEnv = (env: TParsedEnv) => {
|
const handleParsedEnv = async (env: TParsedEnv) => {
|
||||||
const secretsGroupedByKey = secrets?.reduce<Record<string, boolean>>(
|
const envSecretKeys = Object.keys(env);
|
||||||
(prev, curr) => ({ ...prev, [curr.key]: true }),
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
const overlappedSecrets = Object.keys(env)
|
|
||||||
.filter((secKey) => secretsGroupedByKey?.[secKey])
|
|
||||||
.reduce<TParsedEnv>((prev, curr) => ({ ...prev, [curr]: env[curr] }), {});
|
|
||||||
|
|
||||||
const nonOverlappedSecrets = Object.keys(env)
|
if (!envSecretKeys.length) {
|
||||||
.filter((secKey) => !secretsGroupedByKey?.[secKey])
|
|
||||||
.reduce<TParsedEnv>((prev, curr) => ({ ...prev, [curr]: env[curr] }), {});
|
|
||||||
|
|
||||||
if (!Object.keys(overlappedSecrets).length && !Object.keys(nonOverlappedSecrets).length) {
|
|
||||||
createNotification({
|
createNotification({
|
||||||
type: "error",
|
type: "error",
|
||||||
text: "Failed to find secrets"
|
text: "Failed to find secrets"
|
||||||
@@ -107,10 +89,42 @@ export const SecretDropzone = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePopUpOpen("overlapKeyWarning", {
|
try {
|
||||||
update: overlappedSecrets,
|
setIsLoading.on();
|
||||||
create: nonOverlappedSecrets
|
const { secrets: existingSecrets } = await fetchDashboardProjectSecretsByKeys({
|
||||||
});
|
secretPath,
|
||||||
|
environment,
|
||||||
|
projectId: workspaceId,
|
||||||
|
keys: envSecretKeys
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretsGroupedByKey = existingSecrets.reduce<Record<string, boolean>>(
|
||||||
|
(prev, curr) => ({ ...prev, [curr.secretKey]: true }),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateSecrets = Object.keys(env)
|
||||||
|
.filter((secKey) => secretsGroupedByKey[secKey])
|
||||||
|
.reduce<TParsedEnv>((prev, curr) => ({ ...prev, [curr]: env[curr] }), {});
|
||||||
|
|
||||||
|
const createSecrets = Object.keys(env)
|
||||||
|
.filter((secKey) => !secretsGroupedByKey[secKey])
|
||||||
|
.reduce<TParsedEnv>((prev, curr) => ({ ...prev, [curr]: env[curr] }), {});
|
||||||
|
|
||||||
|
handlePopUpOpen("confirmUpload", {
|
||||||
|
update: updateSecrets,
|
||||||
|
create: createSecrets
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to check for secret conflicts",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
handlePopUpClose("confirmUpload");
|
||||||
|
} finally {
|
||||||
|
setIsLoading.off();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseFile = (file?: File, isJson?: boolean) => {
|
const parseFile = (file?: File, isJson?: boolean) => {
|
||||||
@@ -160,7 +174,7 @@ export const SecretDropzone = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveSecrets = async () => {
|
const handleSaveSecrets = async () => {
|
||||||
const { update, create } = popUp?.overlapKeyWarning?.data as TSecOverwriteOpt;
|
const { update, create } = popUp?.confirmUpload?.data as TSecOverwriteOpt;
|
||||||
try {
|
try {
|
||||||
if (Object.keys(create || {}).length) {
|
if (Object.keys(create || {}).length) {
|
||||||
await createSecretBatch({
|
await createSecretBatch({
|
||||||
@@ -195,7 +209,7 @@ export const SecretDropzone = ({
|
|||||||
dashboardKeys.getDashboardSecrets({ projectId: workspaceId, secretPath })
|
dashboardKeys.getDashboardSecrets({ projectId: workspaceId, secretPath })
|
||||||
);
|
);
|
||||||
queryClient.invalidateQueries(secretApprovalRequestKeys.count({ workspaceId }));
|
queryClient.invalidateQueries(secretApprovalRequestKeys.count({ workspaceId }));
|
||||||
handlePopUpClose("overlapKeyWarning");
|
handlePopUpClose("confirmUpload");
|
||||||
createNotification({
|
createNotification({
|
||||||
type: "success",
|
type: "success",
|
||||||
text: isProtectedBranch
|
text: isProtectedBranch
|
||||||
@@ -211,10 +225,16 @@ export const SecretDropzone = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isUploadedDuplicateSecretsEmpty = !Object.keys(
|
const createSecretCount = Object.keys(
|
||||||
(popUp.overlapKeyWarning?.data as TSecOverwriteOpt)?.update || {}
|
(popUp.confirmUpload?.data as TSecOverwriteOpt)?.create || {}
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
|
const updateSecretCount = Object.keys(
|
||||||
|
(popUp.confirmUpload?.data as TSecOverwriteOpt)?.update || {}
|
||||||
|
).length;
|
||||||
|
|
||||||
|
const isNonConflictingUpload = !updateSecretCount;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
@@ -278,7 +298,15 @@ export const SecretDropzone = ({
|
|||||||
<p className="mx-4 text-xs text-mineshaft-400">OR</p>
|
<p className="mx-4 text-xs text-mineshaft-400">OR</p>
|
||||||
<div className="w-1/5 border-t border-mineshaft-700" />
|
<div className="w-1/5 border-t border-mineshaft-700" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center space-x-8">
|
<div className="flex flex-col items-center justify-center gap-4 lg:flex-row">
|
||||||
|
<PasteSecretEnvModal
|
||||||
|
isOpen={popUp.pasteSecEnv.isOpen}
|
||||||
|
onToggle={(isOpen) => handlePopUpToggle("pasteSecEnv", isOpen)}
|
||||||
|
onParsedEnv={handleParsedEnv}
|
||||||
|
environment={environment}
|
||||||
|
secretPath={secretPath}
|
||||||
|
isSmaller={isSmaller}
|
||||||
|
/>
|
||||||
<CopySecretsFromBoard
|
<CopySecretsFromBoard
|
||||||
isOpen={popUp.importSecEnv.isOpen}
|
isOpen={popUp.importSecEnv.isOpen}
|
||||||
onToggle={(isOpen) => handlePopUpToggle("importSecEnv", isOpen)}
|
onToggle={(isOpen) => handlePopUpToggle("importSecEnv", isOpen)}
|
||||||
@@ -301,11 +329,12 @@ export const SecretDropzone = ({
|
|||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
onClick={() => openPopUp(PopUpNames.CreateSecretForm)}
|
onClick={() => openPopUp(PopUpNames.CreateSecretForm)}
|
||||||
variant="star"
|
variant="star"
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
Add a new secret
|
Add a New Secret
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
@@ -315,25 +344,25 @@ export const SecretDropzone = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={popUp?.overlapKeyWarning?.isOpen}
|
isOpen={popUp?.confirmUpload?.isOpen}
|
||||||
onOpenChange={(open) => handlePopUpToggle("overlapKeyWarning", open)}
|
onOpenChange={(open) => handlePopUpToggle("confirmUpload", open)}
|
||||||
>
|
>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title={isUploadedDuplicateSecretsEmpty ? "Confirmation" : "Duplicate Secrets!!"}
|
title="Confirm Secret Upload"
|
||||||
footerContent={[
|
footerContent={[
|
||||||
<Button
|
<Button
|
||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
isDisabled={isSubmitting}
|
isDisabled={isSubmitting}
|
||||||
colorSchema={isUploadedDuplicateSecretsEmpty ? "primary" : "danger"}
|
colorSchema={isNonConflictingUpload ? "primary" : "danger"}
|
||||||
key="overwrite-btn"
|
key="overwrite-btn"
|
||||||
onClick={handleSaveSecrets}
|
onClick={handleSaveSecrets}
|
||||||
>
|
>
|
||||||
{isUploadedDuplicateSecretsEmpty ? "Upload" : "Overwrite"}
|
{isNonConflictingUpload ? "Upload" : "Overwrite"}
|
||||||
</Button>,
|
</Button>,
|
||||||
<Button
|
<Button
|
||||||
key="keep-old-btn"
|
key="keep-old-btn"
|
||||||
className="mr-4"
|
className="ml-4"
|
||||||
onClick={() => handlePopUpClose("overlapKeyWarning")}
|
onClick={() => handlePopUpClose("confirmUpload")}
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
isDisabled={isSubmitting}
|
isDisabled={isSubmitting}
|
||||||
>
|
>
|
||||||
@@ -341,17 +370,27 @@ export const SecretDropzone = ({
|
|||||||
</Button>
|
</Button>
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{isUploadedDuplicateSecretsEmpty ? (
|
{isNonConflictingUpload ? (
|
||||||
<div>Upload secrets from this file</div>
|
<div>
|
||||||
|
Are you sure you want to import {createSecretCount} secret
|
||||||
|
{createSecretCount > 1 ? "s" : ""} to this environment?
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col space-y-2 text-gray-300">
|
<div className="flex flex-col text-gray-300">
|
||||||
<div>Your file contains following duplicate secrets</div>
|
<div>Your project already contains the following {updateSecretCount} secrets:</div>
|
||||||
<div className="text-sm text-gray-400">
|
<div className="mt-2 text-sm text-gray-400">
|
||||||
{Object.keys((popUp?.overlapKeyWarning?.data as TSecOverwriteOpt)?.update || {})
|
{Object.keys((popUp?.confirmUpload?.data as TSecOverwriteOpt)?.update || {})
|
||||||
?.map((key) => key)
|
?.map((key) => key)
|
||||||
.join(", ")}
|
.join(", ")}
|
||||||
</div>
|
</div>
|
||||||
<div>Are you sure you want to overwrite these secrets and create other ones?</div>
|
<div className="mt-6">
|
||||||
|
Are you sure you want to overwrite these secrets
|
||||||
|
{createSecretCount > 0
|
||||||
|
? ` and import ${createSecretCount} new
|
||||||
|
one${createSecretCount > 1 ? "s" : ""}`
|
||||||
|
: ""}
|
||||||
|
?
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user