Merge branch 'main' into feat/#31

This commit is contained in:
mv-turtle
2023-01-12 17:24:48 -08:00
committed by GitHub
46 changed files with 895 additions and 765 deletions
+4 -38
View File
@@ -1,27 +1,11 @@
import React from "react";
import { Switch } from "@headlessui/react";
interface OverrideProps {
id: string;
keyName: string;
value: string;
pos: number;
comment: string;
}
interface ToggleProps {
enabled: boolean;
setEnabled: (value: boolean) => void;
addOverride: (value: OverrideProps) => void;
keyName: string;
value: string;
addOverride: (value: string | undefined, pos: number) => void;
pos: number;
id: string;
comment: string;
deleteOverride: (id: string) => void;
sharedToHide: string[];
setSharedToHide: (values: string[]) => void;
}
/**
@@ -30,41 +14,23 @@ interface ToggleProps {
* @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.addOverride - a function that adds an override to a certain secret
* @param {string} obj.keyName - key of a certain secret
* @param {string} obj.value - value of a certain secret
* @param {number} obj.pos - position of a certain secret
#TODO: make the secret id persistent?
* @param {string} obj.id - id of a certain secret (NOTE: THIS IS THE ID OF THE MAIN SECRET - NOT OF AN OVERRIDE)
* @param {function} obj.deleteOverride - a function that deleted an override for a certain secret
* @param {string[]} obj.sharedToHide - an array of shared secrets that we want to hide visually because they are overriden.
* @param {function} obj.setSharedToHide - a function that updates the array of secrets that we want to hide visually
* @returns
*/
export default function Toggle ({
enabled,
setEnabled,
addOverride,
keyName,
value,
pos,
id,
comment,
deleteOverride,
sharedToHide,
setSharedToHide
pos
}: ToggleProps): JSX.Element {
return (
<Switch
checked={enabled}
onChange={() => {
if (enabled == false) {
addOverride({ id, keyName, value, pos, comment });
setSharedToHide([
...sharedToHide!,
id
])
addOverride('', pos);
} else {
deleteOverride(id);
addOverride(undefined, pos);
}
setEnabled(!enabled);
}}
+1 -2
View File
@@ -3,7 +3,6 @@ import Image from "next/image";
import { IconProp } from "@fortawesome/fontawesome-svg-core";
import {
FontAwesomeIcon,
FontAwesomeIconProps,
} from "@fortawesome/react-fontawesome";
const classNames = require("classnames");
@@ -101,7 +100,7 @@ export default function Button(props: ButtonProps): JSX.Element {
<div
className={`${
props.loading == true ? "opacity-100" : "opacity-0"
} absolute flex items-center px-2 duration-200`}
} absolute flex items-center px-3 bg-primary duration-200 w-full`}
>
<Image
src="/images/loading/loadingblack.gif"
@@ -9,7 +9,7 @@ const REGEX = /([$]{.*?})/g;
interface DashboardInputFieldProps {
position: number;
onChangeHandler: (value: string, position: number) => void;
value: string;
value: string | undefined;
type: 'varName' | 'value';
blurred?: boolean;
isDuplicate?: boolean;
@@ -47,7 +47,7 @@ const DashboardInputField = ({
};
if (type === 'varName') {
const startsWithNumber = !isNaN(Number(value.charAt(0))) && value != '';
const startsWithNumber = !isNaN(Number(value?.charAt(0))) && value != '';
const error = startsWithNumber || isDuplicate;
return (
@@ -141,7 +141,7 @@ const DashboardInputField = ({
{blurred && (
<div className="absolute flex flex-row items-center z-20 peer pr-2 bg-bunker-800 group-hover:hidden peer-hover:hidden peer-focus:hidden peer-active:invisible h-9 w-full rounded-md text-gray-400/50 text-clip">
<div className="px-2 flex flex-row items-center overflow-x-scroll no-scrollbar no-scrollbar::-webkit-scrollbar">
{value.split('').map(() => (
{value?.split('').map(() => (
<FontAwesomeIcon
key={guidGenerator()}
className="text-xxs mx-0.5"
@@ -2,21 +2,12 @@ import { Fragment } from 'react';
import { useTranslation } from "next-i18next";
import { faDownload } from '@fortawesome/free-solid-svg-icons';
import { Menu, Transition } from '@headlessui/react';
import { SecretDataProps } from 'public/data/frequentInterfaces';
import Button from '../basic/buttons/Button';
import downloadDotEnv from '../utilities/secrets/downloadDotEnv';
import downloadYaml from '../utilities/secrets/downloadYaml';
interface SecretDataProps {
type: 'personal' | 'shared';
pos: number;
key: string;
value: string;
id: string;
comment: string;
}
/**
* This is the menu that is used to download secrets as .env ad .yml files (in future we may have more options)
* @param {object} obj
+9 -14
View File
@@ -1,22 +1,15 @@
import React from 'react';
import { faEllipsis, faShuffle, faX } from '@fortawesome/free-solid-svg-icons';
import { faEllipsis } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { SecretDataProps } from 'public/data/frequentInterfaces';
import Button from '../basic/buttons/Button';
import DashboardInputField from './DashboardInputField';
interface SecretDataProps {
type: 'personal' | 'shared';
pos: number;
key: string;
value: string;
id: string;
}
interface KeyPairProps {
keyPair: SecretDataProps;
modifyKey: (value: string, position: number) => void;
modifyValue: (value: string, position: number) => void;
modifyValueOverride: (value: string, position: number) => void;
isBlurred: boolean;
isDuplicate: boolean;
toggleSidebar: (id: string) => void;
@@ -30,6 +23,7 @@ interface KeyPairProps {
* @param {String[]} obj.keyPair - data related to the environment variable (id, pos, key, value, public/private)
* @param {function} obj.modifyKey - modify the key of a certain environment variable
* @param {function} obj.modifyValue - modify the value of a certain environment variable
* @param {function} obj.modifyValueOverride - modify the value of a certain environment variable if it is overriden
* @param {boolean} obj.isBlurred - if the blurring setting is turned on
* @param {boolean} obj.isDuplicate - list of all the duplicates secret names on the dashboard
* @param {function} obj.toggleSidebar - open/close/switch sidebar
@@ -41,6 +35,7 @@ const KeyPair = ({
keyPair,
modifyKey,
modifyValue,
modifyValueOverride,
isBlurred,
isDuplicate,
toggleSidebar,
@@ -50,7 +45,7 @@ const KeyPair = ({
return (
<div className={`mx-1 flex flex-col items-center ml-1 ${isSnapshot && "pointer-events-none"} ${keyPair.id == sidebarSecretId && "bg-mineshaft-500 duration-200"} rounded-md`}>
<div className="relative flex flex-row justify-between w-full max-w-5xl mr-auto max-h-14 my-1 items-start px-1">
{keyPair.type == "personal" && <div className="group font-normal group absolute top-[1rem] left-[0.2rem] z-40 inline-block text-gray-300 underline hover:text-primary duration-200">
{keyPair.valueOverride && <div className="group font-normal group absolute top-[1rem] left-[0.2rem] z-40 inline-block text-gray-300 underline hover:text-primary duration-200">
<div className='w-1 h-1 rounded-full bg-primary z-40'></div>
<span className="absolute z-50 hidden group-hover:flex group-hover:animate-popdown duration-200 w-[10.5rem] -left-[0.4rem] -top-[1.7rem] translate-y-full px-2 py-2 bg-mineshaft-500 rounded-b-md rounded-r-md text-center text-gray-100 text-sm after:content-[''] after:absolute after:left-0 after:bottom-[100%] after:-translate-x-0 after:border-8 after:border-x-transparent after:border-t-transparent after:border-b-mineshaft-500">
This secret is overriden
@@ -70,12 +65,12 @@ const KeyPair = ({
<div className="w-full min-w-xl">
<div className={`flex min-w-xl items-center ${!isSnapshot && "pr-1.5"} rounded-lg mt-4 md:mt-0 max-h-10`}>
<DashboardInputField
onChangeHandler={modifyValue}
onChangeHandler={keyPair.valueOverride ? modifyValueOverride : modifyValue}
type="value"
position={keyPair.pos}
value={keyPair.value}
value={keyPair.valueOverride ? keyPair.valueOverride : keyPair.value}
blurred={isBlurred}
override={keyPair.type == "personal"}
override={Boolean(keyPair.valueOverride)}
/>
</div>
</div>
+18 -35
View File
@@ -16,18 +16,15 @@ import GenerateSecretMenu from './GenerateSecretMenu';
interface SecretProps {
key: string;
value: string;
valueOverride: string | undefined;
pos: number;
type: string;
id: string;
comment: string;
}
interface OverrideProps {
id: string;
keyName: string;
value: string;
pos: number;
comment: string;
valueOverride: string;
}
export interface DeleteRowFunctionProps {
ids: string[];
@@ -39,9 +36,8 @@ interface SideBarProps {
data: SecretProps[];
modifyKey: (value: string, position: number) => void;
modifyValue: (value: string, position: number) => void;
modifyValueOverride: (value: string | undefined, position: number) => void;
modifyComment: (value: string, position: number) => void;
addOverride: (value: OverrideProps) => void;
deleteOverride: (id: string) => void;
buttonReady: boolean;
savePush: () => void;
sharedToHide: string[];
@@ -55,12 +51,9 @@ interface SideBarProps {
* @param {SecretProps[]} obj.data - data of a certain key valeu pair
* @param {function} obj.modifyKey - function that modifies the secret key
* @param {function} obj.modifyValue - function that modifies the secret value
* @param {function} obj.addOverride - override a certain secret
* @param {function} obj.deleteOverride - delete the personal override for a certain secret
* @param {function} obj.modifyValueOverride - function that modifies the secret value if it is an override
* @param {boolean} obj.buttonReady - is the button for saving chagnes active
* @param {function} obj.savePush - save changes andp ush secrets
* @param {string[]} obj.sharedToHide - an array of shared secrets that we want to hide visually because they are overriden.
* @param {function} obj.setSharedToHide - a function that updates the array of secrets that we want to hide visually
* @param {function} obj.deleteRow - a function to delete a certain keyPair
* @returns the sidebar with 'secret's settings'
*/
@@ -69,17 +62,14 @@ const SideBar = ({
data,
modifyKey,
modifyValue,
modifyValueOverride,
modifyComment,
addOverride,
deleteOverride,
buttonReady,
savePush,
sharedToHide,
setSharedToHide,
deleteRow
}: SideBarProps) => {
const [isLoading, setIsLoading] = useState(false);
const [overrideEnabled, setOverrideEnabled] = useState(data.map(secret => secret.type).includes("personal"));
const [overrideEnabled, setOverrideEnabled] = useState(data[0].valueOverride != undefined);
const { t } = useTranslation();
return <div className='absolute border-l border-mineshaft-500 bg-bunker fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between'>
@@ -111,19 +101,19 @@ const SideBar = ({
blurred={false}
/>
</div>
{data.filter(secret => secret.type == "shared")[0]?.value
{data[0]?.value
? <div className={`relative mt-2 px-4 ${overrideEnabled && "opacity-40 pointer-events-none"} duration-200`}>
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.value")}</p>
<DashboardInputField
onChangeHandler={modifyValue}
type="value"
position={data.filter(secret => secret.type == "shared")[0]?.pos}
value={data.filter(secret => secret.type == "shared")[0]?.value}
position={data[0].pos}
value={data[0]?.value}
isDuplicate={false}
blurred={true}
/>
<div className='absolute bg-bunker-800 right-[1.07rem] top-[1.6rem] z-50'>
<GenerateSecretMenu modifyValue={modifyValue} position={data.filter(secret => secret.type == "shared")[0]?.pos} />
<GenerateSecretMenu modifyValue={modifyValue} position={data[0]?.pos} />
</div>
</div>
: <div className='px-4 text-sm text-bunker-300 pt-4'>
@@ -131,39 +121,32 @@ const SideBar = ({
{t("dashboard:sidebar.personal-explanation")}
</div>}
<div className='mt-4 px-4'>
{data.filter(secret => secret.type == "shared")[0]?.value &&
{data[0]?.value &&
<div className='flex flex-row items-center justify-between my-2 pl-1 pr-2'>
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.override")}</p>
<Toggle
enabled={overrideEnabled}
setEnabled={setOverrideEnabled}
addOverride={addOverride}
keyName={data[0]?.key}
value={data[0]?.value}
addOverride={modifyValueOverride}
pos={data[0]?.pos}
id={data[0]?.id}
comment={data[0]?.comment}
deleteOverride={deleteOverride}
sharedToHide={sharedToHide}
setSharedToHide={setSharedToHide}
/>
</div>}
<div className={`relative ${!overrideEnabled && "opacity-40 pointer-events-none"} duration-200`}>
<DashboardInputField
onChangeHandler={modifyValue}
onChangeHandler={modifyValueOverride}
type="value"
position={overrideEnabled ? data.filter(secret => secret.type == "personal")[0]?.pos : data[0]?.pos}
value={overrideEnabled ? data.filter(secret => secret.type == "personal")[0]?.value : data[0]?.value}
position={data[0]?.pos}
value={overrideEnabled ? data[0]?.valueOverride : data[0]?.value}
isDuplicate={false}
blurred={true}
/>
<div className='absolute right-[0.57rem] top-[0.3rem] z-50'>
<GenerateSecretMenu modifyValue={modifyValue} position={overrideEnabled ? data.filter(secret => secret.type == "personal")[0]?.pos : data[0]?.pos} />
<GenerateSecretMenu modifyValue={modifyValueOverride} position={data[0]?.pos} />
</div>
</div>
</div>
<SecretVersionList secretId={data[0]?.id} />
<CommentField comment={data.filter(secret => secret.type == "shared")[0]?.comment} modifyComment={modifyComment} position={data.filter(secret => secret.type == "shared")[0]?.pos} />
<CommentField comment={data[0]?.comment} modifyComment={modifyComment} position={data[0]?.pos} />
</div>
)}
<div className={`flex justify-start max-w-sm mt-4 px-4 mt-full mb-[4.7rem]`}>
@@ -176,7 +159,7 @@ const SideBar = ({
textDisabled="Saved"
/>
<DeleteActionButton
onSubmit={() => deleteRow({ ids: overrideEnabled ? data.map(secret => secret.id) : [data.filter(secret => secret.type == "shared")[0]?.id], secretName: data[0]?.key })}
onSubmit={() => deleteRow({ ids: data.map(secret => secret.id), secretName: data[0]?.key })}
/>
</div>
</div>
+3 -3
View File
@@ -81,7 +81,7 @@ export default function CodeInputStep({ email, incrementStep, setCode, codeError
return (
<div className="bg-bunker w-max mx-auto h-7/12 pt-10 pb-4 px-8 rounded-xl drop-shadow-xl mb-64 md:mb-16">
<p className="text-l flex justify-center text-bunker-300">
{"We've"} sent a verification email to{" "}
{t("signup:step2-message")}
</p>
<p className="text-l flex justify-center font-semibold my-2 text-bunker-300">
{email}{" "}
@@ -119,11 +119,11 @@ export default function CodeInputStep({ email, incrementStep, setCode, codeError
<div className="flex flex-col items-center justify-center w-full max-h-24 max-w-md mx-auto pt-2">
<div className="flex flex-row items-baseline gap-1 text-sm">
<span className="text-bunker-400">
Not seeing an email?
{t("signup:step2-resend-alert")}
</span>
<u className={`font-normal ${isResendingVerificationEmail ? 'text-bunker-400' : 'text-primary-700 hover:text-primary duration-200'}`}>
<button disabled={isLoading} onClick={resendVerificationEmail}>
{isResendingVerificationEmail ? "Resending..." : "Resend"}
{isResendingVerificationEmail ? t("signup:step2-resend-progress") : t("signup:step2-resend-submit")}
</button>
</u>
</div>
@@ -59,11 +59,11 @@ export default function EnterEmailStep({ email, setEmail, incrementStep }: Downl
<div>
<div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-8 md:px-6 mx-1 rounded-xl drop-shadow-xl">
<p className="text-4xl font-semibold flex justify-center text-primary">
{'Let\''}s get started
{t("signup:step1-start")}
</p>
<div className="flex items-center justify-center w-5/6 md:w-full m-auto md:p-2 rounded-lg max-h-24 mt-4">
<InputField
label="Email"
label={t("common:email") ?? ""}
onChangeHandler={setEmail}
type="email"
value={email}
@@ -79,7 +79,7 @@ export default function EnterEmailStep({ email, setEmail, incrementStep }: Downl
{t("signup:step1-privacy")}
</p>
<div className="text-l mt-6 m-2 md:m-8 px-8 py-1 text-lg">
<Button text="Get Started" type="submit" onButtonPressed={emailCheck} size="lg" />
<Button text={t("signup:step1-submit") ?? ""} type="submit" onButtonPressed={emailCheck} size="lg" />
</div>
</div>
</div>
+17 -23
View File
@@ -1,3 +1,5 @@
import { SecretDataProps } from 'public/data/frequentInterfaces';
import Aes256Gcm from '~/components/utilities/cryptography/aes-256-gcm';
import login1 from '~/pages/api/auth/Login1';
import login2 from '~/pages/api/auth/Login2';
@@ -13,14 +15,6 @@ import Telemetry from './telemetry/Telemetry';
import { saveTokenToLocalStorage } from './saveTokenToLocalStorage';
import SecurityClient from './SecurityClient';
interface SecretDataProps {
type: 'personal' | 'shared';
pos: number;
key: string;
value: string;
id: string;
comment: string;
}
const crypto = require("crypto");
const nacl = require('tweetnacl');
@@ -145,53 +139,53 @@ const attemptLogin = async (
);
const secretsToBeAdded: SecretDataProps[] = [{
type: "shared",
pos: 0,
key: "DATABASE_URL",
value: "mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net",
valueOverride: undefined,
comment: "This is an example of secret referencing.",
id: ''
}, {
type: "shared",
pos: 1,
key: "DB_USERNAME",
value: "OVERRIDE_THIS",
valueOverride: undefined,
comment: "This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need",
id: ''
}, {
type: "personal",
pos: 2,
key: "DB_PASSWORD",
value: "OVERRIDE_THIS",
valueOverride: undefined,
comment: "This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need",
id: ''
}, {
pos: 3,
key: "DB_USERNAME",
value: "user1234",
valueOverride: "user1234",
comment: "",
id: ''
}, {
type: "shared",
pos: 3,
key: "DB_PASSWORD",
value: "OVERRIDE_THIS",
comment: "This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need",
id: ''
}, {
type: "personal",
pos: 4,
key: "DB_PASSWORD",
value: "example_password",
valueOverride: "example_password",
comment: "",
id: ''
}, {
type: "shared",
pos: 5,
key: "TWILIO_AUTH_TOKEN",
value: "example_twillio_token",
comment: "This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need",
valueOverride: undefined,
comment: "",
id: ''
}, {
type: "shared",
pos: 6,
key: "WEBSITE_URL",
value: "http://localhost:3000",
comment: "This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need",
valueOverride: undefined,
comment: "",
id: ''
}]
const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeAdded, workspaceId: String(localStorage.getItem('projectData.id')), env: 'dev' })
@@ -1,11 +1,4 @@
interface SecretDataProps {
type: 'personal' | 'shared';
pos: number;
key: string;
value: string;
id: string;
comment: string;
}
import { SecretDataProps } from "public/data/frequentInterfaces";
/**
* This function downloads the secrets as a .env file
@@ -16,16 +9,16 @@ interface SecretDataProps {
const checkOverrides = async ({ data }: { data: SecretDataProps[]; }) => {
let secrets : SecretDataProps[] = data!.map((secret) => Object.create(secret));
const overridenSecrets = data!.filter(
(secret) => secret.type === 'personal'
(secret) => (secret.valueOverride == undefined || secret?.value != secret?.valueOverride) ? 'shared' : 'personal'
);
if (overridenSecrets.length) {
overridenSecrets.forEach((secret) => {
const index = secrets!.findIndex(
(_secret) => _secret.key === secret.key && _secret.type === 'shared'
(_secret) => _secret.key === secret.key && (secret.valueOverride == undefined || secret?.value != secret?.valueOverride)
);
secrets![index].value = secret.value;
});
secrets = secrets!.filter((secret) => secret.type === 'shared');
secrets = secrets!.filter((secret) => (secret.valueOverride == undefined || secret?.value != secret?.valueOverride));
}
return secrets;
}
@@ -1,15 +1,9 @@
import { SecretDataProps } from "public/data/frequentInterfaces";
import { envMapping } from "../../../public/data/frequentConstants";
import checkOverrides from './checkOverrides';
interface SecretDataProps {
type: 'personal' | 'shared';
pos: number;
key: string;
value: string;
id: string;
comment: string;
}
/**
* This function downloads the secrets as a .env file
* @param {object} obj
@@ -1,19 +1,12 @@
// import YAML from 'yaml';
// import { YAMLSeq } from 'yaml/types';
import { SecretDataProps } from "public/data/frequentInterfaces";
// import { envMapping } from "../../../public/data/frequentConstants";
// import checkOverrides from './checkOverrides';
interface SecretDataProps {
type: 'personal' | 'shared';
pos: number;
key: string;
value: string;
id: string;
comment: string;
}
/**
* This function downloads the secrets as a .yml file
* @param {object} obj
@@ -1,3 +1,5 @@
import { SecretDataProps } from "public/data/frequentInterfaces";
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
const crypto = require("crypto");
@@ -9,15 +11,6 @@ const nacl = require("tweetnacl");
nacl.util = require("tweetnacl-util");
interface SecretDataProps {
type: 'personal' | 'shared';
pos: number;
key: string;
value: string;
id: string;
comment: string;
}
interface EncryptedSecretProps {
id: string;
createdAt: string;
@@ -106,7 +99,7 @@ const encryptSecrets = async ({ secretsToEncrypt, workspaceId, env }: { secretsT
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
type: secret.type,
type: (secret.valueOverride == undefined || secret?.value != secret?.valueOverride) ? 'shared' : 'personal',
};
return result;
@@ -115,15 +115,19 @@ const getSecretsForProject = async ({
});
}
const result = tempDecryptedSecrets.map((secret, index) => {
const secretKeys = [...new Set(tempDecryptedSecrets.map(secret => secret.key))];
const result = secretKeys.map((key, index) => {
return {
id: secret['id'],
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: index,
key: secret['key'],
value: secret['value'],
type: secret['type'],
comment: secret['comment']
};
key: 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,
}
});
setData(result);
+1
View File
@@ -20,4 +20,5 @@ export const publicPaths = [
export const languageMap = {
en: "English",
ko: "한국어",
fr: "Français",
};
+43 -18
View File
@@ -13,6 +13,15 @@ import { decryptAssymmetric, decryptSymmetric } from "~/components/utilities/cry
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
export interface SecretDataProps {
pos: number;
key: string;
value: string;
type: string;
id: string;
environment: string;
}
interface SideBarProps {
toggleSidebar: (value: boolean) => void;
setSnapshotData: (value: any) => void;
@@ -43,8 +52,6 @@ interface EncrypetedSecretVersionListProps {
* @param {function} obj.toggleSidebar - function that opens or closes the sidebar
* @param {function} obj.setSnapshotData - state manager for snapshot data
* @param {string} obj.chosenSnaphshot - the snapshot id which is currently selected
*
*
* @returns the sidebar with the options for point-in-time recovery (commits)
*/
const PITRecoverySidebar = ({
@@ -111,7 +118,21 @@ const PITRecoverySidebar = ({
}
})
setSnapshotData({ id: secretSnapshotData._id, version: secretSnapshotData.version, createdAt: secretSnapshotData.createdAt, secretVersions: decryptedSecretVersions })
const secretKeys = [...new Set(decryptedSecretVersions.map((secret: SecretDataProps) => secret.key))];
const result = secretKeys.map((key, index) => {
return {
id: decryptedSecretVersions.filter((secret: SecretDataProps) => secret.key == key && secret.type == 'shared')[0].id,
pos: index,
key: key,
environment: decryptedSecretVersions.filter((secret: SecretDataProps) => secret.key == key && secret.type == 'shared')[0].environment,
value: decryptedSecretVersions.filter((secret: SecretDataProps) => secret.key == key && secret.type == 'shared')[0]?.value,
valueOverride: decryptedSecretVersions.filter((secret: SecretDataProps) => secret.key == key && secret.type == 'personal')[0]?.value,
}
});
setSnapshotData({ id: secretSnapshotData._id, version: secretSnapshotData.version, createdAt: secretSnapshotData.createdAt, secretVersions: result, comment: '' })
}
return <div className={`absolute border-l border-mineshaft-500 ${isLoading ? "bg-bunker-800" : "bg-bunker"} fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between`}>
@@ -125,31 +146,35 @@ const PITRecoverySidebar = ({
></Image>
</div>
) : (
<div className='h-min overflow-y-auto'>
<div className='h-min'>
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
<p className="font-semibold text-lg text-bunker-200">{t("Point-in-time Recovery")}</p>
<div className='p-1' onClick={() => toggleSidebar(false)}>
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/>
</div>
</div>
<div className='flex flex-col px-2 py-2'>
{secretSnapshotsMetadata?.map((snapshot: SnaphotProps, id: number) => <div key={snapshot._id} className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "bg-primary text-black" : "bg-mineshaft-700"} py-3 px-4 mb-2 rounded-md flex flex-row justify-between items-center`}>
<div className="flex flex-row items-start">
<div className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-800" : "text-bunker-200"} text-sm mr-1.5`}>{timeSince(new Date(snapshot.createdAt))}</div>
<div className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-900" : "text-bunker-300"} text-sm `}>{" - " + snapshot.secretVersions.length + " Secrets"}</div>
</div>
<div className='flex flex-col px-2 py-2 overflow-y-auto h-[92vh]'>
{secretSnapshotsMetadata?.map((snapshot: SnaphotProps, id: number) =>
<div
onClick={() => exploreSnapshot({ snapshotId: snapshot._id })}
className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-800 pointer-events-none" : "text-bunker-200 hover:text-primary duration-200 cursor-pointer"} text-sm`}>
{id == 0 ? "Current Version" : chosenSnapshot == snapshot._id ? "Currently Viewing" : "Explore"}
key={snapshot._id}
onClick={() => exploreSnapshot({ snapshotId: snapshot._id })}
className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "bg-primary text-black pointer-events-none" : "bg-mineshaft-700 hover:bg-mineshaft-500 duration-200 cursor-pointer"} py-3 px-4 mb-2 rounded-md flex flex-row justify-between items-center`}
>
<div className="flex flex-row items-start">
<div className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-800" : "text-bunker-200"} text-sm mr-1.5`}>{timeSince(new Date(snapshot.createdAt))}</div>
<div className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-900" : "text-bunker-300"} text-sm `}>{" - " + snapshot.secretVersions.length + " Secrets"}</div>
</div>
<div
className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-800 pointer-events-none" : "text-bunker-200 hover:text-primary duration-200 cursor-pointer"} text-sm`}>
{id == 0 ? "Current Version" : chosenSnapshot == snapshot._id ? "Currently Viewing" : "Explore"}
</div>
</div>)}
<div className='flex justify-center w-full mb-14'>
<div className='items-center w-40'>
<Button text="View More" textDisabled="End of History" active={secretSnapshotsMetadata.length % 15 == 0 ? true : false} onButtonPressed={loadMoreSnapshots} size="md" color="mineshaft"/>
</div>
</div>)}
<div className='flex justify-center w-full mb-14'>
<div className='items-center w-40'>
<Button text="View More" textDisabled="End of History" active={secretSnapshotsMetadata.length % 15 == 0 ? true : false} onButtonPressed={loadMoreSnapshots} size="md" color="mineshaft"/>
</div>
</div>
</div>
</div>
)}
</div>
+26 -21
View File
@@ -52,7 +52,7 @@ const SecretVersionList = ({ secretId }: { secretId: string; }) => {
});
}
const decryptedSecretVersions = encryptedSecretVersions.secretVersions.map((encryptedSecretVersion: EncrypetedSecretVersionListProps) => {
const decryptedSecretVersions = encryptedSecretVersions?.secretVersions.map((encryptedSecretVersion: EncrypetedSecretVersionListProps) => {
return {
createdAt: encryptedSecretVersion.createdAt,
value: decryptSymmetric({
@@ -87,28 +87,33 @@ const SecretVersionList = ({ secretId }: { secretId: string; }) => {
</div>
) : (
<div className='h-48 overflow-y-auto overflow-x-none'>
{secretVersions?.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
.map((version: DecryptedSecretVersionListProps, index: number) =>
<div key={index} className='flex flex-row'>
<div className='pr-1 flex flex-col items-center'>
<div className='p-1'><FontAwesomeIcon icon={index == 0 ? faDotCircle : faCircle} /></div>
<div className='w-0 h-full border-l mt-1'></div>
</div>
<div className='flex flex-col w-full max-w-[calc(100%-2.3rem)]'>
<div className='pr-2 pt-1'>
{(new Date(version.createdAt)).toLocaleDateString('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})}
{secretVersions
? secretVersions?.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
.map((version: DecryptedSecretVersionListProps, index: number) =>
<div key={index} className='flex flex-row'>
<div className='pr-1 flex flex-col items-center'>
<div className='p-1'><FontAwesomeIcon icon={index == 0 ? faDotCircle : faCircle} /></div>
<div className='w-0 h-full border-l mt-1'></div>
</div>
<div className='flex flex-col w-full max-w-[calc(100%-2.3rem)]'>
<div className='pr-2 pt-1'>
{(new Date(version.createdAt)).toLocaleDateString('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})}
</div>
<div className=''><p className='break-words'><span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1.5'>Value:</span>{version.value}</p></div>
</div>
<div className=''><p className='break-words'><span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1.5'>Value:</span>{version.value}</p></div>
</div>
</div>
)}
)
: (
<div className='w-full h-full flex items-center justify-center text-bunker-400'>No version history yet.</div>
)
}
</div>
)}
</div>
+1 -1
View File
@@ -8,7 +8,7 @@ module.exports = {
debug: false,
i18n: {
defaultLocale: "en",
locales: ["en", "ko"],
locales: ["en", "ko", "fr"],
},
fallbackLng: {
default: ["en"],
+178 -256
View File
@@ -48,11 +48,12 @@ type WorkspaceEnv = {
};
interface SecretDataProps {
type: 'personal' | 'shared';
pos: number;
key: string;
value: string;
valueOverride: string | undefined;
id: string;
idOverride: string | undefined;
comment: string;
}
@@ -71,10 +72,11 @@ interface SnapshotProps {
secretVersions: {
id: string;
pos: number;
type: 'personal' | 'shared';
environment: string;
key: string;
value: string;
valueOverride: string;
comment: string;
}[];
}
@@ -102,7 +104,7 @@ function findDuplicates(arr: any[]) {
*/
export default function Dashboard() {
const [data, setData] = useState<SecretDataProps[] | null>();
const [initialData, setInitialData] = useState<SecretDataProps[]>([]);
const [initialData, setInitialData] = useState<SecretDataProps[] | null | undefined>([]);
const [buttonReady, setButtonReady] = useState(false);
const router = useRouter();
const [blurred, setBlurred] = useState(true);
@@ -119,6 +121,7 @@ export default function Dashboard() {
const [sharedToHide, setSharedToHide] = useState<string[]>([]);
const [snapshotData, setSnapshotData] = useState<SnapshotProps>();
const [numSnapshots, setNumSnapshots] = useState<number>();
const [saveLoading, setSaveLoading] = useState(false);
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
@@ -234,16 +237,6 @@ export default function Dashboard() {
setInitialData(dataToSort);
reorderRows(dataToSort);
setSharedToHide(
dataToSort?.filter(row => (dataToSort
?.map((item) => item.key)
.filter(
(item, index) =>
index !==
dataToSort?.map((item) => item.key).indexOf(item)
).includes(row.key) && row.type == 'shared'))?.map((item) => item.id)
)
setIsLoading(false);
} catch (error) {
console.log('Error', error);
@@ -259,49 +252,18 @@ export default function Dashboard() {
...data!,
{
id: guidGenerator(),
idOverride: guidGenerator(),
pos: data!.length,
key: '',
value: '',
type: 'shared',
valueOverride: undefined,
comment: '',
},
]);
};
/**
* This function add an ovverrided version of a certain secret to the current user
* @param {object} obj
* @param {string} obj.id - if of this secret that is about to be overriden
* @param {string} obj.keyName - key name of this secret
* @param {string} obj.value - value of this secret
* @param {string} obj.pos - position of this secret on the dashboard
*/
const addOverride = ({ id, keyName, value, pos, comment }: overrideProps) => {
setIsNew(false);
const tempdata: SecretDataProps[] | 1 = [
...data!,
{
id: id,
pos: pos,
key: keyName,
value: value,
type: 'personal',
comment: comment,
},
];
sortValuesHandler(
tempdata,
sortMethod == 'alhpabetical' ? '-alphabetical' : 'alphabetical'
);
};
const deleteRow = ({
ids,
secretName,
}: {
ids: string[];
secretName: string;
}) => {
const deleteRow = ({ ids, secretName }: { ids: string[]; secretName: string; }) => {
setButtonReady(true);
toggleSidebar('None');
createNotification({
@@ -314,40 +276,6 @@ export default function Dashboard() {
);
};
/**
* This function deleted the override of a certain secrer
* @param {string} id - id of a shared secret; the override with the same key should be deleted
*/
const deleteOverride = (id: string) => {
setButtonReady(true);
// find which shared secret corresponds to the overriden version
const sharedVersionOfOverride = data!.filter(
(secret) =>
secret.type == 'shared' &&
secret.key == data!.filter((row) => row.id == id)[0]?.key
)[0]?.id;
// change the sidebar to this shared secret; and unhide it
toggleSidebar(sharedVersionOfOverride);
setSharedToHide(
sharedToHide!.filter((tempId) => tempId != sharedVersionOfOverride)
);
// resort secrets
const tempData = data!.filter(
(row: SecretDataProps) =>
!(
row.key == data!.filter((row) => row.id == id)[0]?.key &&
row.type == 'personal'
)
);
sortValuesHandler(
tempData,
sortMethod == 'alhpabetical' ? '-alphabetical' : 'alphabetical'
);
};
const modifyValue = (value: string, pos: number) => {
setData((oldData) => {
oldData![pos].value = value;
@@ -356,6 +284,14 @@ export default function Dashboard() {
setButtonReady(true);
};
const modifyValueOverride = (value: string | undefined, pos: number) => {
setData((oldData) => {
oldData![pos].valueOverride = value;
return [...oldData!];
});
setButtonReady(true);
};
const modifyKey = (value: string, pos: number) => {
setData((oldData) => {
oldData![pos].key = value;
@@ -377,6 +313,10 @@ export default function Dashboard() {
modifyValue(value, pos);
}, []);
const listenChangeValueOverride = useCallback((value: string | undefined, pos: number) => {
modifyValueOverride(value, pos);
}, []);
const listenChangeKey = useCallback((value: string, pos: number) => {
modifyKey(value, pos);
}, []);
@@ -389,6 +329,7 @@ export default function Dashboard() {
* Save the changes of environment variables and push them to the database
*/
const savePush = async (dataToPush?: SecretDataProps[]) => {
setSaveLoading(true);
let newData: SecretDataProps[] | null | undefined;
// dataToPush is mostly used for rollbacks, otherwise we always take the current state data
if ((dataToPush ?? [])?.length > 0) {
@@ -397,20 +338,11 @@ export default function Dashboard() {
newData = data;
}
const obj = Object.assign(
{},
...newData!.map((row: SecretDataProps) => ({
[row.type.charAt(0) + row.key]: [row.value, row.comment ?? ''],
}))
);
// Checking if any of the secret keys start with a number - if so, don't do anything
const nameErrors = !Object.keys(obj)
.map((key) => !isNaN(Number(key[0].charAt(0))))
const nameErrors = !newData!
.map((secret) => !isNaN(Number(secret.key.charAt(0))))
.every((v) => v === false);
const duplicatesExist =
findDuplicates(data!.map((item: SecretDataProps) => item.key + item.type))
.length > 0;
const duplicatesExist = findDuplicates(data!.map((item: SecretDataProps) => item.key)).length > 0;
if (nameErrors) {
return createNotification({
@@ -490,7 +422,64 @@ export default function Dashboard() {
env: selectedEnv.slug,
});
secrets && (await updateSecrets({ secrets }));
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(newDataPoint => newDataPoint.id).includes(initDataPoint.id)
&& (newData!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].value != initDataPoint.value
|| newData!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].key != initDataPoint.key
|| newData!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].comment != initDataPoint.comment))
.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)}));
console.log('override add', overridesToBeAdded.length)
const overridesToBeUpdated
= newOverrides!.filter(newDataPoint => initOverrides
.filter(initDataPoint => newOverrides!.map(newDataPoint => newDataPoint.id).includes(initDataPoint.id)
&& (newOverrides!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].valueOverride != initDataPoint.valueOverride
|| newOverrides!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].key != initDataPoint.key
|| newOverrides!.filter(newDataPoint => newDataPoint.id == initDataPoint.id)[0].comment != initDataPoint.comment))
.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)}));
console.log('override update', overridesToBeUpdated.length)
if (secretsToBeDeleted.concat(overridesToBeDeleted).length > 0) {
await deleteSecrets({ secretIds: secretsToBeDeleted.concat(overridesToBeDeleted) });
}
if (secretsToBeAdded.concat(overridesToBeAdded).length > 0) {
const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeAdded.concat(overridesToBeAdded), workspaceId, env: envMapping[env] });
secrets && await addSecrets({ secrets, env: envMapping[env], workspaceId });
}
if (secretsToBeUpdated.concat(overridesToBeUpdated).length > 0) {
const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeUpdated.concat(overridesToBeUpdated), workspaceId, env: envMapping[env] });
secrets && await updateSecrets({ secrets });
}
setInitialData(newData);
// If this user has never saved environment variables before, show them a prompt to read docs
if (!hasUserEverPushed) {
@@ -500,6 +489,7 @@ export default function Dashboard() {
// increasing the number of project commits
setNumSnapshots((numSnapshots ?? 0) + 1);
setSaveLoading(false);
};
const addData = (newData: SecretDataProps[]) => {
@@ -555,36 +545,27 @@ export default function Dashboard() {
content={String(t('dashboard:og-description'))}
/>
</Head>
<div className='flex flex-row'>
{sidebarSecretId != 'None' && (
<SideBar
toggleSidebar={toggleSidebar}
data={data.filter(
(row: SecretDataProps) =>
row.key ==
data.filter((row) => row.id == sidebarSecretId)[0]?.key
)}
modifyKey={listenChangeKey}
modifyValue={listenChangeValue}
modifyComment={listenChangeComment}
addOverride={addOverride}
deleteOverride={deleteOverride}
buttonReady={buttonReady}
savePush={savePush}
sharedToHide={sharedToHide}
setSharedToHide={setSharedToHide}
deleteRow={deleteCertainRow}
/>
)}
{PITSidebarOpen && (
<PITRecoverySidebar
toggleSidebar={togglePITSidebar}
chosenSnapshot={String(snapshotData?.id ? snapshotData.id : '')}
setSnapshotData={setSnapshotData}
/>
)}
<div className='w-full max-h-96 pb-2'>
<NavHeader pageName={t('dashboard:title')} isProjectRelated={true} />
<div className="flex flex-row">
{sidebarSecretId != "None" && <SideBar
toggleSidebar={toggleSidebar}
data={data.filter((row: SecretDataProps) => row.key == data.filter(row => row.id == sidebarSecretId)[0]?.key)}
modifyKey={listenChangeKey}
modifyValue={listenChangeValue}
modifyValueOverride={listenChangeValueOverride}
modifyComment={listenChangeComment}
buttonReady={buttonReady}
savePush={savePush}
sharedToHide={sharedToHide}
setSharedToHide={setSharedToHide}
deleteRow={deleteCertainRow}
/>}
{PITSidebarOpen && <PITRecoverySidebar
toggleSidebar={togglePITSidebar}
chosenSnapshot={String(snapshotData?.id ? snapshotData.id : "")}
setSnapshotData={setSnapshotData}
/>}
<div className="w-full max-h-96 pb-2">
<NavHeader pageName={t("dashboard:title")} isProjectRelated={true} />
{checkDocsPopUpVisible && (
<BottonRightPopup
buttonText={t('dashboard:check-docs.button')}
@@ -651,66 +632,39 @@ export default function Dashboard() {
size='md'
active={buttonReady}
iconDisabled={faCheck}
textDisabled={String(t('common:saved'))}
textDisabled={String(t("common:saved"))}
loading={saveLoading}
/>
</div>
)}
{snapshotData && (
<div className={`flex justify-start max-w-sm mt-1`}>
<Button
text={String(t('Rollback to this snapshot'))}
onButtonPressed={async () => {
// Update secrets in the state only for the current environment
const rolledBackSecrets = snapshotData.secretVersions
.filter((row) => row.environment == selectedEnv.slug)
.map((sv, position) => {
return {
id: sv.id,
pos: position,
type: sv.type,
key: sv.key,
value: sv.value,
comment: '',
};
});
setData(rolledBackSecrets);
{snapshotData && <div className={`flex justify-start max-w-sm mt-1`}>
<Button
text={String(t("Rollback to this snapshot"))}
onButtonPressed={async () => {
// Update secrets in the state only for the current environment
const rolledBackSecrets = snapshotData.secretVersions
.filter(row => reverseEnvMapping[row.environment] == env)
.map((sv, position) => {
return {
id: sv.id, idOverride: sv.id, pos: position, valueOverride: sv.valueOverride, key: sv.key, value: sv.value, comment: ''
}
});
setData(rolledBackSecrets);
setSharedToHide(
rolledBackSecrets
?.filter(
(row) =>
rolledBackSecrets
?.map((item) => item.key)
.filter(
(item, index) =>
index !==
rolledBackSecrets
?.map((item) => item.key)
.indexOf(item)
)
.includes(row.key) && row.type == 'shared'
)
?.map((item) => item.id)
);
// Perform the rollback globally
performSecretRollback({ workspaceId, version: snapshotData.version })
// Perform the rollback globally
performSecretRollback({
workspaceId,
version: snapshotData.version,
});
setSnapshotData(undefined);
createNotification({
text: `Rollback has been performed successfully.`,
type: 'success',
});
}}
color='primary'
size='md'
active={buttonReady}
/>
</div>
)}
setSnapshotData(undefined);
createNotification({
text: `Rollback has been performed successfully.`,
type: 'success'
});
}}
color="primary"
size="md"
active={buttonReady}
/>
</div>}
</div>
</div>
<div className='mx-6 w-full pr-12'>
@@ -826,84 +780,52 @@ export default function Dashboard() {
<div
className={`max-w-5xl mt-1 max-h-[calc(100vh-280px)] overflow-hidden overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar`}
>
<div className='px-1 pt-2 bg-mineshaft-800 rounded-md p-2'>
{!snapshotData &&
data
?.filter((row) =>
row.key
?.toUpperCase()
.includes(searchKeys.toUpperCase())
)
.filter(
(row) =>
!(
sharedToHide.includes(row.id) &&
row.type == 'shared'
)
)
.map((keyPair) => (
<KeyPair
key={keyPair.id}
keyPair={keyPair}
modifyValue={listenChangeValue}
modifyKey={listenChangeKey}
isBlurred={blurred}
isDuplicate={findDuplicates(
data?.map((item) => item.key + item.type)
)?.includes(keyPair.key + keyPair.type)}
toggleSidebar={toggleSidebar}
sidebarSecretId={sidebarSecretId}
isSnapshot={false}
/>
))}
{snapshotData &&
snapshotData.secretVersions
?.sort((a, b) => a.key.localeCompare(b.key))
.filter(
(row) =>
row.environment == selectedSnapshotEnv?.slug
)
.filter((row) =>
row.key
.toUpperCase()
.includes(searchKeys.toUpperCase())
)
.filter(
(row) =>
!(
snapshotData.secretVersions
?.filter(
(row) =>
snapshotData.secretVersions
?.map((item) => item.key)
.filter(
(item, index) =>
index !==
snapshotData.secretVersions
?.map((item) => item.key)
.indexOf(item)
)
.includes(row.key) && row.type == 'shared'
)
?.map((item) => item.id)
.includes(row.id) && row.type == 'shared'
)
)
.map((keyPair) => (
<KeyPair
key={keyPair.id}
keyPair={keyPair}
modifyValue={listenChangeValue}
modifyKey={listenChangeKey}
isBlurred={blurred}
isDuplicate={findDuplicates(
data?.map((item) => item.key + item.type)
)?.includes(keyPair.key + keyPair.type)}
toggleSidebar={toggleSidebar}
sidebarSecretId={sidebarSecretId}
isSnapshot={true}
/>
))}
<div className="px-1 pt-2 bg-mineshaft-800 rounded-md p-2">
{!snapshotData && data?.filter(row => row.key?.toUpperCase().includes(searchKeys.toUpperCase()))
.filter(row => !sharedToHide.includes(row.id)).map((keyPair) => (
<KeyPair
key={keyPair.id}
keyPair={keyPair}
modifyValue={listenChangeValue}
modifyValueOverride={listenChangeValueOverride}
modifyKey={listenChangeKey}
isBlurred={blurred}
isDuplicate={findDuplicates(
data?.map((item) => item.key)
)?.includes(keyPair.key)}
toggleSidebar={toggleSidebar}
sidebarSecretId={sidebarSecretId}
isSnapshot={false}
/>
))}
{snapshotData && snapshotData.secretVersions?.sort((a, b) => a.key.localeCompare(b.key))
.filter(row => reverseEnvMapping[row.environment] == snapshotEnv)
.filter(row => row.key.toUpperCase().includes(searchKeys.toUpperCase()))
.filter(
row => !(snapshotData.secretVersions?.filter(row => (snapshotData.secretVersions
?.map((item) => item.key)
.filter(
(item, index) =>
index !==
snapshotData.secretVersions?.map((item) => item.key).indexOf(item)
).includes(row.key)))?.map((item) => item.id).includes(row.id))
)
.map((keyPair) => (
<KeyPair
key={keyPair.id}
keyPair={keyPair}
modifyValue={listenChangeValue}
modifyValueOverride={listenChangeValueOverride}
modifyKey={listenChangeKey}
isBlurred={blurred}
isDuplicate={findDuplicates(
data?.map((item) => item.key)
)?.includes(keyPair.key)}
toggleSidebar={toggleSidebar}
sidebarSecretId={sidebarSecretId}
isSnapshot={true}
/>
))}
</div>
{!snapshotData && (
<div className='w-full max-w-5xl px-2 pt-3'>
+7 -3
View File
@@ -117,11 +117,15 @@ export default function Login() {
id="current-password"
/>
<div className="absolute top-2 right-3 text-primary-700 hover:text-primary duration-200 cursor-pointer text-sm">
<Link href="/verify-email">Forgot password?</Link>
<Link href="/verify-email">
<button className="text-primary-700 hover:text-primary duration-200 font-normal text-sm underline-offset-4 ml-1.5">
{t("login:forgot-password")}
</button>
</Link>
</div>
</div>
{!isLoading && errorLogin && (
<Error text="Your email and/or password are wrong." />
<Error text={t("login:error-login") ?? ""} />
)}
<div className="flex flex-col items-center justify-center w-full md:p-2 max-h-20 max-w-md mt-4 mx-auto text-sm">
<div className="text-l mt-6 m-8 px-8 py-3 text-lg">
@@ -160,7 +164,7 @@ export default function Login() {
<ListBox
selected={lang}
onChange={setLanguage}
data={["en", "ko"]}
data={["en", "ko", "fr"]}
isFull
text={`${t("common:language")}: `}
/>
+1 -1
View File
@@ -126,7 +126,7 @@ export default function PersonalSettings() {
<ListBox
selected={lang}
onChange={setLanguage}
data={["en", "ko"]}
data={["en", "ko", "fr"]}
width="full"
text={`${t("common:language")}: `}
/>
@@ -0,0 +1,8 @@
export interface SecretDataProps {
pos: number;
key: string;
value: string;
valueOverride: string | undefined;
id: string;
comment: string;
}
+3 -1
View File
@@ -4,5 +4,7 @@
"og-description": "Infisical a simple end-to-end encrypted platform that enables teams to sync and manage their .env files.",
"login": "Log In",
"need-account": "Need an Infisical account?",
"create-account": "Create an account"
"create-account": "Create an account",
"forgot-password": "Forgot your password?",
"error-login": "Wrong credentials."
}
+4 -1
View File
@@ -9,8 +9,11 @@
"step1-start": "Let's get started",
"step1-privacy": "By creating an account, you agree to our Terms and have read and acknowledged the Privacy Policy.",
"step1-submit": "Get Started",
"step2-message": "<wrapper>We've sent a verification email to</wrapper><email>{{email}}</email>",
"step2-message": "We've sent a verification email to",
"step2-code-error": "Oops. Your code is wrong. Please try again.",
"step2-resend-alert": "Don't see the email?",
"step2-resend-submit": "Resend",
"step2-resend-progress": "Resending...",
"step2-spam-alert": "Make sure to check your spam inbox.",
"step3-message": "Almost there!",
"step4-message": "Save your Emergency Kit",
+11
View File
@@ -0,0 +1,11 @@
{
"title": "Journaux d'activité",
"subtitle": "Historique des événements pour ce projet Infisical.",
"event": {
"readSecrets": "Secrets Visualisés",
"updateSecrets": "Secrets Mis à jour",
"addSecrets": "Secrets Ajoutés",
"deleteSecrets": "Secrets Supprimés"
},
"ip-address": "Adresse IP"
}
+28
View File
@@ -0,0 +1,28 @@
{
"title": "Utilisation et Facturation",
"description": "Voir et gérer l'abonnement de votre organisation ici",
"subscription": "Abonnement",
"starter": {
"name": "Starter",
"price-explanation": "jusqu'à 5 membres de l'équipe",
"text": "Gérez n'importe quel projet jusqu'à 5 membres gratuitement!",
"subtext": "$5 par membre / mois par la suite."
},
"professional": {
"name": "Professionnel",
"price-explanation": "/membre/mois",
"subtext": "Comprend des projets et des membres illimités.",
"text": "Suivez la gestion clé à mesure que vous grandissez."
},
"enterprise": {
"name": "Entreprise",
"text": "Suivez la gestion clé à mesure que vous grandissez."
},
"current-usage": "Utilisation actuelle",
"free": "Gratuit",
"downgrade": "Rétrograder",
"upgrade": "Améliorer",
"learn-more": "En savoir plus",
"custom-pricing": "Prix personnalisés",
"schedule-demo": "Planifier une démo"
}
+34
View File
@@ -0,0 +1,34 @@
{
"head-title": "{{title}} | Infiscal",
"error_project-already-exists": "Un projet avec ce nom existe déjà.",
"no-mobile": " Pour utiliser Infisical, veuillez vous connecter avec un appareil avec des dimensions plus grandes. ",
"email": "Email",
"password": "Mot de passe",
"first-name": "Prénom",
"last-name": "Nom",
"logout": "Déconnexion",
"validate-required": "Veuillez saisir votre {{name}}",
"maintenance-alert": "Nous rencontrons des difficultés techniques mineures. Nous travaillons sur leurs résolution dès maintenant. Revenez dans quelques minutes.",
"click-to-copy": "Cliquez pour copiez",
"project-id": "Identifiant du Projet",
"save-changes": "Sauvegarder les modifications",
"saved": "Enregistrée",
"drop-zone": "Glissez et déposez un fichier .env ou .yml ici.",
"drop-zone-keys": "Glissez et déposez un fichier .env ou .yml ici pour ajouter plus de clés.",
"role": "Rôle",
"role_admin": "administrateur",
"display-name": "Nom d'affichage",
"environment": "Environnement",
"expired-in": "Expire dans",
"language": "Langue",
"search": "Recherche...",
"note": "Note",
"view-more": "Voir plus",
"end-of-history": "Fin de l'historique",
"select-event": "Sélectionnez un événement",
"event": "Événement",
"user": "Utilisateur",
"source": "Source",
"time": "Heure",
"timestamp": "Horodatage"
}
+36
View File
@@ -0,0 +1,36 @@
{
"title": "Secrets",
"og-title": "Gérez vos fichiers .env rapidement",
"og-description": "Infisical une plate-forme simple et chiffré de bout en bout qui permet aux équipes de synchroniser et de gérer leurs fichiers .env.",
"search-keys": "Recherche les clefs...",
"add-key": "Ajouter une clef",
"personal": "Personnel",
"personal-description": "Les clés personnelles ne sont visibles que pour vous",
"shared": "Partagé",
"shared-description": "Les clés partagées sont visibles à toute votre équipe",
"make-shared": "Rendre Partagé",
"make-personal": "Rendre Personnel",
"add-secret": "Ajouter un nouveau secret",
"check-docs": {
"button": "Vérifier la documentation",
"title": "Bon travail!",
"line1": "Félicitations pour avoir ajouté plus de secrets.",
"line2": "Voici comment les connecter à votre base de code."
},
"sidebar": {
"secret": "Secret",
"key": "Clef",
"value": "Valeur",
"override": "Remplacer la valeur avec une valeur personnelle",
"version-history": "Historique des versions",
"comments": "Commentaires & Notes",
"personal-explanation": "Ce secret est personnel. Il n'est partagé avec aucun de vos coéquipiers.",
"generate-random-hex": "Générer un Hex aléatoire",
"digits": "chiffres",
"delete-key-dialog": {
"title": "Supprimer la clef",
"confirm-delete-message": "Êtes-vous sûr de vouloir supprimer ce secret? Cela ne peut pas être annulé."
}
}
}
@@ -0,0 +1,16 @@
{
"title": "Intégrations de Projet",
"description": "Gérez vos intégrations d'Infisical avec des services tiers.",
"no-integrations1": "Vous n'avez pas encore d'intégration. Quand vous en aurez, elles apparaîtront ici.",
"no-integrations2": "Pour commencer, cliquez sur l'une des options ci-dessous. La configuration se fait en 5 clics.",
"available": "Intégrations de plate-forme et cloud",
"available-text1": "Cliquez sur l'intégration que vous souhaitez connecter. Cela permettra à vos variables d'environnement de circuler automatiquement dans les services tiers sélectionnés.",
"available-text2": "Remarque: Lors d'une intégration avec Heroku, pour des raisons de sécurité, il est impossible de maintenir le chiffrage de bout en bout. En théorie, cela permet à Infisical de déchiffrer les variables d'environnement. En pratique, nous pouvons vous assurer que cela ne sera jamais fait, et cela nous permet de protéger vos secrets des mauvais acteurs en ligne. Le service Infisical de base restera toujours chiffré de bout en bout. Pour toutes vos intérogations, contactez [email protected].",
"cloud-integrations": "Intégrations Cloud",
"framework-integrations": "Intégrations Framework",
"click-to-start": "Cliquez sur une intégration pour commencer à synchroniser les secrets avec elle.",
"click-to-setup": "Cliquez sur un framework pour obtenir les instructions de configuration.",
"grant-access-to-secrets": "Accordez un accès Infisical à vos secrets",
"why-infisical-needs-access": "La plupart des intégrations cloud nécessitent qu'Infisical puisse déchiffrer vos secrets afin qu'ils puissent être transmis.",
"grant-access-button": "Autoriser l'accès"
}
+10
View File
@@ -0,0 +1,10 @@
{
"title": "Connexion",
"og-title": "Connectez-vous à Infisical",
"og-description": "Infisical, une plate-forme simple et chiffré de bout en bout permettant aux équipes de synchroniser et de gérer leurs fichiers .env.",
"login": "Se connecter",
"need-account": "Besoin d'un compte Infisical?",
"create-account": "Créer un compte",
"forgot-password": "Mot de passe oublié?",
"error-login": "Mauvais identifiants."
}
+22
View File
@@ -0,0 +1,22 @@
{
"support": {
"slack": "[NEW] Rejoignez le forum Slack",
"docs": "Lire les documentations",
"issue": "Ouvrir une issue Github",
"email": "Envoyez-nous un email"
},
"user": {
"signed-in-as": "CONNECTÉ EN TANT QUE",
"current-organization": "ORGANISATION ACTUELLE",
"usage-billing": "Utilisation & Facturation",
"invite": "Inviter des membres",
"other-organizations": "AUTRE ORGANISATION"
},
"menu": {
"project": "PROJET",
"secrets": "Secrets",
"members": "Membres",
"integrations": "Intégrations",
"project-settings": "Paramètres du Projet"
}
}
@@ -0,0 +1,11 @@
{
"incident-contacts": "Contacts Incidents",
"incident-contacts-description": "Ces contacts seront informés dans le cas improbable d'un incident grave.",
"no-incident-contacts": "Aucun contact incident trouvé.",
"add-contact": "Ajouter un contact",
"add-dialog": {
"title": "Ajouter un contact incident",
"description": "Ce contact sera informé dans le cas improbable d'un incident grave.",
"add-incident": "Ajouter un contact incident"
}
}
@@ -0,0 +1,14 @@
{
"add-member": "Ajouter un Membre",
"org-members": "Membres de l'organisation",
"org-members-description": "Gérer les membres de votre organisation. Ces utilisateurs pourraient ensuite être répartis en projets.",
"search-members": "Recherche des membres...",
"add-dialog": {
"add-member-to-project": "Ajoutez un membre à votre projet",
"already-all-invited": "Tous les utilisateurs de votre organisation sont déjà invités.",
"add-user-org-first": "Ajoutez d'abord plus d'utilisateurs à l'organisation.",
"user-will-email": "L'utilisateur recevra un email avec les instructions.",
"looking-add": "<0>Si vous cherchez à ajouter des utilisateurs à votre organisation,</0><1>cliquez ici</1>",
"add-user-to-org": "Ajouter des Utilisateurs à l'Organisation"
}
}
@@ -0,0 +1,11 @@
{
"password": "Mot de passe",
"change": "Changer le mot de passe",
"current": "Mot de passe actuel",
"current-wrong": "Le mot de passe actuel peut être érroné",
"new": "Nouveau mot de passe",
"validate-base": "Le mot de passe doit contenir au moins:",
"validate-length": "14 caractères",
"validate-case": "1 caractère miniscule",
"validate-number": "1 chiffre"
}
@@ -0,0 +1,13 @@
{
"service-tokens": "Jetons de service",
"service-tokens-description": "Chaque jeton de service vous est spécifique, à un certain projet et à un certain environnement dans ce projet.",
"add-new": "Ajouter un nouveau jeton",
"add-dialog": {
"title": "Ajouter un jeton de service pour {{target}}",
"description": "Spécifiez le nom, l'environnement et la période d'expiration. Lorsqu'un jeton est généré, vous ne pourrez le voir qu'une seule fois avant qu'il ne disparaisse. Assurez-vous de le sauvegarder quelque part.",
"name": "Nom du jeton de service",
"add": "Ajouter un jeton de service",
"copy-service-token": "Copiez votre jeton de service",
"copy-service-token-description": "Une fois que vous aurez fermé cette fenêtre, vous ne reverrez plus jamais votre jeton de service"
}
}
@@ -0,0 +1,4 @@
{
"title": "Membres du projet",
"description": "Cette page affiche les membres du projet sélectionné."
}
@@ -0,0 +1,4 @@
{
"title": "Paramètres d'Organisation",
"description": "Gérer les membres de votre organisation. Ces utilisateurs pourraient ensuite être répartis en projets."
}
@@ -0,0 +1,11 @@
{
"title": "Paramètres Personnels",
"description": "Consultez et gérez vos informations personnelles ici.",
"emergency": {
"name": "Kit d'urgence",
"text1": "Votre kit d'urgence contient les informations dont vous aurez besoin pour vous connecter à votre compte Infisical.",
"text2": "Seul le dernier kit d'urgence émis reste valide. Pour obtenir un nouveau kit d'urgence, vérifiez votre mot de passe.",
"download": "Télécharger le kit d'urgence"
},
"change-language": "Changer de langue"
}
@@ -0,0 +1,13 @@
{
"title": "Paramètres du Projet",
"description": "Ces paramètres ne s'appliquent qu'au Projet actuellement sélectionné.",
"danger-zone": "Zone de danger",
"delete-project": "Supprimer le Projet",
"project-to-delete": "Projet à Supprimer",
"danger-zone-note": "Dès que vous supprimez ce projet, vous ne pourrez plus revenir en arrière. Cela supprimera immédiatement toutes les clefs. Si vous voulez toujours le faire, veuillez saisir le nom du projet ci-dessous.",
"delete-project-note": "Remarque: Vous ne pouvez supprimer qu'un projet que si vous en avez plus d'un.",
"project-id-description": "Pour intégrer Infisical dans votre base de code et obtenir une injection automatique de variables d'environnement, vous devez utiliser l'ID du projet suivant.",
"project-id-description2": "Pour plus de conseils, y compris des extraits de code pour diverses langues et frameworks, voir ",
"auto-generated": "Ceci est l'identifiant unique généré automatiquement pour votre projet. Il ne peut pas être modifié.",
"docs": "Documentation Infisical"
}
+28
View File
@@ -0,0 +1,28 @@
{
"title": "S'inscrire",
"og-title": "Remplacez les fichiers .env par 1 ligne de code. Inscrivez-vous à Infisical en 3 minutes.",
"og-description": "Infisical, une plate-forme simple et chiffré de bout en bout qui permet aux équipes de synchroniser et de gérer des clefs API et des variables d'environnement. Fonctionne avec Node.js, Next.js, Gatsby, Nest.js ...",
"signup": "S'inscrire",
"already-have-account": "Déjà inscris? Se connecter",
"forgot-password": "Mot de passe oublié?",
"verify": "Vérifier",
"step1-start": "Bon, on commence!",
"step1-privacy": "En créant votre compte, vous acceptez nos conditions et avez lu et reconnu notre politique de confidentialité.",
"step1-submit": "C'est parti",
"step2-message": "Nous avons envoyé un email de vérification à",
"step2-code-error": "Oops. Votre code est faux. Veuillez réessayer.",
"step2-resend-alert": "Vous ne voyez pas l'email?",
"step2-resend-submit": "Renvoyer",
"step2-resend-progress": "Envoie en cours...",
"step2-spam-alert": "Assurez-vous de vérifier vos spams.",
"step3-message": "Nous y sommes presque!",
"step4-message": "Enregistrez votre kit d'urgence",
"step4-description1": "Si vous n'arrivez plus à vous connecter à votre compte, votre kit d'urgence est le seul moyen d'y arriver.",
"step4-description2": "Nous vous recommandons de le télécharger et de le garder en sécurité.",
"step4-description3": "Il contient votre clef secrète que nous ne pouvons pas récupérer pour vous si vous la perdez.",
"step4-download": "Téléchargez le PDF",
"step5-send-invites": "Envoyer les invitations",
"step5-invite-team": "Invitez votre équipe",
"step5-subtitle": "Infisical a pour but d'être utilisé avec vos coéquipiers. Invitez-les à le tester.",
"step5-skip": "Passer"
}