Merge pull request #338 from Infisical/smoothen-integrations

Smoothen integrations
This commit is contained in:
BlackMagiq
2023-02-18 16:30:56 +07:00
committed by GitHub
16 changed files with 787 additions and 259 deletions
+248 -5
View File
@@ -1,7 +1,8 @@
import to from 'await-to-js'; import to from 'await-to-js';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { ISecret, Membership, Secret, Workspace } from '../../models'; import { ISecret, Secret } from '../../models';
import { IAction } from '../../ee/models';
import { import {
SECRET_PERSONAL, SECRET_PERSONAL,
SECRET_SHARED, SECRET_SHARED,
@@ -20,6 +21,252 @@ import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization';
import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions'; import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions';
import Tag from '../../models/tag'; import Tag from '../../models/tag';
import _ from 'lodash'; import _ from 'lodash';
import {
BatchSecretRequest,
BatchSecret
} from '../../types/secret';
/**
* Peform a batch of any specified CUD secret operations
* @param req
* @param res
*/
export const batchSecrets = async (req: Request, res: Response) => {
const channel = getChannelFromUserAgent(req.headers['user-agent']);
const {
workspaceId,
environment,
requests
}: {
workspaceId: string;
environment: string;
requests: BatchSecretRequest[];
}= req.body;
const createSecrets: BatchSecret[] = [];
const updateSecrets: BatchSecret[] = [];
const deleteSecrets: Types.ObjectId[] = [];
const actions: IAction[] = [];
requests.forEach((request) => {
switch (request.method) {
case 'POST':
createSecrets.push({
...request.secret,
version: 1,
user: request.secret.type === SECRET_PERSONAL ? req.user : undefined,
environment,
workspace: new Types.ObjectId(workspaceId)
});
break;
case 'PATCH':
updateSecrets.push({
...request.secret,
_id: new Types.ObjectId(request.secret._id)
});
break;
case 'DELETE':
deleteSecrets.push(new Types.ObjectId(request.secret._id));
break;
}
});
// handle create secrets
let createdSecrets: ISecret[] = [];
if (createSecrets.length > 0) {
createdSecrets = await Secret.insertMany(createSecrets);
// (EE) add secret versions for new secrets
await EESecretService.addSecretVersions({
secretVersions: createdSecrets.map((n: any) => {
return ({
...n._doc,
_id: new Types.ObjectId(),
secret: n._id,
isDeleted: false
});
})
});
const addAction = await EELogService.createAction({
name: ACTION_ADD_SECRETS,
userId: req.user._id,
workspaceId: new Types.ObjectId(workspaceId),
secretIds: createdSecrets.map((n) => n._id)
}) as IAction;
actions.push(addAction);
if (postHogClient) {
postHogClient.capture({
event: 'secrets added',
distinctId: req.user.email,
properties: {
numberOfSecrets: createdSecrets.length,
environment,
workspaceId,
channel,
userAgent: req.headers?.['user-agent']
}
});
}
}
// handle update secrets
let updatedSecrets: ISecret[] = [];
if (updateSecrets.length > 0 && req.secrets) {
// construct object containing all secrets
let listedSecretsObj: {
[key: string]: {
version: number;
type: string;
}
} = {};
listedSecretsObj = req.secrets.reduce((obj: any, secret: ISecret) => ({
...obj,
[secret._id.toString()]: secret
}), {});
const updateOperations = updateSecrets.map((u) => ({
updateOne: {
filter: { _id: new Types.ObjectId(u._id) },
update: {
$inc: {
version: 1
},
...u,
_id: new Types.ObjectId(u._id)
}
}
}));
await Secret.bulkWrite(updateOperations);
const secretVersions = updateSecrets.map((u) => ({
secret: new Types.ObjectId(u._id),
version: listedSecretsObj[u._id.toString()].version,
workspace: new Types.ObjectId(workspaceId),
type: listedSecretsObj[u._id.toString()].type,
environment,
isDeleted: false,
secretKeyCiphertext: u.secretKeyCiphertext,
secretKeyIV: u.secretKeyIV,
secretKeyTag: u.secretKeyTag,
secretValueCiphertext: u.secretValueCiphertext,
secretValueIV: u.secretValueIV,
secretValueTag: u.secretValueTag,
secretCommentCiphertext: u.secretCommentCiphertext,
secretCommentIV: u.secretCommentIV,
secretCommentTag: u.secretCommentTag,
tags: u.tags
}));
await EESecretService.addSecretVersions({
secretVersions
});
updatedSecrets = await Secret.find({
_id: {
$in: updateSecrets.map((u) => new Types.ObjectId(u._id))
}
});
const updateAction = await EELogService.createAction({
name: ACTION_UPDATE_SECRETS,
userId: req.user._id,
workspaceId: new Types.ObjectId(workspaceId),
secretIds: updatedSecrets.map((u) => u._id)
}) as IAction;
actions.push(updateAction);
if (postHogClient) {
postHogClient.capture({
event: 'secrets modified',
distinctId: req.user.email,
properties: {
numberOfSecrets: updateSecrets.length,
environment,
workspaceId,
channel,
userAgent: req.headers?.['user-agent']
}
});
}
}
// handle delete secrets
if (deleteSecrets.length > 0) {
await Secret.deleteMany({
_id: {
$in: deleteSecrets
}
});
await EESecretService.markDeletedSecretVersions({
secretIds: deleteSecrets
});
const deleteAction = await EELogService.createAction({
name: ACTION_DELETE_SECRETS,
userId: req.user._id,
workspaceId: new Types.ObjectId(workspaceId),
secretIds: deleteSecrets
}) as IAction;
actions.push(deleteAction);
if (postHogClient) {
postHogClient.capture({
event: 'secrets deleted',
distinctId: req.user.email,
properties: {
numberOfSecrets: deleteSecrets.length,
environment,
workspaceId,
channel: channel,
userAgent: req.headers?.['user-agent']
}
});
}
}
if (actions.length > 0) {
// (EE) create (audit) log
await EELogService.createLog({
userId: req.user._id.toString(),
workspaceId: new Types.ObjectId(workspaceId),
actions,
channel,
ipAddress: req.ip
});
}
// // trigger event - push secrets
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId
})
});
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId
});
const resObj: { [key: string]: ISecret[] | string[] } = {}
if (createSecrets.length > 0) {
resObj['createdSecrets'] = createdSecrets;
}
if (updateSecrets.length > 0) {
resObj['updatedSecrets'] = updatedSecrets;
}
if (deleteSecrets.length > 0) {
resObj['deletedSecrets'] = deleteSecrets.map((d) => d.toString());
}
return res.status(200).send(resObj);
}
/** /**
* Create secret(s) for workspace with id [workspaceId] and environment [environment] * Create secret(s) for workspace with id [workspaceId] and environment [environment]
@@ -166,11 +413,9 @@ export const createSecrets = async (req: Request, res: Response) => {
secretKeyCiphertext, secretKeyCiphertext,
secretKeyIV, secretKeyIV,
secretKeyTag, secretKeyTag,
secretKeyHash,
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretValueHash,
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag,
@@ -187,11 +432,9 @@ export const createSecrets = async (req: Request, res: Response) => {
secretKeyCiphertext, secretKeyCiphertext,
secretKeyIV, secretKeyIV,
secretKeyTag, secretKeyTag,
secretKeyHash,
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretValueHash,
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag,
@@ -158,11 +158,9 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
secretKeyCiphertext, secretKeyCiphertext,
secretKeyIV, secretKeyIV,
secretKeyTag, secretKeyTag,
secretKeyHash,
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretValueHash
} = oldSecretVersion; } = oldSecretVersion;
// update secret // update secret
@@ -179,11 +177,9 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
secretKeyCiphertext, secretKeyCiphertext,
secretKeyIV, secretKeyIV,
secretKeyTag, secretKeyTag,
secretKeyHash,
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretValueHash
}, },
{ {
new: true new: true
@@ -204,11 +200,9 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
secretKeyCiphertext, secretKeyCiphertext,
secretKeyIV, secretKeyIV,
secretKeyTag, secretKeyTag,
secretKeyHash,
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag
secretValueHash
}).save(); }).save();
// take secret snapshot // take secret snapshot
+1 -10
View File
@@ -5,22 +5,19 @@ import {
} from '../../variables'; } from '../../variables';
export interface ISecretVersion { export interface ISecretVersion {
_id: Types.ObjectId;
secret: Types.ObjectId; secret: Types.ObjectId;
version: number; version: number;
workspace: Types.ObjectId; // new workspace: Types.ObjectId; // new
type: string; // new type: string; // new
user: Types.ObjectId; // new user?: Types.ObjectId; // new
environment: string; // new environment: string; // new
isDeleted: boolean; isDeleted: boolean;
secretKeyCiphertext: string; secretKeyCiphertext: string;
secretKeyIV: string; secretKeyIV: string;
secretKeyTag: string; secretKeyTag: string;
secretKeyHash: string;
secretValueCiphertext: string; secretValueCiphertext: string;
secretValueIV: string; secretValueIV: string;
secretValueTag: string; secretValueTag: string;
secretValueHash: string;
tags?: string[]; tags?: string[];
} }
@@ -72,9 +69,6 @@ const secretVersionSchema = new Schema<ISecretVersion>(
type: String, // symmetric type: String, // symmetric
required: true required: true
}, },
secretKeyHash: {
type: String
},
secretValueCiphertext: { secretValueCiphertext: {
type: String, type: String,
required: true required: true
@@ -87,9 +81,6 @@ const secretVersionSchema = new Schema<ISecretVersion>(
type: String, // symmetric type: String, // symmetric
required: true required: true
}, },
secretValueHash: {
type: String
},
tags: { tags: {
ref: 'Tag', ref: 'Tag',
type: [Schema.Types.ObjectId], type: [Schema.Types.ObjectId],
+39 -1
View File
@@ -6,14 +6,52 @@ import {
requireSecretsAuth, requireSecretsAuth,
validateRequest validateRequest
} from '../../middleware'; } from '../../middleware';
import { query, check, body } from 'express-validator'; import { query, body } from 'express-validator';
import { secretsController } from '../../controllers/v2'; import { secretsController } from '../../controllers/v2';
import { validateSecrets } from '../../helpers/secret';
import { import {
ADMIN, ADMIN,
MEMBER, MEMBER,
SECRET_PERSONAL, SECRET_PERSONAL,
SECRET_SHARED SECRET_SHARED
} from '../../variables'; } from '../../variables';
import {
BatchSecretRequest
} from '../../types/secret';
router.post(
'/batch',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
location: 'body'
}),
body('workspaceId').exists().isString().trim(),
body('environment').exists().isString().trim(),
body('requests')
.exists()
.custom(async (requests: BatchSecretRequest[], { req }) => {
if (Array.isArray(requests)) {
const secretIds = requests
.map((request) => request.secret._id)
.filter((secretId) => secretId !== undefined)
if (secretIds.length > 0) {
const relevantSecrets = await validateSecrets({
userId: req.user._id.toString(),
secretIds
});
req.secrets = relevantSecrets;
}
}
return true;
}),
validateRequest,
secretsController.batchSecrets
);
router.post( router.post(
'/', '/',
+38
View File
@@ -1,5 +1,7 @@
import { Types } from 'mongoose';
import { Assign, Omit } from 'utility-types'; import { Assign, Omit } from 'utility-types';
import { ISecret } from '../../models'; import { ISecret } from '../../models';
import { mongo } from 'mongoose';
// Everything is required, except the omitted types // Everything is required, except the omitted types
export type CreateSecretRequestBody = Omit<ISecret, "user" | "version" | "environment" | "workspace">; export type CreateSecretRequestBody = Omit<ISecret, "user" | "version" | "environment" | "workspace">;
@@ -12,3 +14,39 @@ export type SanitizedSecretModify = Partial<Omit<ISecret, "user" | "version" | "
// Everything is required, except the omitted types // Everything is required, except the omitted types
export type SanitizedSecretForCreate = Omit<ISecret, "version" | "_id">; export type SanitizedSecretForCreate = Omit<ISecret, "version" | "_id">;
export interface BatchSecretRequest {
id: string;
method: 'POST' | 'PATCH' | 'DELETE';
secret: Secret;
}
export interface BatchSecret {
_id: string;
type: 'shared' | 'personal',
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentCiphertext: string;
secretCommentIV: string;
secretCommentTag: string;
tags: string[];
}
export interface BatchSecret {
_id: string;
type: 'shared' | 'personal',
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentCiphertext: string;
secretCommentIV: string;
secretCommentTag: string;
tags: string[];
}
+6 -6
View File
@@ -3,8 +3,8 @@ import { Switch } from '@headlessui/react';
interface ToggleProps { interface ToggleProps {
enabled: boolean; enabled: boolean;
setEnabled: (value: boolean) => void; setEnabled: (value: boolean) => void;
addOverride: (value: string | undefined, pos: number) => void; addOverride: (value: string | undefined, id: string) => void;
pos: number; id: string;
} }
/** /**
@@ -13,18 +13,18 @@ interface ToggleProps {
* @param {boolean} obj.enabled - whether the toggle is turned on or off * @param {boolean} obj.enabled - whether the toggle is turned on or off
* @param {function} obj.setEnabled - change the state of the toggle * @param {function} obj.setEnabled - change the state of the toggle
* @param {function} obj.addOverride - a function that adds an override to a certain secret * @param {function} obj.addOverride - a function that adds an override to a certain secret
* @param {number} obj.pos - position of a certain secret * @param {number} obj.id - id of a certain secret
* @returns * @returns
*/ */
const Toggle = ({ enabled, setEnabled, addOverride, pos }: ToggleProps): JSX.Element => { const Toggle = ({ enabled, setEnabled, addOverride, id }: ToggleProps): JSX.Element => {
return ( return (
<Switch <Switch
checked={enabled} checked={enabled}
onChange={() => { onChange={() => {
if (enabled === false) { if (enabled === false) {
addOverride('', pos); addOverride('', id);
} else { } else {
addOverride(undefined, pos); addOverride(undefined, id);
} }
setEnabled(!enabled); setEnabled(!enabled);
}} }}
@@ -45,19 +45,19 @@ export const DeleteEnvVar = ({ isOpen, onClose, onSubmit }: Props) => {
leaveFrom="opacity-100 scale-100" leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95" leaveTo="opacity-0 scale-95"
> >
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-grey border border-gray-700 p-6 text-left align-middle shadow-xl transition-all"> <Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-bunker border border-mineshaft-600 p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-400"> <Dialog.Title as="h3" className="text-lg font-medium leading-6 text-bunker-200">
{t('dashboard:sidebar.delete-key-dialog.title')} {t('dashboard:sidebar.delete-key-dialog.title')}
</Dialog.Title> </Dialog.Title>
<div className="mt-2"> <div className="mt-2">
<p className="text-sm text-gray-500"> <p className="text-sm text-bunker-300">
{t('dashboard:sidebar.delete-key-dialog.confirm-delete-message')} {t('dashboard:sidebar.delete-key-dialog.confirm-delete-message')}
</p> </p>
</div> </div>
<div className="mt-6 flex justify-start"> <div className="mt-6 flex justify-start">
<button <button
type="button" type="button"
className="inline-flex justify-center rounded-md border border-transparent bg-red-700 hover:bg-red-600 px-4 py-2 text-sm font-medium text-bunker-200 hover:text-white text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2" className="inline-flex justify-center rounded-md border border-transparent bg-red-500 opacity-80 hover:opacity-100 px-4 py-2 text-sm font-medium text-bunker-100 text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
onClick={onSubmit} onClick={onSubmit}
> >
Delete Delete
@@ -13,7 +13,7 @@ import { Tag } from 'public/data/frequentInterfaces';
* @param {function} obj.modifyTags - modify tags for a certain secret * @param {function} obj.modifyTags - modify tags for a certain secret
* @param {Tag[]} obj.position - currently selected tags for a certain secret * @param {Tag[]} obj.position - currently selected tags for a certain secret
*/ */
const AddTagsMenu = ({ allTags, currentTags, modifyTags, position }: { allTags: Tag[]; currentTags: Tag[]; modifyTags: (value: Tag[], position: number) => void; position: number; }) => { const AddTagsMenu = ({ allTags, currentTags, modifyTags, id }: { allTags: Tag[]; currentTags: Tag[]; modifyTags: (value: Tag[], id: string) => void; id: string; }) => {
const router = useRouter(); const router = useRouter();
return ( return (
<Menu as="div" className="ml-2 relative inline-block text-left"> <Menu as="div" className="ml-2 relative inline-block text-left">
@@ -41,7 +41,7 @@ const AddTagsMenu = ({ allTags, currentTags, modifyTags, position }: { allTags:
<button <button
type="button" type="button"
className={`${currentTags?.map(currentTag => currentTag.name).includes(tag.name) ? "opacity-30 cursor-default" : "hover:bg-mineshaft-700"} w-full text-left bg-mineshaft-800 px-2 py-0.5 text-bunker-200 rounded-sm flex items-center`} className={`${currentTags?.map(currentTag => currentTag.name).includes(tag.name) ? "opacity-30 cursor-default" : "hover:bg-mineshaft-700"} w-full text-left bg-mineshaft-800 px-2 py-0.5 text-bunker-200 rounded-sm flex items-center`}
onClick={() => {if (!currentTags?.map(currentTag => currentTag.name).includes(tag.name)) {modifyTags(currentTags.concat([tag]), position)}}} onClick={() => {if (!currentTags?.map(currentTag => currentTag.name).includes(tag.name)) {modifyTags(currentTags.concat([tag]), id)}}}
> >
{currentTags?.map(currentTag => currentTag.name).includes(tag.name) ? <FontAwesomeIcon icon={faCheckSquare} className="text-xs mr-2 text-primary"/> : <FontAwesomeIcon icon={faSquare} className="text-xs mr-2"/>} {tag.name} {currentTags?.map(currentTag => currentTag.name).includes(tag.name) ? <FontAwesomeIcon icon={faCheckSquare} className="text-xs mr-2 text-primary"/> : <FontAwesomeIcon icon={faSquare} className="text-xs mr-2"/>} {tag.name}
</button> </button>
@@ -6,11 +6,11 @@ import { useTranslation } from 'next-i18next';
const CommentField = ({ const CommentField = ({
comment, comment,
modifyComment, modifyComment,
position id
}: { }: {
comment: string; comment: string;
modifyComment: (value: string, posistion: number) => void; modifyComment: (value: string, id: string) => void;
position: number; id: string;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -20,7 +20,7 @@ const CommentField = ({
<textarea <textarea
className="placeholder:text-bunker-400 dark:[color-scheme:dark] h-32 w-full bg-bunker-800 px-2 py-1.5 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70" className="placeholder:text-bunker-400 dark:[color-scheme:dark] h-32 w-full bg-bunker-800 px-2 py-1.5 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70"
value={comment} value={comment}
onChange={(e) => modifyComment(e.target.value, position)} onChange={(e) => modifyComment(e.target.value, id)}
placeholder="Leave any comments here..." placeholder="Leave any comments here..."
/> />
</div> </div>
@@ -9,15 +9,15 @@ import { PopoverObject } from '../v2/Popover/Popover';
const REGEX = /([$]{.*?})/g; const REGEX = /([$]{.*?})/g;
interface DashboardInputFieldProps { interface DashboardInputFieldProps {
position: number; id: string;
onChangeHandler: (value: string, position: number) => void; onChangeHandler: (value: string, id: string) => void;
value: string | undefined; value: string | undefined;
type: 'varName' | 'value' | 'comment'; type: 'varName' | 'value' | 'comment';
blurred?: boolean; blurred?: boolean;
isDuplicate?: boolean; isDuplicate?: boolean;
isCapitalized?: boolean; isCapitalized?: boolean;
overrideEnabled?: boolean; overrideEnabled?: boolean;
modifyValueOverride?: (value: string | undefined, position: number) => void; modifyValueOverride?: (value: string | undefined, id: string) => void;
isSideBarOpen?: boolean; isSideBarOpen?: boolean;
} }
@@ -37,7 +37,7 @@ interface DashboardInputFieldProps {
*/ */
const DashboardInputField = ({ const DashboardInputField = ({
position, id,
onChangeHandler, onChangeHandler,
type, type,
value, value,
@@ -72,7 +72,7 @@ const DashboardInputField = ({
}`} }`}
> >
<input <input
onChange={(e) => onChangeHandler(isCapitalized ? e.target.value.toUpperCase() : e.target.value, position)} onChange={(e) => onChangeHandler(isCapitalized ? e.target.value.toUpperCase() : e.target.value, id)}
type={type} type={type}
value={value} value={value}
className={`z-10 peer font-mono ph-no-capture bg-transparent h-full caret-bunker-200 text-sm px-2 w-full min-w-16 outline-none ${ className={`z-10 peer font-mono ph-no-capture bg-transparent h-full caret-bunker-200 text-sm px-2 w-full min-w-16 outline-none ${
@@ -105,9 +105,9 @@ const DashboardInputField = ({
<button type="button" onClick={() => { <button type="button" onClick={() => {
if (modifyValueOverride) { if (modifyValueOverride) {
if (overrideEnabled === false) { if (overrideEnabled === false) {
modifyValueOverride('', position); modifyValueOverride('', id);
} else { } else {
modifyValueOverride(undefined, position); modifyValueOverride(undefined, id);
} }
} }
}}> }}>
@@ -126,7 +126,7 @@ const DashboardInputField = ({
const error = startsWithNumber || isDuplicate; const error = startsWithNumber || isDuplicate;
return ( return (
<PopoverObject text={value || ''} onChangeHandler={onChangeHandler} position={position}> <PopoverObject text={value || ''} onChangeHandler={onChangeHandler} id={id}>
<div title={value} className={`relative flex-col w-full h-10 overflow-hidden ${ <div title={value} className={`relative flex-col w-full h-10 overflow-hidden ${
isSideBarOpen && 'bg-mineshaft-700 duration-200' isSideBarOpen && 'bg-mineshaft-700 duration-200'
}`}> }`}>
@@ -157,7 +157,7 @@ const DashboardInputField = ({
)} )}
<input <input
value={value} value={value}
onChange={(e) => onChangeHandler(e.target.value, position)} onChange={(e) => onChangeHandler(e.target.value, id)}
onScroll={syncScroll} onScroll={syncScroll}
className={`${ className={`${
blurred blurred
@@ -175,10 +175,10 @@ const DashboardInputField = ({
} ${overrideEnabled ? 'text-primary-300' : 'text-gray-400'} } ${overrideEnabled ? 'text-primary-300' : 'text-gray-400'}
absolute flex flex-row whitespace-pre font-mono z-0 ${blurred ? 'invisible' : 'visible'} peer-focus:visible mt-0.5 ph-no-capture overflow-x-scroll bg-transparent h-10 text-sm px-2 py-2 w-full min-w-16 outline-none duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`} absolute flex flex-row whitespace-pre font-mono z-0 ${blurred ? 'invisible' : 'visible'} peer-focus:visible mt-0.5 ph-no-capture overflow-x-scroll bg-transparent h-10 text-sm px-2 py-2 w-full min-w-16 outline-none duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`}
> >
{value?.split(REGEX).map((word, id) => { {value?.split(REGEX).map((word) => {
if (word.match(REGEX) !== null) { if (word.match(REGEX) !== null) {
return ( return (
<span className="ph-no-capture text-yellow" key={`${word}.${id + 1}`}> <span className="ph-no-capture text-yellow" key={id}>
{word.slice(0, 2)} {word.slice(0, 2)}
<span className="ph-no-capture text-yellow-200/80"> <span className="ph-no-capture text-yellow-200/80">
{word.slice(2, word.length - 1)} {word.slice(2, word.length - 1)}
@@ -231,7 +231,7 @@ function inputPropsAreEqual(prev: DashboardInputFieldProps, next: DashboardInput
return ( return (
prev.value === next.value && prev.value === next.value &&
prev.type === next.type && prev.type === next.type &&
prev.position === next.position && prev.id === next.id &&
prev.blurred === next.blurred && prev.blurred === next.blurred &&
prev.isCapitalized === next.isCapitalized && prev.isCapitalized === next.isCapitalized &&
prev.overrideEnabled === next.overrideEnabled && prev.overrideEnabled === next.overrideEnabled &&
@@ -10,10 +10,10 @@ import { Menu, Transition } from '@headlessui/react';
*/ */
const GenerateSecretMenu = ({ const GenerateSecretMenu = ({
modifyValue, modifyValue,
position id
}: { }: {
modifyValue: (value: string, position: number) => void; modifyValue: (value: string, id: string) => void;
position: number; id: string;
}) => { }) => {
const [randomStringLength, setRandomStringLength] = useState(32); const [randomStringLength, setRandomStringLength] = useState(32);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -51,7 +51,7 @@ const GenerateSecretMenu = ({
[...Array(randomStringLength)] [...Array(randomStringLength)]
.map(() => Math.floor(Math.random() * 16).toString(16)) .map(() => Math.floor(Math.random() * 16).toString(16))
.join(''), .join(''),
position id
); );
} }
}} }}
+10 -10
View File
@@ -8,11 +8,11 @@ import { DeleteActionButton } from './DeleteActionButton';
interface KeyPairProps { interface KeyPairProps {
keyPair: SecretDataProps; keyPair: SecretDataProps;
modifyKey: (value: string, position: number) => void; modifyKey: (value: string, id: string) => void;
modifyValue: (value: string, position: number) => void; modifyValue: (value: string, id: string) => void;
modifyValueOverride: (value: string | undefined, position: number) => void; modifyValueOverride: (value: string | undefined, id: string) => void;
modifyComment: (value: string, position: number) => void; modifyComment: (value: string, id: string) => void;
modifyTags: (value: Tag[], position: number) => void; modifyTags: (value: Tag[], id: string) => void;
isBlurred: boolean; isBlurred: boolean;
isDuplicate: boolean; isDuplicate: boolean;
toggleSidebar: (id: string) => void; toggleSidebar: (id: string) => void;
@@ -108,7 +108,7 @@ const KeyPair = ({
isCapitalized = {isCapitalized} isCapitalized = {isCapitalized}
onChangeHandler={modifyKey} onChangeHandler={modifyKey}
type="varName" type="varName"
position={keyPair.pos} id={keyPair.id}
value={keyPair.key} value={keyPair.key}
isDuplicate={isDuplicate} isDuplicate={isDuplicate}
overrideEnabled={keyPair.valueOverride !== undefined} overrideEnabled={keyPair.valueOverride !== undefined}
@@ -124,7 +124,7 @@ const KeyPair = ({
<DashboardInputField <DashboardInputField
onChangeHandler={keyPair.valueOverride !== undefined ? modifyValueOverride : modifyValue} onChangeHandler={keyPair.valueOverride !== undefined ? modifyValueOverride : modifyValue}
type="value" type="value"
position={keyPair.pos} id={keyPair.id}
value={keyPair.valueOverride !== undefined ? keyPair.valueOverride : keyPair.value} value={keyPair.valueOverride !== undefined ? keyPair.valueOverride : keyPair.value}
blurred={isBlurred} blurred={isBlurred}
overrideEnabled={keyPair.valueOverride !== undefined} overrideEnabled={keyPair.valueOverride !== undefined}
@@ -137,7 +137,7 @@ const KeyPair = ({
<DashboardInputField <DashboardInputField
onChangeHandler={modifyComment} onChangeHandler={modifyComment}
type="comment" type="comment"
position={keyPair.pos} id={keyPair.id}
value={keyPair.comment} value={keyPair.comment}
isDuplicate={isDuplicate} isDuplicate={isDuplicate}
isSideBarOpen={keyPair.id === sidebarSecretId} isSideBarOpen={keyPair.id === sidebarSecretId}
@@ -149,11 +149,11 @@ const KeyPair = ({
{keyPair.tags?.map((tag, index) => ( {keyPair.tags?.map((tag, index) => (
index < 2 && <div key={keyPair.pos} className={`ml-2 px-1.5 ${tagData.filter(tagDp => tagDp._id === tag._id)[0]?.color} rounded-sm text-sm ${tagData.filter(tagDp => tagDp._id === tag._id)[0]?.colorText} flex items-center`}> index < 2 && <div key={keyPair.pos} className={`ml-2 px-1.5 ${tagData.filter(tagDp => tagDp._id === tag._id)[0]?.color} rounded-sm text-sm ${tagData.filter(tagDp => tagDp._id === tag._id)[0]?.colorText} flex items-center`}>
<span className='mb-0.5 cursor-default'>{tag.name}</span> <span className='mb-0.5 cursor-default'>{tag.name}</span>
<FontAwesomeIcon icon={faXmark} className="ml-1 cursor-pointer p-1" onClick={() => modifyTags(keyPair.tags.filter(ttag => ttag._id !== tag._id), keyPair.pos)}/> <FontAwesomeIcon icon={faXmark} className="ml-1 cursor-pointer p-1" onClick={() => modifyTags(keyPair.tags.filter(ttag => ttag._id !== tag._id), keyPair.id)}/>
</div> </div>
))} ))}
<AddTagsMenu allTags={tags} currentTags={keyPair.tags} modifyTags={modifyTags} position={keyPair.pos} /> <AddTagsMenu allTags={tags} currentTags={keyPair.tags} modifyTags={modifyTags} id={keyPair.id} />
</div> </div>
</div> </div>
<div <div
+11 -11
View File
@@ -33,10 +33,10 @@ export interface DeleteRowFunctionProps {
interface SideBarProps { interface SideBarProps {
toggleSidebar: (value: string) => void; toggleSidebar: (value: string) => void;
data: SecretProps[]; data: SecretProps[];
modifyKey: (value: string, position: number) => void; modifyKey: (value: string, id: string) => void;
modifyValue: (value: string, position: number) => void; modifyValue: (value: string, id: string) => void;
modifyValueOverride: (value: string | undefined, position: number) => void; modifyValueOverride: (value: string | undefined, id: string) => void;
modifyComment: (value: string, position: number) => void; modifyComment: (value: string, id: string) => void;
buttonReady: boolean; buttonReady: boolean;
savePush: () => void; savePush: () => void;
sharedToHide: string[]; sharedToHide: string[];
@@ -110,7 +110,7 @@ const SideBar = ({
<DashboardInputField <DashboardInputField
onChangeHandler={modifyKey} onChangeHandler={modifyKey}
type="varName" type="varName"
position={data[0]?.pos} id={data[0]?.id}
value={data[0]?.key} value={data[0]?.key}
isDuplicate={false} isDuplicate={false}
blurred={false} blurred={false}
@@ -128,14 +128,14 @@ const SideBar = ({
<DashboardInputField <DashboardInputField
onChangeHandler={modifyValue} onChangeHandler={modifyValue}
type="value" type="value"
position={data[0].pos} id={data[0].id}
value={data[0]?.value} value={data[0]?.value}
isDuplicate={false} isDuplicate={false}
blurred blurred
/> />
</div> </div>
<div className="absolute bg-bunker-800 right-[1.07rem] top-[1.6rem] z-50"> <div className="absolute bg-bunker-800 right-[1.07rem] top-[1.6rem] z-50">
<GenerateSecretMenu modifyValue={modifyValue} position={data[0]?.pos} /> <GenerateSecretMenu modifyValue={modifyValue} id={data[0]?.id} />
</div> </div>
</div> </div>
) : ( ) : (
@@ -154,7 +154,7 @@ const SideBar = ({
enabled={overrideEnabled} enabled={overrideEnabled}
setEnabled={setOverrideEnabled} setEnabled={setOverrideEnabled}
addOverride={modifyValueOverride} addOverride={modifyValueOverride}
pos={data[0]?.pos} id={data[0]?.id}
/> />
</div> </div>
)} )}
@@ -167,14 +167,14 @@ const SideBar = ({
<DashboardInputField <DashboardInputField
onChangeHandler={modifyValueOverride} onChangeHandler={modifyValueOverride}
type="value" type="value"
position={data[0]?.pos} id={data[0]?.id}
value={overrideEnabled ? data[0]?.valueOverride : data[0]?.value} value={overrideEnabled ? data[0]?.valueOverride : data[0]?.value}
isDuplicate={false} isDuplicate={false}
blurred blurred
/> />
</div> </div>
<div className="absolute right-[0.57rem] top-[0.3rem] z-50"> <div className="absolute right-[0.57rem] top-[0.3rem] z-50">
<GenerateSecretMenu modifyValue={modifyValueOverride} position={data[0]?.pos} /> <GenerateSecretMenu modifyValue={modifyValueOverride} id={data[0]?.id} />
</div> </div>
</div> </div>
</div> </div>
@@ -182,7 +182,7 @@ const SideBar = ({
<CommentField <CommentField
comment={data[0]?.comment} comment={data[0]?.comment}
modifyComment={modifyComment} modifyComment={modifyComment}
position={data[0]?.pos} id={data[0]?.id}
/> />
</div> </div>
)} )}
@@ -5,13 +5,13 @@ import * as Popover from '@radix-ui/react-popover';
type Props = { type Props = {
children: any; children: any;
text: string; text: string;
onChangeHandler: (value: string, position: number) => void; onChangeHandler: (value: string, id: string) => void;
position: number; id: string;
}; };
export type PopoverProps = Props; export type PopoverProps = Props;
export const PopoverObject = ({children, text, onChangeHandler, position}: Props) => ( export const PopoverObject = ({children, text, onChangeHandler, id}: Props) => (
<Popover.Root> <Popover.Root>
<Popover.Trigger asChild className='data-[state=open]:outline data-[state=open]:outline-primary data-[state=closed]:hover:outline data-[state=closed]:hover:outline-mineshaft-400'> <Popover.Trigger asChild className='data-[state=open]:outline data-[state=open]:outline-primary data-[state=closed]:hover:outline data-[state=closed]:hover:outline-mineshaft-400'>
{children} {children}
@@ -26,7 +26,7 @@ export const PopoverObject = ({children, text, onChangeHandler, position}: Props
<div className="flex flex-col pt-2 dark"> <div className="flex flex-col pt-2 dark">
<p className="text-bunker-200 text-[15px] leading-[0px] font-medium mb-5">Comment</p> <p className="text-bunker-200 text-[15px] leading-[0px] font-medium mb-5">Comment</p>
<textarea <textarea
onChange={(e) => onChangeHandler(e.target.value, position)} onChange={(e) => onChangeHandler(e.target.value, id)}
// type={type} // type={type}
value={text} value={text}
className='z-10 dark:[color-scheme:dark] peer h-[20rem] ph-no-capture bg-bunker-600 border border-mineshaft-500 rounded-md py-2.5 caret-bunker-200 text-sm px-2 w-full outline-none text-bunker-300 focus:text-bunker-100 placeholder:text-bunker-400 placeholder:focus:text-transparent placeholder duration-200' className='z-10 dark:[color-scheme:dark] peer h-[20rem] ph-no-capture bg-bunker-600 border border-mineshaft-500 rounded-md py-2.5 caret-bunker-200 text-sm px-2 w-full outline-none text-bunker-300 focus:text-bunker-100 placeholder:text-bunker-400 placeholder:focus:text-transparent placeholder duration-200'
@@ -0,0 +1,38 @@
import { apiRequest } from "@app/config/request";
interface RequestType {
method: string;
secret: {
type: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentCiphertext: string;
secretCommentIV: string;
secretCommentTag: string;
tags: string[];
}
}
const batchSecrets = async ({
workspaceId,
environment,
requests
}: {
workspaceId: string;
environment: string;
requests: RequestType[];
}) => {
const { data } = await apiRequest.post('/api/v2/secrets/batch', {
workspaceId,
environment,
requests
});
return data;
}
export default batchSecrets;
+365 -179
View File
@@ -30,6 +30,7 @@ import DropZone from '@app/components/dashboard/DropZone';
import KeyPair from '@app/components/dashboard/KeyPair'; import KeyPair from '@app/components/dashboard/KeyPair';
import SideBar from '@app/components/dashboard/SideBar'; import SideBar from '@app/components/dashboard/SideBar';
import NavHeader from '@app/components/navigation/NavHeader'; import NavHeader from '@app/components/navigation/NavHeader';
import { decryptAssymmetric, decryptSymmetric } from '@app/components/utilities/cryptography/crypto';
import guidGenerator from '@app/components/utilities/randomId'; import guidGenerator from '@app/components/utilities/randomId';
import encryptSecrets from '@app/components/utilities/secrets/encryptSecrets'; import encryptSecrets from '@app/components/utilities/secrets/encryptSecrets';
import getSecretsForProject from '@app/components/utilities/secrets/getSecretsForProject'; import getSecretsForProject from '@app/components/utilities/secrets/getSecretsForProject';
@@ -41,12 +42,14 @@ import performSecretRollback from '@app/ee/api/secrets/PerformSecretRollback';
import PITRecoverySidebar from '@app/ee/components/PITRecoverySidebar'; import PITRecoverySidebar from '@app/ee/components/PITRecoverySidebar';
import { useLeaveConfirm } from '@app/hooks'; import { useLeaveConfirm } from '@app/hooks';
import addSecrets from '../api/files/AddSecrets'; // import addSecrets from '../api/files/AddSecrets';
import deleteSecrets from '../api/files/DeleteSecrets'; // import deleteSecrets from '../api/files/DeleteSecrets';
import updateSecrets from '../api/files/UpdateSecrets'; // import updateSecrets from '../api/files/UpdateSecrets';
import batchSecrets from '../api/files/batchSecrets';
import getUser from '../api/user/getUser'; import getUser from '../api/user/getUser';
import checkUserAction from '../api/userActions/checkUserAction'; import checkUserAction from '../api/userActions/checkUserAction';
import registerUserAction from '../api/userActions/registerUserAction'; import registerUserAction from '../api/userActions/registerUserAction';
import getLatestFileKey from '../api/workspace/getLatestFileKey';
import getWorkspaceEnvironments from '../api/workspace/getWorkspaceEnvironments'; import getWorkspaceEnvironments from '../api/workspace/getWorkspaceEnvironments';
import getWorkspaces from '../api/workspace/getWorkspaces'; import getWorkspaces from '../api/workspace/getWorkspaces';
import getWorkspaceTags from '../api/workspace/getWorkspaceTags'; import getWorkspaceTags from '../api/workspace/getWorkspaceTags';
@@ -94,6 +97,32 @@ interface SnapshotProps {
}[]; }[];
} }
interface EncryptedSecretProps {
_id: string;
createdAt: string;
environment: string;
secretCommentCiphertext: string;
secretCommentIV: string;
secretCommentTag: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
type: 'personal' | 'shared';
tags: Tag[];
}
interface SecretProps {
key: string;
value: string | undefined;
type: 'personal' | 'shared';
comment: string;
id: string;
tags: Tag[];
}
/** /**
* this function finds the teh duplicates in an array * this function finds the teh duplicates in an array
* @param arr - array of anything (e.g., with secret keys and types (personal/shared)) * @param arr - array of anything (e.g., with secret keys and types (personal/shared))
@@ -302,50 +331,50 @@ export default function Dashboard() {
); );
}; };
const modifyValue = (value: string, pos: number) => { const modifyValue = (value: string, id: string) => {
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, value } : e))); setData((oldData) => oldData?.map((e) => (e.id === id ? { ...e, value } : e)));
setHasUnsavedChanges(true); setHasUnsavedChanges(true);
}; };
const modifyValueOverride = (valueOverride: string | undefined, pos: number) => { const modifyValueOverride = (valueOverride: string | undefined, id: string) => {
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, valueOverride } : e))); setData((oldData) => oldData?.map((e) => (e.id === id ? { ...e, valueOverride } : e)));
setHasUnsavedChanges(true); setHasUnsavedChanges(true);
}; };
const modifyKey = (key: string, pos: number) => { const modifyKey = (key: string, id: string) => {
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, key } : e))); setData((oldData) => oldData?.map((e) => (e.id === id ? { ...e, key } : e)));
setHasUnsavedChanges(true); setHasUnsavedChanges(true);
}; };
const modifyComment = (comment: string, pos: number) => { const modifyComment = (comment: string, id: string) => {
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, comment } : e))); setData((oldData) => oldData?.map((e) => (e.id === id ? { ...e, comment } : e)));
setHasUnsavedChanges(true); setHasUnsavedChanges(true);
}; };
const modifyTags = (tags: Tag[], pos: number) => { const modifyTags = (tags: Tag[], id: string) => {
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, tags } : e))); setData((oldData) => oldData?.map((e) => (e.id === id ? { ...e, tags } : e)));
setHasUnsavedChanges(true); setHasUnsavedChanges(true);
}; };
// For speed purposes and better perforamance, we are using useCallback // For speed purposes and better perforamance, we are using useCallback
const listenChangeValue = useCallback((value: string, pos: number) => { const listenChangeValue = useCallback((value: string, id: string) => {
modifyValue(value, pos); modifyValue(value, id);
}, []); }, []);
const listenChangeValueOverride = useCallback((value: string | undefined, pos: number) => { const listenChangeValueOverride = useCallback((value: string | undefined, id: string) => {
modifyValueOverride(value, pos); modifyValueOverride(value, id);
}, []); }, []);
const listenChangeKey = useCallback((value: string, pos: number) => { const listenChangeKey = useCallback((value: string, id: string) => {
modifyKey(value, pos); modifyKey(value, id);
}, []); }, []);
const listenChangeComment = useCallback((value: string, pos: number) => { const listenChangeComment = useCallback((value: string, id: string) => {
modifyComment(value, pos); modifyComment(value, id);
}, []); }, []);
const listenChangeTags = useCallback((value: Tag[], pos: number) => { const listenChangeTags = useCallback((value: Tag[], id: string) => {
modifyTags(value, pos); modifyTags(value, id);
}, []); }, []);
/** /**
@@ -353,174 +382,331 @@ export default function Dashboard() {
*/ */
// TODO(akhilmhdh): split and make it small // TODO(akhilmhdh): split and make it small
const savePush = async (dataToPush?: SecretDataProps[]) => { const savePush = async (dataToPush?: SecretDataProps[]) => {
setSaveLoading(true); try {
let newData: SecretDataProps[] | null | undefined; setSaveLoading(true);
// dataToPush is mostly used for rollbacks, otherwise we always take the current state data let newData: SecretDataProps[] | null | undefined;
if ((dataToPush ?? [])?.length > 0) { // dataToPush is mostly used for rollbacks, otherwise we always take the current state data
newData = dataToPush; if ((dataToPush ?? [])?.length > 0) {
} else { newData = dataToPush;
newData = data; } else {
} newData = data;
}
// Checking if any of the secret keys start with a number - if so, don't do anything // Checking if any of the secret keys start with a number - if so, don't do anything
const nameErrors = !newData! const nameErrors = !newData!
.map((secret) => !Number.isNaN(Number(secret.key.charAt(0)))) .map((secret) => !Number.isNaN(Number(secret.key.charAt(0))))
.every((v) => v === false); .every((v) => v === false);
const duplicatesExist = const duplicatesExist =
findDuplicates(data!.map((item: SecretDataProps) => item.key)).length > 0; findDuplicates(data!.map((item: SecretDataProps) => item.key)).length > 0;
if (nameErrors) { if (nameErrors) {
setSaveLoading(false); setSaveLoading(false);
return createNotification({ return createNotification({
text: 'Solve all name errors before saving secrets.', text: 'Solve all name errors before saving secrets.',
type: 'error' type: 'error'
}); });
} }
if (duplicatesExist) { if (duplicatesExist) {
setSaveLoading(false); setSaveLoading(false);
return createNotification({ return createNotification({
text: 'Remove duplicated secret names before saving.', text: 'Remove duplicated secret names before saving.',
type: 'error' type: 'error'
}); });
} }
if (selectedEnv?.isWriteDenied) { if (selectedEnv?.isWriteDenied) {
setSaveLoading(false); setSaveLoading(false);
return createNotification({ return createNotification({
text: 'You are not allowed to edit this environment', text: 'You are not allowed to edit this environment',
type: 'error' type: 'error'
}); });
} }
// Once "Save changes" is clicked, disable that button // Once "Save changes" is clicked, disable that button
setHasUnsavedChanges(false); setHasUnsavedChanges(false);
const secretsToBeDeleted = initialData! const secretsToBeDeleted = initialData!
.filter(
(initDataPoint) =>
!newData!.map((newDataPoint) => newDataPoint.id).includes(initDataPoint.id)
)
.map((secret) => secret.id);
console.log('delete', secretsToBeDeleted.length);
const secretsToBeAdded = newData!.filter(
(newDataPoint) =>
!initialData!.map((initDataPoint) => initDataPoint.id).includes(newDataPoint.id)
);
console.log('add', secretsToBeAdded.length);
const secretsToBeUpdated = newData!.filter((newDataPoint) =>
initialData!
.filter(
(initDataPoint) =>
newData!.map((dataPoint) => dataPoint.id).includes(initDataPoint.id) &&
(newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].value !==
initDataPoint.value ||
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !==
initDataPoint.key ||
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].comment !==
initDataPoint.comment ||
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]?.tags !==
initDataPoint?.tags)
)
.map((secret) => secret.id)
.includes(newDataPoint.id)
);
console.log('update', secretsToBeUpdated.length);
const newOverrides = newData!.filter(
(newDataPoint) => newDataPoint.valueOverride !== undefined
);
const initOverrides = initialData!.filter(
(initDataPoint) => initDataPoint.valueOverride !== undefined
);
const overridesToBeDeleted = initOverrides
.filter(
(initDataPoint) =>
!newOverrides!.map((newDataPoint) => newDataPoint.id).includes(initDataPoint.id)
)
.map((secret) => String(secret.idOverride));
console.log('override delete', overridesToBeDeleted.length);
const overridesToBeAdded = newOverrides!
.filter(
(newDataPoint) =>
!initOverrides.map((initDataPoint) => initDataPoint.id).includes(newDataPoint.id)
)
.map((override) => ({
pos: override.pos,
key: override.key,
value: String(override.valueOverride),
valueOverride: override.valueOverride,
comment: '',
id: String(override.idOverride),
idOverride: String(override.idOverride),
tags: override.tags
}));
console.log('override add', overridesToBeAdded.length);
const overridesToBeUpdated = newOverrides!
.filter((newDataPoint) =>
initOverrides
.filter( .filter(
(initDataPoint) => (initDataPoint) =>
newOverrides!.map((dataPoint) => dataPoint.id).includes(initDataPoint.id) && !newData!.map((newDataPoint) => newDataPoint.id).includes(initDataPoint.id)
(newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]
.valueOverride !== initDataPoint.valueOverride ||
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !==
initDataPoint.key ||
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]
.comment !== initDataPoint.comment ||
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]?.tags !== initDataPoint?.tags)
) )
.map((secret) => secret.id) .map((secret) => secret.id);
.includes(newDataPoint.id) console.log('delete', secretsToBeDeleted.length);
)
.map((override) => ({
pos: override.pos,
key: override.key,
value: String(override.valueOverride),
valueOverride: override.valueOverride,
comment: '',
id: String(override.idOverride),
idOverride: String(override.idOverride),
tags: override.tags
}));
console.log('override update', overridesToBeUpdated.length);
if (secretsToBeDeleted.concat(overridesToBeDeleted).length > 0) { const secretsToBeAdded = newData!.filter(
await deleteSecrets({ secretIds: secretsToBeDeleted.concat(overridesToBeDeleted) }); (newDataPoint) =>
} !initialData!.map((initDataPoint) => initDataPoint.id).includes(newDataPoint.id)
if (selectedEnv && secretsToBeAdded.concat(overridesToBeAdded).length > 0) { );
const secrets = await encryptSecrets({ console.log('add', secretsToBeAdded.length);
secretsToEncrypt: secretsToBeAdded.concat(overridesToBeAdded),
workspaceId,
env: selectedEnv.slug
});
if (secrets) await addSecrets({ secrets, env: selectedEnv.slug, workspaceId });
}
if (selectedEnv && !selectedEnv.isReadDenied && secretsToBeUpdated.concat(overridesToBeUpdated).length > 0) {
const secrets = await encryptSecrets({
secretsToEncrypt: secretsToBeUpdated.concat(overridesToBeUpdated),
workspaceId,
env: selectedEnv.slug
});
if (secrets) await updateSecrets({ secrets });
}
setInitialData(structuredClone(newData)); const secretsToBeUpdated = newData!.filter((newDataPoint) =>
initialData!
.filter(
(initDataPoint) =>
newData!.map((dataPoint) => dataPoint.id).includes(initDataPoint.id) &&
(newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].value !==
initDataPoint.value ||
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !==
initDataPoint.key ||
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].comment !==
initDataPoint.comment ||
JSON.stringify(newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]?.tags) !==
JSON.stringify(initDataPoint?.tags))
)
.map((secret) => secret.id)
.includes(newDataPoint.id)
);
console.log('update', secretsToBeUpdated.length);
// If this user has never saved environment variables before, show them a prompt to read docs const newOverrides = newData!.filter(
if (!hasUserEverPushed) { (newDataPoint) => newDataPoint.valueOverride !== undefined
setCheckDocsPopUpVisible(true); );
await registerUserAction({ action: 'first_time_secrets_pushed' }); const initOverrides = initialData!.filter(
} (initDataPoint) => initDataPoint.valueOverride !== undefined
);
// increasing the number of project commits const overridesToBeDeleted = initOverrides
setNumSnapshots((numSnapshots ?? 0) + 1); .filter(
setSaveLoading(false); (initDataPoint) =>
!newOverrides!.map((newDataPoint) => newDataPoint.id).includes(initDataPoint.id)
)
.map((secret) => String(secret.idOverride));
console.log('override delete', overridesToBeDeleted.length);
const overridesToBeAdded = newOverrides!
.filter(
(newDataPoint) =>
!initOverrides.map((initDataPoint) => initDataPoint.id).includes(newDataPoint.id)
)
.map((override) => ({
pos: override.pos,
key: override.key,
value: String(override.valueOverride),
valueOverride: override.valueOverride,
comment: '',
id: String(override.idOverride),
idOverride: String(override.idOverride),
tags: override.tags
}));
console.log('override add', overridesToBeAdded.length);
const overridesToBeUpdated = newOverrides!
.filter((newDataPoint) =>
initOverrides
.filter(
(initDataPoint) =>
newOverrides!.map((dataPoint) => dataPoint.id).includes(initDataPoint.id) &&
(newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]
.valueOverride !== initDataPoint.valueOverride ||
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !==
initDataPoint.key ||
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]
.comment !== initDataPoint.comment ||
JSON.stringify(newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]?.tags) !==
JSON.stringify(initDataPoint?.tags))
)
.map((secret) => secret.id)
.includes(newDataPoint.id)
)
.map((override) => ({
pos: override.pos,
key: override.key,
value: String(override.valueOverride),
valueOverride: override.valueOverride,
comment: '',
id: String(override.idOverride),
idOverride: String(override.idOverride),
tags: override.tags
}));
console.log('override update', overridesToBeUpdated.length);
const requests: any = []; // TODO: fix any
if (secretsToBeDeleted.concat(overridesToBeDeleted).length > 0) {
secretsToBeDeleted.concat(overridesToBeDeleted).forEach((_id: string) => {
requests.push({
method: 'DELETE',
secret: {
_id
}
});
});
}
if (selectedEnv && secretsToBeAdded.concat(overridesToBeAdded).length > 0) {
const secrets = await encryptSecrets({
secretsToEncrypt: secretsToBeAdded.concat(overridesToBeAdded),
workspaceId,
env: selectedEnv.slug
});
if (secrets) {
secrets.forEach((secret) => {
requests.push({
method: 'POST',
secret: {
type: secret.type,
secretKeyCiphertext: secret.secretKeyCiphertext,
secretKeyIV: secret.secretKeyIV,
secretKeyTag: secret.secretKeyTag,
secretValueCiphertext: secret.secretValueCiphertext,
secretValueIV: secret.secretValueIV,
secretValueTag: secret.secretValueTag,
secretCommentCiphertext: secret.secretCommentCiphertext,
secretCommentIV: secret.secretCommentIV,
secretCommentTag: secret.secretCommentTag,
tags: secret.tags
}
})
});
}
}
if (selectedEnv && !selectedEnv.isReadDenied && secretsToBeUpdated.concat(overridesToBeUpdated).length > 0) {
const secrets = await encryptSecrets({
secretsToEncrypt: secretsToBeUpdated.concat(overridesToBeUpdated),
workspaceId,
env: selectedEnv.slug
});
if (secrets) {
secrets.forEach((secret) => {
requests.push({
method: 'PATCH',
secret: {
_id: secret.id,
type: secret.type,
secretKeyCiphertext: secret.secretKeyCiphertext,
secretKeyIV: secret.secretKeyIV,
secretKeyTag: secret.secretKeyTag,
secretValueCiphertext: secret.secretValueCiphertext,
secretValueIV: secret.secretValueIV,
secretValueTag: secret.secretValueTag,
secretCommentCiphertext: secret.secretCommentCiphertext,
secretCommentIV: secret.secretCommentIV,
secretCommentTag: secret.secretCommentTag,
tags: secret.tags
}
});
});
}
}
let newSecrets;
if (selectedEnv && requests.length > 0) {
newSecrets = await batchSecrets({
workspaceId,
environment: selectedEnv.slug,
requests
});
}
let formattedNewDecryptedKeys;
if (newSecrets.createdSecrets) {
const latestKey = await getLatestFileKey({ workspaceId });
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
const tempDecryptedSecrets: SecretProps[] = [];
if (latestKey) {
// assymmetrically decrypt symmetric key with local private key
const key = decryptAssymmetric({
ciphertext: latestKey.latestKey.encryptedKey,
nonce: latestKey.latestKey.nonce,
publicKey: latestKey.latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
// decrypt secret keys, values, and comments
newSecrets.createdSecrets.forEach((secret: EncryptedSecretProps) => {
const plainTextKey = decryptSymmetric({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key
});
let plainTextValue;
if (secret.secretValueCiphertext !== undefined) {
plainTextValue = decryptSymmetric({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key
});
} else {
plainTextValue = undefined;
}
let plainTextComment;
if (secret.secretCommentCiphertext) {
plainTextComment = decryptSymmetric({
ciphertext: secret.secretCommentCiphertext,
iv: secret.secretCommentIV,
tag: secret.secretCommentTag,
key
});
} else {
plainTextComment = '';
}
tempDecryptedSecrets.push({
id: secret._id,
key: plainTextKey,
value: plainTextValue,
type: secret.type,
comment: plainTextComment,
tags: secret.tags
});
});
}
const secretKeys = [...new Set(tempDecryptedSecrets.map((secret) => secret.key))];
formattedNewDecryptedKeys = secretKeys.map((key, index) => ({
id: tempDecryptedSecrets.filter((secret) => secret.key === key && secret.type === 'shared')[0]
?.id,
idOverride: tempDecryptedSecrets.filter(
(secret) => secret.key === key && secret.type === 'personal'
)[0]?.id,
pos: (newData?.filter(dp => !dp.id.includes('-'))?.length ?? 0) + index,
key,
value: tempDecryptedSecrets.filter(
(secret) => secret.key === key && secret.type === 'shared'
)[0]?.value,
valueOverride: tempDecryptedSecrets.filter(
(secret) => secret.key === key && secret.type === 'personal'
)[0]?.value,
comment: tempDecryptedSecrets.filter(
(secret) => secret.key === key && secret.type === 'shared'
)[0]?.comment,
tags: tempDecryptedSecrets.filter(
(secret) => secret.key === key && secret.type === 'shared'
)[0]?.tags
}));
setInitialData(structuredClone(newData?.filter(dp => !dp.id.includes('-')).concat(formattedNewDecryptedKeys.filter(dk => dk.id))));
setData(structuredClone(newData?.filter(dp => !dp.id.includes('-')).concat(formattedNewDecryptedKeys.filter(dk => dk.id))))
} else {
setInitialData(structuredClone(newData));
}
// If this user has never saved environment variables before, show them a prompt to read docs
if (!hasUserEverPushed) {
setCheckDocsPopUpVisible(true);
await registerUserAction({ action: 'first_time_secrets_pushed' });
}
// increasing the number of project commits
setNumSnapshots((numSnapshots ?? 0) + 1);
setSaveLoading(false);
createNotification({
text: `Successfully saved secrets.`,
type: 'success'
});
} catch (error) {
console.log("Something went wrong while saving secrets: ", error)
createNotification({
text: `Something went wrong while saving secrets.`,
type: 'error'
});
}
return undefined; return undefined;
}; };
@@ -820,7 +1006,7 @@ export default function Dashboard() {
</div> </div>
</div> </div>
<div className="w-[calc(10%)] border-r border-mineshaft-600"> <div className="w-[calc(10%)] border-r border-mineshaft-600">
<div className="flex items-center max-h-16"> <div className="flex items-center max-h-16 overflow-hidden">
<div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-3/12'>Comment</div> <div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-3/12'>Comment</div>
</div> </div>
</div> </div>